class MainWindow(QMainWindow): # class EventFilter(QObject): # def __init__(self, parent): # super().__init__(parent) # # def eventFilter(self, obj, e): # #print(obj.metaObject().className()) # # if e.type() == QEvent.KeyPress or e.type() == QEvent.ShortcutOverride: # key = e.key() # mod = e.modifiers() # # print(str(e) + ' ' + str(e.type()) ) # # if mod == Qt.AltModifier: # print('*'*30) # print('alt pressed') # if key == Qt.Key_Left or key == Qt.Key_Right: # print('alt-left') if key == Qt.Key_Left else print('alt-right') ## action = QAbstractItemView.MoveLeft if key == Qt.Key_Left else QAbstractItemView.MoveRight ## idx = obj.moveCursor(action, Qt.NoModifier) ## item = obj.itemFromIndex(idx) ## obj.setCurrentItem(item) # return True # # return False PROGRAM_NAME = 'KiCad Schematic Component Manager' #-------------------------------------------------------------------------------- class EventFilter(QObject): def __init__(self, parent): super().__init__(parent) def eventFilter(self, obj, e): if e.type() == QEvent.KeyPress or e.type() == QEvent.ShortcutOverride: key = e.key() mod = e.modifiers() #print(obj.focusWidget().metaObject().className()) return False #-------------------------------------------------------------------------------- def scroll_left(self): print('alt-left') if self.ToolIndex == 3 or self.ToolIndex == 2: self.ToolList[self.ToolIndex].finish_edit() self.ToolIndex -= 1 if self.ToolIndex < 0: self.ToolIndex = len(self.ToolList) - 1 print('Tool Index: ' + str(self.ToolIndex)) self.ToolList[self.ToolIndex].setFocus() #-------------------------------------------------------------------------------- def scroll_right(self): print('alt-right') if self.ToolIndex == 3 or self.ToolIndex == 2: self.ToolList[self.ToolIndex].finish_edit() self.ToolIndex += 1 if self.ToolIndex == len(self.ToolList): self.ToolIndex = 0 print('Tool Index: ' + str(self.ToolIndex)) self.ToolList[self.ToolIndex].setFocus() #-------------------------------------------------------------------------------- def mouse_change_tool(self, s): print('Tool ' + s) if s == 'CmpTable': self.ToolIndex = 0 elif s == 'Selector': self.ToolIndex = 1 elif s == 'Inspector': self.ToolIndex = 2 elif s == 'FieldInspector': self.ToolIndex = 3 if self.ToolIndex != 3: self.ToolList[3].finish_edit() # save field properties when leave field inspector #-------------------------------------------------------------------------------- def add_user_property(self): self.Inspector.save_cmps() self.FieldInspector.save_fields() self.Inspector.add_property() #-------------------------------------------------------------------------------- def remove_user_property(self): #self.Inspector.save_cmps() self.FieldInspector.save_fields() self.Inspector.remove_property() #-------------------------------------------------------------------------------- def rename_user_property(self): #self.Inspector.save_cmps() self.FieldInspector.save_fields() self.Inspector.rename_property() #-------------------------------------------------------------------------------- def __init__(self): super().__init__() self.initUI() self.installEventFilter(self.EventFilter(self)) self.setFocusPolicy(Qt.WheelFocus) self.setTabOrder(self.CmpTable, self.Inspector ) self.setTabOrder(self.Inspector, self.Selector) self.setTabOrder(self.Selector, self.FieldInspector) #self.setTabOrder(self.FieldInspector, self.CmpTable) #---------------------------------------------------- # # Application Hotkeys # self.shortcutLeft = QShortcut(QKeySequence(Qt.ALT + Qt.Key_Left), self) self.shortcutRight = QShortcut(QKeySequence(Qt.ALT + Qt.Key_Right), self) self.shortcutLeft.setContext(Qt.ApplicationShortcut) self.shortcutRight.setContext(Qt.ApplicationShortcut) self.shortcutLeft.activated.connect(self.scroll_left) self.shortcutRight.activated.connect(self.scroll_right) #-------------------------------------------------------------------------------- def initUI(self): #---------------------------------------------------- # # Main Window # work_zone = QWidget(self) Layout = QHBoxLayout(work_zone) self.setCentralWidget(work_zone) openAction = QAction(QIcon( os.path.join(resources_path, 'open24.png') ), 'Open', self) openAction.setShortcut('Ctrl+O') openAction.setStatusTip('Open Schematic File') openAction.triggered.connect(self.open_file) saveAction = QAction(QIcon( os.path.join(resources_path, 'save24.png') ), 'Save', self) saveAction.setShortcut('Ctrl+S') saveAction.setStatusTip('Save Schematic File') saveAction.triggered.connect(self.save_file) saveAsAction = QAction(QIcon( os.path.join(resources_path, 'save-as24.png') ), 'Save As...', self) saveAsAction.setShortcut('Ctrl+Shift+S') saveAsAction.setStatusTip('Save Schematic File As...') saveAsAction.triggered.connect(self.save_file_as) exitAction = QAction(QIcon( os.path.join(resources_path, 'exit24.png') ), 'Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') exitAction.triggered.connect(self.close) settingsAction = QAction(QIcon( os.path.join(resources_path, 'settings24.png') ), 'Settings', self) settingsAction.setShortcut('Ctrl+Alt+S') settingsAction.setStatusTip('Edit settings') settingsAction.triggered.connect(self.edit_settings) helpAction = QAction(QIcon( os.path.join(resources_path, 'help_book24.png') ), 'User\'s Manual', self) helpAction.setShortcut('F1') helpAction.setStatusTip('User\'s Manual') helpAction.triggered.connect(self.show_user_manual_slot) helpSDAction = QAction(QIcon( os.path.join(resources_path, 'gear24.png') ), 'Settings Dialog', self) helpSDAction.setShortcut('Ctrl+F1') helpSDAction.setStatusTip('Settings Dialog Help') helpSDAction.triggered.connect(self.show_setting_dialog_help_slot) helpHKAction = QAction(QIcon( os.path.join(resources_path, 'rocket24.png') ), 'Hotkeys', self) helpHKAction.setShortcut('Shift+F1') helpHKAction.setStatusTip('Hotkeys Help') helpHKAction.triggered.connect(self.show_hotkeys_help_slot) self.statusBar().showMessage('Ready') #-------------------------------------------- # # Main Menu # menubar = self.menuBar() fileMenu = menubar.addMenu('&File') fileMenu.addAction(openAction) fileMenu.addAction(saveAction) fileMenu.addAction(saveAsAction) fileMenu.addAction(exitAction) #-------------------------------------------- # # Options Menu # optionsMenu = menubar.addMenu('&Options') optionsMenu.addAction(settingsAction) #-------------------------------------------- # # Help Menu # helpMenu = menubar.addMenu('&Help') helpMenu.addAction(helpAction) helpMenu.addAction(helpSDAction) helpMenu.addAction(helpHKAction) #-------------------------------------------- # # Toolbar # toolbar = self.addToolBar('Exit') toolbar.addAction(exitAction) toolbar.addAction(openAction) toolbar.addAction(saveAction) toolbar.addAction(saveAsAction) toolbar.addAction(settingsAction) toolbar.addAction(helpAction) #---------------------------------------------------- # # Settings Dialog # #---------------------------------------------------- # # Components Table # self.CmpTabBox = QGroupBox('Components', self) self.CmpTabLayout = QVBoxLayout(self.CmpTabBox) self.CmpTabLayout.setContentsMargins(4,10,4,4) self.CmpTabLayout.setSpacing(10) self.CmpTabLayout.setSizeConstraint(QVBoxLayout.SetMaximumSize) self.CmpTable = ComponentsTable(self) #self.CmpChooseButton = QPushButton('Choose', self) self.CmpTabLayout.addWidget(self.CmpTable) #self.CmpTabLayout.addWidget(self.CmpChooseButton) #---------------------------------------------------- # # Selector # self.SelectorBox = QGroupBox('Selector', self) self.SelectorLayout = QVBoxLayout(self.SelectorBox) self.SelectorLayout.setContentsMargins(4,10,4,4) self.SelectorLayout.setSpacing(2) self.SelectorBtnWidget = QWidget(self) self.SelectorBtnLayout = QHBoxLayout(self.SelectorBtnWidget) self.SelectorBtnLayout.setContentsMargins(4,10,4,4) self.SelectorBtnLayout.setSpacing(10) self.Selector = Selector(self) self.SelApplyButton = QPushButton('Apply', self) self.SelApplyButton.setToolTip('Alt+S: Apply selection patterns to components') self.SelClearButton = QPushButton('Clear', self) self.SelClearButton.setToolTip('Alt+C: Clear selection patterns') self.SelTemplateButton = QPushButton('Use Component', self) self.SelTemplateButton.setToolTip('Alt+T: Use Selected Component As Template') self.SelectorLayout.addWidget(self.Selector) self.SelectorBtnLayout.addWidget(self.SelTemplateButton) self.SelectorBtnLayout.addWidget(self.SelApplyButton) self.SelectorBtnLayout.addWidget(self.SelClearButton) self.SelectorLayout.addWidget(self.SelectorBtnWidget) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_S), self) self.shortcutSelApply.activated.connect(self.Selector.apply_slot) self.shortcutSelClear = QShortcut(QKeySequence(Qt.ALT + Qt.Key_C), self) self.shortcutSelClear.activated.connect(self.Selector.clear_slot) self.shortcutSelTemplate = QShortcut(QKeySequence(Qt.ALT + Qt.Key_T), self) self.shortcutSelTemplate.activated.connect(self.Selector.use_comp_as_template_slot) #---------------------------------------------------- # # Inspector # self.Inspector = Inspector(self) self.FieldInspector = FieldInspector(self) self.InspectorBtnWidget = QWidget(self) self.InspectorBtnLayout = QHBoxLayout(self.InspectorBtnWidget) self.InspectorBtnLayout.setContentsMargins(4,10,4,4) self.InspectorBtnLayout.setSpacing(10) self.AddUserProperty = QPushButton('Add Property', self) self.AddUserProperty.setToolTip('Alt+A: Add new user property') self.DeleteUserProperty = QPushButton('Delete Property', self) self.DeleteUserProperty.setToolTip('Alt+Delete: Delete user property') self.RenameUserProperty = QPushButton('Rename Property', self) self.RenameUserProperty.setToolTip('Alt+R: Rename user property') self.InspectorBox = QGroupBox('Inspector', self) self.InspectorSplit = QSplitter(Qt.Vertical, self) self.InspectorLayout = QVBoxLayout(self.InspectorBox) self.InspectorLayout.setContentsMargins(4,10,4,4) self.InspectorLayout.setSpacing(2) self.InspectorSplit.addWidget(self.Inspector) self.InspectorSplit.addWidget(self.FieldInspector) self.InspectorLayout.addWidget(self.InspectorSplit) self.InspectorBtnLayout.addWidget(self.AddUserProperty) self.InspectorBtnLayout.addWidget(self.DeleteUserProperty) self.InspectorBtnLayout.addWidget(self.RenameUserProperty) self.InspectorLayout.addWidget(self.InspectorBtnWidget) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_A), self) self.shortcutSelApply.activated.connect(self.add_user_property) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_Delete), self) self.shortcutSelApply.activated.connect(self.remove_user_property) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_R), self) self.shortcutSelApply.activated.connect(self.rename_user_property) #---------------------------------------------------- self.Splitter = QSplitter(self) self.Splitter.addWidget(self.CmpTabBox) self.Splitter.addWidget(self.SelectorBox) self.Splitter.addWidget(self.InspectorBox) self.centralWidget().layout().addWidget(self.Splitter) #---------------------------------------------------- # # Signals and Slots connections # self.CmpTable.cells_chosen.connect(self.Inspector.load_cmp) self.CmpTable.cells_chosen.connect(self.Selector.comp_template_slot) self.CmpTable.file_load.connect(self.file_loaded_slot) self.CmpTable.cmps_updated.connect(self.Selector.process_comps_slot) self.CmpTable.cmps_selected.connect(self.set_status_text_slot) self.SelApplyButton.clicked.connect(self.Selector.apply_slot) self.SelClearButton.clicked.connect(self.Selector.clear_slot) self.SelTemplateButton.clicked.connect(self.Selector.use_comp_as_template_slot) self.Selector.select_comps_signal.connect(self.CmpTable.select_comps_slot) self.Inspector.load_field.connect(self.FieldInspector.load_field_slot) self.Inspector.update_comps.connect(self.data_changed_slot) self.Inspector.update_comps.connect(self.CmpTable.update_cmp_list_slot) self.FieldInspector.data_changed.connect(self.data_changed_slot) CmpMgr.file_saved.connect(self.file_saved_slot) self.CmpTable.mouse_click.connect(self.mouse_change_tool) self.Inspector.mouse_click.connect(self.mouse_change_tool) self.FieldInspector.mouse_click.connect(self.mouse_change_tool) self.Inspector.header().sectionResized.connect(self.FieldInspector.column_resize) self.AddUserProperty.clicked.connect(self.add_user_property) self.DeleteUserProperty.clicked.connect(self.remove_user_property) self.RenameUserProperty.clicked.connect(self.rename_user_property) #---------------------------------------------------- self.ToolList = [] self.ToolList.append(self.CmpTable) self.ToolList.append(self.Selector) self.ToolList.append(self.Inspector) self.ToolList.append(self.FieldInspector) self.ToolIndex = 0 #---------------------------------------------------- # # Window # self.setWindowTitle(self.PROGRAM_NAME) Settings = QSettings('kicad-tools', 'Schematic Component Manager') #print(Settings.allKeys()) if Settings.contains('geometry'): self.restoreGeometry( Settings.value('geometry') ) else: self.setGeometry(100, 100, 1024, 768) if Settings.contains('cmptable'): w0, w1 = Settings.value('cmptable') self.CmpTable.setColumnWidth( 0, int(w0) ) self.CmpTable.setColumnWidth( 1, int(w1) ) if Settings.contains('selector'): w0, w1 = Settings.value('selector') self.Selector.setColumnWidth( 0, int(w0) ) self.Selector.setColumnWidth( 1, int(w1) ) if Settings.contains('inspector'): w0, w1 = Settings.value('inspector') self.Inspector.setColumnWidth( 0, int(w0) ) self.Inspector.setColumnWidth( 1, int(w1) ) self.FieldInspector.setColumnWidth( 0, int(w0) ) self.FieldInspector.setColumnWidth( 1, int(w1) ) #self.Inspector.setColumnWidth( 2, int(w2) ) if Settings.contains('splitter'): self.Splitter.restoreState( Settings.value('splitter') ) if Settings.contains('inssplitter'): self.InspectorSplit.restoreState( Settings.value('inssplitter') ) #---------------------------------------------------- # # Process command line arguments # if len(sys.argv) > 1: fname = sys.argv[1] if os.path.exists(fname): self.CmpTable.load_file(fname) else: print('E: input file "' + fname + '"does not exist') self.show() #--------------------------------------------------------------------------- def closeEvent(self, event): Settings = QSettings('kicad-tools', 'Schematic Component Manager') Settings.setValue( 'geometry', self.saveGeometry() ) Settings.setValue( 'cmptable', [self.CmpTable.columnWidth(0), self.CmpTable.columnWidth(1)] ) Settings.setValue( 'selector', [self.Selector.columnWidth(0), self.Selector.columnWidth(1)] ) Settings.setValue( 'inspector', [self.Inspector.columnWidth(0), self.Inspector.columnWidth(1)] ) Settings.setValue( 'splitter', self.Splitter.saveState() ) Settings.setValue( 'inssplitter', self.InspectorSplit.saveState() ) QWidget.closeEvent(self, event) #--------------------------------------------------------------------------- def open_file(self): #filename = QFileDialog.getOpenFileName(self, 'Open schematic file', '/opt/cad/kicad', 'KiCad Schematic Files (*.sch)') dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setNameFilter('KiCad Schematic Files (*.sch)') filenames = [] if dialog.exec_(): filenames = dialog.selectedFiles() if len(filenames) == 0: return CmpMgr.set_curr_file_path( filenames[0] ) self.CmpTable.load_file( filenames[0] ) #--------------------------------------------------------------------------- def save_file(self): self.FieldInspector.save_fields() self.Inspector.save_cmps() curr_file = CmpMgr.curr_file_path() message = 'Save File "' + curr_file + '"' print(message) self.statusBar().showMessage(message) CmpMgr.save_file(curr_file) #--------------------------------------------------------------------------- def save_file_as(self): self.Inspector.save_cmps() self.FieldInspector.save_fields() filenames = QFileDialog.getSaveFileName(self, 'Save File As...', '', 'KiCad Schematic Files (*.sch)') if filenames[0] == '': return print('Save File As "' + filenames[0] + '"') CmpMgr.save_file(filenames[0]) CmpMgr.set_curr_file_path( filenames[0] ) #--------------------------------------------------------------------------- def file_loaded_slot(self): text = CmpMgr.curr_file_path() self.set_title(text) #--------------------------------------------------------------------------- def data_changed_slot(self): text = CmpMgr.curr_file_path() + ' *' self.set_title(text) #--------------------------------------------------------------------------- def file_saved_slot(self): text = CmpMgr.curr_file_path() self.set_title(text) #--------------------------------------------------------------------------- def set_title(self, text = ''): text = ' - ' + text if len(text) > 0 else '' self.setWindowTitle(self.PROGRAM_NAME + ' v' + VERSION + text) #--------------------------------------------------------------------------- def set_status_text_slot(self, text): self.statusBar().showMessage(text) #--------------------------------------------------------------------------- def edit_settings(self): print('edit settings') SettingsDialog = TSettingsDialog(self) SettingsDialog.resize(400, 400) SettingsDialog.Tabs.setMinimumWidth(800) SettingsDialog.show() #--------------------------------------------------------------------------- def show_user_manual_slot(self): help = THelpForm(self, 'User\'s Manual', 'main.html') #--------------------------------------------------------------------------- def show_setting_dialog_help_slot(self): help = THelpForm(self, 'Settings Dialog', 'settings.html') #--------------------------------------------------------------------------- def show_hotkeys_help_slot(self): help = THelpForm(self, 'Hotkeys', 'hotkeys.html')
class MainWindow(QWidget): def __init__(self, appConfig, projectorControl, parent=None): self.PHOTO_DELTAS_FILE_NAME = "//MACALLAN/Photos/Photo Info/Deltas/PhotoDeltas.txt" QWidget.__init__(self, parent) self._projectorControl = projectorControl # File used for delta changes to photos self.photoDeltas = PhotoDeltas(self.PHOTO_DELTAS_FILE_NAME) # Frameless window self.setWindowFlags(Qt.FramelessWindowHint) # Background to black self.setAutoFillBackground(True) palette = QPalette() palette.setColor(QPalette.Background, Qt.black) self.setPalette(palette) # Clock self.clock = AnimatedClock() # Calendar self.calendar = AnimatedCalendar(calUpdateSecs=600, calServerUrl=appConfig["calServerUrl"]) # Image # self.photos = AnimatedPhotos("//macallan/photos/PhotosMain/", ["jpg"], maxCols=3, maxRows=4, borders=[0,0,0,0], xBetweenPics=5, yBetweenPics=5, animationSpeed=1.0, picChangeMs=5000) self.photos = StaticPhotos("//macallan/photos/PhotosMain/", ["jpg"], self.photoDeltas, picChangeMs=5000) #self.photos = CaptionedPhotos("//macallan/photos/PhotosMain/", ["jpg"], picChangeMs=5000) # Toolbar self.windowToolbar = WindowToolbar(self.close, self) # Left pane of page self.leftPane = QSplitter(Qt.Vertical) self.leftPane.addWidget(self.clock) self.leftPane.addWidget(self.calendar) # Right pane of page self.rightPane = QSplitter(Qt.Vertical) self.rightPane.addWidget(self.windowToolbar) self.rightPane.addWidget(self.photos) # Splitter between left and right panes self.horzSplitter = QSplitter(Qt.Horizontal) self.horzSplitter.addWidget(self.leftPane) self.horzSplitter.addWidget(self.rightPane) self.layout = QHBoxLayout(self) self.layout.addWidget(self.horzSplitter) self.setLayout(self.layout) # Remember the locations of the splitter bars to restore next time the program is run settings = QSettings("PhotoCalendar") settings.beginGroup("MainWindow") position = settings.value("Position", QVariant(QPoint(0, 0))) self.move(position) size = settings.value("Size", QVariant(QSize(1920, 1200))) self.resize(size) if settings.value("HorzSplitter") is not None: self.horzSplitter.restoreState(settings.value("HorzSplitter")) #print("Restoring horz", settings.value("HorzSplitter")) if settings.value("LeftPaneSplitter") is not None: self.leftPane.restoreState(settings.value("LeftPaneSplitter")) #print("Restoring left pane", settings.value("LeftPaneSplitter")) if settings.value("RightPaneSplitter") is not None: self.rightPane.restoreState(settings.value("RightPaneSplitter")) #print("Restoring right pane", settings.value("RightPaneSplitter")) settings.endGroup() # Start rotating photos self.photos.start() # # Grid layout # layout = QGridLayout() # # layout.setContentsMargins(0,0,0,0) # layout.setSpacing(0) # layout.addWidget(self.clock, 0, 0) # layout.addWidget(self.calendar, 1, 0) # layout.addWidget(self.photos, 0, 1, 2, 1) # layout.setColumnStretch(0, 1) # layout.setColumnStretch(1, 2.5) # self.setLayout(layout) # Start photo animation self.photos.start() def closeEvent(self, event): print("Main window close event") # Save layout settings settings = QSettings("PhotoCalendar") settings.beginGroup("MainWindow") curSize = self.size() settings.setValue("Size", QVariant(curSize)) curPos = self.pos() settings.setValue("Position", QVariant(curPos)) #settings.setValue("MainWindow/State", QVariant(self.saveState())) horzSplitterState = self.horzSplitter.saveState() #print("HorzSplitter save", horzSplitterState) settings.setValue("HorzSplitter", QVariant(horzSplitterState)) leftPaneSplitterState = self.leftPane.saveState() settings.setValue("LeftPaneSplitter", QVariant(leftPaneSplitterState)) #print("LeftPaneSplitter save", leftPaneSplitterState) rightPaneSplitterState = self.rightPane.saveState() settings.setValue("RightPaneSplitter", QVariant(rightPaneSplitterState)) #print("RightPaneSplitter save", leftPaneSplitterState) settings.endGroup() # Stop the sub-elements self.calendar.stop() self.clock.stop() self.photos.stop() # Accept the close event event.accept() def resizeEvent(self, evt=None): xWindowSize = self.width() yWindowSize = self.height() print("MainWindow size x,y", xWindowSize, yWindowSize); def test(self): self._projectorControl.test() def keyPressEvent(self, event): #QKeyEvent key = event.key() if key == QtCore.Qt.Key_Left: # print('Left') self.photos.movePrev() elif key == QtCore.Qt.Key_Right: # print('Right') self.photos.moveNext() elif key == QtCore.Qt.Key_0: self.photoDeltas.setRating(self.photos, 0) self.photos.reshow() elif key == QtCore.Qt.Key_1: self.photoDeltas.setRating(self.photos, 1) self.photos.reshow() elif key == QtCore.Qt.Key_2: self.photoDeltas.setRating(self.photos, 2) self.photos.reshow() elif key == QtCore.Qt.Key_3: self.photoDeltas.setRating(self.photos, 3) self.photos.reshow() elif key == QtCore.Qt.Key_4: self.photoDeltas.setRating(self.photos, 4) self.photos.reshow() elif key == QtCore.Qt.Key_5: self.photoDeltas.setRating(self.photos, 5) self.photos.reshow() elif key == QtCore.Qt.Key_D: self.photoDeltas.setDateError() self.photos.reshow() elif key == QtCore.Qt.Key_L: self.photoDeltas.setLocationError() self.photos.reshow()
class MainWindow(QMainWindow): def __init__(self, settings): super().__init__() # Save the settings object for now and shutdown time. self.settings = settings # Initialize extras and dicts paths first, as other modules use them paths.initialize(settings) # Initialize our font db fonts.initialize(settings) # Set our font, which will propogate to our child widgets. fonts.notify_me(self._font_change) # ask for a signal self._font_change(False) # fake a signal now # Initialize the dictionary apparatus dictionaries.initialize(settings) # Initialize the color choices colors.initialize(settings) # Initialize the sequence number for opened files self.book_number = 0 # Initialize the path to the last-opened file, used to # start file-open dialogs. self.last_open_path = '.' # Initialize our dict of active panels self.panel_dict = PANEL_DICT.copy() # Initialize our dict of open documents {seqno:Book} self.open_books = {} self.focus_book = None # seqno of book in focus, see _focus_me # Initialize the list of recent files self.recent_files = [] # Initialize the handle of a help display widget self.help_widget = None # later, if at all # Finished initializing after the app is running QTimer.singleShot(300, self.finish_init) # Create the main window and set up the menus. self._uic() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # As part of setup we often need to show the user a dialog, but when our # __init__ is first called, our window has not been shown and the # app.exec_() call has not been made. If ok_cancel_msg() is used in that # condition, there is a big delay and spinning cursor on the mac. So this # code is called from a one-shot timer 300ms after the window has been # created, so we are sure the app is processing events etc. def finish_init(self): #self.finish_init = False # never do this again # Initialize the set of files actually open when we shut down. last_session = self._read_flist('mainwindow/open_files') if len(last_session): # there were some files open if len(last_session) == 1: msg = _TR('Start-up dialog', 'One book was open at the end of the last session.') else: msg = _TR('Start-up dialog', '%n books were open at the end of the last session.', n=len(last_session)) info = _TR("Start-up dialog", "Click OK to re-open all") if utilities.ok_cancel_msg(msg, info, parent=self): for file_path in last_session: ftbs = utilities.path_to_stream(file_path) if ftbs: self._open(ftbs) if 0 == len(self.open_books): # We did not re-open any books, either because there were # none, or the user said No, or perhaps they were not found. self._new() # open one, new, book. # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Slot to receive the currentChanged signal from the editview tabset. # Look through self.open_books and find the one whose edit widget is # now current, and do a focus_me for it. def _editview_change(self, index): if index > -1: eview = self.editview_tabset.widget(index) for (seqno, book) in self.open_books.items(): if eview == book.get_edit_view(): self.focus_me(seqno) return mainwindow_logger.error('cannot relate editview tab index to book') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Make a selected book the focus of all panels. This is called explicitly # when a book is first created, and when the editview tabset changes the # current selection. It is called also when an editview gets a focus-in # event. # Display that Book's various "-view" objects in panels, in the order # that the user left them and with the same active panel as before. Note # that a book (editview) can get a focus-in event when it was already the # focus in this sense, for example if this app was hidden and then # brought to the front. So be prepared for redundant calls. def focus_me(self, book_index): outgoing = self.focus_book if book_index == outgoing: return # redundant call mainwindow_logger.debug('focusing {0} = {1}'.format( book_index, self.open_books[book_index].get_book_name())) self.focus_book = book_index # Record the user's arrangement of panels for the outgoing book, # as a list of tuples ('tabname', widget) in correct sequence. if outgoing is not None: # false first time and after File>Close out_panel_dict = self.open_books[outgoing].panel_dict widg_list = [] for ix in range(self.panel_tabset.count()): widg_list.append((self.panel_tabset.tabText(ix), self.panel_tabset.widget(ix))) out_panel_dict['tab_list'] = widg_list out_panel_dict['current'] = self.panel_tabset.currentIndex() # Change all the panels to the widgets, in the sequence, of the new book in_panel_dict = self.open_books[book_index].panel_dict widg_list = in_panel_dict['tab_list'] self.panel_tabset.clear() for ix in range(len(widg_list)): (tab_text, widget) = widg_list[ix] self.panel_tabset.insertTab(ix, widget, tab_text) self.panel_tabset.setCurrentIndex(in_panel_dict['current']) self.editview_tabset.setCurrentIndex( self.editview_tabset.indexOf( self.open_books[book_index].get_edit_view())) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Called by the current book to make a particular tab the visible one, # e.g. to make the Find visible on a ^F. The argument is a widget # that should occupy one of the current tabs. Ask the tabset for its # index, and if it is found, make that the current index. (If it is # not found, log it and do nothing.) def make_tab_visible(self, tabwidg): ix = self.panel_tabset.indexOf(tabwidg) if ix >= 0: # widget exists in this tabset self.panel_tabset.setCurrentIndex(ix) return mainwindow_logger.error('Request to show nonexistent widget') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>New: # Create a Book object # Call its new_empty() method, # Add it to the open_books dict keyed by its sequence number, # Display its text editor in a tab with the document name, and # Give it the focus. def _new(self): seq = self.book_number self.book_number += 1 new_book = book.Book(seq, self) new_book.new_empty() self.open_books[seq] = new_book index = self.editview_tabset.addTab(new_book.get_edit_view(), new_book.get_book_name()) self.editview_tabset.setTabToolTip( index, _TR('Tooltip of edit of new unsaved file', 'this file has not been saved')) self.focus_me(seq) # # For use from translators, do the New operation and return the Book # that is created so it can be loaded with translated text. def do_new(self): self._new() return self.open_books[self.focus_book] # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Quick check to see if a file path is already open. Called from _open # and from _build_recent (menu). Returned value is the sequence number # of the open book, or None. def _is_already_open(self, path): for (seq, book_object) in self.open_books.items(): if path == book_object.get_book_full_path(): return seq return None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>Open. Dialog with the user (file dialog starts with # last-used book path). Result is None or a FileBasedTextStream that we # pass to _open(). def _file_open(self): fbts = utilities.ask_existing_file(_TR('File:Open dialog', 'Select a book file to open'), parent=self, starting_path=self.last_open_path) if fbts: # yes a readable file was chosen. self._open(fbts) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Open a file, given the document as a FileBasedTextStream, and META # means our metafile suffix (C.METAFILE_SUFFIX). # * If file opened is fname.META, look for a file named fname; if it # exists open it instead, e.g. given foo.txt.META, open foo.txt. # If it doesn't exist, tell the user and exit. # * If a file of the same name and path is already open, just focus # it and exit. # * Determine if there is a .META file, a .bin file, or neither # * Create a metadata input stream if possible # * If no .META, look for good_words and bad_words # * If the only open book is an "Untitled-n" and # it is unmodified, delete it. # * Call Book.old_book() or .new_book() as appropriate # * Add this book's editview to the edit tabset # * Give this book the focus. def _open(self, fbts): # look for opening a .META file if C.METAFILE_SUFFIX == fbts.suffix(): fb2 = utilities.file_less_suffix(fbts) if fb2 is None: m1 = _TR('File:Open', 'Cannot open a metadata file alone') m2 = _TR('File:Open', 'There is no book file matching ', 'filename follows this') + fbts.filename() utilities.warning_msg(m1, m2, parent=self) return # we see foo.txt with foo.txt.META, silently open it fbts = fb2 # look for already-open file seq = self._is_already_open(fbts.fullpath()) if seq is not None: self.focus_me(seq) return # start collecting auxiliary streams gw_stream = None bw_stream = None #gg_stream = None # open the metadata stream, which is always UTF-8 meta_stream = utilities.related_suffix(fbts, C.METAFILE_SUFFIX, encoding=C.ENCODING_UTF) if meta_stream is None: # opening book without metadata; look for .bin which is always LTN1 # This is no longer supported - somebody who cares, can write a # .bin-to-JSON utility if they want. #bin_stream = utilities.related_suffix(fbts,'bin',encoding=C.ENCODING_LATIN) #if bin_stream : #gg_stream = metadata.translate_bin(bin_stream,fbts) # Look for good_words.txt, bad_words.txt. gw_stream = utilities.related_file(fbts, 'good_words*.*') bw_stream = utilities.related_file(fbts, 'bad_words*.*') seq = self.book_number # If the only open book is the new one created at startup or when all # books are closed (which will have key 0), and it has not been # modified, get rid of it. if len(self.open_books) == 1 \ and 0 == list(self.open_books.keys())[0] \ and self.open_books[0].get_book_name().startswith('Untitled-') \ and not self.open_books[0].get_save_needed() : self.editview_tabset.clear() self.panel_tabset.clear() self.focus_book = None seq = 0 else: # Some other book open, or user typed into the default New one. self.book_number += 1 # Make the Book object and stow it in our open book dict a_book = book.Book(seq, self) self.open_books[seq] = a_book if meta_stream: # opening a book we previously saved a_book.old_book(fbts, meta_stream) else: a_book.new_book(fbts, gw_stream, bw_stream) index = self.editview_tabset.addTab(a_book.get_edit_view(), a_book.get_book_name()) self.editview_tabset.setTabToolTip(index, a_book.get_book_folder()) self.focus_me(seq) self.last_open_path = fbts.folderpath() # start for next open or save self._add_to_recent(fbts.fullpath()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Save the book that is currently in focus under its present name, if it # is modified. Return True if the save completed, else False. # If the active book is a New one, force a Save-As action instead. def _save(self): active_book = self.open_books[self.focus_book] if active_book.get_save_needed(): if active_book.get_book_name().startswith('Untitled-'): return self._save_as() doc_stream = utilities.path_to_output( active_book.get_book_full_path()) if doc_stream: # successfully opened for output meta_stream = utilities.related_output(doc_stream, C.METAFILE_SUFFIX) if not meta_stream: utilities.warning_msg( _TR('File:Save', 'Unable to open metadata file for writing.'), _TR('File:Save', 'Use loglevel=error for details.'), parent=self) return False else: utilities.warning_msg(_TR( 'File:Save', 'Unable to open book file for writing.'), _TR('File:Save', 'Use loglevel=error for details.'), parent=self) return False return active_book.save_book(doc_stream, meta_stream) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Save As. Query the user for a file path and get that as an # output FileBasedTextStream. Call the book to rename itself, which makes # it modified. Change the text in the edit tab to match. Discard the FBTS # and call _save which will make another one. def _save_as(self): active_book = self.open_books[self.focus_book] fbts = utilities.ask_saving_file( _TR('File:Save As dialog', 'Choose a new location and filename for this book'), self, active_book.get_book_folder()) if fbts: active_book.rename_book(fbts) self.editview_tabset.setTabText( self.editview_tabset.currentIndex(), fbts.filename()) self.editview_tabset.setTabToolTip( self.editview_tabset.currentIndex(), active_book.get_book_folder()) self._add_to_recent(fbts.fullpath()) fbts = None # discard that object return self._save() else: return False # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Close. If the active book is modified, ask if it should # be saved. If it is 'Untitled-' that will turn into Save As. def _close(self): target_index = self.focus_book # active edit tab is to close target_book = self.open_books[target_index] if target_book.get_save_needed(): # Compose message of translated parts because _TR does not # allow for incorporating strings, only numbers. msg = _TR('File Close dialog', 'Book file ', 'filename follows here') msg += target_book.get_book_name() msg += _TR('File Close dialog', ' has been modified!', 'filename precedes this') ret = utilities.save_discard_cancel_msg( msg, info=_TR('File Close dialog', 'Save it, Discard changes, or Cancel Closing?'), parent=self) if ret is None: # Cancel return if ret: # True==Save self._save() # Now, get rid of the active book in 3 steps, # 1, close the book's tab in the editview tabset. We don't know which # tab it is, because the user can drag tabs around. i = self.editview_tabset.indexOf(target_book.get_edit_view()) # The following causes another tab to be focussed, changing self.focus_book # and saving target_book's tabs in target_book, not that we care. self.editview_tabset.removeTab(i) # 2, remove the book from our dict of open books. del self.open_books[target_index] # 3, if there are any open books remaining, the tab widget has # activated one of them by its rules, which caused a show signal and # entry to _focus_me already. However if there are no remaining books # there was no show signal or focus_me and the closed book's panels # are still in the tabset. if 0 == len(self.open_books): self.book_number = 0 # restart the sequence self.focus_book = None self._new() # One way or the other, a focus_me has removed all references to # active_book's view panels except those in its PANEL_DICT. So the # following assignment should remove the last reference to the book, # and schedule the book and associated objects for garbage collect. target_book = None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement a Translate... submenu command. Call translators.xlt_book # with the book object that is currently in focus. If the process works # xlt_book will create a new book by calling do_new() and load it. # def _xlt_a_book(self): book = self.open_books[self.focus_book] datum = self.sender().data() translators.xlt_book(book, datum, self) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement loading and saving find panel user buttons. Start the search # for files in the active book's folder. User can navigate to extras # if need be. def _find_save(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_saving_file( _TR('File:Save Find Buttons open dialog', 'Choose file to contain find button definitions'), self, starting_path=target_book.get_last_find_button_path(), encoding='UTF-8') if stream: # is not None, file is open target_book.set_last_find_button_path(stream.fullpath()) find_panel.user_button_output(stream) # else user hit cancel, forget it def _find_load(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_existing_file( _TR('File:Load Find Buttons open dialog', 'Choose a file of find button definitions'), self, starting_path=target_book.get_last_find_button_path(), encoding='UTF-8') if stream: # is not None, we opened it target_book.set_last_find_button_path(stream.fullpath()) find_panel.user_button_input(stream) target_book.metadata_modified(True, C.MD_MOD_FLAG) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Maintain the list of "recent" file paths. The list is kept in usage # order, so if a path is in the list now, delete it and then add it to # the front. Keep it at a max of 9 items by deleting the oldest if # necessary. def _add_to_recent(self, path): if path in self.recent_files: del self.recent_files[self.recent_files.index(path)] self.recent_files.insert(0, path) self.recent_files = self.recent_files[:9] # Upon the aboutToShow signal from the File menu, populate the Recent # submenu with a list of files, but only the ones that are currently # accessible. If one is on a volume (e.g. USB stick) and you unmount the # volume, the path should not appear in the menu until the volume is # mounted again. def _open_recent(self): path = self.sender().data() fbts = utilities.path_to_stream(path) if fbts: self._open(fbts) def _build_recent(self): active_files = [] self.recent_menu.clear() self.recent_menu.setEnabled(False) for path in self.recent_files: seq = self._is_already_open(path) if (seq is None) and utilities.file_is_accessible(path): active_files.append(path) if 0 == len(active_files): return self.recent_menu.setEnabled(True) for (i, path) in enumerate(active_files, start=1): (folder, fname) = os.path.split(path) act = self.recent_menu.addAction('{0} {1} {2}'.format( i, fname, folder)) act.setData(path) act.triggered.connect(self._open_recent) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # User has chosen a different font; if it is the general font, set # that here so it will propogate to our children. n.b. this is never # used as the preference for UI font is not implemented. def _font_change(self, is_mono): if not is_mono: self.setFont(fonts.get_general()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Preferences menu action triggered. Create a Preferences dialog and # show it. def _preferences(self): p = preferences.PreferenceDialog(self) r = p.exec_() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Help menu action triggered. If the Help widget has not yet been # created, create it. Otherwise just show it and raise it. def _show_help(self): if self.help_widget is None: self.help_widget = helpview.HelpWidget() self.help_widget.show() self.help_widget.raise_() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Create the UI contained within this QMainWindow object. This is a lean # main window indeed. We have no toolbar, no status bar, no dock, # nothing. Just a splitter with, on the left, a tabset for editviews, and # on the right, a scrollbar containing a tabset for panels. (Qt Designer # note: it is not possible to build this structure with the Designer. It # will not let you put the scroll area into the splitter.) # # TODO: create a custom QTabWidget using a custom QTabBar to implement # drag-out-of-tabset behavior, and use those here. def _uic(self): global _EDIT_MENU # Create the tabset that displays editviews self.editview_tabset = QTabWidget() self.editview_tabset.setMovable(True) # let user move tabs around self.editview_tabset.currentChanged.connect(self._editview_change) # Create the tabset that displays find, notes, help &etc. self.panel_tabset = QTabWidget() self.panel_tabset.setMovable(True) # Create the splitter that contains the above two parts. self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.setChildrenCollapsible(False) # Give just a little margin to the left of the editor self.splitter.setContentsMargins(8, 0, 0, 0) self.splitter.addWidget(self.editview_tabset) self.splitter.addWidget(self.panel_tabset) # Set that splitter as the main window's central (and only) widget self.setCentralWidget(self.splitter) # Populate the panel tabset with empty widgets just so there will # be tabs that _swap can reference. for key in self.panel_dict.keys(): widj = QWidget() self.panel_tabset.addTab(widj, key) self.panel_dict[key] = widj # Size and position ourself based on saved settings. self.move(self.settings.value("mainwindow/position", QPoint(50, 50))) self.resize( self.settings.value("mainwindow/size", C.STARTUP_DEFAULT_SIZE)) self.splitter.restoreState( self.settings.value("mainwindow/splitter", C.STARTUP_DEFAULT_SPLITTER)) self.restoreState( self.settings.value("mainwindow/windowstate", QByteArray())) # Store a reference to the application menubar. In Mac OS this # is a parentless menubar; other platforms it is the default. if C.PLATFORM_IS_MAC: self.menu_bar = QMenuBar() # parentless menu bar for Mac OS else: self.menu_bar = self.menuBar() # refer to the default one # Create the File menu, located in our menu_bar. self.file_menu = self.menu_bar.addMenu(_TR('Menu name', '&File')) # Populate the File menu with actions. # File:New -> _new() work = self.file_menu.addAction(_TR('File menu command', '&New')) work.setShortcut(QKeySequence.New) work.setToolTip(_TR('File:New tooltip', 'Create a new, empty document')) work.triggered.connect(self._new) # File:Open -> _file_open() work = self.file_menu.addAction(_TR('File menu command', '&Open')) work.setShortcut(QKeySequence.Open) work.setToolTip(_TR('File:Open tooltip', 'Open an existing book')) work.triggered.connect(self._file_open) # File:Save -> _file_save() work = self.file_menu.addAction(_TR('File menu command', '&Save')) work.setShortcut(QKeySequence.Save) work.setToolTip(_TR('File:Save tooltip', 'Save the active book')) work.triggered.connect(self._save) # Save As -> _file_save_as() work = self.file_menu.addAction(_TR('File menu command', 'Save &As')) work.setShortcut(QKeySequence.SaveAs) work.setToolTip( _TR('File:Save As tooltip', 'Save the active book under a new name')) work.triggered.connect(self._save_as) # Close -> _close() work = self.file_menu.addAction(_TR('File menu command', 'Close')) work.setShortcut(QKeySequence.Close) work.setToolTip(_TR('File:Close tooltip', 'Close the active book')) work.triggered.connect(self._close) # Load Find Buttons -> _find_load() work = self.file_menu.addAction( _TR('File menu command', 'Load Find Buttons')) work.setToolTip( _TR( 'File:Load Find Buttons tooltip', 'Load a file of definitions for the custom buttons in the Find panel' )) work.triggered.connect(self._find_load) # Save Find Buttons -> _find_save() work = self.file_menu.addAction( _TR('File menu command', 'Save Find Buttons')) work.setToolTip( _TR('File:Save Find Buttons tooltip', 'Save definitions of the custom buttons in the Find panel')) work.triggered.connect(self._find_save) # Translate... gets a submenu with an entry for every Translator # in extras/Translators (if any). The actions connect to _xlt_a_book. self.translate_submenu = translators.build_xlt_menu( self, self._xlt_a_book) self.file_menu.addMenu(self.translate_submenu) # Open Recent gets a submenu that is added to the File menu. # The aboutToShow signal is connected to our _build_recent slot. self.recent_menu = QMenu(_TR('Sub-menu name', '&Recent Files')) work = self.file_menu.addMenu(self.recent_menu) work.setToolTip( _TR('File:Recent tooltip', 'List of recently-used files to open')) self.file_menu.aboutToShow.connect(self._build_recent) # Put in a divider above the Help, Preferences and Quit actions. self.file_menu.addSeparator() # Help opens or un-hides the Help viewer work = self.file_menu.addAction(_TR('Help menu item', 'Help')) work.setToolTip( _TR('Help menu item tooltip', 'Display the Help/User Manual in a separate window')) work.triggered.connect(self._show_help) self.file_menu.addAction(work) # Preferences: On the Mac, Preferences is automatically moved to the app menu. work = self.file_menu.addAction( _TR('Preferences menu item', 'Preferences')) work.setToolTip( _TR( 'Preferences menu item tooltip', 'Open the Preferences dialog to set paths, fonts, and text styles' )) work.setMenuRole(QAction.PreferencesRole) work.triggered.connect(self._preferences) # Quit choice, with the menu role that moves it to the app menu work = QAction(_TR('Quit command', '&Quit'), self) work.setMenuRole(QAction.QuitRole) work.setShortcut(QKeySequence.Quit) work.triggered.connect(self.close) self.file_menu.addAction(work) # Initialize the list of "recent" files for the File sub-menu. # These files were not necessarily open at shutdown, just sometime # in the not too distant past. self.recent_files = self._read_flist('mainwindow/recent_files') # Create the Edit menu in the menu_bar, store a reference to it # in a static global, and immediately clear it. _EDIT_MENU = self.menu_bar.addMenu(C.ED_MENU_EDIT) hide_edit_menu() # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Functions related to shutdown and management of settings. # # Factor out the job of reading/writing a list of files in the settings. # Input is a settings array key string like 'mainwindow/recent_files' # Output is a possibly empty list of canonical-file-path strings. def _read_flist(self, array_key): f_list = [] f_count = self.settings.beginReadArray(array_key) for f in range(f_count): # which may be 0 self.settings.setArrayIndex(f) f_list.append(self.settings.value('filepath')) self.settings.endArray() return f_list # Input is an array key and a possibly empty list of path strings def _write_flist(self, file_list, array_key): if len(file_list): self.settings.beginWriteArray(array_key, len(file_list)) for f in range(len(file_list)): self.settings.setArrayIndex(f) self.settings.setValue('filepath', file_list[f]) self.settings.endArray() # Reimplement QWidget.closeEvent in order to save any open files # and update the settings. def closeEvent(self, event): # If there are any unsaved books, ask the user if they should be # saved. If the answer is yes, try to do so. unsaved = [] for (seq, book_object) in self.open_books.items(): if book_object.get_save_needed(): unsaved.append(seq) if len(unsaved): if len(unsaved) == 1: msg = _TR('Shutdown message', 'There is one unsaved file') else: msg = _TR('Shutdown message', 'There are %n unsaved files', n=len(unsaved)) ret = utilities.save_discard_cancel_msg( msg, _TR('Shutdown message', 'Save, Discard changes, or Cancel Quit?'), parent=self) if ret is None: # user wants to cancel shutdown event.ignore() return if ret: # User want to save. Focus each unsaved file and call _save. # For all but "Untitled-n" documents this will be silent. For # those, it will open a save-as dialog. We ignore the return # from this because we cannot distinguish between a cancelled # file-open dialog and a file write error. for seq in unsaved: self.focus_me(seq) self._save() # Clear the settings so that old values don't hang around self.settings.clear() # Tell the submodules to save their current global values. colors.shutdown(self.settings) fonts.shutdown(self.settings) dictionaries.shutdown(self.settings) paths.shutdown(self.settings) # Save the list of currently-open files in the settings, but do not # save any whose filename matches "Untitled-#" because that is an # unsaved New file (which the user chose not to save, above). open_paths = [] for (index, book_obj) in self.open_books.items(): if not book_obj.get_book_name().startswith('Untitled-'): open_paths.append(book_obj.get_book_full_path()) self._write_flist(open_paths, 'mainwindow/open_files') # Save the list of "recent" files in the settings. self._write_flist(self.recent_files, 'mainwindow/recent_files') # Save this window's position and size and splitter state self.settings.setValue("mainwindow/size", self.size()) self.settings.setValue("mainwindow/position", self.pos()) self.settings.setValue("mainwindow/splitter", self.splitter.saveState()) self.settings.setValue("mainwindow/windowstate", self.saveState()) # If the Help window is open, close it -- as a modeless widget, # it won't close just because we do. if self.help_widget: # is not None, self.help_widget.close() # and that's it, we are done finished, over & out. event.accept()
class MainWindow(QMainWindow): KEY_WINDOW_SIZE = 'main_window/size' KEY_WINDOW_MAXIMIZED = 'main_window/maximized' KEY_WINDOW_POSITION = 'main_window/position' KEY_H_SPLITTER_STATE = 'main_window/h_splitter_state' def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('Mojuru') app_icon = QIcon('images/mojuru_logo.png') self.setWindowIcon(app_icon) reload_modules_action = QAction('Reload MainWindow', self) reload_modules_action.setShortcut('ctrl+shift+alt+r') reload_modules_action.triggered.connect(self.reload_central_widget) self.addAction(reload_modules_action) quit_action = QAction('Quit', self) quit_action.setShortcut('ctrl+q') quit_action.triggered.connect(self.on_quit) self.addAction(quit_action) self.vertical_splitter = QSplitter(Qt.Vertical, self) self.setCentralWidget(self.vertical_splitter) self.load_central_widget() self.file_menu = self.menuBar().addMenu(self.tr('&File')) self.file_menu.addAction(quit_action) self.file_menu.addSeparator() self.module_menu = self.menuBar().addMenu(self.tr('&Modules')) self.module_menu.addAction(reload_modules_action) self.module_menu.addSeparator() Alter.invoke_all('main_window_init', self) #restore main window state size = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_SIZE, QSize(600, 400)) maximized = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_MAXIMIZED, False) position = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_POSITION, QPoint(0, 0)) if maximized == 'true': self.showMaximized() else: self.resize(size) self.move(position) def closeEvent(self, event): self.on_quit() def save_state(self): ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_SIZE, self.size()) ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_MAXIMIZED, self.isMaximized()) ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_POSITION, self.pos()) ModuleManager.core['settings'].Settings.set_value( self.KEY_H_SPLITTER_STATE, self.horizontal_splitter.saveState()) def on_quit(self): self.save_state() self.close() def load_central_widget(self): self.populate_central_widget() self.connect_widgets() def populate_central_widget(self): self.vertical_widgets = collections.OrderedDict() self.horizontal_splitter = QSplitter(Qt.Horizontal, self.vertical_splitter) self.horizontal_widgets = collections.OrderedDict() self.vertical_widgets["horizontal_splitter"] = self.horizontal_splitter Alter.invoke_all('main_window_add_vertical_widget', self.vertical_widgets, self) for widget in self.vertical_widgets.values(): self.vertical_splitter.addWidget(widget) Alter.invoke_all('main_window_add_horizontal_widget', self.horizontal_widgets, self.vertical_splitter) for widget in self.horizontal_widgets.values(): self.horizontal_splitter.addWidget(widget) #restore horizontal splitter state state = ModuleManager.core['settings'].Settings.value( self.KEY_H_SPLITTER_STATE, None) if state: self.horizontal_splitter.restoreState(state) def connect_widgets(self): Alter.invoke_all('main_window_connect_widgets', self.vertical_widgets, self.horizontal_widgets) def reload_central_widget(self): self.save_state() for index in range(self.vertical_splitter.count()): widget = self.vertical_splitter.widget(index) widget.hide() widget.setParent(None) del widget Alter.clear() ModuleManager.reload_all_modules('core') ModuleManager.reload_all_modules('custom') self.load_central_widget()
class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.groupsList = QListWidget() self.listWidget.itemDoubleClicked.connect(self.grouplistshowItem) self.editor = QTextEdit() self.messageView = QTextBrowser() self.messageSplitter = QSplitter(Qt.Vertical) self.messageSplitter.addWidget(self.editor) self.messageSplitter.addWidget(self.messageView) self.mainSplitter = QSplitter(Qt.Horizontal) self.mainSplitter.addWidget(self.groupsList) self.mainSplitter.addWidget(self.messageSplitter) self.setCentralWidget(self.mainSplitter) self.mainSplitter.setStretchFactor(0, 1) self.mainSplitter.setStretchFactor(1, 3) self.messageSplitter.setStretchFactor(0, 1) self.messageSplitter.setStretchFactor(1, 2) self.createMenusAndToolbars() settings = QSettings() if settings.value("MainWindow/Geometry") or \ settings.value("MainWindow/State") or \ settings.value("MainSplitter"): self.restoreGeometry( QByteArray(settings.value("MainWindow/Geometry"))) self.restoreState( QByteArray(settings.value("MainWindow/State"))) self.messageSplitter.restoreState( QByteArray(settings.value("MessageSplitter"))) self.mainSplitter.restoreState( QByteArray(settings.value("MainSplitter"))) status = self.statusBar() status.setSizeGripEnabled(False) status.showMessage("Ready", 5000) self.setWindowTitle("News Reader") self.generateFakeData() def createMenusAndToolbars(self): menubar = self.menuBar() fileMenu = menubar.addMenu('File') fileMenu.addAction("new...",self.newFile, "Ctrl+N") fileMenu.addAction("open...",self.openfile,"Ctrl+O") fileMenu.addAction("save...",self.savefile,"Ctrl+S") fileMenu.addAction("save_as...",self.save_asfile,"Ctrl+Shift+S") fileMenu.addAction("E&xit", QApplication.instance().quit, "Ctrl+Q") fileMenu2 = menubar.addMenu('Settings') fileMenu2.addAction("Preference", self.setting, "Ctrl+T") app.setStyleSheet('menuBar::item{spacing:100px;margin:100px;}') screen = QDesktopWidget().screenGeometry() self.setGeometry(50, 50, screen.width() - 100, screen.height() - 100) def newFile(self): self.editor.clear() login = Login() login.show() login.exec_() def grouplistshowItem(self): self.databasename=split(self.currentItem().text(),".")[0] self.tablename=split(self.currentItem().text(),".")[1] # add file location def openfile(self,path=None): if not path: path, _ = QFileDialog.getOpenFileName(self, "Open File", '', "SQL Files (*.sql,*.*)") if path: inFile = QFile(path) if inFile.open(QFile.ReadOnly | QFile.Text): text = inFile.readAll() try: # Python v3. text = str(text, encoding='ascii') except TypeError: # Python v2. text = str(text) self.editor.setPlainText(text) def savefile(self): ... def save_asfile(self): ... def setting(self): ... def closeEvent(self, event): if self.okToContinue(): settings = QSettings() settings.setValue("MainWindow/Geometry", self.saveGeometry()) settings.setValue("MainWindow/State", self.saveState()) settings.setValue("MessageSplitter", self.messageSplitter.saveState()) settings.setValue("MainSplitter", self.mainSplitter.saveState()) else: event.ignore() def okToContinue(self): return True def generateFakeData(self): for group in ("ada", "apl", "asm.*", "asm370", "awk", "basic.*", "beta", "c.*", "c++.*", "clarion", "clipper.*", "clos", "clu", "cobol", "dylan", "eiffel", "forth.*", "fortran.*", "functional", "haskell", "hermes", "icon", "idl", "idl-pvwave"): self.groupsList.addItem("comp.lang.{0}".format(group)) for topic, author in ( ): self.editor.addItem("{0} from {1}".format(topic, author))
class IPWaveformWidget(QWidget): """ The IPWaveformWidget holds the waveform and inventory data. The IPPlotViewer plots the data, The IPFilterSettingsWidget holds the filter settings and tells the WaveformWidget when to update that data. The IPStatsView displays the trace data, and the IPStationView displays the station data. """ _sts = None # streams _sts_filtered = None # filtered streams _inv = None # inventory def __init__(self, parent=None, pool=None, project=None): super().__init__(parent) self._parent = parent self.settings = parent.settings self._mp_pool = pool self.buildUI() def buildUI(self): self.stationViewer = IPStationView.IPStationView(self) self.statsViewer = IPStatsView.IPStatsView(self) self.info_tabs = QTabWidget() self.info_tabs.addTab(self.statsViewer, 'Trace Info') self.info_tabs.addTab(self.stationViewer, 'Station Info') self.filterSettingsWidget = IPFilterSettingsWidget.IPFilterSettingsWidget(self) self.spectraWidget = IPPSDWidget.IPPSDWidget(self) self.plotViewer = IPPlotViewer.IPPlotViewer(self, self.filterSettingsWidget) self.lh_splitter = QSplitter(Qt.Vertical) self.lh_splitter.setStyleSheet("QSplitter::handle{ background-color: #DDD}") self.lh_splitter.addWidget(self.plotViewer) self.lh_splitter.addWidget(self.info_tabs) self.rh_splitter = QSplitter(Qt.Vertical) self.rh_splitter.setStyleSheet("QSplitter::handle{ background-color: #DDD}") self.rh_splitter.addWidget(self.spectraWidget) self.rh_splitter.addWidget(self.filterSettingsWidget) self.main_splitter = QSplitter(Qt.Horizontal) self.main_splitter.setStyleSheet("QSplitter::handle{ background-color: #DDD}") self.main_splitter.addWidget(self.lh_splitter) self.main_splitter.addWidget(self.rh_splitter) main_layout = QGridLayout() main_layout.addWidget(self.main_splitter) self.setLayout(main_layout) self.connect_signals_and_slots() def connect_signals_and_slots(self): self.filterSettingsWidget.sig_filter_changed.connect(self.update_filtered_data) self.filterSettingsWidget.sig_filter_display_changed.connect(self.plotViewer.show_hide_lines) self.statsViewer.removeTrace.connect(self.remove_trace) self.plotViewer.lr_settings_widget.noiseSpinsChanged.connect(self._parent.beamformingWidget.bottomSettings.setNoiseValues) self.plotViewer.lr_settings_widget.signalSpinsChanged.connect(self._parent.beamformingWidget.bottomSettings.setSignalValues) self.plotViewer.lr_settings_widget.signalSpinsChanged.connect(self._parent.beamformingWidget.updateWaveformRange) self.plotViewer.pl_widget.sig_active_plot_changed.connect(self.update_widgets) self.spectraWidget.f1_Spin.valueChanged.connect(self._parent.beamformingWidget.bottomSettings.setFmin) self.spectraWidget.f2_Spin.valueChanged.connect(self._parent.beamformingWidget.bottomSettings.setFmax) self.spectraWidget.psdPlot.getFreqRegion().sigRegionChanged.connect(self._parent.beamformingWidget.bottomSettings.setFreqValues) def get_project(self): return self._parent.getProject() def errorPopup(self, message, title="Oops..."): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText(message) msgBox.setWindowTitle(title) msgBox.exec_() @pyqtSlot(obspy.core.stream.Stream, obspy.core.inventory.inventory.Inventory) def appendTraces(self, newTraces, newInventory): if newTraces is None: return if self._sts is None: self._sts = newTraces else: self._sts += newTraces self.update_inventory(newInventory) for trace in self._sts: trace.data = trace.data - np.mean(trace.data) self._sts.merge(fill_value=0) # it's possible, if the open failed, that self.waveformWidget._sts is still None, so if it is, bail out # if not populate the trace stats viewer and plot the traces if self._sts is not None: # TODO...is there a better way of doing this? self._parent.beamformingWidget.setStreams(self._sts) self.stationViewer.setInventory(self._inv) self.statsViewer.setStats(self._sts) self.update_streams(self._sts) self._parent.setStatus("Ready", 5000) else: return @pyqtSlot(obspy.core.stream.Stream, obspy.core.inventory.inventory.Inventory) def replaceTraces(self, newTraces, newInventory): # same as append, just clear out the old traces and inventory first self._sts = None self._inv = None self.stationViewer.setInventory(self._inv) self.appendTraces(newTraces, newInventory) @pyqtSlot(Inventory) def update_inventory(self, new_inventory): if self._inv is None: self._inv = new_inventory else: self._inv += new_inventory self.stationViewer.setInventory(self._inv) def remove_from_inventory(self, net, sta, loc, cha): self.inv_remove(self._inv, network=net, station=sta, location=loc, channel=cha, keep_empty=False) self.update_inventory(new_inventory) def get_streams(self): return self._sts def get_filtered_streams(self): return self._sts_filtered def get_inventory(self): return self._inv def set_inventory(self, new_inv): self._inv = new_inv def getTraceName(self, trace): traceName = trace.stats['network'] + '.' + trace.stats['station'] + \ '.' + trace.stats['location'] + '.' + trace.stats['channel'] return traceName def get_earliest_start_time(self): return self.plotViewer.pl_widget.earliest_start_time @pyqtSlot(Stream) def update_streams(self, new_stream): # this should be called when you load new streams, or remove traces self._sts = new_stream self._sts_filtered = self.filter_stream(self._sts, self.filterSettingsWidget.get_filter_settings()) self.plotViewer.set_streams(self._sts, self._sts_filtered, self.filterSettingsWidget.get_filter_display_settings()) self.statsViewer.setStats(new_stream) def debug_trace(self): # for debugging, you have to call pyqtRemoveInputHook before set_trace() from PyQt5.QtCore import pyqtRemoveInputHook from pdb import set_trace pyqtRemoveInputHook() set_trace() @pyqtSlot(dict) def update_filtered_data(self, filter_settings): # this should be called when settings in the filter widget are changed if self._sts is None: # Nothing to filter, clear out the filtered_streams and return self._sts_filtered = None return self._sts_filtered = self.filter_stream(self._sts, filter_settings) self.plotViewer.pl_widget.update_filtered_line_data(self._sts_filtered) index = self.plotViewer.pl_widget.get_active_plot() self.update_widgets(index, self.plotViewer.get_plot_lines(), self.plotViewer.get_filtered_plot_lines(), self.plotViewer.pl_widget.plot_list[index].getSignalRegionRange()) def filter_stream(self, stream, cfs): # cfs: Current Filter Settings if stream is None: # nothing to do return None filtered_stream = Stream() for trace in stream: filtered_trace = trace.copy() filtType = cfs['type'] if filtType == 'High Pass': try: filtered_trace.filter('highpass', freq=cfs['F_high'], corners=cfs['order'], zerophase=cfs['zphase']) except ValueError as e: self.errorPopup(str(e)) elif filtType == 'Low Pass': try: filtered_trace.filter('lowpass', freq=cfs['F_low'], corners=cfs['order'], zerophase=cfs['zphase']) except ValueError as e: self.errorPopup(str(e)) elif filtType == 'Band Pass': try: filtered_trace.filter('bandpass', freqmin=cfs['F_high'], freqmax=cfs['F_low'], corners=cfs['order'], zerophase=cfs['zphase']) except ValueError as e: self.errorPopup(str(e)) else: self.errorPopup(filtType + ' filter not implemented yet') return filtered_stream += filtered_trace return filtered_stream def saveWindowGeometrySettings(self): self._parent.settings.beginGroup('WaveformWidget') self._parent.settings.setValue("main_splitterSettings", self.main_splitter.saveState()) self._parent.settings.setValue("rh_splitterSettings", self.rh_splitter.saveState()) self._parent.settings.setValue("lh_splitterSettings", self.lh_splitter.saveState()) self._parent.settings.setValue("plotviewer_splitterSettings", self.plotViewer.saveState()) self._parent.settings.endGroup() def restoreWindowGeometrySettings(self): # Restore settings self._parent.settings.beginGroup('WaveformWidget') main_splitterSettings = self._parent.settings.value("main_splitterSettings") if main_splitterSettings: self.main_splitter.restoreState(main_splitterSettings) rh_splitterSettings = self._parent.settings.value("rh_splitterSettings") if rh_splitterSettings: self.rh_splitter.restoreState(rh_splitterSettings) lh_splitterSettings = self._parent.settings.value("lh_splitterSettings") if lh_splitterSettings: self.lh_splitter.restoreState(lh_splitterSettings) pv_splitterSettings = self._parent.settings.value("plotviewer_splitterSettings") if pv_splitterSettings: self.plotViewer.restoreState(pv_splitterSettings) else: pv_width = self.plotViewer.width() wsw = pv_width//6 pww = pv_width - wsw self.plotViewer.setSizes([wsw, pww]) self._parent.settings.endGroup() @QtCore.pyqtSlot(str) def remove_trace(self, trace_id): for trace in self._sts.select(id=trace_id): self._sts.remove(trace) self.removeStation(trace.stats['network'], trace.stats['station']) self.statsViewer.setStats(self._sts) if len(self._sts) == 0: self._sts = None self.update_streams(self._sts) def removeStation(self, net_id, station_id): if self._inv is not None: try: self._inv = self.inv_remove(self._inv, network=net_id, station=station_id) except AttributeError as e: print(e) self.stationViewer.setInventory(self._inv) def inv_remove(self, _inventory, network='*', station='*', location='*', channel='*', keep_empty=False): selected = _inventory.select(network=network, station=station, location=location, channel=channel) selected_networks = [net for net in selected] selected_stations = [sta for net in selected_networks for sta in net] selected_channels = [cha for net in selected_networks for sta in net for cha in sta] networks = [] for net in _inventory: if net in selected_networks and station == '*' and \ location == '*' and channel == '*': continue stations = [] for sta in net: if sta in selected_stations and location == '*' and channel == '*': continue channels = [] for cha in sta: if cha in selected_channels: continue channels.append(cha) if not channels and not keep_empty: continue sta = copy.copy(sta) sta.channels = channels stations.append(sta) if not stations and not keep_empty: continue net = copy.copy(net) net.stations = stations networks.append(net) return obspy.core.inventory.inventory.Inventory(networks, 'source') def clearWaveforms(self): # empty out the streams self._sts = None self._sts_filtered = None # empty out the child widgets self.statsViewer.clear() self.stationViewer.clear() self.plotViewer.clear() self.spectraWidget.clearPlot() @pyqtSlot(object) def update_signal_PSD(self, signal_region_item): if len(self._sts) == 0: self.spectraWidget.clearPlot() return signal_region = signal_region_item.getRegion() active_plot = self.plotViewer.pl_widget.get_active_plot() # calculate the PSD of the ---------------------------tart and finish dt = self._sts[active_plot].stats.delta start = int(signal_region[0] / dt) stop = int(signal_region[1] / dt) self.spectraWidget.updateSignalPSD(self._sts[active_plot][start:stop]) @pyqtSlot(object) def update_noise_PSD(self, noise_region_item): if len(self._sts) == 0: self.spectraWidget.clearPlot() return noise_region = noise_region_item.getRegion() active_plot = self.plotViewer.pl_widget.get_active_plot() # calculate the PSD of the data in the current noise region dt = self._sts[active_plot].stats.delta start = int(noise_region[0] / dt) stop = int(noise_region[1] / dt) self.spectraWidget.updateNoisePSD(self._sts[active_plot][start:stop]) @pyqtSlot(int, list, list, tuple) def update_widgets(self, index, lines, filtered_lines, signal_region): # the -1 is sent if none of the plots are visible if len(self._sts) < 1 or index == -1: self.spectraWidget.set_title('...') self.spectraWidget.clearPlot() else: self.spectraWidget.set_title(self._sts[index].id) self.spectraWidget.set_fs(self._sts[index].stats.sampling_rate) # self.spectraWidget.updateSignalPSD() # self.spectraWidget.updateNoiesPSD() noise_region_item = self.plotViewer.pl_widget.plot_list[index].getNoiseRegion() noise_region_item.sigRegionChanged.emit(noise_region_item) signal_region_item = self.plotViewer.pl_widget.plot_list[index].getSignalRegion() signal_region_item.sigRegionChanged.emit(signal_region_item) current_filter_display_settings = self.filterSettingsWidget.get_filter_display_settings() if current_filter_display_settings['apply']: self._parent.beamformingWidget.setWaveform(filtered_lines[index], signal_region) else: self._parent.beamformingWidget.setWaveform(lines[index], signal_region)
def restoreSplitter(widget: QSplitter, key: str) -> None: key += "Splitter" if aqt.mw.pm.profile.get(key): widget.restoreState(aqt.mw.pm.profile[key])
class MainWindow(QMainWindow): KEY_WINDOW_SIZE = 'main_window/size' KEY_WINDOW_MAXIMIZED = 'main_window/maximized' KEY_WINDOW_POSITION = 'main_window/position' KEY_H_SPLITTER_STATE = 'main_window/h_splitter_state' def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('Mojuru') app_icon = QIcon('images/mojuru_logo.png') self.setWindowIcon(app_icon) reload_modules_action = QAction('Reload MainWindow', self) reload_modules_action.setShortcut('ctrl+shift+alt+r') reload_modules_action.triggered.connect(self.reload_central_widget) self.addAction(reload_modules_action) quit_action = QAction('Quit', self) quit_action.setShortcut('ctrl+q') quit_action.triggered.connect(self.on_quit) self.addAction(quit_action) self.vertical_splitter = QSplitter(Qt.Vertical, self) self.setCentralWidget(self.vertical_splitter) self.load_central_widget() self.file_menu = self.menuBar().addMenu(self.tr('&File')) self.file_menu.addAction(quit_action) self.file_menu.addSeparator() self.module_menu = self.menuBar().addMenu(self.tr('&Modules')) self.module_menu.addAction(reload_modules_action) self.module_menu.addSeparator() Alter.invoke_all('main_window_init', self) #restore main window state size = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_SIZE, QSize(600, 400)) maximized = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_MAXIMIZED, False) position = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_POSITION, QPoint(0,0)) if maximized == 'true': self.showMaximized() else: self.resize(size) self.move(position) def closeEvent(self, event): self.on_quit() def save_state(self): ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_SIZE, self.size()) ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_MAXIMIZED, self.isMaximized()) ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_POSITION, self.pos()) ModuleManager.core['settings'].Settings.set_value( self.KEY_H_SPLITTER_STATE, self.horizontal_splitter.saveState()) def on_quit(self): self.save_state() self.close() def load_central_widget(self): self.populate_central_widget() self.connect_widgets() def populate_central_widget(self): self.vertical_widgets = collections.OrderedDict() self.horizontal_splitter = QSplitter( Qt.Horizontal, self.vertical_splitter) self.horizontal_widgets = collections.OrderedDict() self.vertical_widgets["horizontal_splitter"] = self.horizontal_splitter Alter.invoke_all( 'main_window_add_vertical_widget', self.vertical_widgets, self ) for widget in self.vertical_widgets.values(): self.vertical_splitter.addWidget(widget) Alter.invoke_all( 'main_window_add_horizontal_widget', self.horizontal_widgets, self.vertical_splitter ) for widget in self.horizontal_widgets.values(): self.horizontal_splitter.addWidget(widget) #restore horizontal splitter state state = ModuleManager.core['settings'].Settings.value( self.KEY_H_SPLITTER_STATE, None ) if state: self.horizontal_splitter.restoreState(state) def connect_widgets(self): Alter.invoke_all( 'main_window_connect_widgets', self.vertical_widgets, self.horizontal_widgets ) def reload_central_widget(self): self.save_state() for index in range(self.vertical_splitter.count()): widget = self.vertical_splitter.widget(index) widget.hide() widget.setParent(None) del widget Alter.clear() ModuleManager.reload_all_modules('core') ModuleManager.reload_all_modules('custom') self.load_central_widget() def add_action(self, name, callback, **kwargs): """ Ajoute une action au context menu et au widget lui même. Créer une fonction à la volé pour fournir des arguments aux fonctions de rappel. """ action = QAction(name, self) if 'icon' in kwargs: action.setIcon(kwargs['icon']) if 'shortcut' in kwargs: action.setShortcut(kwargs['shortcut']) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.__wrapper(callback, **kwargs)) self.addAction(action) if 'menu' in kwargs: kwargs['menu'].addAction(action) def add_separator(self, menu): """Simple abstraction of self.context_menu.addSeparator()""" menu.addSeparator() def __wrapper(self, callback, **kwargs): def __new_function(): """ __new_function représente la forme de tous les callbacks connecté à une action pour pouvoir utiliser les raccourcis en même temps que le menu contextuel. """ args = [ kwargs['instance'] if 'instance' in kwargs else self ] callback(*args) return __new_function
class DatabaseContainer(QSplitter): def __init__(self, orientation=Qt.Horizontal): QSplitter.__init__(self, orientation) self.pfile = None self.lateral_widget = lateral_widget.LateralWidget() self.table_widget = table_widget.TableWidget() self.query_container = query_container.QueryContainer(self) self._vsplitter = QSplitter(Qt.Vertical) self._vsplitter.addWidget(self.table_widget) self._vsplitter.addWidget(self.query_container) self.addWidget(self.lateral_widget) self.addWidget(self._vsplitter) self.modified = False self.__nquery = 1 # Connections # FIXME self.lateral_widget.relationClicked.connect(self._on_relation_clicked) # lambda i: self.table_widget.stacked.setCurrentIndex(i)) # For change table widget item when up/down # see issue #39 self.lateral_widget.relationSelectionChanged.connect( lambda i: self.table_widget.stacked.setCurrentIndex(i)) self.query_container.saveEditor['PyQt_PyObject'].connect( self.save_query) self.setSizes([1, 1]) def _on_relation_clicked(self, index): if not self.table_widget._other_tab.isVisible(): self.table_widget._tabs.setCurrentIndex(0) self.table_widget.stacked.setCurrentIndex(index) def dbname(self): """ Return display name """ return self.pfile.display_name def is_new(self): return self.pfile.is_new def create_database(self, data): for table in data.get('tables'): # Get data table_name = table.get('name') header = table.get('header') tuples = table.get('tuples') # Creo el objeto Relation rela = relation.Relation() rela.header = header # Relleno el objeto con las tuplas for _tuple in tuples: rela.insert(_tuple) # Se usa el patrón Modelo/Vista/Delegado # Para entender más, leer el código de cáda módulo # src.gui.model # src.gui.view # src.gui.delegate _view = self.create_table(rela, table_name) # Add relation to relations dict self.table_widget.add_relation(table_name, rela) # Add table to stacked self.table_widget.stacked.addWidget(_view) # Add table name to list widget self.lateral_widget.relation_list.add_item(table_name, rela.cardinality(), rela.degree()) # Select first item # self.lateral_widget.relation_list.select_first() def create_table(self, relation_obj, relation_name, editable=True): """ Se crea la vista, el model y el delegado para @relation_obj """ _view = view.View() header = view.Header() _model = model.Model(relation_obj) _model.modelModified[bool].connect(self.__on_model_modified) _model.cardinalityChanged[int].connect(self.__on_cardinality_changed) if not editable: _model.editable = False header.editable = False _view.setModel(_model) _view.setItemDelegate(delegate.Delegate()) _view.setHorizontalHeader(header) return _view @pyqtSlot(bool) def __on_model_modified(self, modified): self.modified = modified @pyqtSlot(int) def __on_cardinality_changed(self, value): # self.lateral_widget.update_item(value) self.lateral_widget.relation_list.update_cardinality(value) def load_relation(self, filenames): for filename in filenames: with open(filename) as f: csv_reader = csv.reader(f) header = next(csv_reader) rel = relation.Relation() rel.header = header for i in csv_reader: rel.insert(i) relation_name = file_manager.get_basename(filename) if not self.table_widget.add_relation(relation_name, rel): QMessageBox.information( self, self.tr("Información"), self.tr("Ya existe una relación " "con el nombre " "'{}'".format(relation_name))) return False self.table_widget.add_table(rel, relation_name) # self.lateral_widget.add_item(relation_name, rel.cardinality()) return True def delete_relation(self): name = self.lateral_widget.relation_list.current_text() index = self.lateral_widget.relation_list.current_index() if not name: return msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle(self.tr("Confirmación")) msgbox.setText( self.tr( "Está seguro de eliminar la relación <b>{}</b>?".format(name))) msgbox.addButton(self.tr("No!"), QMessageBox.NoRole) si = msgbox.addButton(self.tr("Si, estoy seguro"), QMessageBox.YesRole) palette = QPalette() palette.setColor(QPalette.Button, QColor("#cc575d")) palette.setColor(QPalette.ButtonText, QColor("white")) si.setPalette(palette) msgbox.exec_() if msgbox.clickedButton() == si: self.lateral_widget.relation_list.remove_item(index) self.table_widget.remove_table(index) self.table_widget.remove_relation(name) return True return False def __on_data_table_changed(self, row, col, data): # current_relation = self.lateral_widget.current_text() # # Relation to be update # rela = self.table_widget.relations.get(current_relation) # # Clear old content # rela.clear() # current_table = self.table_widget.stacked.currentWidget() # model = current_table.model() # for i in range(model.rowCount()): # reg = [] # for j in range(model.columnCount()): # if row == i and col == j: # reg.append(data) # else: # reg.append(model.item(i, j).text()) # # Insert new content # rela.insert(reg) # # Update relation # self.table_widget.relations[current_relation] = rela pass def new_query(self, filename): editor_tab_at = self.query_container.is_open(filename) if editor_tab_at != -1: self.query_container.set_focus_editor_tab(editor_tab_at) else: query_widget = query_container.QueryWidget() # Create object file ffile = pfile.File(filename) editor = query_widget.get_editor() editor.pfile = ffile if not filename: ffile.filename = 'untitled_{n}.pqf'.format(n=self.__nquery) else: content = ffile.read() editor.setPlainText(content) self.query_container.add_tab(query_widget, ffile.display_name) self.__nquery += 1 def save_query(self, editor): if not editor: editor = self.query_container.currentWidget().get_editor() if editor.is_new: return self.save_query_as(editor) # Get content of editor content = editor.toPlainText() try: editor.pfile.save(data=content) except Exception as reason: QMessageBox.critical( self, "Error", self.tr("El archivo no se puede abrir!" "\n\n{}".format(reason))) return False editor.saved() return editor.pfile.filename def save_query_as(self, editor=None): filename = QFileDialog.getSaveFileName(self, self.tr("Guardar Archivo"), editor.name, "Pireal query files(*.pqf)") filename = filename[0] if not filename: return # Get the content content = editor.toPlainText() # Write the file editor.pfile.save(data=content, path=filename) editor.saved() def execute_queries(self): self.query_container.execute_queries() def execute_selection(self): editor = self.query_container.currentWidget().get_editor() text_cursor = editor.textCursor() if text_cursor.hasSelection(): query = text_cursor.selectedText() self.query_container.execute_queries(query) def showEvent(self, event): QSplitter.showEvent(self, event) qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat) vsizes = qsettings.value('vsplitter_sizes', None) if vsizes is not None: self._vsplitter.restoreState(vsizes) else: self._vsplitter.setSizes([self.height() / 3, self.height() / 6]) hsizes = qsettings.value('hsplitter_sizes', None) if hsizes is not None: self.restoreState(hsizes) else: self.setSizes([self.width() / 10, self.width() / 3]) def save_sizes(self): """ Save sizes of Splitters """ qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat) qsettings.setValue('vsplitter_sizes', self._vsplitter.saveState()) qsettings.setValue('hsplitter_sizes', self.saveState())
class MainWindow(QMainWindow): # class EventFilter(QObject): # def __init__(self, parent): # super().__init__(parent) # # def eventFilter(self, obj, e): # #print(obj.metaObject().className()) # # if e.type() == QEvent.KeyPress or e.type() == QEvent.ShortcutOverride: # key = e.key() # mod = e.modifiers() # # print(str(e) + ' ' + str(e.type()) ) # # if mod == Qt.AltModifier: # print('*'*30) # print('alt pressed') # if key == Qt.Key_Left or key == Qt.Key_Right: # print('alt-left') if key == Qt.Key_Left else print('alt-right') ## action = QAbstractItemView.MoveLeft if key == Qt.Key_Left else QAbstractItemView.MoveRight ## idx = obj.moveCursor(action, Qt.NoModifier) ## item = obj.itemFromIndex(idx) ## obj.setCurrentItem(item) # return True # # return False PROGRAM_NAME = 'KiCad Schematic Component Manager' #-------------------------------------------------------------------------------- class EventFilter(QObject): def __init__(self, parent): super().__init__(parent) def eventFilter(self, obj, e): if e.type() == QEvent.KeyPress or e.type( ) == QEvent.ShortcutOverride: key = e.key() mod = e.modifiers() #print(obj.focusWidget().metaObject().className()) return False #-------------------------------------------------------------------------------- def scroll_left(self): print('alt-left') if self.ToolIndex == 3 or self.ToolIndex == 2: self.ToolList[self.ToolIndex].finish_edit() self.ToolIndex -= 1 if self.ToolIndex < 0: self.ToolIndex = len(self.ToolList) - 1 print('Tool Index: ' + str(self.ToolIndex)) self.ToolList[self.ToolIndex].setFocus() #-------------------------------------------------------------------------------- def scroll_right(self): print('alt-right') if self.ToolIndex == 3 or self.ToolIndex == 2: self.ToolList[self.ToolIndex].finish_edit() self.ToolIndex += 1 if self.ToolIndex == len(self.ToolList): self.ToolIndex = 0 print('Tool Index: ' + str(self.ToolIndex)) self.ToolList[self.ToolIndex].setFocus() #-------------------------------------------------------------------------------- def mouse_change_tool(self, s): print('Tool ' + s) if s == 'CmpTable': self.ToolIndex = 0 elif s == 'Selector': self.ToolIndex = 1 elif s == 'Inspector': self.ToolIndex = 2 elif s == 'FieldInspector': self.ToolIndex = 3 if self.ToolIndex != 3: self.ToolList[3].finish_edit( ) # save field properties when leave field inspector #-------------------------------------------------------------------------------- def add_user_property(self): self.Inspector.save_cmps() self.FieldInspector.save_fields() self.Inspector.add_property() #-------------------------------------------------------------------------------- def remove_user_property(self): #self.Inspector.save_cmps() self.FieldInspector.save_fields() self.Inspector.remove_property() #-------------------------------------------------------------------------------- def rename_user_property(self): #self.Inspector.save_cmps() self.FieldInspector.save_fields() self.Inspector.rename_property() #-------------------------------------------------------------------------------- def __init__(self): super().__init__() self.initUI() self.installEventFilter(self.EventFilter(self)) self.setFocusPolicy(Qt.WheelFocus) self.setTabOrder(self.CmpTable, self.Inspector) self.setTabOrder(self.Inspector, self.Selector) self.setTabOrder(self.Selector, self.FieldInspector) #self.setTabOrder(self.FieldInspector, self.CmpTable) #---------------------------------------------------- # # Application Hotkeys # self.shortcutLeft = QShortcut(QKeySequence(Qt.ALT + Qt.Key_Left), self) self.shortcutRight = QShortcut(QKeySequence(Qt.ALT + Qt.Key_Right), self) self.shortcutLeft.setContext(Qt.ApplicationShortcut) self.shortcutRight.setContext(Qt.ApplicationShortcut) self.shortcutLeft.activated.connect(self.scroll_left) self.shortcutRight.activated.connect(self.scroll_right) #-------------------------------------------------------------------------------- def initUI(self): #---------------------------------------------------- # # Main Window # work_zone = QWidget(self) Layout = QHBoxLayout(work_zone) self.setCentralWidget(work_zone) openAction = QAction(QIcon(os.path.join(resources_path, 'open24.png')), 'Open', self) openAction.setShortcut('Ctrl+O') openAction.setStatusTip('Open Schematic File') openAction.triggered.connect(self.open_file) saveAction = QAction(QIcon(os.path.join(resources_path, 'save24.png')), 'Save', self) saveAction.setShortcut('Ctrl+S') saveAction.setStatusTip('Save Schematic File') saveAction.triggered.connect(self.save_file) saveAsAction = QAction( QIcon(os.path.join(resources_path, 'save-as24.png')), 'Save As...', self) saveAsAction.setShortcut('Ctrl+Shift+S') saveAsAction.setStatusTip('Save Schematic File As...') saveAsAction.triggered.connect(self.save_file_as) exitAction = QAction(QIcon(os.path.join(resources_path, 'exit24.png')), 'Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') exitAction.triggered.connect(self.close) settingsAction = QAction( QIcon(os.path.join(resources_path, 'settings24.png')), 'Settings', self) settingsAction.setShortcut('Ctrl+Alt+S') settingsAction.setStatusTip('Edit settings') settingsAction.triggered.connect(self.edit_settings) helpAction = QAction( QIcon(os.path.join(resources_path, 'help_book24.png')), 'User\'s Manual', self) helpAction.setShortcut('F1') helpAction.setStatusTip('User\'s Manual') helpAction.triggered.connect(self.show_user_manual_slot) helpSDAction = QAction( QIcon(os.path.join(resources_path, 'gear24.png')), 'Settings Dialog', self) helpSDAction.setShortcut('Ctrl+F1') helpSDAction.setStatusTip('Settings Dialog Help') helpSDAction.triggered.connect(self.show_setting_dialog_help_slot) helpHKAction = QAction( QIcon(os.path.join(resources_path, 'rocket24.png')), 'Hotkeys', self) helpHKAction.setShortcut('Shift+F1') helpHKAction.setStatusTip('Hotkeys Help') helpHKAction.triggered.connect(self.show_hotkeys_help_slot) self.statusBar().showMessage('Ready') #-------------------------------------------- # # Main Menu # menubar = self.menuBar() fileMenu = menubar.addMenu('&File') fileMenu.addAction(openAction) fileMenu.addAction(saveAction) fileMenu.addAction(saveAsAction) fileMenu.addAction(exitAction) #-------------------------------------------- # # Options Menu # optionsMenu = menubar.addMenu('&Options') optionsMenu.addAction(settingsAction) #-------------------------------------------- # # Help Menu # helpMenu = menubar.addMenu('&Help') helpMenu.addAction(helpAction) helpMenu.addAction(helpSDAction) helpMenu.addAction(helpHKAction) #-------------------------------------------- # # Toolbar # toolbar = self.addToolBar('Exit') toolbar.addAction(exitAction) toolbar.addAction(openAction) toolbar.addAction(saveAction) toolbar.addAction(saveAsAction) toolbar.addAction(settingsAction) toolbar.addAction(helpAction) #---------------------------------------------------- # # Settings Dialog # #---------------------------------------------------- # # Components Table # self.CmpTabBox = QGroupBox('Components', self) self.CmpTabLayout = QVBoxLayout(self.CmpTabBox) self.CmpTabLayout.setContentsMargins(4, 10, 4, 4) self.CmpTabLayout.setSpacing(10) self.CmpTabLayout.setSizeConstraint(QVBoxLayout.SetMaximumSize) self.CmpTable = ComponentsTable(self) #self.CmpChooseButton = QPushButton('Choose', self) self.CmpTabLayout.addWidget(self.CmpTable) #self.CmpTabLayout.addWidget(self.CmpChooseButton) #---------------------------------------------------- # # Selector # self.SelectorBox = QGroupBox('Selector', self) self.SelectorLayout = QVBoxLayout(self.SelectorBox) self.SelectorLayout.setContentsMargins(4, 10, 4, 4) self.SelectorLayout.setSpacing(2) self.SelectorBtnWidget = QWidget(self) self.SelectorBtnLayout = QHBoxLayout(self.SelectorBtnWidget) self.SelectorBtnLayout.setContentsMargins(4, 10, 4, 4) self.SelectorBtnLayout.setSpacing(10) self.Selector = Selector(self) self.SelApplyButton = QPushButton('Apply', self) self.SelApplyButton.setToolTip( 'Alt+S: Apply selection patterns to components') self.SelClearButton = QPushButton('Clear', self) self.SelClearButton.setToolTip('Alt+C: Clear selection patterns') self.SelTemplateButton = QPushButton('Use Component', self) self.SelTemplateButton.setToolTip( 'Alt+T: Use Selected Component As Template') self.SelectorLayout.addWidget(self.Selector) self.SelectorBtnLayout.addWidget(self.SelTemplateButton) self.SelectorBtnLayout.addWidget(self.SelApplyButton) self.SelectorBtnLayout.addWidget(self.SelClearButton) self.SelectorLayout.addWidget(self.SelectorBtnWidget) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_S), self) self.shortcutSelApply.activated.connect(self.Selector.apply_slot) self.shortcutSelClear = QShortcut(QKeySequence(Qt.ALT + Qt.Key_C), self) self.shortcutSelClear.activated.connect(self.Selector.clear_slot) self.shortcutSelTemplate = QShortcut(QKeySequence(Qt.ALT + Qt.Key_T), self) self.shortcutSelTemplate.activated.connect( self.Selector.use_comp_as_template_slot) #---------------------------------------------------- # # Inspector # self.Inspector = Inspector(self) self.FieldInspector = FieldInspector(self) self.InspectorBtnWidget = QWidget(self) self.InspectorBtnLayout = QHBoxLayout(self.InspectorBtnWidget) self.InspectorBtnLayout.setContentsMargins(4, 10, 4, 4) self.InspectorBtnLayout.setSpacing(10) self.AddUserProperty = QPushButton('Add Property', self) self.AddUserProperty.setToolTip('Alt+A: Add new user property') self.DeleteUserProperty = QPushButton('Delete Property', self) self.DeleteUserProperty.setToolTip('Alt+Delete: Delete user property') self.RenameUserProperty = QPushButton('Rename Property', self) self.RenameUserProperty.setToolTip('Alt+R: Rename user property') self.InspectorBox = QGroupBox('Inspector', self) self.InspectorSplit = QSplitter(Qt.Vertical, self) self.InspectorLayout = QVBoxLayout(self.InspectorBox) self.InspectorLayout.setContentsMargins(4, 10, 4, 4) self.InspectorLayout.setSpacing(2) self.InspectorSplit.addWidget(self.Inspector) self.InspectorSplit.addWidget(self.FieldInspector) self.InspectorLayout.addWidget(self.InspectorSplit) self.InspectorBtnLayout.addWidget(self.AddUserProperty) self.InspectorBtnLayout.addWidget(self.DeleteUserProperty) self.InspectorBtnLayout.addWidget(self.RenameUserProperty) self.InspectorLayout.addWidget(self.InspectorBtnWidget) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_A), self) self.shortcutSelApply.activated.connect(self.add_user_property) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_Delete), self) self.shortcutSelApply.activated.connect(self.remove_user_property) self.shortcutSelApply = QShortcut(QKeySequence(Qt.ALT + Qt.Key_R), self) self.shortcutSelApply.activated.connect(self.rename_user_property) #---------------------------------------------------- self.Splitter = QSplitter(self) self.Splitter.addWidget(self.CmpTabBox) self.Splitter.addWidget(self.SelectorBox) self.Splitter.addWidget(self.InspectorBox) self.centralWidget().layout().addWidget(self.Splitter) #---------------------------------------------------- # # Signals and Slots connections # self.CmpTable.cells_chosen.connect(self.Inspector.load_cmp) self.CmpTable.cells_chosen.connect(self.Selector.comp_template_slot) self.CmpTable.file_load.connect(self.file_loaded_slot) self.CmpTable.cmps_updated.connect(self.Selector.process_comps_slot) self.CmpTable.cmps_selected.connect(self.set_status_text_slot) self.SelApplyButton.clicked.connect(self.Selector.apply_slot) self.SelClearButton.clicked.connect(self.Selector.clear_slot) self.SelTemplateButton.clicked.connect( self.Selector.use_comp_as_template_slot) self.Selector.select_comps_signal.connect( self.CmpTable.select_comps_slot) self.Inspector.load_field.connect(self.FieldInspector.load_field_slot) self.Inspector.update_comps.connect(self.data_changed_slot) self.Inspector.update_comps.connect(self.CmpTable.update_cmp_list_slot) self.FieldInspector.data_changed.connect(self.data_changed_slot) CmpMgr.file_saved.connect(self.file_saved_slot) self.CmpTable.mouse_click.connect(self.mouse_change_tool) self.Inspector.mouse_click.connect(self.mouse_change_tool) self.FieldInspector.mouse_click.connect(self.mouse_change_tool) self.Inspector.header().sectionResized.connect( self.FieldInspector.column_resize) self.AddUserProperty.clicked.connect(self.add_user_property) self.DeleteUserProperty.clicked.connect(self.remove_user_property) self.RenameUserProperty.clicked.connect(self.rename_user_property) #---------------------------------------------------- self.ToolList = [] self.ToolList.append(self.CmpTable) self.ToolList.append(self.Selector) self.ToolList.append(self.Inspector) self.ToolList.append(self.FieldInspector) self.ToolIndex = 0 #---------------------------------------------------- # # Window # self.setWindowTitle(self.PROGRAM_NAME) Settings = QSettings('kicad-tools', 'Schematic Component Manager') #print(Settings.allKeys()) if Settings.contains('geometry'): self.restoreGeometry(Settings.value('geometry')) else: self.setGeometry(100, 100, 1024, 768) if Settings.contains('cmptable'): w0, w1 = Settings.value('cmptable') self.CmpTable.setColumnWidth(0, int(w0)) self.CmpTable.setColumnWidth(1, int(w1)) if Settings.contains('selector'): w0, w1 = Settings.value('selector') self.Selector.setColumnWidth(0, int(w0)) self.Selector.setColumnWidth(1, int(w1)) if Settings.contains('inspector'): w0, w1 = Settings.value('inspector') self.Inspector.setColumnWidth(0, int(w0)) self.Inspector.setColumnWidth(1, int(w1)) self.FieldInspector.setColumnWidth(0, int(w0)) self.FieldInspector.setColumnWidth(1, int(w1)) #self.Inspector.setColumnWidth( 2, int(w2) ) if Settings.contains('splitter'): self.Splitter.restoreState(Settings.value('splitter')) if Settings.contains('inssplitter'): self.InspectorSplit.restoreState(Settings.value('inssplitter')) #---------------------------------------------------- # # Process command line arguments # if len(sys.argv) > 1: fname = sys.argv[1] if os.path.exists(fname): self.CmpTable.load_file(fname) else: print('E: input file "' + fname + '"does not exist') self.show() #--------------------------------------------------------------------------- def closeEvent(self, event): Settings = QSettings('kicad-tools', 'Schematic Component Manager') Settings.setValue('geometry', self.saveGeometry()) Settings.setValue( 'cmptable', [self.CmpTable.columnWidth(0), self.CmpTable.columnWidth(1)]) Settings.setValue( 'selector', [self.Selector.columnWidth(0), self.Selector.columnWidth(1)]) Settings.setValue( 'inspector', [self.Inspector.columnWidth(0), self.Inspector.columnWidth(1)]) Settings.setValue('splitter', self.Splitter.saveState()) Settings.setValue('inssplitter', self.InspectorSplit.saveState()) QWidget.closeEvent(self, event) #--------------------------------------------------------------------------- def open_file(self): #filename = QFileDialog.getOpenFileName(self, 'Open schematic file', '/opt/cad/kicad', 'KiCad Schematic Files (*.sch)') dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setNameFilter('KiCad Schematic Files (*.sch)') filenames = [] if dialog.exec_(): filenames = dialog.selectedFiles() if len(filenames) == 0: return CmpMgr.set_curr_file_path(filenames[0]) self.CmpTable.load_file(filenames[0]) #--------------------------------------------------------------------------- def save_file(self): self.FieldInspector.save_fields() self.Inspector.save_cmps() curr_file = CmpMgr.curr_file_path() message = 'Save File "' + curr_file + '"' print(message) self.statusBar().showMessage(message) CmpMgr.save_file(curr_file) #--------------------------------------------------------------------------- def save_file_as(self): self.Inspector.save_cmps() self.FieldInspector.save_fields() filenames = QFileDialog.getSaveFileName( self, 'Save File As...', '', 'KiCad Schematic Files (*.sch)') if filenames[0] == '': return print('Save File As "' + filenames[0] + '"') CmpMgr.save_file(filenames[0]) CmpMgr.set_curr_file_path(filenames[0]) #--------------------------------------------------------------------------- def file_loaded_slot(self): text = CmpMgr.curr_file_path() self.set_title(text) #--------------------------------------------------------------------------- def data_changed_slot(self): text = CmpMgr.curr_file_path() + ' *' self.set_title(text) #--------------------------------------------------------------------------- def file_saved_slot(self): text = CmpMgr.curr_file_path() self.set_title(text) #--------------------------------------------------------------------------- def set_title(self, text=''): text = ' - ' + text if len(text) > 0 else '' self.setWindowTitle(self.PROGRAM_NAME + ' v' + VERSION + text) #--------------------------------------------------------------------------- def set_status_text_slot(self, text): self.statusBar().showMessage(text) #--------------------------------------------------------------------------- def edit_settings(self): print('edit settings') SettingsDialog = TSettingsDialog(self) SettingsDialog.resize(400, 400) SettingsDialog.Tabs.setMinimumWidth(800) SettingsDialog.show() #--------------------------------------------------------------------------- def show_user_manual_slot(self): help = THelpForm(self, 'User\'s Manual', 'main.html') #--------------------------------------------------------------------------- def show_setting_dialog_help_slot(self): help = THelpForm(self, 'Settings Dialog', 'settings.html') #--------------------------------------------------------------------------- def show_hotkeys_help_slot(self): help = THelpForm(self, 'Hotkeys', 'hotkeys.html')
class DatabaseContainer(QSplitter): def __init__(self, orientation=Qt.Vertical): QSplitter.__init__(self, orientation) self.pfile = None self._hsplitter = QSplitter(Qt.Horizontal) self.lateral_widget = lateral_widget.LateralWidget() self._hsplitter.addWidget(self.lateral_widget) self.table_widget = table_widget.TableWidget() self._hsplitter.addWidget(self.table_widget) self.addWidget(self._hsplitter) self.query_container = query_container.QueryContainer(self) self.addWidget(self.query_container) self.modified = False self.__nquery = 1 # Connections # FIXME self.lateral_widget.itemClicked.connect( lambda: self.table_widget.stacked.setCurrentIndex( self.lateral_widget.row())) # For change table widget item when up/down # see issue #39 self.lateral_widget.itemSelectionChanged.connect( lambda: self.table_widget.stacked.setCurrentIndex( self.lateral_widget.row())) self.query_container.saveEditor['PyQt_PyObject'].connect( self.save_query) self.setSizes([1, 1]) def dbname(self): """ Return display name """ return self.pfile.display_name def is_new(self): return self.pfile.is_new def create_database(self, data): for table in data.get('tables'): # Get data table_name = table.get('name') header = table.get('header') tuples = table.get('tuples') # Create relation rela = relation.Relation() rela.header = header # Table view widget table_view = custom_table.Table() # Model model = QStandardItemModel() model.setHorizontalHeaderLabels(header) # Populate table view row_count = 0 for row in tuples: for col_count, i in enumerate(row): item = QStandardItem(i) # Set read only item.setFlags(item.flags() & ~Qt.ItemIsEditable) model.setItem(row_count, col_count, item) rela.insert(row) row_count += 1 # Set table model table_view.setModel(model) # Add relation to relations dict self.table_widget.add_relation(table_name, rela) # Add table to stacked self.table_widget.stacked.addWidget(table_view) # Add table name to list widget self.lateral_widget.add_item(table_name, rela.count()) # Select first item first_item = self.lateral_widget.topLevelItem(0) first_item.setSelected(True) def load_relation(self, filenames): for filename in filenames: with open(filename) as f: csv_reader = csv.reader(f) header = next(csv_reader) rel = relation.Relation() rel.header = header for i in csv_reader: rel.insert(i) relation_name = file_manager.get_basename(filename) if not self.table_widget.add_relation(relation_name, rel): QMessageBox.information(self, self.tr("Information"), self.tr("There is already a " "relationship with name " "'{}'".format( relation_name))) return False self.table_widget.add_table(rel, relation_name) self.lateral_widget.add_item(relation_name, rel.count()) return True def delete_relation(self): selected_items = self.lateral_widget.selectedItems() if not selected_items: return False current_row = 0 if self.lateral_widget.row() != -1: current_row = self.lateral_widget.row() if len(selected_items) > 1: msg = self.tr("Are you sure you want to delete " "the selected relations?") else: msg = self.tr("Are you sure you want to delete " "the relation <b>{}</b>?".format( self.lateral_widget.item_text(current_row))) msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle(self.tr("Confirmation")) msgbox.setText(msg) msgbox.addButton(self.tr("No"), QMessageBox.NoRole) yes_btn = msgbox.addButton(self.tr("Yes"), QMessageBox.YesRole) palette = QPalette() palette.setColor(QPalette.Button, QColor("#cc575d")) palette.setColor(QPalette.ButtonText, QColor("white")) yes_btn.setPalette(palette) msgbox.exec_() r = msgbox.clickedButton() if r == yes_btn: for item in selected_items: index = self.lateral_widget.indexOfTopLevelItem(item) # Remove from list self.lateral_widget.takeTopLevelItem(index) # Remove table self.table_widget.remove_table(index) # Remove relation self.table_widget.remove_relation(item.name) return True def __on_data_table_changed(self, row, col, data): current_relation = self.lateral_widget.current_text() # Relation to be update rela = self.table_widget.relations.get(current_relation) # Clear old content rela.clear() current_table = self.table_widget.stacked.currentWidget() model = current_table.model() for i in range(model.rowCount()): reg = [] for j in range(model.columnCount()): if row == i and col == j: reg.append(data) else: reg.append(model.item(i, j).text()) # Insert new content rela.insert(reg) # Update relation self.table_widget.relations[current_relation] = rela def new_query(self, filename): editor_tab_at = self.query_container.is_open(filename) if editor_tab_at != -1: self.query_container.set_focus_editor_tab(editor_tab_at) else: query_widget = query_container.QueryWidget() # Create object file ffile = pfile.File(filename) editor = query_widget.get_editor() editor.pfile = ffile if not filename: ffile.filename = 'untitled_{n}.pqf'.format(n=self.__nquery) else: content = ffile.read() editor.setPlainText(content) self.query_container.add_tab(query_widget, ffile.display_name) self.__nquery += 1 def save_query(self, editor): if not editor: editor = self.query_container.currentWidget().get_editor() if editor.is_new: return self.save_query_as(editor) # Get content of editor content = editor.toPlainText() try: editor.pfile.save(content=content) except Exception as reason: QMessageBox.critical(self, "Error", self.tr("The file couldn't be saved!" "\n\n{}".format(reason))) return False editor.saved() return editor.pfile.filename def save_query_as(self, editor=None): filename = QFileDialog.getSaveFileName(self, self.tr("Save File"), editor.name, "Pireal query files(*.pqf)") filename = filename[0] if not filename: return # Get the content content = editor.toPlainText() # Write the file editor.pfile.save(content=content, new_fname=filename) editor.saved() def execute_queries(self): self.query_container.execute_queries() def execute_selection(self): editor = self.query_container.currentWidget().get_editor() text_cursor = editor.textCursor() if text_cursor.hasSelection(): query = text_cursor.selectedText() self.query_container.execute_queries(query) def showEvent(self, event): QSplitter.showEvent(self, event) qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat) hsizes = qsettings.value('hsplitter_sizes', None) if hsizes is not None: self._hsplitter.restoreState(hsizes) else: self._hsplitter.setSizes([1, self._hsplitter.width() / 3]) vsizes = qsettings.value('vsplitter_sizes', None) if vsizes is not None: self.restoreState(vsizes) else: self.setSizes([self.height() / 3, self.height() / 3]) def save_sizes(self): """ Save sizes of Splitters """ qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat) qsettings.setValue('hsplitter_sizes', self._hsplitter.saveState()) qsettings.setValue('vsplitter_sizes', self.saveState())
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Executes a Python script." icon = "icons/python_script.png" priority = 1 inputs = [("in_object_1", object, "setObject1"), ("in_object_2", object, "setObject2"), ("in_object_3", object, "setObject3"), ("in_object_4", object, "setObject4"), ("in_object_5", object, "setObject5"), ("in_object_6", object, "setObject6"), ("in_object_7", object, "setObject7"), ("in_object_8", object, "setObject8"), ("in_object_9", object, "setObject9"), ("in_object_10", object, "setObject10")] outputs = [("out_object", object, widget.Dynamic)] libraryListSource = \ Setting([Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) splitterState = Setting(None) auto_execute = Setting(False) fonts = ["8", "9", "10", "11", "12", "14", "16", "20", "24"] font_size = Setting(4) def __init__(self): super().__init__() self.in_data = None self.in_distance = None self.in_learner = None self.in_classifier = None self.in_object_1 = None self.in_object_2 = None self.in_object_3 = None self.in_object_4 = None self.in_object_5 = None self.in_object_6 = None self.in_object_7 = None self.in_object_8 = None self.in_object_9 = None self.in_object_10 = None # MODIFIED BY LUCA REBUFFI 14/10/2014 #self.auto_execute = False for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.widgetBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + \ "<li>".join([self.inputs[0].name, ".",".",".", self.inputs[-1].name]) + \ "</ul></p><p>Output variables:<ul><li>" + \ "<li>".join(t.name for t in self.outputs) + \ "</ul></p>" ) self.optionBox = oasysgui.widgetBox(self.controlArea, 'Options') gui.comboBox(self.optionBox, self, "font_size", label="Font Size", labelWidth=120, items=self.fonts, sendSelectedValue=False, orientation="horizontal", callback=self.changeFont) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.widgetBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import a script from a file", self) save_to_file = QAction("Save selected script to a file", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.runBox = gui.widgetBox(self.controlArea, 'Run') gui.button(self.runBox, self, "Execute", callback=self.execute) gui.checkBox(self.runBox, self, "auto_execute", "Auto execute", tooltip="Run the script automatically whenever " + "the inputs to the widget change.") self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.widgetBox(self, 'Python script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.widgetBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole(self.__dict__, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState( QByteArray(bytearray(self.splitterState, "ascii"))) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) self.changeFont() def setExampleTable(self, et): self.in_data = et def setDistanceMatrix(self, dm): self.in_distance = dm def setLearner(self, learner): self.in_learner = learner def setClassifier(self, classifier): self.in_classifier = classifier def setObject1(self, obj): self.in_object_1 = obj def setObject2(self, obj): self.in_object_2 = obj def setObject3(self, obj): self.in_object_3 = obj def setObject4(self, obj): self.in_object_4 = obj def setObject5(self, obj): self.in_object_5 = obj def setObject6(self, obj): self.in_object_6 = obj def setObject7(self, obj): self.in_object_7 = obj def setObject8(self, obj): self.in_object_8 = obj def setObject9(self, obj): self.in_object_9 = obj def setObject10(self, obj): self.in_object_10 = obj def handleNewSignals(self): if self.auto_execute: self.execute() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", "", 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)')[0] filename = str(filename) if filename: name = os.path.basename(filename) contents = open(filename, "rb").read().decode("utf-8", errors="ignore") self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QtGui.QTextDocument(self) doc.setDocumentLayout(QtWidgets.QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = str(self.splitCanvas.saveState()) def updateSelecetdScriptState(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] self.libraryList[index] = Script(script.name, self.text.toPlainText(), 0) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)')[0] if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def execute(self): self._script = str(self.text.toPlainText()) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for out in self.outputs: signal = out.name self.send(signal, getattr(self, signal, None)) def changeFont(self): font = QFont(self.defaultFont) font.setPixelSize(int(self.fonts[self.font_size])) self.text.setFont(font) self.console.setFont(font)
class FontsDialog(QDialog): """Dialog to show available fonts""" _selected_fonts = { 'music': _default_fonts['music'], 'brace': _default_fonts['brace'], 'roman': _default_fonts['roman'], 'sans': _default_fonts['sans'], 'typewriter': _default_fonts['typewriter'] } def __init__(self, parent, lilypond_info=None, show_music=True): QApplication.setOverrideCursor(Qt.WaitCursor) super(FontsDialog, self).__init__(parent, ) self.info = lilypond_info if not lilypond_info: import documentinfo self.info = documentinfo.lilyinfo(parent.currentDocument()) self.result = '' self.available_fonts = fonts.available(self.info) # Notation fonts (and preview) are limited to LilyPond >= 2.19.12 # At some point we may remove the old dialog altogether # and instead make this dialog behave differently # (i.e. hide the music font stuff and use old font selection code) # self.show_music = self.info.version() >= (2, 19, 12). # For now this distinction is made by the action and simply # the dialog to be used is chosen. At some point the old # "Set document fonts" dialog should be dropped. # # Also, it may at some point be indicated to make this # dialog usable to *only* choose text fonts, e.g. from # the "Fonts & Colors" Preference page. # # NOTE: All facilities that *seemed* to support this functionality # have been removed to avoid confusion. self.show_music = show_music # Basic dialog attributes self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowModality(Qt.WindowModal) layout = QVBoxLayout() self.setLayout(layout) # Create a QSplitter as main widget self.splitter = QSplitter() self.splitter.setOrientation(Qt.Horizontal) layout.addWidget(self.splitter) # Left side layout self.left_column = QWidget() left_layout = QVBoxLayout() self.left_column.setLayout(left_layout) self.splitter.addWidget(self.left_column) # Status area # TODO: Populate that widget with a QStackedLayout where # different widgets are shown corresponding to the visible tab. self.status_area = QWidget() left_layout.addWidget(self.status_area) # Create the QTabWidget for the dialog's left part self.tab_widget = QTabWidget() left_layout.addWidget(self.tab_widget) # Text Fonts Tab self.tab_widget.addTab(textfonts.TextFontsWidget(self), '') # Music Fonts Tab self.tab_widget.addTab(musicfonts.MusicFontsWidget(self), '') # Show/configure the generated font setting command self.font_command_tab = fontcommand.FontCommandWidget(self) self.tab_widget.addTab(self.font_command_tab, '') # Show various fontconfig information self.tab_widget.addTab( textfonts.MiscFontsInfoWidget(self.available_fonts), '') # Create the RHS score preview pane. self.preview_pane = preview.FontsPreviewWidget(self) self.splitter.addWidget(self.preview_pane) # Bottom area: button box self._button_box = bb = QDialogButtonBox() layout.addWidget(bb) self.restore_button = bb.addButton(QDialogButtonBox.RestoreDefaults) self.copy_button = bb.addButton(QDialogButtonBox.Save) self.insert_button = bb.addButton(QDialogButtonBox.Ok) self.close_button = bb.addButton(QDialogButtonBox.Close) # Add and connect help button userguide.addButton(self._button_box, "documentfonts") app.translateUI(self) self.loadSettings() self.connectSignals() # Trigger the generation of a preview self.invalidate_command() def connectSignals(self): self.finished.connect(self.saveSettings) self.restore_button.clicked.connect(self.restore) self.copy_button.clicked.connect(self.copy_result) self.insert_button.clicked.connect(self.insert_result) self.close_button.clicked.connect(self.close) def translateUI(self): self.setWindowTitle(app.caption(_("Document Fonts"))) self.copy_button.setText(_("&Copy")) self.copy_button.setToolTip(_("Copy font command to clipboard")) self.insert_button.setText(_("&Use")) self.insert_button.setToolTip( _("Insert font command at the current cursor position")) self.tab_widget.setTabText(0, _("Text Fonts")) self.tab_widget.setTabText(1, _("Music Fonts")) self.tab_widget.setTabText(2, _("Font Command")) self.tab_widget.setTabText(3, _("Miscellaneous")) def loadSettings(self): s = QSettings() s.beginGroup('document-fonts-dialog') self.select_font('music', s.value('music-font', 'emmentaler', str)) self.select_font('brace', s.value('brace-font', 'emmentaler', str)) self.select_font('roman', s.value('roman-font', 'TeXGyre Schola', str)) self.select_font('sans', s.value('sans-font', 'TeXGyre Heros', str)) self.select_font('typewriter', s.value('typewriter-font', 'TeXGyre Cursor', str)) self.splitter.restoreState( s.value('splitter-state', QByteArray(), QByteArray)) def select_font(self, family, name): self._selected_fonts[family] = name def selected_font(self, family): return self._selected_fonts[family] def saveSettings(self): s = QSettings() s.beginGroup('document-fonts-dialog') s.setValue('music-font', self.selected_font('music')) s.setValue('brace-font', self.selected_font('brace')) s.setValue('roman-font', self.selected_font('roman')) s.setValue('sans-font', self.selected_font('sans')) s.setValue('typewriter-font', self.selected_font('typewriter')) # Dialog layout s.setValue('splitter-state', self.splitter.saveState()) def copy_result(self): """Copies the font command (as shown) to the clipboard.""" from PyQt5.QtGui import QGuiApplication cmd = self.font_cmd() if cmd[-1] != '\n': cmd = cmd + '\n' QGuiApplication.clipboard().setText(cmd) self.accept() def font_cmd(self, approach=None): """Return the font setting command as shown in the Font Command tab.""" approach = approach or self.font_command_tab.approach return self.font_command_tab.command(approach) def font_full_cmd(self, approach=None): """Return the "full" command with all properties/fonts.""" approach = approach or self.font_command_tab.approach return self.font_command_tab.full_cmd(approach) def insert_result(self): """Inserts the font command (as shown) at the current position""" self.result = self.font_cmd() self.accept() def invalidate_command(self): """Triggers a regeneration of the font command and new display of the example (if necessary). """ self.font_command_tab.invalidate_command() def restore(self): """Reset fonts to defaults""" for name in _default_fonts: self.select_font(name, _default_fonts[name]) self.invalidate_command() def show_sample(self): """Call the preview pane to show the current sample.""" self.preview_pane.show_sample()
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.11" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.fulltopic_queue = [] self.settings = QSettings() self.setMinimumSize(QSize(1280,800)) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.setSingleShot(True) self.queue_timer.timeout.connect(self.mqtt_ask_for_fulltopic) self.build_cons_ctx_menu() self.load_window_state() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() self.console_view = TableView() self.console_view.setModel(self.sorted_console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.setSortingEnabled(True) self.console_view.sortByColumn(CnsMdl.TIMESTAMP, Qt.DescendingOrder) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) self.console_view.setContextMenuPolicy(Qt.CustomContextMenu) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.console_view.customContextMenuRequested.connect(self.show_cons_ctx_menu) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=32, label_position=Qt.ToolButtonIconOnly) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Configure MQTT broker", self.setup_broker) agBroker = QActionGroup(self) agBroker.setExclusive(True) self.actConnect = CheckableAction(QIcon("./GUI/icons/connect.png"), "Connect to the broker", agBroker) self.actDisconnect = CheckableAction(QIcon("./GUI/icons/disconnect.png"), "Disconnect from broker", agBroker) self.actDisconnect.setChecked(True) self.actConnect.triggered.connect(self.mqtt_connect) self.actDisconnect.triggered.connect(self.mqtt_disconnect) main_toolbar.addActions(agBroker.actions()) main_toolbar.addSeparator() def initial_query(self, idx): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) self.mqtt.publish(topic, q) q = q if q else '' self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_() == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.statusBar().showMessage("Connected to {}:{} as {}".format(self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_ask_for_fulltopic(self): for i in range(len(self.fulltopic_queue)): self.mqtt.publish(self.fulltopic_queue.pop(0)) def mqtt_disconnected(self): self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actDisconnect.setChecked(True) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) elif msg == "Online": self.console_log(topic, "LWT for unknown device '{}'. Asking for FullTopic.".format(found.topic), msg, False) self.fulltopic_queue.append("cmnd/{}/fulltopic".format(found.topic)) self.fulltopic_queue.append("{}/cmnd/fulltopic".format(found.topic)) self.queue_timer.start(1500) elif found.reply == 'RESULT': full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log(topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice(TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log(topic, "Added {} with fulltopic {}, querying for STATE".format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) if new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log(topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get(found.topic) if tele_idx: self.telemetry_model.setDeviceName(tele_idx, new_topic) self.telemetry_model.devices[new_topic] = self.telemetry_model.devices.pop(found.topic) if template_name: self.device_model.updateValue(found.index, DevMdl.MODULE, template_name) elif found.index.isValid(): if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = loads(msg)['Status'] self.device_model.updateValue(found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName(self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) self.tview.resizeColumnToContents(0) module = payload['Module'] if module == '0': self.mqtt.publish(self.device_model.commandTopic(found.index)+"template") else: self.device_model.updateValue(found.index, DevMdl.MODULE, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = loads(msg)['StatusPRM'] self.device_model.updateValue(found.index, DevMdl.RESTART_REASON, payload['RestartReason']) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = loads(msg)['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = loads(msg)['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = loads(msg)['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply == 'STATUS8': self.console_log(topic, "Received telemetry", msg) payload = loads(msg)['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply == 'STATUS11': self.console_log(topic, "Received device state", msg) payload = loads(msg)['StatusSTS'] self.parse_state(found.index, payload) elif found.reply == 'SENSOR': self.console_log(topic, "Received telemetry", msg) payload = loads(msg) self.parse_telemetry(found.index, payload) elif found.reply == 'STATE': self.console_log(topic, "Received device state", msg) payload = loads(msg) self.parse_state(found.index, payload) elif found.reply.startswith('POWER'): self.console_log(topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) def parse_power(self, index, payload): old = self.device_model.power(index) power = {k: payload[k] for k in payload.keys() if k.startswith("POWER")} needs_update = False if old: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel')) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.parse_power(index, payload) tele_idx = self.telemetry_model.devices.get(self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName(tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get(self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload, known=True): device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.console_model.addEntry(topic, fname, description, payload, known) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data(self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data(self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data(self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def build_cons_ctx_menu(self): self.cons_ctx_menu = QMenu() self.cons_ctx_menu.addAction("View payload", lambda: self.view_payload(self.cons_idx)) self.cons_ctx_menu.addSeparator() self.cons_ctx_menu.addAction("Show only this device", lambda: self.cons_set_filter(self.cons_idx)) self.cons_ctx_menu.addAction("Show all devices", self.cons_set_filter) def show_cons_ctx_menu(self, at): self.select_cons_entry(self.console_view.indexAt(at)) self.cons_ctx_menu.popup(self.console_view.viewport().mapToGlobal(at)) def cons_set_filter(self, idx=None): if idx: idx = self.sorted_console_model.mapToSource(idx) topic = self.console_model.data(self.console_model.index(idx.row(), CnsMdl.FRIENDLY_NAME)) self.sorted_console_model.setFilterFixedString(topic) else: self.sorted_console_model.setFilterFixedString("") def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class MainWindow(QMainWindow): KEY_WINDOW_SIZE = 'main_window/size' KEY_WINDOW_MAXIMIZED = 'main_window/maximized' KEY_WINDOW_POSITION = 'main_window/position' KEY_H_SPLITTER_STATE = 'main_window/h_splitter_state' def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('Mojuru') app_icon = QIcon('images/mojuru_logo.png') self.setWindowIcon(app_icon) reload_modules_action = QAction('Reload MainWindow', self) reload_modules_action.setShortcut('ctrl+shift+alt+r') reload_modules_action.triggered.connect(self.reload_central_widget) self.addAction(reload_modules_action) quit_action = QAction('Quit', self) quit_action.setShortcut('ctrl+q') quit_action.triggered.connect(self.on_quit) self.addAction(quit_action) self.vertical_splitter = QSplitter(Qt.Vertical, self) self.setCentralWidget(self.vertical_splitter) self.load_central_widget() self.file_menu = self.menuBar().addMenu(self.tr('&File')) self.file_menu.addAction(quit_action) self.file_menu.addSeparator() self.module_menu = self.menuBar().addMenu(self.tr('&Modules')) self.module_menu.addAction(reload_modules_action) self.module_menu.addSeparator() Alter.invoke_all('main_window_init', self) #restore main window state size = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_SIZE, QSize(600, 400)) maximized = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_MAXIMIZED, False) position = ModuleManager.core['settings'].Settings.value( self.KEY_WINDOW_POSITION, QPoint(0,0)) if maximized == 'true': self.showMaximized() else: self.resize(size) self.move(position) def closeEvent(self, event): self.on_quit() def save_state(self): ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_SIZE, self.size()) ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_MAXIMIZED, self.isMaximized()) ModuleManager.core['settings'].Settings.set_value( self.KEY_WINDOW_POSITION, self.pos()) ModuleManager.core['settings'].Settings.set_value( self.KEY_H_SPLITTER_STATE, self.horizontal_splitter.saveState()) def on_quit(self): self.save_state() self.close() def load_central_widget(self): self.populate_central_widget() self.connect_widgets() def populate_central_widget(self): self.vertical_widgets = collections.OrderedDict() self.horizontal_splitter = QSplitter( Qt.Horizontal, self.vertical_splitter) self.horizontal_widgets = collections.OrderedDict() self.vertical_widgets["horizontal_splitter"] = self.horizontal_splitter Alter.invoke_all( 'main_window_add_vertical_widget', self.vertical_widgets, self ) for widget in self.vertical_widgets.values(): self.vertical_splitter.addWidget(widget) Alter.invoke_all( 'main_window_add_horizontal_widget', self.horizontal_widgets, self.vertical_splitter ) for widget in self.horizontal_widgets.values(): self.horizontal_splitter.addWidget(widget) #restore horizontal splitter state state = ModuleManager.core['settings'].Settings.value( self.KEY_H_SPLITTER_STATE, None ) if state: self.horizontal_splitter.restoreState(state) def connect_widgets(self): Alter.invoke_all( 'main_window_connect_widgets', self.vertical_widgets, self.horizontal_widgets ) def reload_central_widget(self): self.save_state() for index in range(self.vertical_splitter.count()): widget = self.vertical_splitter.widget(index) widget.hide() widget.setParent(None) del widget Alter.clear() ModuleManager.reload_all_modules('core') ModuleManager.reload_all_modules('custom') self.load_central_widget()
class MainWindow(QMainWindow): EXIT_CODE_REBOOT = 520 def __init__(self): super().__init__() Config.initialize() self.initUI() def initUI(self): self.setAttribute(Qt.WA_DeleteOnClose) self.spliter = QSplitter(Qt.Vertical) self.spliter.addWidget(TestUnitArea()) self.spliter.addWidget(TestResultArea()) self.spliter.setHandleWidth(1) self.setCentralWidget(self.spliter) tool_menu = QMenu('工具', self.menuBar()) tool_menu.addAction('数据监听', self.onDebugWindow) tool_menu.addAction('单步测试', self.onSingleStep) tool_menu.addAction('记录查询', self.onViewData) tool_menu.addAction('条码打印', self.onPrintBarCode) tool_menu.addAction('异常信息', self.onExceptionWindow) setting_menu = QMenu('选项', self.menuBar()) setting_menu.addAction('参数设置', self.onSetting) # setting_menu.addAction('软件重启', self.onRestart) help_menu = QMenu('帮助', self.menuBar()) help_menu.addAction('关于', self.onAbout) self.menuBar().addMenu(setting_menu) self.menuBar().addMenu(tool_menu) self.menuBar().addMenu(help_menu) QApplication.setWindowIcon(QIcon(Config.LOGO_IMG)) QApplication.instance().aboutToQuit.connect(self.onApplicationQuit) QApplication.setOrganizationName(Config.ORGANIZATION) QApplication.setApplicationName(Config.APP_NAME) QApplication.setApplicationVersion(Config.APP_VERSION) self.restoreQSettings() self.createSystemTray() def onDebugWindow(self): if not ui.DebugDialog.prev_actived: self.debugWin = ui.DebugDialog() self.debugWin.show() else: QApplication.setActiveWindow(ui.DebugDialog.prev_window) ui.DebugDialog.prev_window.showNormal() def onSingleStep(self): if not ui.SingleStepFrame.prev_actived: self.singleWin = ui.SingleStepFrame() self.singleWin.show() else: QApplication.setActiveWindow(ui.SingleStepFrame.prev_window) ui.SingleStepFrame.prev_window.showNormal() def onViewData(self): if not ui.SearchWindow.prev_actived: self.searchWin = ui.SearchWindow() self.searchWin.show() else: QApplication.setActiveWindow(ui.SearchWindow.prev_window) ui.SearchWindow.prev_window.showNormal() def onPrintBarCode(self): if not CodeDialog.prev_actived: self.codeWin = CodeDialog() self.codeWin.show() else: QApplication.setActiveWindow(CodeDialog.prev_window) CodeDialog.prev_window.showNormal() def onExceptionWindow(self): if not ui.ExceptionWindow.prev_actived: self.excptionWin = ui.ExceptionWindow() self.excptionWin.show() else: QApplication.setActiveWindow(ui.ExceptionWindow.prev_window) ui.ExceptionWindow.prev_window.showNormal() def restoreQSettings(self): main_win_geo = Config.QSETTING.value('MainWindow/geometry') main_win_centerwgt_state = Config.QSETTING.value( 'MainWindow/CenterWidget/state') if main_win_geo: self.restoreGeometry(main_win_geo) if main_win_centerwgt_state: self.spliter.restoreState(main_win_centerwgt_state) def onSetting(self): dlg = ui.SettingDialog(self) dlg.move(self.x() + 50, self.y() + 50) dlg.exec() def onRestart(self): QApplication.exit(self.EXIT_CODE_REBOOT) def onAbout(self): dlg = ui.AboutDialog(Config.ABOUT_HTML) dlg.resize(400, 300) dlg.exec() def createSystemTray(self): self.systray = QSystemTrayIcon(self) self.systray.setIcon(QIcon(Config.LOGO_IMG)) self.systray.show() trayMenu = QMenu() trayMenu.addAction('最大化', self.showMaximized) trayMenu.addAction('最小化', self.showMinimized) trayMenu.addAction('显示窗口', self.showNormal) stayOnTop = QAction('总在最前', trayMenu, checkable=True, triggered=self.stayOnTop) trayMenu.addAction(stayOnTop) trayMenu.addSeparator() trayMenu.addAction('退出', QApplication.quit) username = platform.node() ip = socket.gethostbyname(socket.gethostname()) self.systray.setToolTip('用户:{}\nIP:{}'.format(username, ip)) self.systray.activated.connect(self.onSystemTrayActivated) self.systray.setContextMenu(trayMenu) def onSystemTrayActivated(self, reason): if reason in [QSystemTrayIcon.DoubleClick, QSystemTrayIcon.Trigger]: self.showNormal() def stayOnTop(self, checked): self.setWindowFlag(Qt.WindowStaysOnTopHint, self.sender().isChecked()) self.show() def onApplicationQuit(self): Config.QSETTING.setValue('MainWindow/geometry', self.saveGeometry()) Config.QSETTING.setValue('MainWindow/CenterWidget/state', self.spliter.saveState()) Config.finalize() self.systray.deleteLater() def closeEvent(self, event): Config.QSETTING.setValue('MainWindow/geometry', self.saveGeometry()) Config.QSETTING.setValue('MainWindow/CenterWidget/state', self.spliter.saveState()) if self.systray.isVisible(): self.hide() event.ignore()
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.20" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.mqtt_queue = [] self.devices = {} self.fulltopic_queue = [] old_settings = QSettings() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) self.setMinimumSize(QSize(1280, 800)) for k in old_settings.allKeys(): self.settings.setValue(k, old_settings.value(k)) old_settings.remove(k) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.timeout.connect(self.mqtt_publish_queue) self.queue_timer.start(500) self.auto_timer = QTimer() self.auto_timer.timeout.connect(self.autoupdate) self.load_window_state() if self.settings.value("connect_on_startup", False, bool): self.actToggleConnect.trigger() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() hl_filter = HLayout() self.cbFilter = QCheckBox("Console filtering") self.cbxFilterDevice = QComboBox() self.cbxFilterDevice.setEnabled(False) self.cbxFilterDevice.setFixedWidth(200) self.cbxFilterDevice.setModel(self.device_model) self.cbxFilterDevice.setModelColumn(DevMdl.FRIENDLY_NAME) hl_filter.addWidgets([self.cbFilter, self.cbxFilterDevice]) hl_filter.addStretch(0) vl_console.addLayout(hl_filter) self.console_view = TableView() self.console_view.setModel(self.console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.cbFilter.toggled.connect(self.toggle_console_filter) self.cbxFilterDevice.currentTextChanged.connect( self.select_console_filter) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=16, label_position=Qt.ToolButtonTextBesideIcon) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Broker", self.setup_broker) self.actToggleConnect = QAction(QIcon("./GUI/icons/disconnect.png"), "MQTT") self.actToggleConnect.setCheckable(True) self.actToggleConnect.toggled.connect(self.toggle_connect) main_toolbar.addAction(self.actToggleConnect) self.actToggleAutoUpdate = QAction(QIcon("./GUI/icons/automatic.png"), "Auto telemetry") self.actToggleAutoUpdate.setCheckable(True) self.actToggleAutoUpdate.toggled.connect(self.toggle_autoupdate) main_toolbar.addAction(self.actToggleAutoUpdate) main_toolbar.addSeparator() main_toolbar.addAction(QIcon("./GUI/icons/bssid.png"), "BSSId", self.bssid) main_toolbar.addAction(QIcon("./GUI/icons/export.png"), "Export list", self.export) def initial_query(self, idx, queued=False): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) if queued: self.mqtt_queue.append([topic, q]) else: self.mqtt.publish(topic, q, 1) self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_( ) == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def toggle_autoupdate(self, state): if state: self.auto_timer.setInterval(5000) self.auto_timer.start() def toggle_connect(self, state): if state and self.mqtt.state == self.mqtt.Disconnected: self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) self.mqtt.connectToHost() elif not state and self.mqtt.state == self.mqtt.Connected: self.mqtt_disconnect() def autoupdate(self): if self.mqtt.state == self.mqtt.Connected: for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) cmnd = self.device_model.commandTopic(idx) self.mqtt.publish(cmnd + "STATUS", payload=8) def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/connect.png")) self.statusBar().showMessage("Connected to {}:{} as {}".format( self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_publish_queue(self): for q in self.mqtt_queue: t, p = q self.mqtt.publish(t, p) self.mqtt_queue.pop(self.mqtt_queue.index(q)) def mqtt_disconnected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/disconnect.png")) self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actToggleConnect.setChecked(False) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) self.initial_query(found.index, queued=True) elif msg == "Online": self.console_log( topic, "LWT for unknown device '{}'. Asking for FullTopic.". format(found.topic), msg, False) self.mqtt_queue.append( ["cmnd/{}/fulltopic".format(found.topic), ""]) self.mqtt_queue.append( ["{}/cmnd/fulltopic".format(found.topic), ""]) elif found.reply == 'RESULT': try: full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') ota_url = loads(msg).get('OtaUrl') teleperiod = loads(msg).get('TelePeriod') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log( topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice( TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log( topic, "Added {} with fulltopic {}, querying for STATE". format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) elif new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log( topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get( found.topic) if tele_idx: self.telemetry_model.setDeviceName( tele_idx, new_topic) self.telemetry_model.devices[ new_topic] = self.telemetry_model.devices.pop( found.topic) elif template_name: self.device_model.updateValue( found.index, DevMdl.MODULE, "{} (0)".format(template_name)) elif ota_url: self.device_model.updateValue(found.index, DevMdl.OTA_URL, ota_url) elif teleperiod: self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, teleperiod) except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) elif found.index.isValid(): ok = False try: if msg.startswith("{"): payload = loads(msg) else: payload = msg ok = True except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) if ok: try: if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = payload['Status'] self.device_model.updateValue( found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName( self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) module = payload['Module'] if module == 0: self.mqtt.publish( self.device_model.commandTopic(found.index) + "template") else: self.device_model.updateValue( found.index, DevMdl.MODULE, modules.get(module, 'Unknown')) self.device_model.updateValue(found.index, DevMdl.MODULE_ID, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = payload['StatusPRM'] self.device_model.updateValue( found.index, DevMdl.RESTART_REASON, payload.get('RestartReason')) self.device_model.updateValue(found.index, DevMdl.OTA_URL, payload.get('OtaUrl')) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = payload['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = payload['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = payload['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply in ('STATE', 'STATUS11'): self.console_log(topic, "Received device state", msg) if found.reply == 'STATUS11': payload = payload['StatusSTS'] self.parse_state(found.index, payload) elif found.reply in ('SENSOR', 'STATUS8'): self.console_log(topic, "Received telemetry", msg) if found.reply == 'STATUS8': payload = payload['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply.startswith('POWER'): self.console_log( topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) except KeyError as k: self.console_log( topic, "JSON key error. Check error.log for additional info.") with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\tKeyError: {}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, payload, k.args[0])) def parse_power(self, index, payload, from_state=False): old = self.device_model.power(index) power = { k: payload[k] for k in payload.keys() if k.startswith("POWER") } # TODO: fix so that number of relays get updated properly after module/no. of relays change needs_update = False if old: # if from_state and len(old) != len(power): # needs_update = True # # else: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel', "n/a")) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.device_model.updateValue(index, DevMdl.LINKCOUNT, payload['Wifi'].get('LinkCount', "n/a")) self.device_model.updateValue(index, DevMdl.DOWNTIME, payload['Wifi'].get('Downtime', "n/a")) self.parse_power(index, payload, True) tele_idx = self.telemetry_model.devices.get( self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName( tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get( self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][ sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData( pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) # self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload="", known=True): longest_tp = 0 longest_fn = 0 short_topic = "/".join(topic.split("/")[0:-1]) fname = self.devices.get(short_topic, "") if not fname: device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.devices.update({short_topic: fname}) self.console_model.addEntry(topic, fname, description, payload, known) if len(topic) > longest_tp: longest_tp = len(topic) self.console_view.resizeColumnToContents(1) if len(fname) > longest_fn: longest_fn = len(fname) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): if self.cbFilter.isChecked(): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data( self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data( self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data( self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def export(self): fname, _ = QFileDialog.getSaveFileName(self, "Export device list as...", directory=QDir.homePath(), filter="CSV files (*.csv)") if fname: if not fname.endswith(".csv"): fname += ".csv" with open(fname, "w", encoding='utf8') as f: column_titles = [ 'mac', 'topic', 'friendly_name', 'full_topic', 'cmnd_topic', 'stat_topic', 'tele_topic', 'module', 'module_id', 'firmware', 'core' ] c = csv.writer(f) c.writerow(column_titles) for r in range(self.device_model.rowCount()): d = self.device_model.index(r, 0) c.writerow([ self.device_model.mac(d), self.device_model.topic(d), self.device_model.friendly_name(d), self.device_model.fullTopic(d), self.device_model.commandTopic(d), self.device_model.statTopic(d), self.device_model.teleTopic(d), modules.get(self.device_model.module(d)), self.device_model.module(d), self.device_model.firmware(d), self.device_model.core(d) ]) def bssid(self): BSSIdDialog().exec_() # if dlg.exec_() == QDialog.Accepted: def toggle_console_filter(self, state): self.cbxFilterDevice.setEnabled(state) if state: self.console_view.setModel(self.sorted_console_model) else: self.console_view.setModel(self.console_model) def select_console_filter(self, fname): self.sorted_console_model.setFilterFixedString(fname) def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class IPLocationWidget(QWidget): axes = None p1 = None t1 = None label_items = [] markers = [] _transform = None _projection = None _ellipse = None _bisl_result = None _gt_marker = None # this will hold the reference to the ground truth marker _detections = [] _trimmed_detections = [] _dist_matrix = None center = [] # will hold the coordinates of the current center of the map start_mouse_loc = [ ] # will hold the coordinates of the mouse, used in the mouse_move_event mouse_moved = True # this is used for zooming in and out startx = 0 starty = 0 _mp_pool = None signal_start_dist_calc = pyqtSignal() signal_start_BISL_calc = pyqtSignal() signal_start_cluster_calc = pyqtSignal() signal_draw_map = pyqtSignal() def __init__(self, parent, pool): super().__init__() self._parent = parent self._mp_pool = pool self.buildUI() def buildUI(self): # BottomTab widgets go here... self.consoleBox = QTextEdit() self.consoleBox.setReadOnly(True) # set up the map widget self.mapWidget = IPMapWidget.IPMapWidget(self) # set up the distance matrix viewer self.dm_view = IPDistanceMatrixWidget() # set up dendrogram widget self.dendrogram = IPDendrogramWidget() # set up the bisl settings widget self.bislSettings = BISLSettings(self) # set up showgroundtruth widget self.showgroundtruth = ShowGroundTruth(self) # set up association settings widget self.assocSettings = AssociationSettings(self) # right hand widgets layout holds the settings widgets rh_widget = QWidget() rh_layout = QVBoxLayout() rh_layout.addWidget(self.bislSettings) rh_layout.addWidget(self.showgroundtruth) rh_layout.addWidget(self.assocSettings) rh_layout.addStretch() rh_widget.setLayout(rh_layout) # splitter holding the association plots self.assoc_splitter = QSplitter(Qt.Vertical) self.assoc_splitter.addWidget(self.dm_view) self.assoc_splitter.addWidget(self.dendrogram) self.assoc_splitter.setSizes([100000, 100000]) # splitter holding the map canvas and the association plots self.loc_splitter = QSplitter(Qt.Horizontal) self.loc_splitter.addWidget(self.mapWidget) self.loc_splitter.addWidget(self.assoc_splitter) # large splitter holding the map, association plots, and the console self.mapSplitter = QSplitter(Qt.Vertical) self.mapSplitter.addWidget(self.loc_splitter) self.mapSplitter.addWidget(self.consoleBox) self.mainSplitter = QSplitter(Qt.Horizontal) self.mainSplitter.addWidget(self.mapSplitter) self.mainSplitter.addWidget(rh_widget) main_layout = QBoxLayout(QBoxLayout.TopToBottom) main_layout.addWidget(self.mainSplitter) self.setLayout(main_layout) self.connectSignalsAndSlots() # Create threads for the distancematrix calculation, BISL, and clustering self.dmThread = QThread() self.bislThread = QThread() self.clusterThread = QThread() def connectSignalsAndSlots(self): self.showgroundtruth.sig_groundtruth_changed.connect( self.mapWidget.plot_ground_truth) self.showgroundtruth.showGT_cb.toggled.connect( self.mapWidget.show_hide_ground_truth) self.bislSettings.run_bisl_button.clicked.connect(self.run_bisl) self.bislSettings.update_dm_button.clicked.connect( self.calc_distance_matrix) self.bislSettings.rng_max_edit.valueChanged.connect( self.mapWidget.update_range_max) self.bislSettings.confidence_edit.valueChanged.connect( self.bislSettings.enable_update_dm_button) self.bislSettings.confidence_edit.valueChanged.connect( self.calc_conf_ellipse) self.assocSettings.dist_max_edit.valueChanged.connect( self.dm_adjust_max_distance) self.assocSettings.threshold_edit.valueChanged.connect( self.cluster_adjust_threshold) self.assocSettings.update_assoc_button.clicked.connect( self.calc_associations) self.dm_view.signal_trim_detections.connect(self.trim_detections) self.dendrogram.signal_new_colors.connect(self.dm_view.set_colors) @pyqtSlot() def detections_cleared(self): self.mapWidget.clear_plot() self._detections = [] self.mapWidget.clear_detections() self.dm_view.clear() self.dendrogram.clear_plot() self.consoleBox.clear() @pyqtSlot(list) def update_detections(self, new_detections, detection_type='ip_detections', recalc_assoc=True): if new_detections is None: return # Nothing to do if len(new_detections) < 1: self.detections_cleared() return self._detections = [] if detection_type == "ip_detections": # we need to covert to InfrasoundDetections for detection in new_detections: self._detections.append(detection.to_InfrasoundDetection()) else: for detection in new_detections: self._detections.append(detection) self.mapWidget.update_detections( self._detections, self.bislSettings.rng_max_edit.value() * 1000.0) if recalc_assoc: self.calc_distance_matrix() @pyqtSlot(list, str) def trim_detections(self, indicies, linecolor='gray'): self._trimmed_detections = [] if len(self._detections) < 1: return # nothing to do # lets pick out the detections that we want to show for index in indicies: self._trimmed_detections.append(self._detections[index]) self._trimmed_detections[-1].index = index self.mapWidget.update_detections( self._trimmed_detections, self.bislSettings.rng_max_edit.value() * 1000., linecolor=linecolor) def run_bisl(self): if self._trimmed_detections is None: self.errorPopup( "no detections loaded. \n You need at least two detections to run BISL." ) return # nothing to do if len(self._trimmed_detections) < 2: self.errorPopup( "not enough detections loaded. \n You need two or more detections to run BISL." ) return # you need at least 2 detections to calculate the dist matrix self.bisl_workerObject = BISLWorkerObject( self._trimmed_detections, beam_width=self.bislSettings.bm_width_edit.value(), rad_min=self.bislSettings.rad_min_edit.value(), rad_max=self.bislSettings.rad_max_edit.value(), rng_max=self.bislSettings.rng_max_edit.value(), resol=self.bislSettings.resolution_edit.value()) self.bisl_workerObject.moveToThread(self.bislThread) self.signal_start_BISL_calc.connect(self.bisl_workerObject.run) self.bisl_workerObject.signal_runFinished.connect( self.bisl_run_finished) # start the thread self.consoleBox.setText("...Calculating...") self.bislThread.start() self.signal_start_BISL_calc.emit() @pyqtSlot(dict) def bisl_run_finished(self, result): self._bisl_result = result self.consoleBox.setText( bisl.summarize(result, self.bislSettings.confidence_edit.value())) self.calc_conf_ellipse(self.bislSettings.confidence_edit.value()) @pyqtSlot(int) def calc_conf_ellipse(self, confidence): if self._bisl_result is None: return # nothing to plot conf_dx, conf_dy = bisl.calc_conf_ellipse([0.0, 0.0], [ self._bisl_result['EW_stdev'], self._bisl_result['NS_stdev'], self._bisl_result['covar'] ], confidence) # tell the mapWidget to plot the results self.mapWidget.plot_bisl_result(self._bisl_result['lon_mean'], self._bisl_result['lat_mean']) self.mapWidget.plot_conf_ellipse(self._bisl_result['lon_mean'], self._bisl_result['lat_mean'], conf_dx, conf_dy) @pyqtSlot() def calc_distance_matrix(self): if len(self._detections) < 1: self.errorPopup( "No detections loaded.\n You need two or more detections to calculate a distance matrix." ) return # nothing to do if len(self._detections) < 2: # self.errorPopup("not enough detections loaded. \n You need 2 or more detections to calculate a distance matrix.") return # you need at least 2 detections to calculate the dist matrix self.dist_matrix = None self.dm_workerObject = DistanceMatrixWorkerObject( self._detections, beam_width=self.bislSettings.bm_width_edit.value(), rng_max=self.bislSettings.rng_max_edit.value(), rad_min=self.bislSettings.rad_min_edit.value(), rad_max=self.bislSettings.rad_max_edit.value(), resol=self.bislSettings.resolution_edit.value(), pool=self._mp_pool) self.dm_workerObject.moveToThread(self.dmThread) self.signal_start_dist_calc.connect(self.dm_workerObject.run) self.dm_workerObject.signal_runFinished.connect(self.dm_run_finished) # start the thread self.dmThread.start() self.signal_start_dist_calc.emit() self.dm_view.showCalculatingText() self.bislSettings.update_dm_button.setEnabled(False) @pyqtSlot(np.ndarray) def dm_run_finished(self, data): self.dist_matrix_orig = data # keep this around incase someone twiddles with the max_distance setting self.dm_adjust_max_distance() self.dm_view.hideCalculatingText() if self.dist_matrix is not None: self.dm_view.set_data(self.dist_matrix) # Now that the distance matrix is set, calculate the association dendrogram self.calc_associations() def dm_adjust_max_distance(self): self.dist_matrix = self.dist_matrix_orig.copy() self.dist_matrix[ self.dist_matrix_orig > self.assocSettings.dist_max_edit.value( )] = self.assocSettings.dist_max_edit.value() self.assocSettings.update_assoc_button.setEnabled(True) def cluster_adjust_threshold(self): self.assocSettings.update_assoc_button.setEnabled(True) @pyqtSlot() def calc_associations(self): if self.dist_matrix is None: self.errorPopup("No distance matrix...I need a distance matrix") return # Nothing to do self.cluster_workerObject = ClusterWorkerObject( self.dist_matrix, threshold=self.assocSettings.threshold_edit.value()) self.cluster_workerObject.moveToThread(self.clusterThread) self.signal_start_cluster_calc.connect(self.cluster_workerObject.run) self.cluster_workerObject.signal_runFinished.connect( self.cluster_run_finished) # start the thread self.clusterThread.start() self.signal_start_cluster_calc.emit() self.assocSettings.update_assoc_button.setEnabled(False) @pyqtSlot(np.ndarray, np.ndarray) def cluster_run_finished(self, links, labels): self.dendrogram.set_data(links, self.assocSettings.threshold_edit.value()) # Sort the distance matrix using the labels det_cnt = len(self.dist_matrix) sorting = np.array([]) for n in range(max(labels + 1)): sorting = np.concatenate( (sorting, np.arange(det_cnt)[labels == n])) sorting = sorting.astype(int) distance_matrix_sorted = np.empty_like(self.dist_matrix) for n1 in range(det_cnt): for n2 in range(det_cnt): distance_matrix_sorted[n1][n2] = self.dist_matrix[sorting[n1], sorting[n2]] self.dm_view.set_data(distance_matrix_sorted, labels) self.update_detections(self._detections, detection_type='detections', recalc_assoc=False) def errorPopup(self, message): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText(message) msgBox.setWindowTitle("Oops...") msgBox.exec_() def saveWindowGeometrySettings(self): self._parent.settings.beginGroup('LocationWidget') self._parent.settings.setValue("windowSize", self.size()) self._parent.settings.setValue("windowPos", self.pos()) self._parent.settings.setValue("mapSplitterSettings", self.mapSplitter.saveState()) self._parent.settings.setValue("mainSplitterSettings", self.mainSplitter.saveState()) self._parent.settings.setValue("assocSplitterSettings", self.assoc_splitter.saveState()) self._parent.settings.setValue("loc_splitterSettings", self.loc_splitter.saveState()) self._parent.settings.endGroup() def restoreWindowGeometrySettings(self): # Restore settings self._parent.settings.beginGroup('LocationWidget') mapSplitterSettings = self._parent.settings.value( "mapSplitterSettings") if mapSplitterSettings: self.mapSplitter.restoreState(mapSplitterSettings) mainSplitterSettings = self._parent.settings.value( "mainSplitterSettings") if mainSplitterSettings: self.mainSplitter.restoreState(mainSplitterSettings) assocSplitterSettings = self._parent.settings.value( "assocSplitterSettings") if assocSplitterSettings: self.assoc_splitter.restoreState(assocSplitterSettings) locSplitterSettings = self._parent.settings.value( "loc_splitterSettings") if locSplitterSettings: self.loc_splitter.restoreState(locSplitterSettings) self._parent.settings.endGroup()
class SubTabWidget(QWidget): _tabChanged = pyqtSignal(int, name = "tabChanged") def __init__(self, subtitleData, videoWidget, parent = None): super(SubTabWidget, self).__init__(parent) self._subtitleData = subtitleData self.__initTabWidget(videoWidget) def __initTabWidget(self, videoWidget): settings = SubSettings() mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(0, 0, 0, 0) mainLayout.setSpacing(0) #TabBar self.tabBar = QTabBar(self) # Splitter (bookmarks + pages) self.splitter = QSplitter(self) self.splitter.setObjectName("sidebar_splitter") self._toolbox = ToolBox(self._subtitleData, self) self._toolbox.setObjectName("sidebar") self._toolbox.setMinimumWidth(100) self._toolbox.addTool(Details(self._subtitleData, self)) self._toolbox.addTool(Synchronizer(videoWidget, self._subtitleData, self)) self._toolbox.addTool(History(self)) self.rightWidget = QWidget() rightLayout = QGridLayout() rightLayout.setContentsMargins(0, 0, 0, 0) self.rightWidget.setLayout(rightLayout) self._mainTab = FileList(_("Subtitles"), self._subtitleData, self) self.pages = QStackedWidget(self) rightLayout.addWidget(self.pages, 0, 0) self.tabBar.addTab(self._mainTab.name) self.pages.addWidget(self._mainTab) self.splitter.addWidget(self._toolbox) self.splitter.addWidget(self.rightWidget) self.__drawSplitterHandle(1) # Setting widgets mainLayout.addWidget(self.tabBar) mainLayout.addWidget(self.splitter) # Widgets settings self.tabBar.setMovable(True) self.tabBar.setTabsClosable(True) self.tabBar.setExpanding(False) # Don't resize left panel if it's not needed leftWidgetIndex = self.splitter.indexOf(self._toolbox) rightWidgetIndex = self.splitter.indexOf(self.rightWidget) self.splitter.setStretchFactor(leftWidgetIndex, 0) self.splitter.setStretchFactor(rightWidgetIndex, 1) self.splitter.setCollapsible(leftWidgetIndex, False) self.splitter.setSizes([250]) # Some signals self.tabBar.currentChanged.connect(self.showTab) self.tabBar.tabCloseRequested.connect(self.closeTab) self.tabBar.tabMoved.connect(self.moveTab) self._mainTab.requestOpen.connect(self.openTab) self._mainTab.requestRemove.connect(self.removeFile) self.tabChanged.connect(lambda i: self._toolbox.setContentFor(self.tab(i))) self.setLayout(mainLayout) def __addTab(self, filePath): """Returns existing tab index. Creates a new one if it isn't opened and returns its index otherwise.""" for i in range(self.tabBar.count()): widget = self.pages.widget(i) if not widget.isStatic and filePath == widget.filePath: return i tab = SubtitleEditor(filePath, self._subtitleData, self) newIndex = self.tabBar.addTab(self._createTabName(tab.name, tab.history.isClean())) tab.history.cleanChanged.connect( lambda clean: self._cleanStateForFileChanged(filePath, clean)) self.pages.addWidget(tab) return newIndex def __drawSplitterHandle(self, index): splitterHandle = self.splitter.handle(index) splitterLayout = QVBoxLayout(splitterHandle) splitterLayout.setSpacing(0) splitterLayout.setContentsMargins(0, 0, 0, 0) line = QFrame(splitterHandle) line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) splitterLayout.addWidget(line) splitterHandle.setLayout(splitterLayout) def _createTabName(self, name, cleanState): if cleanState is True: return name else: return "%s +" % name def _cleanStateForFileChanged(self, filePath, cleanState): page = self.tabByPath(filePath) if page is not None: for i in range(self.tabBar.count()): if self.tabBar.tabText(i)[:len(page.name)] == page.name: self.tabBar.setTabText(i, self._createTabName(page.name, cleanState)) return def saveWidgetState(self, settings): settings.setState(self.splitter, self.splitter.saveState()) settings.setHidden(self._toolbox, self._toolbox.isHidden()) def restoreWidgetState(self, settings): self.showPanel(not settings.getHidden(self._toolbox)) splitterState = settings.getState(self.splitter) if not splitterState.isEmpty(): self.splitter.restoreState(settings.getState(self.splitter)) @pyqtSlot(str, bool) def openTab(self, filePath, background=False): if self._subtitleData.fileExists(filePath): tabIndex = self.__addTab(filePath) if background is False: self.showTab(tabIndex) else: log.error(_("SubtitleEditor not created for %s!" % filePath)) @pyqtSlot(str) def removeFile(self, filePath): tab = self.tabByPath(filePath) command = RemoveFile(filePath) if tab is not None: index = self.pages.indexOf(tab) if self.closeTab(index): self._subtitleData.execute(command) else: self._subtitleData.execute(command) @pyqtSlot(int) def closeTab(self, index): tab = self.tab(index) if tab.canClose(): widgetToRemove = self.pages.widget(index) self.tabBar.removeTab(index) self.pages.removeWidget(widgetToRemove) widgetToRemove.deleteLater() return True return False def count(self): return self.tabBar.count() def currentIndex(self): return self.tabBar.currentIndex() def currentPage(self): return self.pages.currentWidget() @pyqtSlot(int, int) def moveTab(self, fromIndex, toIndex): fromWidget = self.pages.widget(fromIndex) toWidget = self.pages.widget(toIndex) if fromWidget.isStatic or toWidget.isStatic: self.tabBar.blockSignals(True) # signals would cause infinite recursion self.tabBar.moveTab(toIndex, fromIndex) self.tabBar.blockSignals(False) return else: self.pages.removeWidget(fromWidget) self.pages.removeWidget(toWidget) if fromIndex < toIndex: self.pages.insertWidget(fromIndex, toWidget) self.pages.insertWidget(toIndex, fromWidget) else: self.pages.insertWidget(toIndex, fromWidget) self.pages.insertWidget(fromIndex, toWidget) # Hack # Qt changes tabs during mouse drag and dropping. The next line is added # to prevent it. self.showTab(self.tabBar.currentIndex()) @pyqtSlot(int) def showTab(self, index): showWidget = self.pages.widget(index) if showWidget: self.pages.setCurrentWidget(showWidget) self.tabBar.blockSignals(True) self.tabBar.setCurrentIndex(index) self.tabBar.blockSignals(False) # Try to update current tab. showWidget.updateTab() self._tabChanged.emit(index) def showPanel(self, val): if val is True: self._toolbox.show() else: self._toolbox.hide() def togglePanel(self): if self._toolbox.isHidden(): self._toolbox.show() else: self._toolbox.hide() def tab(self, index): return self.pages.widget(index) def tabByPath(self, path): for i in range(self.pages.count()): page = self.tab(i) if not page.isStatic and page.filePath == path: return page return None @property def fileList(self): return self._mainTab
class MainForm(QDialog): def __init__(self, parent = None): QDialog.__init__(self, parent) # If a Nemu instance is already running, this is as far as we go self.connectToRunning() self.holdOpen = False self.menuItems = [] self.allItems = [] self.favorites = [] self.currentItem = None self.menuFile = os.path.expanduser('~/.nemu/menu') self.favoritesFile = os.path.expanduser('~/.nemu/favorites') # NOTE: If you change this, also update migrate-settings self.settingsFile = os.path.expanduser('~/.nemu/settings') self.initSettings() self.server = QLocalServer() self.server.newConnection.connect(self.handleConnection) QLocalServer.removeServer('nemuSocket') self.server.listen('nemuSocket') self.configDir = os.path.expanduser('~/.nemu') if not os.path.isdir(self.configDir): os.mkdir(self.configDir) self.menuItems = self.loadConfig(self.menuFile, self.menuItems) self.favorites = self.loadConfig(self.favoritesFile, self.favorites) # Don't load directly into self.settings so we can add new default values as needed try: tempSettings = self.loadConfig(self.settingsFile, self.settings) for key, value in tempSettings.items(): self.settings[key] = value except SystemError: print('ERROR: Failed to load settings. You may need to run migrate-settings.') raise # This should never happen, but unfortunately bugs do, so clean up orphaned items. # We need to do this because these items won't show up in the UI, but may interfere with # merges if they duplicate something that is being merged in. self.menuItems[:] = [i for i in self.menuItems if i.parent == None or i.parent in self.menuItems] # Look for broken icon paths needSave = False for i in self.menuItems + self.favorites: if not os.path.exists(i.icon): i.findIcon() needSave = True if needSave: self.saveMenu() for i in self.menuItems: if not hasattr(i, 'imported'): i.imported = False self.setupUI() self.setContextMenuPolicy(Qt.ActionsContextMenu) self.createMenu(self) self.refresh(False) if len(self.menuItems) == 0: self.firstRun() self.show() self.keepaliveTimer = QTimer(self) self.keepaliveTimer.timeout.connect(self.keepalive) self.keepaliveTimer.start(60000) def initSettings(self): self.settings = dict() self.settings['width'] = 400 self.settings['height'] = 400 self.settings['quit'] = False self.settings['imported'] = [] self.settings['iconTheme'] = None def loadConfig(self, filename, default): if os.path.exists(filename): with open(filename, 'rb') as f: data = f.read().replace('PyQt4', 'PyQt5') return cPickle.loads(data) else: return default def setupUI(self): self.resize(self.settings['width'], self.settings['height']) self.setWindowFlags(Qt.FramelessWindowHint | Qt.CustomizeWindowHint | Qt.WindowStaysOnTopHint) #self.setWindowFlags(Qt.X11BypassWindowManagerHint) self.setWindowTitle('Nemu') self.setMouseTracking(True) iconPath = os.path.join(os.path.dirname(__file__), 'images') iconPath = os.path.join(iconPath, 'nemu.png') self.setWindowIcon(IconCache()[iconPath]) self.place() self.buttonListLayout = QVBoxLayout(self) self.setMargins(self.buttonListLayout) self.buttonLayout = QHBoxLayout() self.setMargins(self.buttonLayout) # Settings and Filter box self.filterLayout = QHBoxLayout() self.settingsButton = QPushButton() self.settingsButton.setIcon(QIcon(iconPath)) self.settingsButton.setMinimumHeight(35) self.settingsButton.clicked.connect(self.settingsClicked) self.filterLayout.addWidget(self.settingsButton, 0) self.filterLabel = QLabel("Filter") self.filterLayout.addWidget(self.filterLabel) self.filterBox = QLineEdit() self.filterBox.textChanged.connect(self.refresh) self.filterLayout.addWidget(self.filterBox) self.sizeGrip = QSizeGrip(self) self.sizeGrip.setMinimumSize(QSize(25, 25)) self.filterLayout.addWidget(self.sizeGrip, 0, Qt.AlignRight | Qt.AlignTop) self.buttonListLayout.addLayout(self.filterLayout) # Top buttons and labels self.backButton = QPushButton('Favorites') self.backButton.setMinimumHeight(35) self.backButton.clicked.connect(self.backClicked) self.buttonLayout.addWidget(self.backButton, 1) self.currentLabel = QLabel() self.currentLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.buttonLayout.addWidget(self.currentLabel, 1) self.buttonListLayout.addLayout(self.buttonLayout, 0) # Menu item display self.listSplitter = QSplitter() self.buttonListLayout.addWidget(self.listSplitter, 1) self.leftList = ListWidget(self.clearListMouseOver) self.listSplitter.addWidget(self.leftList) self.rightList = ListWidget(self.clearListMouseOver) self.listSplitter.addWidget(self.rightList) # Has to be done after adding widgets to the splitter or the size will get reset again if 'splitterState' in self.settings: self.listSplitter.restoreState(self.settings['splitterState']) def setMargins(self, layout, margin = 0): layout.setSpacing(margin) layout.setContentsMargins(margin, margin, margin, margin) def createMenu(self, widget): addFavoriteAction = QAction('Add to Favorites', self) addFavoriteAction.triggered.connect(self.addFavoriteClicked) widget.insertAction(None, addFavoriteAction) addAction = QAction("New...", self) addAction.triggered.connect(self.newClicked) widget.insertAction(None, addAction) editAction = QAction("Edit...", self) editAction.triggered.connect(self.editClicked) widget.insertAction(None, editAction) deleteAction = QAction("Delete", self) deleteAction.triggered.connect(self.deleteClicked) widget.insertAction(None, deleteAction) def hideOrClose(self): if self.settings['quit']: self.close() else: self.hide() def closeEvent(self, event): self.saveSettings() def hideEvent(self, event): self.releaseMouse() self.saveSettings() def mouseMoveEvent(self, event): if self.hasMouse(): self.releaseMouse() def leaveEvent(self, event): # If we set holdOpen, it means that we've opened a dialog, so we shouldn't grab if not self.hasMouse(): self.grabMouse() def mousePressEvent(self, event): if not self.hasMouse(): self.hideOrClose() def hasMouse(self): return self.geometry().contains(QCursor.pos()) def saveSettings(self): self.settings['splitterState'] = self.listSplitter.saveState() self.settings['width'] = self.width() self.settings['height'] = self.height() with open(self.settingsFile, 'wb') as f: cPickle.dump(self.settings, f) def place(self): desktop = qApp.desktop() screenSize = desktop.availableGeometry(QCursor.pos()) self.move(screenSize.x(), screenSize.y() + screenSize.height() - self.height()) def newClicked(self): form = AddForm() self.holdOpen = True form.exec_() self.checkMouse() self.holdOpen = False if form.accepted: item = MenuItem() item.name = form.name item.command = form.command item.working = form.working item.folder = form.folder item.icon = form.icon item.findIcon() clicked = self.getClicked() if clicked: parent = clicked.item.parent elif self.leftList.mouseOver: if self.currentItem != None: parent = self.currentItem.parent else: parent = None else: parent = self.currentItem item.parent = parent self.menuItems.append(item) self.refresh() def editClicked(self): form = AddForm() clicked = self.getClicked() if clicked == None: return item = clicked.item form.name = item.name form.command = item.command form.working = item.working form.folder = item.folder form.icon = item.icon form.populateFields() self.holdOpen = True form.exec_() self.checkMouse() self.holdOpen = False if form.accepted: item.name = form.name item.command = form.command item.working = form.working item.folder = form.folder item.icon = form.icon item.imported = False item.findIcon() self.refresh() def checkMouse(self): if not self.hasMouse(): self.grabMouse() def deleteClicked(self): clicked = self.getClicked() if clicked == None: return self.delete(clicked.item) self.refresh() # Delete item and all of its children so we don't leave around orphaned items def delete(self, item): for i in self.menuItems: if i.parent == item: i.deleted = True if item in self.menuItems: item.deleted = True item.imported = False if item in self.favorites: self.favorites.remove(item) def addFavoriteClicked(self): newFavorite = copy.copy(self.getClicked().item) newFavorite.parent = None self.favorites.append(newFavorite) self.refresh() def getClicked(self): for i in self.allItems: if i.mouseOver: return i def clearMouseOver(self): for i in self.allItems: i.mouseOver = False def clearListMouseOver(self): self.leftList.mouseOver = False self.rightList.mouseOver = False def refresh(self, save = True): self.leftList.clear() self.rightList.clear() self.allItems = [] sortedLeft = [] sortedRight = [] self.updateFilter() if self.currentItem != None: currParent = self.currentItem.parent for i in self.menuItems: if i.parent == currParent and not i.deleted and i.matchedFilter: sortedLeft.append(i) else: for i in self.favorites: sortedLeft.append(i) for i in self.menuItems: if i.parent == self.currentItem and not i.deleted and i.matchedFilter: sortedRight.append(i) sortedLeft.sort(key = lambda x: x.name) sortedLeft.sort(key = lambda x: not x.folder) sortedRight.sort(key = lambda x: x.name) sortedRight.sort(key = lambda x: not x.folder) for i in sortedLeft: self.leftList.add(self.createItem(i)) for i in sortedRight: self.rightList.add(self.createItem(i)) if save: self.saveMenu() def saveMenu(self): # Save the current menu status with open(self.menuFile, 'wb') as f: cPickle.dump(self.menuItems, f) with open(self.favoritesFile, 'wb') as f: cPickle.dump(self.favorites, f) def createItem(self, item): newItem = ListItem(item, self.clearMouseOver) newItem.clicked.connect(self.itemClicked) self.allItems.append(newItem) return newItem def updateFilter(self): filterValue = str(self.filterBox.text()) for i in self.menuItems: i.checkFilter(filterValue) def itemClicked(self): sender = self.sender() if sender.item.folder: self.setCurrentItem(sender.item) self.refresh(False) else: flags = ['f', 'F', 'u', 'U', 'd', 'D', 'n', 'N', 'i', 'k', 'v', 'm'] command = sender.item.command for i in flags: command = command.replace('%' + i, '') # %c needs a proper value in some cases command = command.replace('%c', '"%s"' % sender.item.name) working = sender.item.working if not os.path.isdir(working): working = None # Need to redirect stdout and stderr so if the process writes something it won't fail with open(os.path.devnull, 'w') as devnull: Popen(command + '&', stdout=devnull, stderr=devnull, shell=True, cwd=working) self.hideOrClose() def backClicked(self): if self.currentItem: self.setCurrentItem(self.currentItem.parent) self.refresh(False) def setCurrentItem(self, item): self.currentItem = item if item != None: self.currentLabel.setText(item.name) if item.parent != None: self.backButton.setText(item.parent.name) else: self.backButton.setText('Favorites') else: self.currentLabel.setText('') self.backButton.setText('Favorites') def settingsClicked(self): form = SettingsForm(self) form.quitCheck.setChecked(self.settings['quit']) theme = self.settings.get('iconTheme') if theme: form.themeCombo.setCurrentIndex(form.themeCombo.findText(theme)) self.holdOpen = True form.exec_() self.checkMouse() self.holdOpen = False if form.accepted: self.settings['quit'] = form.quitCheck.isChecked() def firstRun(self): QMessageBox.information(self, 'First Time?', 'Your menu is currently empty. It is recommended that you import an existing menu file.') self.settingsClicked() def connectToRunning(self): self.socket = QLocalSocket() self.socket.connectToServer('nemuSocket') self.socket.waitForConnected(1000) if self.socket.state() == QLocalSocket.ConnectedState: print 'Server found' if self.socket.waitForReadyRead(3000): line = self.socket.readLine() print line else: print self.socket.errorString() sys.exit() else: print 'No server running' def handleConnection(self): import datetime print "Got connection", datetime.datetime.now() connection = self.server.nextPendingConnection() connection.write('connected') del connection self.setCurrentItem(None) self.filterBox.setText('') self.refresh(False) self.show() print "Showed", datetime.datetime.now() return # Call periodically to keep data resident in memory (hopefully) def keepalive(self): if self.isHidden(): self.refresh(False)
class MainWindow(QMainWindow): def __init__(self, settings): super().__init__() # Save the settings object for now and shutdown time. self.settings = settings # Initialize extras and dicts paths first, as other modules use them paths.initialize(settings) # Initialize our font db fonts.initialize(settings) # Set our font, which will propogate to our child widgets. fonts.notify_me(self._font_change) # ask for a signal self._font_change(False) # fake a signal now # Initialize the dictionary apparatus dictionaries.initialize(settings) # Initialize the color choices colors.initialize(settings) # Initialize the sequence number for opened files self.book_number = 0 # Initialize the path to the last-opened file, used to # start file-open dialogs. self.last_open_path = '.' # Initialize our dict of active panels self.panel_dict = PANEL_DICT.copy() # Initialize our dict of open documents {seqno:Book} self.open_books = {} self.focus_book = None # seqno of book in focus, see _focus_me # Initialize the list of recent files self.recent_files = [] # Create the main window and set up the menus. self._uic() # Initialize the set of files actually open when we shut down. last_session = self._read_flist('mainwindow/open_files') if len(last_session) : # there were some files open if len(last_session) == 1 : msg = _TR('Start-up dialog', 'One book was open at the end of the last session.') else: msg = _TR('Start-up dialog', '%n books were open at the end of the last session.', n=len(last_session) ) info = _TR("Start-up dialog", "Click OK to re-open all") if utilities.ok_cancel_msg( msg, info) : for file_path in last_session : ftbs = utilities.path_to_stream(file_path) if ftbs : self._open(ftbs) if 0 == len(self.open_books) : # We did not re-open any books, either because there were # none, or the user said No, or perhaps they were not found. self._new() # open one, new, book. # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Slot to receive the currentChanged signal from the editview tabset. # Look through self.open_books and find the one whose edit widget is # now current, and do a focus_me for it. def _editview_change(self, index): if index > -1 : eview = self.editview_tabset.widget(index) for (seqno, book) in self.open_books.items() : if eview == book.get_edit_view() : self.focus_me(seqno) return mainwindow_logger.error('cannot relate editview tab index to book') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Make a selected book the focus of all panels. This is called explicitly # when a book is first created, and when the editview tabset changes the # current selection. It is called also when an editview gets a focus-in # event. # Display that Book's various "-view" objects in panels, in the order # that the user left them and with the same active panel as before. Note # that a book (editview) can get a focus-in event when it was already the # focus in this sense, for example if this app was hidden and then # brought to the front. So be prepared for redundant calls. def focus_me(self, book_index): outgoing = self.focus_book if book_index == outgoing : return # redundant call mainwindow_logger.debug( 'focusing {0} = {1}'.format(book_index,self.open_books[book_index].get_book_name()) ) self.focus_book = book_index # Record the user's arrangement of panels for the outgoing book, # as a list of tuples ('tabname', widget) in correct sequence. if outgoing is not None : # false first time and after File>Close out_panel_dict = self.open_books[outgoing].panel_dict widg_list = [] for ix in range( self.panel_tabset.count() ): widg_list.append ( (self.panel_tabset.tabText(ix), self.panel_tabset.widget(ix)) ) out_panel_dict['tab_list'] = widg_list out_panel_dict['current'] = self.panel_tabset.currentIndex() # Change all the panels to the widgets, in the sequence, of the new book in_panel_dict = self.open_books[book_index].panel_dict widg_list = in_panel_dict['tab_list'] self.panel_tabset.clear() for ix in range( len(widg_list) ): (tab_text, widget) = widg_list[ix] self.panel_tabset.insertTab(ix, widget, tab_text) self.panel_tabset.setCurrentIndex(in_panel_dict['current']) self.editview_tabset.setCurrentIndex( self.editview_tabset.indexOf( self.open_books[book_index].get_edit_view() ) ) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Called by the current book to make a particular tab the visible one, # e.g. to make the Find visible on a ^F. The argument is a widget # that should occupy one of the current tabs. Ask the tabset for its # index, and if it is found, make that the current index. (If it is # not found, log it and do nothing.) def make_tab_visible(self, tabwidg): ix = self.panel_tabset.indexOf(tabwidg) if ix >= 0 : # widget exists in this tabset self.panel_tabset.setCurrentIndex(ix) return mainwindow_logger.error('Request to show nonexistent widget') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>New: # Create a Book object # Call its new_empty() method, # Add it to the open_books dict keyed by its sequence number, # Display its text editor in a tab with the document name, and # Give it the focus. def _new(self): seq = self.book_number self.book_number += 1 new_book = book.Book( seq, self ) new_book.new_empty() self.open_books[seq] = new_book index = self.editview_tabset.addTab( new_book.get_edit_view(), new_book.get_book_name() ) self.editview_tabset.setTabToolTip(index, _TR('Tooltip of edit of new unsaved file', 'this file has not been saved') ) self.focus_me(seq) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Quick check to see if a file path is already open. Called from _open # and from _build_recent (menu). Returned value is the sequence number # of the open book, or None. def _is_already_open(self, path): for (seq, book_object) in self.open_books.items(): if path == book_object.get_book_full_path() : return seq return None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement File>Open. Dialog with the user (file dialog starts with # last-used book path). Result is None or a FileBasedTextStream that we # pass to _open(). def _file_open(self) : fbts = utilities.ask_existing_file( _TR( 'File:Open dialog','Select a book file to open'), parent=self, starting_path=self.last_open_path) if fbts : # yes a readable file was chosen. self._open( fbts ) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Open a file, given the document as a FileBasedTextStream # * If file opened is fname.meta, look for a file named fname; if it # exists open it instead, e.g. given foo.txt.meta, open foo.txt. # If it doesn't exist, tell the user and exit. # * If a file of the same name and path is already open, just focus # it and exit. # * Determine if there is a .meta file, a .bin file, or neither # * Create a metadata input stream if possible # * If no .meta, look for good_words and bad_words # * If the only open book is an "Untitled-n" and # it is unmodified, delete it. # * Call Book.old_book() or .new_book() as appropriate # * Add this book's editview to the edit tabset # * Give this book the focus. def _open(self, fbts): # look for opening a .meta file if 'meta' == fbts.suffix(): fb2 = utilities.file_less_suffix(fbts) if fb2 is None : m1 = _TR('File:Open','Cannot open a .meta file alone') m2 = _TR('File:Open','There is no book file matching ', 'filename follows this') + fbts.filename() utilities.warning_msg(m1, m2) return # we see foo.txt with foo.txt.meta, silently open it fbts = fb2 # look for already-open file seq = self._is_already_open(fbts.fullpath()) if seq is not None : self.focus_me(seq) return # start collecting auxiliary streams gw_stream = None bw_stream = None gg_stream = None # open the metadata stream, which is always UTF-8 meta_stream = utilities.related_suffix(fbts, 'meta', encoding=C.ENCODING_UTF) if meta_stream is None : # opening book without .meta; look for .bin which is always LTN1 bin_stream = utilities.related_suffix(fbts,'bin',encoding=C.ENCODING_LATIN) if bin_stream : gg_stream = metadata.translate_bin(bin_stream,fbts) # Look for good_words.txt, bad_words.txt. gw_stream = utilities.related_file( fbts, 'good_words*.*' ) bw_stream = utilities.related_file( fbts, 'bad_words*.*' ) seq = self.book_number # If the only open book is the new one created at startup or when all # books are closed (which will have key 0), and it has not been # modified, get rid of it. if len(self.open_books) == 1 \ and 0 == list(self.open_books.keys())[0] \ and self.open_books[0].get_book_name().startswith('Untitled-') \ and not self.open_books[0].get_save_needed() : self.editview_tabset.clear() self.panel_tabset.clear() self.focus_book = None seq = 0 else: # Some other book open, or user typed into the default New one. self.book_number += 1 # Make the Book object and stow it in our open book dict a_book = book.Book( seq, self ) self.open_books[seq] = a_book if meta_stream : # opening a book we previously saved a_book.old_book( fbts, meta_stream ) else : a_book.new_book( fbts, gg_stream, gw_stream, bw_stream ) index = self.editview_tabset.addTab( a_book.get_edit_view(), a_book.get_book_name()) self.editview_tabset.setTabToolTip(index, a_book.get_book_folder() ) self.focus_me(seq) self.last_open_path = fbts.folderpath() # start for next open or save self._add_to_recent(fbts.fullpath()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Save the book that is currently in focus under its present name, if it # is modified. Return True if the save completed, else False. # If the active book is a New one, force a Save-As action instead. def _save(self): active_book = self.open_books[self.focus_book] if active_book.get_save_needed() : if active_book.get_book_name().startswith('Untitled-'): return self._save_as() doc_stream = utilities.path_to_output( active_book.get_book_full_path() ) if doc_stream : # successfully opened for output meta_stream = utilities.related_output(doc_stream,'meta') if not meta_stream: utilities.warning_msg( _TR('File:Save', 'Unable to open metadata file for writing.'), _TR('File:Save', 'Use loglevel=error for details.') ) return False else: utilities.warning_msg( _TR('File:Save', 'Unable to open book file for writing.'), _TR('File:Save', 'Use loglevel=error for details.') ) return False return active_book.save_book(doc_stream, meta_stream) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Save As. Query the user for a file path and get that as an # output FileBasedTextStream. Call the book to rename itself, which makes # it modified. Change the text in the edit tab to match. Discard the FBTS # and call _save which will make another one. def _save_as(self): active_book = self.open_books[self.focus_book] fbts = utilities.ask_saving_file( _TR('File:Save As dialog', 'Choose a new location and filename for this book' ), self, active_book.get_book_folder() ) if fbts : active_book.rename_book(fbts) self.editview_tabset.setTabText( self.editview_tabset.currentIndex(), fbts.filename() ) self._add_to_recent(fbts.fullpath()) fbts = None # discard that object return self._save() else: return False # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement Close. If the active book is modified, ask if it should # be saved. If it is 'Untitled-' that will turn into Save As. def _close(self): target_index = self.focus_book # active edit tab is to close target_book = self.open_books[target_index] if target_book.get_save_needed() : # Compose message of translated parts because _TR does not # allow for incorporating strings, only numbers. msg = _TR('File Close dialog', 'Book file ', 'filename follows here') msg += target_book.get_book_name() msg += _TR('File Close dialog', ' has been modified!', 'filename precedes this') ret = utilities.save_discard_cancel_msg( msg, info = _TR('File Close dialog', 'Save it, Discard changes, or Cancel Closing?') ) if ret is None : # Cancel return if ret : # True==Save self._save() # Now, get rid of the active book in 3 steps, # 1, close the book's tab in the editview tabset. We don't know which # tab it is, because the user can drag tabs around. i = self.editview_tabset.indexOf(target_book.get_edit_view()) # The following causes another tab to be focussed, changing self.focus_book # and saving target_book's tabs in target_book, not that we care. self.editview_tabset.removeTab(i) # 2, remove the book from our dict of open books. del self.open_books[target_index] # 3, if there are any open books remaining, the tab widget has # activated one of them by its rules, which caused a show signal and # entry to _focus_me already. However if there are no remaining books # there was no show signal or focus_me and the closed book's panels # are still in the tabset. if 0 == len(self.open_books) : self.book_number = 0 # restart the sequence self.focus_book = None self._new() # One way or the other, a focus_me has removed all references to # active_book's view panels except those in its PANEL_DICT. So the # following assignment should remove the last reference to the book, # and schedule the book and associated objects for garbage collect. target_book = None # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Implement loading and saving find panel user buttons. Start the search # for files in the active book's folder. User can navigate to extras # if need be. def _find_save(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_saving_file( _TR('File:Save Find Buttons open dialog', 'Choose file to contain find button definitions'), self, starting_path=target_book.get_book_full_path(), encoding='UTF-8') if stream : # is not None, file is open find_panel.user_button_output(stream) # else user hit cancel, forget it def _find_load(self): target_book = self.open_books[self.focus_book] find_panel = target_book.get_find_panel() stream = utilities.ask_existing_file( _TR('File:Load Find Buttons open dialog', 'Choose a file of find button definitions'), self, starting_path=target_book.get_book_full_path(), encoding='UTF-8') if stream :# is not None, we opened it find_panel.user_button_input(stream) target_book.metadata_modified(True,C.MD_MOD_FLAG) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Maintain the list of "recent" file paths. The list is kept in usage # order, so if a path is in the list now, delete it and then add it to # the front. Keep it at a max of 9 items by deleting the oldest if # necessary. def _add_to_recent(self, path): if path in self.recent_files : del self.recent_files[self.recent_files.index(path)] self.recent_files.insert(0,path) self.recent_files = self.recent_files[:9] # Upon the aboutToShow signal from the File menu, populate the Recent # submenu with a list of files, but only the ones that are currently # accessible. If one is on a volume (e.g. USB stick) and you unmount the # volume, the path should not appear in the menu until the volume is # mounted again. def _open_recent(self, path): fbts = utilities.path_to_stream(path) if fbts : self._open(fbts) def _build_recent(self): active_files = [] for path in self.recent_files: seq = self._is_already_open(path) if (seq is None) and utilities.file_is_accessible(path) : active_files.append( (utilities.file_split(path),path) ) if 0 == len(active_files): self.recent_menu.setEnabled(False) return self.recent_menu.setEnabled(True) self.recent_menu.clear() i = 1 for ((fname, folder), path) in active_files: act = self.recent_menu.addAction( '{0} {1} {2}'.format(i,fname,folder) ) act.triggered.connect( lambda: self._open_recent(path) ) i += 1 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # User has chosen a different font; if it is the general font, set # that here so it will propogate to our children. def _font_change(self, is_mono): if not is_mono: self.setFont(fonts.get_general()) # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Create the UI contained within this QMainWindow object. This is a lean # main window indeed. We have no toolbar, no status bar, no dock, # nothing. Just a splitter with, on the left, a tabset for editviews, and # on the right, a scrollbar containing a tabset for panels. (Qt Designer # note: it is not possible to build this structure with the Designer. It # will not let you put the scroll area into the splitter.) # # TODO: create a custom QTabWidget using a custom QTabBar to implement # drag-out-of-tabset behavior, and use those here. def _uic(self): # Create the tabset that displays editviews self.editview_tabset = QTabWidget() self.editview_tabset.setMovable(True) # let user move tabs around self.editview_tabset.currentChanged.connect(self._editview_change) # Create the tabset that displays find, notes, help &etc. self.panel_tabset = QTabWidget() self.panel_tabset.setMovable(True) # Create the splitter that contains the above two parts. self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.setChildrenCollapsible(False) # Give just a little margin to the left of the editor self.splitter.setContentsMargins(8,0,0,0) self.splitter.addWidget(self.editview_tabset) self.splitter.addWidget(self.panel_tabset) # Set that splitter as the main window's central (and only) widget self.setCentralWidget(self.splitter) # Populate the panel tabset with empty widgets just so there will # be tabs that _swap can reference. for key in self.panel_dict.keys(): widj = QWidget() self.panel_tabset.addTab(widj,key) self.panel_dict[key] = widj # Size and position ourself based on saved settings. self.move(self.settings.value("mainwindow/position", QPoint(50,50))) self.resize(self.settings.value("mainwindow/size", C.STARTUP_DEFAULT_SIZE)) self.splitter.restoreState( self.settings.value("mainwindow/splitter",C.STARTUP_DEFAULT_SPLITTER) ) # Store a reference to the application menubar. In Mac OS this # is a parentless menubar; other platforms it is the default. if C.PLATFORM_IS_MAC : self.menu_bar = QMenuBar() # parentless menu bar for Mac OS else : self.menu_bar = self.menuBar # refer to the default one set_menu_bar(self.menu_bar) # Create the File menu, located in our menu_bar. self.file_menu = self.menu_bar.addMenu(_TR('Menu name', '&File')) # Populate the File menu with actions. # File:New -> _new() work = self.file_menu.addAction( _TR('File menu command','&New') ) work.setShortcut(QKeySequence.New) work.setToolTip( _TR('File:New tooltip','Create a new, empty document') ) work.triggered.connect(self._new) # File:Open -> _file_open() work = self.file_menu.addAction( _TR('File menu command','&Open') ) work.setShortcut(QKeySequence.Open) work.setToolTip( _TR('File:Open tooltip','Open an existing book') ) work.triggered.connect(self._file_open) # File:Save -> _file_save() work = self.file_menu.addAction( _TR('File menu command', '&Save') ) work.setShortcut(QKeySequence.Save) work.setToolTip( _TR('File:Save tooltip','Save the active book') ) work.triggered.connect(self._save) # Save As -> _file_save_as() work = self.file_menu.addAction( _TR('File menu command', 'Save &As') ) work.setShortcut(QKeySequence.SaveAs) work.setToolTip( _TR('File:Save As tooltip','Save the active book under a new name') ) work.triggered.connect(self._save_as) # Close -> _close() work = self.file_menu.addAction( _TR('File menu command', 'Close') ) work.setShortcut(QKeySequence.Close) work.setToolTip( _TR('File:Close tooltip', 'Close the active book') ) work.triggered.connect(self._close) # Load Find Buttons -> _find_load() work = self.file_menu.addAction( _TR('File menu command', 'Load Find Buttons') ) work.setToolTip( _TR('File:Load Find Buttons tooltip', 'Load a file of definitions for the custom buttons in the Find panel' ) ) work.triggered.connect(self._find_load) # Save Find Buttons -> _find_save() work = self.file_menu.addAction( _TR('File menu command', 'Save Find Buttons') ) work.setToolTip( _TR('File:Save Find Buttons tooltip', 'Save definitions of the custom buttons in the Find panel' ) ) work.triggered.connect(self._find_save) # Open Recent gets a submenu that is added to the File menu. # The aboutToShow signal is connected to our _build_recent slot. self.recent_menu = QMenu( _TR('Sub-menu name', '&Recent Files') ) work = self.file_menu.addMenu( self.recent_menu ) work.setToolTip( _TR('File:Recent tooltip', 'List of recently-used files to open') ) self.file_menu.aboutToShow.connect(self._build_recent) # divider if not Mac if not C.PLATFORM_IS_MAC: self.file_menu.addSeparator() # TODO Preferences with the menu role that on mac, moves to the app menu # Quit with the menu role that moves it to the app menu work = QAction( _TR('Quit command','&Quit'), self ) work.setMenuRole(QAction.QuitRole) work.setShortcut(QKeySequence.Quit) work.triggered.connect(self.close) self.file_menu.addAction(work) # Initialize the list of "recent" files for the File sub-menu. # These files were not necessarily open at shutdown, just sometime # in the not too distant past. self.recent_files = self._read_flist('mainwindow/recent_files') # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Functions related to shutdown and management of settings. # # Factor out the job of reading/writing a list of files in the settings. # Input is a settings array key string like 'mainwindow/recent_files' # Output is a possibly empty list of canonical-file-path strings. def _read_flist(self, array_key): f_list = [] f_count = self.settings.beginReadArray(array_key) for f in range(f_count): # which may be 0 self.settings.setArrayIndex(f) f_list.append( self.settings.value('filepath') ) self.settings.endArray() return f_list # Input is an array key and a possibly empty list of path strings def _write_flist(self, file_list, array_key): if len(file_list): self.settings.beginWriteArray( array_key, len(file_list) ) for f in range(len(file_list)) : self.settings.setArrayIndex( f ) self.settings.setValue( 'filepath',file_list[f] ) self.settings.endArray() # Reimplement QWidget.closeEvent in order to save any open files # and update the settings. def closeEvent(self, event): # If there are any unsaved books, ask the user if they should be # saved. If the answer is yes, try to do so. unsaved = [] for (seq, book_object) in self.open_books.items() : if book_object.get_save_needed() : unsaved.append(seq) if len(unsaved): if len(unsaved) == 1 : msg = _TR('Shutdown message', 'There is one unsaved file') else : msg = _TR('Shutdown message', 'There are %n unsaved files', n=len(unsaved)) ret = utilities.save_discard_cancel_msg( msg, _TR('Shutdown message', 'Save, Discard changes, or Cancel Quit?') ) if ret is None : # user wants to cancel shutdown event.ignore() return if ret : # User want to save. Focus each unsaved file and call _save. # For all but "Untitled-n" documents this will be silent. For # those, it will open a save-as dialog. We ignore the return # from this because we cannot distinguish between a cancelled # file-open dialog and a file write error. for seq in unsaved : self.focus_me(seq) self._save() # Clear the settings so that old values don't hang around self.settings.clear() # Tell the submodules to save their current global values. colors.shutdown(self.settings) fonts.shutdown(self.settings) dictionaries.shutdown(self.settings) paths.shutdown(self.settings) # Save the list of currently-open files in the settings, but do not # save any whose filename matches "Untitled-#" because that is an # unsaved New file (which the user chose not to save, above). open_paths = [] for (index, book_obj) in self.open_books.items() : if not book_obj.get_book_name().startswith('Untitled-'): open_paths.append( book_obj.get_book_full_path() ) self._write_flist( open_paths, 'mainwindow/open_files' ) # Save the list of "recent" files in the settings. self._write_flist(self.recent_files, 'mainwindow/recent_files') # Save this window's position and size and splitter state self.settings.setValue("mainwindow/size",self.size()) self.settings.setValue("mainwindow/position",self.pos()) self.settings.setValue("mainwindow/splitter",self.splitter.saveState()) # and that's it, we are done finished, over & out. event.accept()
class MainWindow(QMainWindow): def __init__(self, all_actions): super().__init__() self._actions = all_actions self._splitter = QSplitter() self.setCentralWidget(QFrame(self)) QHBoxLayout(self.centralWidget()).addWidget(self._splitter) self.menuBar().addMenu(ManageMenu(self)) self.menuBar().addMenu(EditMenu(self)) self.menuBar().addMenu(NavigateMenu(self)) self.addToolBar(ManageToolBar(self)) self.addToolBar(EditToolBar(self)) self.addToolBar(NavigateToolBar(self)) self.setStatusBar(StatusBar(self)) self._tree = Tree(self._splitter) self._editor = Editor(self._splitter) self._reference = Reference(self._splitter) self._parseTimer = QTimer(interval=1000) self.statusBar().gotoError.connect(self._editor.setCursorPosition) self.statusBar().gotoError.connect(lambda *_: self._editor.setFocus(Qt.OtherFocusReason)) self.statusBar().errorChanged.connect(self._editor.setParseError) self._editor.modificationChanged.connect(self.onModificationChanged) self._editor.cursorPositionChanged.connect(self.statusBar().setCursorPosition) self._editor.textChanged.connect(self._parseTimer.start) self._openScript = None self._confirmClose = ConfirmCloseMessage(self).exec self._parseTimer.timeout.connect(self.onParseTimer) self.onModificationChanged() def show(self, desktop): settings = QSettings() try: self.restoreGeometry(settings.value('windows/main/geometry')) except TypeError: geometry = desktop.availableGeometry() size = geometry.size() size = QSize(size.width() * 0.75, size.height() * 0.75) self.setGeometry(QStyle.alignedRect(Qt.LeftToRight, Qt.AlignCenter, size, geometry)) try: self.restoreState(settings.value('windows/main/window-state')) except TypeError: pass try: self._splitter.restoreState(settings.value('windows/main/splitter-state')) except TypeError: pass super().show() def action(self, action_type): return self._actions[action_type] def tree(self): return self._tree def editor(self): return self._editor def reference(self): return self._reference def openScript(self): return self._openScript def setOpenScript(self, item, content='', *, force=False): if self._openScript: if not force and not self._confirmClose(): self._editor.setFocus(Qt.OtherFocusReason) return False self._openScript.open = False self._openScript = None self._editor.close() if item: self._editor.open(content) self._openScript = item self._openScript.open = True self.statusBar().setScript(self._openScript) self.onModificationChanged(False) return True def onModificationChanged(self, isModified=False): title = 'Strainer' if self._openScript: title = f"{title} ({self._openScript.parent().name}: {self._openScript.name}{'*' if isModified else ''})" self.setWindowTitle(title) def onParseTimer(self): self._parseTimer.stop() self.statusBar().parseScript(bytes(self._editor.bytes(0, self._editor.length())[:-1])) def closeEvent(self, event): if self._confirmClose(): settings = QSettings() settings.setValue('windows/main/splitter-state', self._splitter.saveState()) settings.setValue('windows/main/window-state', self.saveState()) settings.setValue('windows/main/geometry', self.saveGeometry()) event.accept() else: event.ignore()