def webViewController(self): #Why am I using lazy instantiation here? We'll leave it like this for now. if self._webViewController is None: self._webViewController = WebViewController(putAjaxConsole = self.consoles_window.putAjaxMessage, putSageConsole = self.consoles_window.putSageProcessMessage) #self._webViewController = WebViewController(putAjaxConsole = self.simpleConsole, putSageConsole = self.simpleConsole) return self._webViewController
class MainWindow(QMainWindow, Ui_MainWindow): #Keep track of the MainWindow objects. instances = list() #NextID provides numbers to append to new documents: "Untitled-1.sws", "Untitled-2.sws", etc. NextID = 0 #When the user closes the last document window, we transform the window into a Welcome window. #On the other hand, if the user selects Quit, we just close the window. We use the isQuitting #variable to distinguish between these two cases. isQuitting = False recentFiles = [] def __init__(self, parent=None, isWelcome=False, file_name=None, isNewFile=False): super(MainWindow, self).__init__(parent) #Add self to the set of open instances. MainWindow.instances.append(self) #We want to reclaim the resources of this window when it is closed. self.setAttribute(Qt.WA_DeleteOnClose) self.file_name = file_name self.lastRemoteError = 0 #Consoles are just windows displaying logs of various things going on. self.consoles_window = Consoles(self) #Hidden until shown. #Lazy instantiation. Seems unnecessary at this point, to be honest. self._webViewController = None #See the WelcomeHandler class for more information. self._welcomeHandler = None #Determines if this is a welcome page window. self.isWelcome = isWelcome #titleName is the filename without the path. We need a separate entity for titleName #because new worksheets do not have a true filename--they aren't files yet. self.titleName = None self.isDirty = False self.setupUi() if self.isWelcome: self.restoreSettings() #restoreSettings() has initialized ServerConfigurations (among other things). #If no servers are configured, open the ServerListDlg so the user can configure a server. if not ServerConfigurations.server_list: self.doActionSageServer() if isNewFile: if not self.loadNewFile(): #There was an error loading a new file. Close the window. self.close() return #This window may be editing a worksheet associated to a file. if self.file_name is not None: self.isWelcome = True #Tricks loadFile into loading the file in the current window. if not self.loadFile(file_name): #Loading the file failed, so close this window. self.close() return def setupUi(self): #The superclass sets up most of the UI. super(MainWindow, self).setupUi(self) #self.setUnifiedTitleAndToolBarOnMac(True) webview = self.webViewController().webView() self.setCentralWidget(webview) #Connect all the actions. #File actions self.connect(self.actionNew, SIGNAL("triggered()"), self.doActionNew) self.connect(self.actionOpen, SIGNAL("triggered()"), self.doActionOpen) self.connect(self.actionSave, SIGNAL("triggered()"), self.doActionSave) self.connect(self.actionSaveAs, SIGNAL("triggered()"), self.doActionSaveAs) self.connect(self.actionSaveAll, SIGNAL("triggered()"), self.doActionSaveAll) self.connect(self.actionPrint, SIGNAL("triggered()"), self.doActionPrint) self.connect(self.actionQuit, SIGNAL("triggered()"), self.doActionQuit) #Edit actions #These actions are provided by the WebView instance. self.actionCopy = webview.pageAction(QWebPage.Copy) iconCopy = QIcon() iconCopy.addPixmap(QPixmap(":/images/images/copy-item.png"), QIcon.Normal, QIcon.Off) self.actionCopy.setIcon(iconCopy) self.actionCopy.setObjectName("actionCopy") self.actionCut = webview.pageAction(QWebPage.Cut) iconCut = QIcon() iconCut.addPixmap(QPixmap(":/images/images/Scissors.png"), QIcon.Normal, QIcon.Off) self.actionCut.setIcon(iconCut) self.actionCut.setObjectName("actionCut") self.actionPaste = webview.pageAction(QWebPage.Paste) iconPaste = QIcon() iconPaste.addPixmap(QPixmap(":/images/images/paste2_30.png"), QIcon.Normal, QIcon.Off) self.actionPaste.setIcon(iconPaste) self.actionPaste.setObjectName("actionPaste") self.actionUndo = webview.pageAction(QWebPage.Undo) self.actionUndo.setObjectName("actionUndo") self.actionRedo = webview.pageAction(QWebPage.Redo) self.actionRedo.setObjectName("actionRedo") self.menuEdit.addAction(self.actionCopy) self.menuEdit.addAction(self.actionCut) self.menuEdit.addAction(self.actionPaste) self.menuEdit.addAction(self.actionUndo) self.menuEdit.addAction(self.actionRedo) self.toolBar.insertAction(self.actionEvaluateWorksheet, self.actionCopy) self.toolBar.insertAction(self.actionEvaluateWorksheet, self.actionCut) self.toolBar.insertAction(self.actionEvaluateWorksheet, self.actionPaste) self.toolBar.insertSeparator(self.actionEvaluateWorksheet) #This is normally done in retranslateUi, but that method has already been called. self.retranslateEditMenuUi() #Worksheet actions self.connect(self.actionWorksheetProperties, SIGNAL("triggered()"), self.doActionWorksheetProperties) self.connect(self.actionEvaluateWorksheet, SIGNAL("triggered()"), self.doActionEvaluateWorksheet) self.connect(self.actionInterrupt, SIGNAL("triggered()"), self.doActionInterrupt) self.connect(self.actionRestartWorksheet, SIGNAL("triggered()"), self.doActionRestartWorksheet) self.connect(self.actionHideAllOutput, SIGNAL("triggered()"), self.doActionHideAllOutput) self.connect(self.actionShowAllOutput, SIGNAL("triggered()"), self.doActionShowAllOutput) self.connect(self.actionDeleteAllOutput, SIGNAL("triggered()"), self.doActionDeleteAllOutput) #Miscellaneous actions self.connect(self.actionAbout, SIGNAL("triggered()"), self.doActionAbout) self.connect(self.actionOnlineDocumentation, SIGNAL("triggered()"), self.doActionOnlineDocumentation) self.connect(self.actionSageServer, SIGNAL("triggered()"), self.doActionSageServer) if self.isWelcome: #Display the welcome screen. self.showWelcome() def retranslateEditMenuUi(self): #This is normally done in retranslateUi, but that method has already been called. self.actionCopy.setText(QApplication.translate("MainWindow", "&Copy", None, QApplication.UnicodeUTF8)) self.actionCopy.setToolTip(QApplication.translate("MainWindow", "Copy", None, QApplication.UnicodeUTF8)) self.actionCopy.setShortcut(QApplication.translate("MainWindow", "Ctrl+C", None, QApplication.UnicodeUTF8)) self.actionCut.setText(QApplication.translate("MainWindow", "Cu&t", None, QApplication.UnicodeUTF8)) self.actionCut.setToolTip(QApplication.translate("MainWindow", "Cut", None, QApplication.UnicodeUTF8)) self.actionCut.setShortcut(QApplication.translate("MainWindow", "Ctrl+X", None, QApplication.UnicodeUTF8)) self.actionPaste.setText(QApplication.translate("MainWindow", "&Paste", None, QApplication.UnicodeUTF8)) self.actionPaste.setToolTip(QApplication.translate("MainWindow", "Paste from clipboard", None, QApplication.UnicodeUTF8)) self.actionPaste.setShortcut(QApplication.translate("MainWindow", "Ctrl+V", None, QApplication.UnicodeUTF8)) self.actionUndo.setText(QApplication.translate("MainWindow", "&Undo", None, QApplication.UnicodeUTF8)) self.actionUndo.setToolTip(QApplication.translate("MainWindow", "Undo last edit", None, QApplication.UnicodeUTF8)) self.actionUndo.setShortcut(QApplication.translate("MainWindow", "Ctrl+Z", None, QApplication.UnicodeUTF8)) self.actionRedo.setText(QApplication.translate("MainWindow", "&Redo", None, QApplication.UnicodeUTF8)) self.actionRedo.setToolTip(QApplication.translate("MainWindow", "Redo last edit", None, QApplication.UnicodeUTF8)) self.actionRedo.setShortcut(QApplication.translate("MainWindow", "Shift+Ctrl+Z", None, QApplication.UnicodeUTF8)) def restoreSettings(self): settings = QSettings() #Restore recent files list. MainWindow.recentFiles = settings.value("RecentFiles") if MainWindow.recentFiles is None: MainWindow.recentFiles = [] self.updateRecentFilesMenu() #Restore window geometry self.restoreGeometry(settings.value("MainWindow/Geometry")) self.restoreState(settings.value("MainWindow/State")) #Populate the list of available Sage servers. sage_servers = settings.value("ServerConfigurations") if sage_servers is None: sage_servers = [] ServerConfigurations.restoreFromList(sage_servers) def saveSettings(self): settings = QSettings() settings.setValue("RecentFiles", MainWindow.recentFiles) settings.setValue("MainWindow/Geometry", self.saveGeometry()) settings.setValue("MainWindow/State", self.saveState()) settings.setValue("ServerConfigurations", ServerConfigurations.server_list) def webViewController(self): #Why am I using lazy instantiation here? We'll leave it like this for now. if self._webViewController is None: self._webViewController = WebViewController(putAjaxConsole = self.consoles_window.putAjaxMessage, putSageConsole = self.consoles_window.putSageProcessMessage) #self._webViewController = WebViewController(putAjaxConsole = self.simpleConsole, putSageConsole = self.simpleConsole) return self._webViewController def showWelcome(self): #Make the UI changes necessary for showing the welcome page. #This method is only called if we're a welcome page, so... self.isWelcome = True self.file_name = None self.titleName = "Welcome" self.setWindowTitle("Guru") self.dirty(False) self.updateWindowMenu() #Hide the toolbar. self.toolBar.hide() #Disable the relevant actions. self.enableEditingActions(False) #Hook up the javscript bridge. if self._welcomeHandler is None: self._welcomeHandler = WelcomeHandler(self) self._welcomeHandler.NewFunction = self.doActionNew self._welcomeHandler.OpenFunction = self.doActionOpen self._welcomeHandler.OpenRecentFunction = self.loadFile #Connect the javascript bridge. self.connect(self.webViewController().webView().page().mainFrame(), SIGNAL("javaScriptWindowObjectCleared()"), self.addJavascriptBridge) #When the welcome page finishes loading, we add the recent files. self.connect(self.webViewController().webView(), SIGNAL('loadFinished(bool)'), self.addRecentFilesToWelcomePage) #Display the welcome page. self.webViewController().showHtmlFile("guru_welcome.html") def addRecentFilesToWelcomePage(self): #If we are displaying the welcome page, add the recent files to the recent files list. if self.isWelcome: for recent_file in MainWindow.recentFiles: self.webViewController().addToRecentFiles(recent_file) def addJavascriptBridge(self): #This method is called whenever new content is loaded into the webFrame. #Each time this happens, we need to reconnect the Python-javascript bridge. self.webViewController().webView().page().mainFrame().addToJavaScriptWindowObject("GuruWelcome", self._welcomeHandler) #This is the counterpart to showWelcome(). It undoes the UI changes #that showWelcome() does. Probably a silly name. def hideWelcome(self): #We aren't a welcome page anymore. self.isWelcome = False #Enable the relevant actions. self.enableEditingActions() #Unhide the toolbar self.toolBar.show() #Disconnect the recent files update so we don't add them twice the second time around. self.disconnect(self.webViewController().webView(), SIGNAL('loadFinished(bool)'), self.addRecentFilesToWelcomePage) #The editing actions are only relevant if we are editing a worksheet. #Otherwise (i.e. for the welcome page), they should be disabled. def enableEditingActions(self, enabling=True): #Worksheet Menu self.actionInterrupt.setEnabled(enabling) self.actionHideAllOutput.setEnabled(enabling) self.actionShowAllOutput.setEnabled(enabling) self.actionDeleteAllOutput.setEnabled(enabling) self.actionEvaluateWorksheet.setEnabled(enabling) self.actionInterrupt.setEnabled(enabling) self.actionRestartWorksheet.setEnabled(enabling) #Edit Menu #These are taken care of by the webview itself. # self.actionPaste.setEnabled(enabling) # self.actionSave.setEnabled(enabling) # self.actionCut.setEnabled(enabling) # self.actionCopy.setEnabled(enabling) # self.actionUndo.setEnabled(enabling) # self.actionRedo.setEnabled(enabling) #Miscellaneous #self.actionSageServer.setEnabled(enabling) self.actionWorksheetProperties.setEnabled(enabling) #File Menu self.actionPrint.setEnabled(enabling) self.actionSaveAs.setEnabled(enabling) self.actionSaveAll.setEnabled(enabling) def showConsoles(self): self.consoles_window.show() def simpleConsole(self, text): print text #This method is called whenever the window is closed. def closeEvent(self, event): shouldClose = True if not self.isWelcome: if self.isDirty: #If the worksheet is dirty, ask the user if they want to save. response = QMessageBox.question(self, "Guru - Unsaved Changes", "You have unsaved changes. Close anyway?", QMessageBox.Ok|QMessageBox.Cancel) if response == QMessageBox.Ok: shouldClose = True else: shouldClose = False if len(MainWindow.instances) == 1 and MainWindow.isQuitting is False and shouldClose is True: #This is the last remaining MainWindow open. #Transform the current window into a welcome page. shouldClose = False self.webViewController().clear() self.showWelcome() else: shouldClose = True if shouldClose: if self._webViewController: self._webViewController.cleanup() MainWindow.instances.remove(self) #Is this the last window to close? if not MainWindow.instances: #This is the last window, so save application settings. self.saveSettings() else: #This is not the last window, so update the Window menu. self.updateWindowMenu() event.accept() else: #The close was cancelled. MainWindow.isQuitting = False event.ignore() def updateRecentFilesMenu(self): #If there is a current file open, add it to the recent files list. if self.file_name and MainWindow.recentFiles.count(self.file_name) == 0: #Prepend the file to recentFiles MainWindow.recentFiles.insert(0, self.file_name) #This this is the only time files get added to recentFiles, #enforce the maximum length of recentFiles now. while len(MainWindow.recentFiles) > 9: MainWindow.recentFiles.pop() recent_files = [] #For each file in recentFiles, check that it exists and is readable. for fname in MainWindow.recentFiles: if os.access(fname, os.F_OK): recent_files.append(fname) MainWindow.recentFiles = recent_files #Now create an action for each file. if recent_files: action_list = [] for i, fname in enumerate(recent_files): new_action = QAction("%d. %s"%(i, fname), self, triggered=self.openRecentFile) new_action.setData(fname) action_list.append(new_action) #Now update the recent files menu on every window. for window in MainWindow.instances: window.menuRecent.clear() window.menuRecent.addActions(action_list) def openRecentFile(self): action = self.sender() if not isinstance(action, QAction): return self.loadFile(action.data()) def updateWindowMenu(self): #aboutToShow() does not do what Mark Summerfield thinks it does--at least in PySide. #Generate a list of actions to add to the Window menu. window_menu_actions = [] for i, window in enumerate(MainWindow.instances): #The following line removes the "[*]" from the titleName. title_name = ''.join(window.titleName.split("[*]")) action_text = "%d. %s" % (i, title_name) new_action = QAction(action_text, self, triggered=self.raiseWindow) new_action.setData(window.titleName) window_menu_actions.append(new_action) for window in MainWindow.instances: window.menuWindow.clear() for new_action in window_menu_actions: window.menuWindow.addAction(new_action) def raiseWindow(self): action = self.sender() if not isinstance(action, QAction): return title_name = action.data() #title_text = title_text[title_text.index('.')+2:] #Peels off the initial numbering, i.e. "2. Untitled.sws". for window in MainWindow.instances: if window.titleName == title_name: window.activateWindow() window.raise_() break def setUniqueWindowTitle(self): #This method does the following: # 1. gets the filename of the working file, or sets it to Untitled.sws; # 2. determines if there are already titleNames with that filename, and if so, how many; # 3. using (1) and (2), creates a unique titleName of the form "filename.sws (n)"; # 4. sets the window title appropriately; # 5. calls updateWindowMenu(). #1. gets the filename of the working file, or sets it to Untitled.sws; if self.file_name is not None: title_text = os.path.basename(self.file_name) else: title_text = "Untitled.sws" title_text += "[*]" #Dirty flag. #2. determines if there are already titleNames with that filename, and if so, how many; existing_titles = [window.titleName for window in MainWindow.instances if window is not self] if existing_titles.count(title_text) > 0: counter = 1 while existing_titles.count(title_text + " (%d)"%counter) > 0: counter += 1 #3. using (1) and (2), creates a unique titleName of the form "filename.sws (n)"; title_text += " (%d)"%counter #4. sets the window title appropriately; self.setWindowTitle("Guru - %s" % title_text) self.titleName = title_text #5. calls updateWindowMenu(). self.updateWindowMenu() def dirty(self, isDirty): self.isDirty = isDirty self.setWindowModified(isDirty) @Slot(str) def handleRemoteCommandError(self, message): #When an error happens, it may generate a cascade of errors. We don't want to flood the user #with these errors. if time.time() - self.lastRemoteError > GURU_ERROR_REPEAT_TIME: QMessageBox.critical(self, "Connection Error", message) self.lastRemoteError = time.time() ############### Actions ############### @Slot() def doActionNew(self): #Which MainWindow object we create the new worksheet in #depends on if doActionNew() is fired on a welcome page #or not. main_window = None if self.isWelcome: self.hideWelcome() if not self.loadNewFile(): #There was an error loading the new file. Return to a welcome page. self.showWelcome() else: #Create a new MainWindow object to use for the new worksheet. main_window = MainWindow(isNewFile=True) main_window.show() def loadNewFile(self): #Create a new worksheet try: self.webViewController().newWorksheetFile() except Exception as e: QMessageBox.critical(self, "New File Error", "Could not create a new file:\n%s" % e.message) return False self.dirty(False) self.connectWorksheetSignals() self.file_name = None #Set the title appropriately. self.setUniqueWindowTitle() return True @Slot() def doActionOpen(self): file_name = QFileDialog.getOpenFileName(self, "Open Worksheet", filter="Sage Worksheets (*.sws *.txt *.html)")[0] if not file_name: #User clicked cancel. return #Check if the file is already opened. If it is, just raise that window. window_list = [window for window in MainWindow.instances if window.file_name == file_name] if len(window_list)==0: #The file is not open, so open it. self.loadFile(file_name) else: #The file is already open. Just raise its window. window_list[0].activateWindow() window_list[0].raise_() def loadFile(self, file_name): #Which MainWindow object we create the new worksheet in depends on if loadFile() #is fired on a welcome page or not. Returns True on success. if self.isWelcome: #Use the current MainWindow object to create the worksheet. self.hideWelcome() #Set the working filename self.file_name = file_name #We set the window title. self.setUniqueWindowTitle() #Open the worksheet in the webView. try: self.webViewController().openWorksheetFile(file_name) except Exception as e: QMessageBox.critical(self, "File Open Error", "Could not open the file:\n%s" % e.message) self.showWelcome() return False #Set the dirty flag handler. self.dirty(False) self.connectWorksheetSignals() self.updateRecentFilesMenu() else: #Create a new MainWindow object to use for the new worksheet. main_window = MainWindow(file_name=file_name) main_window.show() main_window.activateWindow() main_window.raise_() return True def connectWorksheetSignals(self): #Worksheets emit a couple of signals that we want to listen for. self.connect(self.webViewController().worksheet_controller, SIGNAL("dirty(bool)"), self.dirty) #self.connect(self.webViewController().worksheet_controller, SIGNAL("remoteCommandError(str)"), self.handleRemoteCommandError) def doActionSave(self): if self.file_name: self.saveFile(self.file_name) else: self.doActionSaveAs() def doActionSaveAs(self): if self.file_name: suggested_filename = self.file_name else: suggested_filename = self.webViewController().worksheet_controller.getTitle() #Make sure the file extension is in the filename. if not suggested_filename.endswith(".sws"): suggested_filename += ".sws" suggested_filename = os.path.join(os.curdir, suggested_filename) #getSaveFileName([parent=None[, caption=""[, dir=""[, filter=""[, selectedFilter=""[, options=0]]]]]]) filename = QFileDialog.getSaveFileName(self, caption="Save Worksheet", dir=suggested_filename, filter="Sage Worksheet (*.sws);;All files (*)", selectedFilter="Sage Worksheets (*.sws)")[0] if not filename: #The user clicked cancel. return #Make sure the file extension is in the filename. if not filename.endswith(".sws"): filename += ".sws" self.saveFile(filename) def doActionSaveAll(self): for window in MainWindow.instances: window.doActionSave() def saveFile(self, file_name): #Save the file, overwriting any existing file. self.webViewController().saveWorksheetFile(file_name) #Set the working filename for this MainWindow instance. self.file_name = file_name self.setUniqueWindowTitle() self.updateRecentFilesMenu() def doActionPrint(self): QMessageBox.information(self, "Not Implemented", "Not implemented.") def doActionQuit(self): MainWindow.isQuitting = True while MainWindow.instances: window = MainWindow.instances[-1] if not window.close(): break def doActionWorksheetProperties(self): self.showConsoles() def doActionEvaluateWorksheet(self): self.webViewController().worksheet_controller.evaluateAll() def doActionInterrupt(self): self.webViewController().worksheet_controller.interrupt() def doActionHideAllOutput(self): self.webViewController().worksheet_controller.hideAllOutput() def doActionShowAllOutput(self): self.webViewController().worksheet_controller.showAllOutput() def doActionDeleteAllOutput(self): self.webViewController().worksheet_controller.deleteAllOutput() def doActionRestartWorksheet(self): self.webViewController().worksheet_controller.restartWorksheet() def doActionTypesetOutput(self): pass #self.webViewController().worksheet_controller.typesetOutput(True) def doActionAbout(self): f = open(os.path.join(GURU_ROOT, 'guru', 'guru_about.html')) file_contents = f.read() message_text = file_contents % (platform.python_version(), QT_VERSION_STR, PYSIDE_VERSION_STR, platform.system()) QMessageBox.about(self, "About Guru", message_text) def doActionOnlineDocumentation(self): os.system('open %s 1>&2 > /dev/null &'% GURU_ONLINE_DOCUMENTATION) def doActionSageServer(self): server_list_dialog = ServerListDlg(self) #Get a reference to the WorksheetController associated to this window. wsc = self.webViewController().worksheet_controller #Select the server associated to this window, if there is one. if wsc and wsc.server_configuration: server_list_dialog.selectServer(wsc.server_configuration) #Show the dialog. server_list_dialog.exec_() #It's possible that the user will delete all of the servers. It's not clear how to cleanly handle this case. #We choose to give the user a choice to fix the situation. while not ServerConfigurations.server_list: #No servers? message_text = "Guru needs a Sage server configured in order to evaluate cells. " \ "Add a Sage server configuration in the server configuration dialog?" response = QMessageBox.question(self, "Sage Not Configured", message_text, QMessageBox.Yes, QMessageBox.No) if response == QMessageBox.No: return server_list_dialog.exec_() #Execution only reaches this point if there exists a server. server_name = server_list_dialog.ServerListView.currentItem().text() if wsc: new_config = ServerConfigurations.getServerByName(server_name) try: wsc.useServerConfiguration(new_config) except Exception as e: QMessageBox.critical(self, "Error Switching Servers", "Could not switch servers:\n%s" % e.message)