class SpectateBattle(QWidget): def __init__(self): super(SpectateBattle, self).__init__() self.exitButton = QPushButton('Exit to menu') self.exitButton.clicked.connect(self.interruptBattle) self.menu = BotBattleMenu() self.menu.startButton.clicked.connect(self.startBattle) self.battle = None self.stack = QStackedWidget() self.stack.addWidget(self.menu) layout = QVBoxLayout() layout.addWidget(self.stack) layout.addWidget(self.exitButton) self.setLayout(layout) def startBattle(self): bot1 = game.players.ai.select_bot(self.menu.bot1Level) bot2 = game.players.ai.select_bot(self.menu.bot2Level) self.battle = BotBattle(bot1, bot2) self.stack.addWidget(self.battle) self.stack.setCurrentWidget(self.battle) def interruptBattle(self): if self.battle: self.battle.interrupt()
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.centralWidget = QStackedWidget() self.setCentralWidget(self.centralWidget) startWidget = StartWidget(self) self.centralWidget.addWidget(startWidget) self.setWindowTitle("Mountain King") self.resize(210, 50) def host(self): hostWidget = HostWidget(self) self.centralWidget.addWidget(hostWidget) self.centralWidget.setCurrentWidget(hostWidget) self.setWindowTitle("Hosting Mountain King") self.resize(255, 50) def join(self): joinWidget = JoinWidget(self) self.centralWidget.addWidget(joinWidget) self.centralWidget.setCurrentWidget(joinWidget) self.setWindowTitle("Joining Mountain King") self.resize(255, 50)
class MultiPlayer(QWidget): def __init__(self): super(MultiPlayer, self).__init__() self.exitButton = QPushButton('Exit to menu') self.exitButton.clicked.connect(self.exit) self.menu = MultiPlayerMenu() self.menu.connectButton.clicked.connect(self.connect) self.menu.hostButton.clicked.connect(self.host) self.menu.hotseatButton.clicked.connect(self.hotseat) self.stack = QStackedWidget() self.stack.addWidget(self.menu) layout = QVBoxLayout() layout.addWidget(self.stack) layout.addWidget(self.exitButton) self.setLayout(layout) self.clientGame = None self.serverGame = None self.hotseatGame = None def host(self): name = self.menu.nameField.text() port = self.menu.portSpinBox.value() self.serverGame = ServerGame(name, port) self.showGame(self.serverGame) def connect(self): name = self.menu.nameField.text() ip = self.menu.ipField.text() port = self.menu.portSpinBox.value() self.clientGame = ClientGame(name, ip, port) self.showGame(self.clientGame) def hotseat(self): self.hotseatGame = HotSeatGame() self.showGame(self.hotseatGame) def showGame(self, game): self.stack.addWidget(game) self.stack.setCurrentWidget(game) def exit(self): self.stack.setCurrentWidget(self.menu) if self.serverGame: self.serverGame.end() if self.clientGame: self.clientGame.end()
def initUI(self): # create menus menu.GuiMenu() menu.WidgetMenu() menu.SettingsMenu() menu.LoggingMenu() # main layout mainVLayout = QVBoxLayout() elementHLayouts = [] # top element: toolbar, status t = QLabel("Toolbar") s = QLabel("Status") toolbarStatusHLayout = QHBoxLayout() toolbarStatusHLayout.addWidget(t) toolbarStatusHLayout.addWidget(s) elementHLayouts.append(toolbarStatusHLayout) # mid element: menu buttons menubuttonsHLayout = QHBoxLayout() stackedButtonsWidget = QStackedWidget() buttonGroup = QButtonGroup(self) buttonGroup.setExclusive(True) for m in menu.Menu.menus: btn = QPushButton(m.name) btn.setCheckable(True) btn.pressed.connect(lambda m=m: stackedButtonsWidget.setCurrentWidget(m)) menubuttonsHLayout.addWidget(btn) buttonGroup.addButton(btn) stackedButtonsWidget.addWidget(m) elementHLayouts.append(menubuttonsHLayout) # bot element: menu specific widgets menuwidgetsHLayout = QHBoxLayout() menuwidgetsHLayout.addWidget(stackedButtonsWidget) elementHLayouts.append(menuwidgetsHLayout) # click first button for defined initial state if len(buttonGroup.buttons()) > 0: buttonGroup.buttons()[0].click() for l in elementHLayouts: mainVLayout.addLayout(l) self.setLayout(mainVLayout)
class MainGame(QWidget): def __init__(self): super(MainGame, self).__init__() self.menu = MainGameMenu() self.menu.singlePlayerButton.clicked.connect(self.startSinglePlayer) self.menu.multiPlayerButton.clicked.connect(self.startMultiPlayer) self.menu.spectateButton.clicked.connect(self.startSpectateBattle) self.stack = QStackedWidget() self.stack.addWidget(self.menu) layout = QVBoxLayout() layout.addWidget(self.stack) self.setLayout(layout) self.setWindowTitle('Ultimate tic-tac-toe') self.resize(400, 200) self.singlePlayer = None self.multiPlayer = None self.spectate = None self.showMenu() def showMenu(self): self.stack.setCurrentWidget(self.menu) def startSinglePlayer(self): self.singlePlayer = SinglePlayer() self.singlePlayer.exitButton.clicked.connect(self.showMenu) self.stack.addWidget(self.singlePlayer) self.stack.setCurrentWidget(self.singlePlayer) def startMultiPlayer(self): if not self.multiPlayer: self.multiPlayer = MultiPlayer() self.multiPlayer.exitButton.clicked.connect(self.showMenu) self.stack.addWidget(self.multiPlayer) self.stack.setCurrentWidget(self.multiPlayer) def startSpectateBattle(self): self.spectate = SpectateBattle() self.spectate.exitButton.clicked.connect(self.showMenu) self.stack.addWidget(self.spectate) self.stack.setCurrentWidget(self.spectate)
class DataExportGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget( self ): return self def appletDrawer(self): return self.drawer def menus( self ): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, index): pass def stopAndCleanUp(self): for editor in list(self.layerViewerGuis.values()): self.viewerStack.removeWidget( editor ) editor.stopAndCleanUp() self.layerViewerGuis.clear() def imageLaneAdded(self, laneIndex): pass def imageLaneRemoved(self, laneIndex, finalLength): pass def allowLaneSelectionChange(self): return False ########################################### ########################################### def __init__(self, parentApplet, topLevelOperator): super(DataExportGui, self).__init__() self.drawer = None self.topLevelOperator = topLevelOperator self.threadRouter = ThreadRouter(self) self._thunkEventHandler = ThunkEventHandler(self) self._initAppletDrawerUic() self.initCentralUic() self.initViewerControls() self.parentApplet = parentApplet self.progressSignal = parentApplet.progressSignal self.overwrite = False @threadRoutedWithRouter(self.threadRouter) def handleNewDataset( multislot, index ): # Make room in the GUI table self.batchOutputTableWidget.insertRow( index ) # Update the table row data when this slot has new data # We can't bind in the row here because the row may change in the meantime. multislot[index].notifyReady( bind( self.updateTableForSlot ) ) if multislot[index].ready(): self.updateTableForSlot( multislot[index] ) multislot[index].notifyUnready( self._updateExportButtons ) multislot[index].notifyReady( self._updateExportButtons ) self.topLevelOperator.ExportPath.notifyInserted( bind( handleNewDataset ) ) # For each dataset that already exists, update the GUI for i, subslot in enumerate(self.topLevelOperator.ExportPath): handleNewDataset( self.topLevelOperator.ExportPath, i ) if subslot.ready(): self.updateTableForSlot(subslot) @threadRoutedWithRouter(self.threadRouter) def handleLaneRemoved( multislot, index, finalLength ): if self.batchOutputTableWidget.rowCount() <= finalLength: return # Remove the row we don't need any more self.batchOutputTableWidget.removeRow( index ) # Remove the viewer for this dataset imageMultiSlot = self.topLevelOperator.Inputs[index] if imageMultiSlot in list(self.layerViewerGuis.keys()): layerViewerGui = self.layerViewerGuis[imageMultiSlot] self.viewerStack.removeWidget( layerViewerGui ) self._viewerControlWidgetStack.removeWidget( layerViewerGui.viewerControlWidget() ) layerViewerGui.stopAndCleanUp() self.topLevelOperator.Inputs.notifyRemove( bind( handleLaneRemoved ) ) def _initAppletDrawerUic(self, drawerPath=None): """ Load the ui file for the applet drawer, which we own. """ if drawerPath is None: localDir = os.path.split(__file__)[0] drawerPath = os.path.join( localDir, "dataExportDrawer.ui") self.drawer = uic.loadUi(drawerPath) self.drawer.settingsButton.clicked.connect( self._chooseSettings ) self.drawer.exportAllButton.clicked.connect( self.exportAllResults ) self.drawer.exportAllButton.setIcon( QIcon(ilastikIcons.Save) ) self.drawer.deleteAllButton.clicked.connect( self.deleteAllResults ) self.drawer.deleteAllButton.setIcon( QIcon(ilastikIcons.Clear) ) @threadRoutedWithRouter(self.threadRouter) def _handleNewSelectionNames( *args ): input_names = self.topLevelOperator.SelectionNames.value self.drawer.inputSelectionCombo.addItems( input_names ) self.topLevelOperator.SelectionNames.notifyDirty( _handleNewSelectionNames ) _handleNewSelectionNames() self.drawer.inputSelectionCombo.currentIndexChanged.connect(self._handleInputComboSelectionChanged) def _handleInputComboSelectionChanged( self, index ): assert index < len(self.topLevelOperator.SelectionNames.value) if self.drawer.inputSelectionCombo.currentText() == self.topLevelOperator.TableOnlyName.value: self.topLevelOperator.TableOnly.setValue(True) else: self.topLevelOperator.TableOnly.setValue(False) self.topLevelOperator.InputSelection.setValue( index ) def initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] uic.loadUi(localDir+"/dataExport.ui", self) self.batchOutputTableWidget.resizeRowsToContents() self.batchOutputTableWidget.resizeColumnsToContents() self.batchOutputTableWidget.setAlternatingRowColors(True) self.batchOutputTableWidget.setShowGrid(False) self.batchOutputTableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Interactive) self.batchOutputTableWidget.horizontalHeader().resizeSection(Column.Dataset, 200) self.batchOutputTableWidget.horizontalHeader().resizeSection(Column.ExportLocation, 250) self.batchOutputTableWidget.horizontalHeader().resizeSection(Column.Action, 100) self.batchOutputTableWidget.verticalHeader().hide() # Set up handlers self.batchOutputTableWidget.itemSelectionChanged.connect(self.handleTableSelectionChange) # Set up the viewer area self.initViewerStack() self.splitter.setSizes([150, 850]) def initViewerStack(self): self.layerViewerGuis = {} self.viewerStack.addWidget( QWidget() ) def initViewerControls(self): self._viewerControlWidgetStack = QStackedWidget(parent=self) def showEvent(self, event): super( DataExportGui, self ).showEvent(event) self.showSelectedDataset() def hideEvent(self, event): super( DataExportGui, self ).hideEvent(event) # Make sure all 'on disk' layers are discarded so we aren't using those files any more. for opLaneView in self.topLevelOperator: opLaneView.cleanupOnDiskView() def _chooseSettings(self): opExportModelOp, opSubRegion = get_model_op( self.topLevelOperator ) if opExportModelOp is None: QMessageBox.information( self, "Image not ready for export", "Export isn't possible yet: No images are ready for export. " "Please configure upstream pipeline with valid settings, " "check that images were specified in the (batch) input applet and try again." ) return settingsDlg = DataExportOptionsDlg(self, opExportModelOp) if settingsDlg.exec_() == DataExportOptionsDlg.Accepted: # Copy the settings from our 'model op' into the real op setting_slots = [ opExportModelOp.RegionStart, opExportModelOp.RegionStop, opExportModelOp.InputMin, opExportModelOp.InputMax, opExportModelOp.ExportMin, opExportModelOp.ExportMax, opExportModelOp.ExportDtype, opExportModelOp.OutputAxisOrder, opExportModelOp.OutputFilenameFormat, opExportModelOp.OutputInternalPath, opExportModelOp.OutputFormat ] # Disconnect the special 'transaction' slot to prevent these # settings from triggering many calls to setupOutputs. self.topLevelOperator.TransactionSlot.disconnect() for model_slot in setting_slots: real_inslot = getattr(self.topLevelOperator, model_slot.name) if model_slot.ready(): real_inslot.setValue( model_slot.value ) else: real_inslot.disconnect() # Re-connect the 'transaction' slot to apply all settings at once. self.topLevelOperator.TransactionSlot.setValue(True) # Discard the temporary model op opExportModelOp.cleanUp() opSubRegion.cleanUp() # Update the gui with the new export paths for index, slot in enumerate(self.topLevelOperator.ExportPath): self.updateTableForSlot(slot) def getSlotIndex(self, multislot, subslot ): # Which index is this slot? for index, slot in enumerate(multislot): if slot == subslot: return index return -1 @threadRouted def updateTableForSlot(self, slot): """ Update the table row that corresponds to the given slot of the top-level operator (could be either input slot) """ row = self.getSlotIndex( self.topLevelOperator.ExportPath, slot ) assert row != -1, "Unknown input slot!" if not self.topLevelOperator.ExportPath[row].ready() or\ not self.topLevelOperator.RawDatasetInfo[row].ready(): return try: nickname = self.topLevelOperator.RawDatasetInfo[row].value.nickname exportPath = self.topLevelOperator.ExportPath[row].value except Slot.SlotNotReadyError: # Sadly, it is possible to get here even though we checked for .ready() immediately beforehand. # That's because the graph has a diamond-shaped DAG of connections, but the graph has no transaction mechanism # (It's therefore possible for RawDatasetInfo[row] to be ready() even though it's upstream partner is NOT ready. return self.batchOutputTableWidget.setItem( row, Column.Dataset, QTableWidgetItem( nickname ) ) self.batchOutputTableWidget.setItem( row, Column.ExportLocation, QTableWidgetItem( exportPath ) ) exportNowButton = QPushButton("Export") exportNowButton.setToolTip("Generate individual batch output dataset.") exportNowButton.clicked.connect( bind(self.exportResultsForSlot, self.topLevelOperator[row] ) ) self.batchOutputTableWidget.setCellWidget( row, Column.Action, exportNowButton ) # Select a row if there isn't one already selected. selectedRanges = self.batchOutputTableWidget.selectedRanges() if len(selectedRanges) == 0: self.batchOutputTableWidget.selectRow(0) def setEnabledIfAlive(self, widget, enable): if not sip.isdeleted(widget): widget.setEnabled(enable) def _updateExportButtons(self, *args): """Called when at least one dataset became 'unready', so we have to disable the export button.""" all_ready = True # Enable/disable the appropriate export buttons in the table. # Use ThunkEvents to ensure that this happens in the Gui thread. for row, slot in enumerate( self.topLevelOperator.ImageToExport ): all_ready &= slot.ready() export_button = self.batchOutputTableWidget.cellWidget( row, Column.Action ) if export_button is not None: executable_event = ThunkEvent( partial(self.setEnabledIfAlive, export_button, slot.ready()) ) QApplication.instance().postEvent( self, executable_event ) # Disable the "Export all" button unless all slots are ready. executable_event = ThunkEvent( partial(self.setEnabledIfAlive, self.drawer.exportAllButton, all_ready) ) QApplication.instance().postEvent( self, executable_event ) def handleTableSelectionChange(self): """ Any time the user selects a new item, select the whole row. """ self.selectEntireRow() self.showSelectedDataset() def selectEntireRow(self): # FIXME: There is a better way to do this... # Figure out which row is selected selectedItemRows = set() selectedRanges = self.batchOutputTableWidget.selectedRanges() for rng in selectedRanges: for row in range(rng.topRow(), rng.bottomRow()+1): selectedItemRows.add(row) # Disconnect from selection change notifications while we do this self.batchOutputTableWidget.itemSelectionChanged.disconnect(self.handleTableSelectionChange) for row in selectedItemRows: self.batchOutputTableWidget.selectRow(row) # Reconnect now that we're finished self.batchOutputTableWidget.itemSelectionChanged.connect(self.handleTableSelectionChange) def exportSlots(self, laneViewList ): try: # Set the busy flag so the workflow knows not to allow # upstream changes or shell changes while we're exporting self.parentApplet.busy = True self.parentApplet.appletStateUpdateRequested() # Disable our own gui QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self.drawer, False) ) ) QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self, False) ) ) # Start with 1% so the progress bar shows up self.progressSignal(0) self.progressSignal(1) def signalFileProgress(slotIndex, percent): self.progressSignal(old_div((100 * slotIndex + percent), len(laneViewList))) # Client hook self.parentApplet.prepare_for_entire_export() for i, opLaneView in enumerate(laneViewList): lane_index = self.topLevelOperator.innerOperators.index(opLaneView) logger.debug("Exporting result {}".format(i)) # If the operator provides a progress signal, use it. slotProgressSignal = opLaneView.progressSignal slotProgressSignal.subscribe(partial(signalFileProgress, i)) try: # Client hook self.parentApplet.prepare_lane_for_export(lane_index) # Export the image opLaneView.run_export() # Client hook if self.parentApplet.postprocessCanCheckForExistingFiles(): exportSuccessful = self.parentApplet.post_process_lane_export(lane_index, checkOverwriteFiles=True) if not exportSuccessful: userSelection = [None] self.showOverwriteQuestion(userSelection) if userSelection[0]: self.parentApplet.post_process_lane_export(lane_index, checkOverwriteFiles=False) else: self.parentApplet.post_process_lane_export(lane_index) except Exception as ex: if opLaneView.ExportPath.ready(): msg = "Failed to generate export file: \n" msg += opLaneView.ExportPath.value msg += "\n{}".format( ex ) else: msg = "Failed to generate export file." msg += "\n{}".format( ex ) log_exception( logger, msg ) self.showExportError(msg) # We're finished with this file. self.progressSignal(100 * (i + 1) / float(len(laneViewList))) # Client hook self.parentApplet.post_process_entire_export() # Ensure the shell knows we're really done. self.progressSignal(100) except: # Cancel our progress. self.progressSignal(0, True) raise finally: # We're not busy any more. Tell the workflow. self.parentApplet.busy = False self.parentApplet.appletStateUpdateRequested() # Re-enable our own gui QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self.drawer, True) ) ) QApplication.instance().postEvent( self, ThunkEvent( partial(self.setEnabledIfAlive, self, True) ) ) def postProcessLane(self, lane_index): """ Called immediately after the result for each lane is exported. Can be overridden by subclasses for post-processing purposes. """ pass @threadRouted def showExportError(self, msg): QMessageBox.critical(self, "Failed to export", msg ) @threadRouted def showOverwriteQuestion(self, userSelection): assert isinstance(userSelection, list) reply = QMessageBox.question(self, 'Warning!', 'This filename already exists. Are you sure you want to overwrite?', QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: userSelection[0] = True else: userSelection[0] = False def exportResultsForSlot(self, opLane): # Make sure all 'on disk' layers are discarded so we aren't using those files any more. for opLaneView in self.topLevelOperator: opLaneView.cleanupOnDiskView() # Do this in a separate thread so the UI remains responsive exportThread = threading.Thread(target=bind(self.exportSlots, [opLane]), name="DataExportThread") exportThread.start() def exportAllResults(self): # Make sure all 'on disk' layers are discarded so we aren't using those files any more. for opLaneView in self.topLevelOperator: opLaneView.cleanupOnDiskView() # Do this in a separate thread so the UI remains responsive exportThread = threading.Thread(target=bind(self.exportSlots, self.topLevelOperator), name="DataExportThread") exportThread.start() def deleteAllResults(self): for innerOp in self.topLevelOperator: operatorView = innerOp operatorView.cleanupOnDiskView() pathComp = PathComponents(operatorView.ExportPath.value, operatorView.WorkingDirectory.value) if os.path.exists(pathComp.externalPath): os.remove(pathComp.externalPath) operatorView.setupOnDiskView() # we need to toggle the dirts state in order to enforce a frech dirty signal operatorView.Dirty.setValue( False ) operatorView.Dirty.setValue( True ) def showSelectedDataset(self): """ Show the exported file in the viewer """ # Get the selected row and corresponding slot value selectedRanges = self.batchOutputTableWidget.selectedRanges() if len(selectedRanges) == 0: return row = selectedRanges[0].topRow() # Hide all layers that come from the disk. for opLaneView in self.topLevelOperator: opLaneView.cleanupOnDiskView() # Activate the 'on disk' layers for this lane (if possible) opLane = self.topLevelOperator.getLane(row) opLane.setupOnDiskView() # Create if necessary imageMultiSlot = self.topLevelOperator.Inputs[row] if imageMultiSlot not in list(self.layerViewerGuis.keys()): layerViewer = self.createLayerViewer(opLane) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.layerViewerGuis[imageMultiSlot] = layerViewer self.viewerStack.addWidget( layerViewer ) self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() ) # Show the right one layerViewer = self.layerViewerGuis[imageMultiSlot] self.viewerStack.setCurrentWidget( layerViewer ) self._viewerControlWidgetStack.setCurrentWidget( layerViewer.viewerControlWidget() ) def createLayerViewer(self, opLane): """ This method provides an instance of LayerViewerGui for the given data lane. If this GUI class is subclassed, this method can be reimplemented to provide custom layer types for the exported layers. """ return DataExportLayerViewerGui(self.parentApplet, opLane)
class ParamsByType(QWidget, MooseWidget): """ Has a QComboBox for the different allowed types. On switching type a new ParamsByGroup is shown. """ needBlockList = pyqtSignal(list) blockRenamed = pyqtSignal(object, str) changed = pyqtSignal() def __init__(self, block, type_block_map, **kwds): """ Constructor. Input: block[BlockInfo]: The block to show. """ super(ParamsByType, self).__init__(**kwds) self.block = block self.combo = QComboBox() self.types = [] self.type_params_map = {} self.type_block_map = type_block_map self.table_stack = QStackedWidget() self.type_table_map = {} for t in sorted(self.block.types.keys()): self.types.append(t) params_list = [] for p in self.block.parameters_list: params_list.append(self.block.parameters[p]) t_block = self.block.types[t] for p in t_block.parameters_list: params_list.append(t_block.parameters[p]) self.type_params_map[t] = params_list self.combo.addItems(sorted(self.block.types.keys())) self.combo.currentTextChanged.connect(self.setBlockType) self.top_layout = WidgetUtils.addLayout(vertical=True) self.top_layout.addWidget(self.combo) self.top_layout.addWidget(self.table_stack) self.setLayout(self.top_layout) self.user_params = [] self.setDefaultBlockType() self.setup() def _syncUserParams(self, current, to): """ Sync user added parameters that are on the main block into each type ParamsByGroup. Input: current[ParamsByGroup]: The current group parameter table to[ParamsByGroup]: The new group parameter table """ ct = current.findTable("Main") tot = to.findTable("Main") if not ct or not tot or ct == tot: return tot.removeUserParams() params = ct.getUserParams() tot.addUserParams(params) to.syncParamsFrom(current) # Make sure the name parameter stays the same idx = ct.findRow("Name") if idx >= 0: name = ct.item(idx, 1).text() idx = tot.findRow("Name") if idx >= 0: tot.item(idx, 1).setText(name) def currentType(self): return self.combo.currentText() def save(self): """ Look at the user params in self.block.parameters. update the type tables Save type on block """ t = self.getTable() if t: t.save() self.block.setBlockType(self.combo.currentText()) def reset(self): t = self.getTable() t.reset() def getOrCreateTypeTable(self, type_name): """ Gets the table for the type name or create it if it doesn't exist. Input: type_name[str]: Name of the type Return: ParamsByGroup: The parameters corresponding to the type """ t = self.type_table_map.get(type_name) if t: return t t = ParamsByGroup(self.block, self.type_params_map.get(type_name, self.block.orderedParameters()), self.type_block_map) t.needBlockList.connect(self.needBlockList) t.blockRenamed.connect(self.blockRenamed) t.changed.connect(self.changed) self.type_table_map[type_name] = t self.table_stack.addWidget(t) return t def setDefaultBlockType(self): param = self.block.getParamInfo("type") if param and param.value: self.setBlockType(param.value) elif self.block.types: self.setBlockType(sorted(self.block.types.keys())[0]) def setBlockType(self, type_name): if type_name not in self.block.types: return t = self.getOrCreateTypeTable(type_name) t.updateWatchers() self.combo.blockSignals(True) self.combo.setCurrentText(type_name) self.combo.blockSignals(False) t.updateType(type_name) current = self.table_stack.currentWidget() self._syncUserParams(current, t) self.table_stack.setCurrentWidget(t) self.changed.emit() def addUserParam(self, param): t = self.table_stack.currentWidget() t.addUserParam(param) def setWatchedBlockList(self, path, children): for i in range(self.table_stack.count()): t = self.table_stack.widget(i) t.setWatchedBlockList(path, children) def updateWatchers(self): for i in range(self.table_stack.count()): t = self.table_stack.widget(i) t.updateWatchers() def getTable(self): return self.table_stack.currentWidget() def paramValue(self, name): for i in range(self.table_stack.count()): t = self.table_stack.widget(i) if t.paramValue(name): return t.paramValue(name)
class DataSelectionGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget(self): return self def appletDrawer(self): return self._drawer def menus(self): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, imageIndex): if imageIndex is not None: self.laneSummaryTableView.selectRow(imageIndex) for detailWidget in self._detailViewerWidgets: detailWidget.selectRow(imageIndex) def stopAndCleanUp(self): self._cleaning_up = True for editor in list(self.volumeEditors.values()): self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget(editor.viewerControlWidget()) editor.stopAndCleanUp() self.volumeEditors.clear() def imageLaneAdded(self, laneIndex): if len(self.laneSummaryTableView.selectedIndexes()) == 0: self.laneSummaryTableView.selectRow(laneIndex) # We don't have any real work to do because this gui initiated the lane addition in the first place if self.guiMode != GuiMode.Batch: if len(self.topLevelOperator.DatasetGroup) != laneIndex + 1: import warnings warnings.warn( "DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex) ) def imageLaneRemoved(self, laneIndex, finalLength): # There's nothing to do here because the GUI already # handles operator resizes via slot callbacks. pass def allowLaneSelectionChange(self): return False ########################################### ########################################### class UserCancelledError(Exception): # This exception type is raised when the user cancels the # addition of dataset files in the middle of the process somewhere. # It isn't an error -- it's used for control flow. pass def __init__( self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False, ): """ Constructor. :param dataSelectionOperator: The top-level operator. Must be of type :py:class:`OpMultiLaneDataSelectionGroup`. :param serializer: The applet's serializer. Must be of type :py:class:`DataSelectionSerializer` :param instructionText: A string to display in the applet drawer. :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``. Currently, there is no difference between normal and batch mode. :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow. If ``None``, there is no maximum. """ super(DataSelectionGui, self).__init__() self._cleaning_up = False self.parentApplet = parentApplet self._max_lanes = max_lanes self.show_axis_details = show_axis_details self._viewerControls = QWidget() self.topLevelOperator = dataSelectionOperator self.guiMode = guiMode self.serializer = serializer self.threadRouter = ThreadRouter(self) self._initCentralUic() self._initAppletDrawerUic(instructionText) self._viewerControlWidgetStack = QStackedWidget(self) self._default_h5n5_volumes: Dict[int, Set[str]] = {} def handleImageRemove(multislot, index, finalLength): # Remove the viewer for this dataset datasetSlot = self.topLevelOperator.DatasetGroup[index] if datasetSlot in list(self.volumeEditors.keys()): editor = self.volumeEditors[datasetSlot] self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget(editor.viewerControlWidget()) editor.stopAndCleanUp() self.topLevelOperator.DatasetGroup.notifyRemove(bind(handleImageRemove)) opWorkflow = self.topLevelOperator.parent assert hasattr( opWorkflow.shell, "onSaveProjectActionTriggered" ), "This class uses the IlastikShell.onSaveProjectActionTriggered function. Did you rename it?" @property def project_file(self) -> h5py.File: return self.topLevelOperator.ProjectFile.value def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] + "/" uic.loadUi(localDir + "/dataSelection.ui", self) self._initTableViews() self._initViewerStack() self.splitter.setSizes([150, 850]) def _initAppletDrawerUic(self, instructionText): """ Load the ui file for the applet drawer, which we own. """ localDir = os.path.split(__file__)[0] + "/" self._drawer = uic.loadUi(localDir + "/dataSelectionDrawer.ui") self._drawer.instructionLabel.setText(instructionText) def _initTableViews(self): self.fileInfoTabWidget.setTabText(0, "Summary") self.laneSummaryTableView.setModel(DataLaneSummaryTableModel(self, self.topLevelOperator)) self.laneSummaryTableView.dataLaneSelected.connect(self.showDataset) self.laneSummaryTableView.addFilesRequested.connect(self.addFiles) self.laneSummaryTableView.addStackRequested.connect(self.addStack) self.laneSummaryTableView.removeLanesRequested.connect(self.handleRemoveLaneButtonClicked) # These two helper functions enable/disable an 'add files' button for a given role # based on the the max lane index for that role and the overall permitted max_lanes def _update_button_status(viewer, role_index): if self._max_lanes: viewer.setEnabled(self._findFirstEmptyLane(role_index) < self._max_lanes) def _handle_lane_added(button, role_index, lane_slot, lane_index): def _handle_role_slot_added(role_slot, added_slot_index, *args): if added_slot_index == role_index: role_slot.notifyReady(bind(_update_button_status, button, role_index)) role_slot.notifyUnready(bind(_update_button_status, button, role_index)) lane_slot[lane_index].notifyInserted(_handle_role_slot_added) self._retained = [] # Retain menus so they don't get deleted self._detailViewerWidgets = [] for roleIndex, role in enumerate(self.topLevelOperator.DatasetRoles.value): detailViewer = DatasetDetailedInfoTableView(self) detailViewer.setModel(DatasetDetailedInfoTableModel(self, self.topLevelOperator, roleIndex)) self._detailViewerWidgets.append(detailViewer) # Button detailViewer.addFilesRequested.connect(partial(self.addFiles, roleIndex)) detailViewer.addStackRequested.connect(partial(self.addStack, roleIndex)) detailViewer.addPrecomputedVolumeRequested.connect(partial(self.addPrecomputedVolume, roleIndex)) detailViewer.addRemoteVolumeRequested.connect(partial(self.addDvidVolume, roleIndex)) # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab self.topLevelOperator.DatasetGroup.notifyInserted(bind(_handle_lane_added, detailViewer, roleIndex)) self.topLevelOperator.DatasetGroup.notifyRemoved(bind(_update_button_status, detailViewer, roleIndex)) # While we're at it, do the same for the buttons in the summary table, too self.topLevelOperator.DatasetGroup.notifyInserted( bind(_handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex) ) self.topLevelOperator.DatasetGroup.notifyRemoved( bind(_update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex) ) # Context menu detailViewer.replaceWithFileRequested.connect(partial(self.handleReplaceFile, roleIndex)) detailViewer.replaceWithStackRequested.connect(partial(self.addStack, roleIndex)) detailViewer.editRequested.connect(partial(self.editDatasetInfo, roleIndex)) detailViewer.resetRequested.connect(partial(self.handleClearDatasets, roleIndex)) # Drag-and-drop detailViewer.addFilesRequestedDrop.connect(partial(self.addFileNames, roleIndex=roleIndex)) # Selection handling def showFirstSelectedDataset(_roleIndex, lanes): if lanes: self.showDataset(lanes[0], _roleIndex) detailViewer.dataLaneSelected.connect(partial(showFirstSelectedDataset, roleIndex)) self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role) self.fileInfoTabWidget.currentChanged.connect(self.handleSwitchTabs) self.fileInfoTabWidget.setCurrentIndex(0) def handleSwitchTabs(self, tabIndex): if tabIndex < len(self._detailViewerWidgets): roleIndex = tabIndex # If summary tab is moved to the front, change this line. detailViewer = self._detailViewerWidgets[roleIndex] selectedLanes = detailViewer.selectedLanes if selectedLanes: self.showDataset(selectedLanes[0], roleIndex) def _initViewerStack(self): self.volumeEditors = {} self.viewerStack.addWidget(QWidget()) def handleRemoveLaneButtonClicked(self): """ The user clicked the "Remove" button. Remove the currently selected row(s) from both the GUI and the top-level operator. """ # Figure out which lanes to remove selectedIndexes = self.laneSummaryTableView.selectedIndexes() rows = set() for modelIndex in selectedIndexes: rows.add(modelIndex.row()) # Don't remove the last row, which is just buttons. rows.discard(self.laneSummaryTableView.model().rowCount() - 1) # Remove in reverse order so row numbers remain consistent for row in reversed(sorted(rows)): # Remove lanes from the operator. # The table model will notice the changes and update the rows accordingly. finalSize = len(self.topLevelOperator.DatasetGroup) - 1 self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize) @threadRouted def showDataset(self, laneIndex, roleIndex=None): if self._cleaning_up: return if laneIndex == -1: self.viewerStack.setCurrentIndex(0) return assert threading.current_thread().name == "MainThread" if laneIndex >= len(self.topLevelOperator.DatasetGroup): return datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex] # Create if necessary if datasetSlot not in list(self.volumeEditors.keys()): class DatasetViewer(LayerViewerGui): def moveToTop(self, roleIndex): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return datasetRoles = opLaneView.DatasetRoles.value if roleIndex >= len(datasetRoles): return roleName = datasetRoles[roleIndex] try: layerIndex = [l.name for l in self.layerstack].index(roleName) except ValueError: return else: self.layerstack.selectRow(layerIndex) self.layerstack.moveSelectedToTop() def setupLayers(self): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return [] layers = [] datasetRoles = opLaneView.DatasetRoles.value for roleIndex, slot in enumerate(opLaneView.ImageGroup): if slot.ready(): roleName = datasetRoles[roleIndex] layer = self.createStandardLayerFromSlot(slot) layer.name = roleName layers.append(layer) return layers opLaneView = self.topLevelOperator.getLane(laneIndex) layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.volumeEditors[datasetSlot] = layerViewer self.viewerStack.addWidget(layerViewer) self._viewerControlWidgetStack.addWidget(layerViewer.viewerControlWidget()) # Show the right one viewer = self.volumeEditors[datasetSlot] displayedRole = self.fileInfoTabWidget.currentIndex() viewer.moveToTop(displayedRole) self.viewerStack.setCurrentWidget(viewer) self._viewerControlWidgetStack.setCurrentWidget(viewer.viewerControlWidget()) def handleReplaceFile(self, roleIndex, startingLaneNum): self.addFiles(roleIndex, startingLaneNum) def addFiles(self, roleIndex, startingLaneNum=None): """ The user clicked the "Add File" button. Ask him to choose a file (or several) and add them to both the GUI table and the top-level operator inputs. """ # Launch the "Open File" dialog paths = ImageFileDialog(self).getSelectedPaths() self.addFileNames(paths, startingLaneNum, roleIndex) def addFileNames(self, paths: List[Path], startingLaneNum: int, roleIndex: int): # If the user didn't cancel if paths: try: new_infos = self._createDatasetInfos(roleIndex, paths) self.addLanes(new_infos, roleIndex=roleIndex, startingLaneNum=startingLaneNum) except DataSelectionGui.UserCancelledError: pass except Exception as ex: log_exception(logger) QMessageBox.critical(self, "Error loading file", str(ex)) def _findFirstEmptyLane(self, roleIndex): opTop = self.topLevelOperator # Determine the number of files this role already has # Search for the last valid value. firstNewLane = 0 for laneIndex, slot in reversed(list(zip(list(range(len(opTop.DatasetGroup))), opTop.DatasetGroup))): if slot[roleIndex].ready(): firstNewLane = laneIndex + 1 break return firstNewLane def getNumLanes(self) -> int: return len(self.topLevelOperator.DatasetGroup) def getInfoSlots(self, roleIndex: int): return [self.topLevelOperator.DatasetGroup[laneIndex][roleIndex] for laneIndex in range(self.getNumLanes())] def addLanes(self, new_infos: List[DatasetInfo], roleIndex, startingLaneNum=None): """ Add the given filenames to both the GUI table and the top-level operator inputs. If startingLaneNum is None, the filenames will be *appended* to the role's list of files. """ originalNumLanes = self.getNumLanes() startingLaneNum, endingLaneNum = self._determineLaneRange(new_infos, startingLaneNum) if originalNumLanes < endingLaneNum + 1: self.topLevelOperator.DatasetGroup.resize(endingLaneNum + 1) info_slots = self.getInfoSlots(roleIndex)[startingLaneNum : endingLaneNum + 1] try: if not self.applyDatasetInfos(new_infos, info_slots): self.topLevelOperator.DatasetGroup.resize(originalNumLanes) return self._checkDataFormatWarnings(roleIndex, startingLaneNum, endingLaneNum) # Show the first image self.showDataset(startingLaneNum, roleIndex) # if only adding new lanes, notify the workflow if startingLaneNum >= originalNumLanes: workflow = self.parentApplet.topLevelOperator.parent workflow.handleNewLanesAdded() # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() except Exception as e: self.topLevelOperator.DatasetGroup.resize(originalNumLanes) QMessageBox.critical(self, "File selection error", str(e)) def applyDatasetInfos(self, new_infos: List[DatasetInfo], info_slots: List[Slot]): original_infos = [] def revert(): for slot, original_info in zip(info_slots, original_infos): if original_info is not None: slot.setValue(original_info) try: for new_info, info_slot in zip(new_infos, info_slots): original_infos.append(info_slot.value if info_slot.ready() else None) while True: try: info_slot.setValue(new_info) break except DatasetConstraintError as e: QMessageBox.warning(self, "Incompatible dataset", str(e)) info_editor = DatasetInfoEditorWidget(self, [new_info], self.serializer) if info_editor.exec_() == QDialog.Rejected: revert() return False new_info = info_editor.edited_infos[0] return True except Exception as e: revert() raise e finally: self.parentApplet.appletStateUpdateRequested() def _determineLaneRange(self, infos: List[DatasetInfo], startingLaneNum=None): """ Determine which lanes should be configured if the user wants to add the given infos starting at startingLaneNum. If startingLaneNum is None, assume the user wants to APPEND the files to the role's slots. """ if startingLaneNum is None or startingLaneNum == -1: startingLaneNum = len(self.topLevelOperator.DatasetGroup) endingLane = startingLaneNum + len(infos) - 1 else: assert startingLaneNum < len(self.topLevelOperator.DatasetGroup) max_files = len(self.topLevelOperator.DatasetGroup) - startingLaneNum if len(infos) > max_files: raise Exception( f"You selected {len(infos)} files for {max_files} slots. To add new files use " "the 'Add new...' option in the context menu or the button in the last row." ) endingLane = min(startingLaneNum + len(infos) - 1, len(self.topLevelOperator.DatasetGroup)) if self._max_lanes and endingLane >= self._max_lanes: raise Exception("You may not add more than {self._max_lanes} file(s) to this workflow.") return (startingLaneNum, endingLane) def _createDatasetInfos(self, roleIndex: int, filePaths: List[Path], rois=None): """ Create a list of DatasetInfos for the given filePaths and rois rois may be None, in which case it is ignored. """ if rois is None: rois = [None] * len(filePaths) assert len(rois) == len(filePaths) infos = [] for filePath, roi in zip(filePaths, rois): info = self._createDatasetInfo(roleIndex, filePath, roi) infos.append(info) return infos def _add_default_inner_path(self, roleIndex: int, inner_path: str): paths = self._default_h5n5_volumes.get(roleIndex, set()) paths.add(inner_path) self._default_h5n5_volumes[roleIndex] = paths def _get_previously_used_inner_paths(self, roleIndex: int) -> Set[str]: previous_paths = self._default_h5n5_volumes.get(roleIndex, set()) return previous_paths.copy() def _createDatasetInfo(self, roleIndex: int, filePath: Path, roi=None) -> FilesystemDatasetInfo: """ Create a DatasetInfo object for the given filePath and roi. roi may be None, in which case it is ignored. """ cwd = self.topLevelOperator.WorkingDirectory.value try: data_path = filePath.absolute().relative_to(cwd) except ValueError: data_path = filePath.absolute() if DatasetInfo.fileHasInternalPaths(str(data_path)): datasetNames = DatasetInfo.getPossibleInternalPathsFor(filePath.absolute()) if len(datasetNames) == 0: raise RuntimeError(f"File {data_path} has no image datasets") if len(datasetNames) == 1: selected_dataset = datasetNames.pop() else: auto_inner_paths = self._get_previously_used_inner_paths(roleIndex).intersection(set(datasetNames)) if len(auto_inner_paths) == 1: selected_dataset = auto_inner_paths.pop() else: # Ask the user which dataset to choose dlg = SubvolumeSelectionDlg(datasetNames, self) if dlg.exec_() == QDialog.Accepted: selected_index = dlg.combo.currentIndex() selected_dataset = str(datasetNames[selected_index]) else: raise DataSelectionGui.UserCancelledError() self._add_default_inner_path(roleIndex=roleIndex, inner_path=selected_dataset) data_path = data_path / re.sub("^/", "", selected_dataset) return RelativeFilesystemDatasetInfo.create_or_fallback_to_absolute( filePath=data_path.as_posix(), project_file=self.project_file, allowLabels=(self.guiMode == GuiMode.Normal), subvolume_roi=roi, ) def _checkDataFormatWarnings(self, roleIndex, startingLaneNum, endingLane): warn_needed = False opTop = self.topLevelOperator for lane_index in range(startingLaneNum, endingLane + 1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.inefficient_format: warn_needed = True if warn_needed: QMessageBox.warning( self, "Inefficient Data Format", "Your data cannot be accessed efficiently in its current format. " "Check the console output for details.\n" "(For HDF5 files, be sure to enable chunking on your dataset.)", ) def addStack(self, roleIndex, laneIndex): """ The user clicked the "Import Stack Files" button. """ stackDlg = StackFileSelectionWidget(self) stackDlg.exec_() if stackDlg.result() != QDialog.Accepted or not stackDlg.selectedFiles: return # FIXME: ask first if stack should be internalized to project file # also, check prefer_2d, size/volume and presence of 'z' to determine this nickname = DatasetInfo.create_nickname(stackDlg.selectedFiles) try: # FIXME: do this inside a Request self.parentApplet.busy = True inner_path = self.serializer.importStackAsLocalDataset( abs_paths=stackDlg.selectedFiles, sequence_axis=stackDlg.sequence_axis ) info = ProjectInternalDatasetInfo(inner_path=inner_path, nickname=nickname, project_file=self.project_file) finally: self.parentApplet.busy = False self.addLanes([info], roleIndex, laneIndex) def handleClearDatasets(self, roleIndex, selectedRows): for row in selectedRows: self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect() # Remove all operators that no longer have any connected slots laneIndexes = list(range(len(self.topLevelOperator.DatasetGroup))) for laneIndex, multislot in reversed(list(zip(laneIndexes, self.topLevelOperator.DatasetGroup))): any_ready = False for slot in multislot: any_ready |= slot.ready() if not any_ready: self.topLevelOperator.DatasetGroup.removeSlot(laneIndex, len(self.topLevelOperator.DatasetGroup) - 1) # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() def editDatasetInfo(self, roleIndex, laneIndexes): all_info_slots = self.getInfoSlots(roleIndex) selected_info_slots = [all_info_slots[idx] for idx in laneIndexes] infos = [slot.value for slot in selected_info_slots] editorDlg = DatasetInfoEditorWidget(self, infos, self.serializer) if editorDlg.exec_() == QDialog.Accepted: self.applyDatasetInfos(editorDlg.edited_infos, selected_info_slots) def addPrecomputedVolume(self, roleIndex, laneIndex): # add history... history = [] browser = PrecomputedVolumeBrowser(history=history, parent=self) if browser.exec_() == PrecomputedVolumeBrowser.Rejected: return precomputed_url = browser.selected_url self.addFileNames([precomputed_url], laneIndex, roleIndex) def addDvidVolume(self, roleIndex, laneIndex): group = "DataSelection" recent_hosts_key = "Recent DVID Hosts" recent_hosts = preferences.get(group, recent_hosts_key) if not recent_hosts: recent_hosts = ["localhost:8000"] recent_hosts = [ h for h in recent_hosts if h ] # There used to be a bug where empty strings could be saved. Filter those out. recent_nodes_key = "Recent DVID Nodes" recent_nodes = preferences.get(group, recent_nodes_key) or {} from .dvidDataSelectionBrowser import DvidDataSelectionBrowser browser = DvidDataSelectionBrowser(recent_hosts, recent_nodes, parent=self) if browser.exec_() == DvidDataSelectionBrowser.Rejected: return if None in browser.get_selection(): QMessageBox.critical("Couldn't use your selection.") return rois = None hostname, repo_uuid, volume_name, node_uuid, typename = browser.get_selection() dvid_url = f"http://{hostname}/api/node/{node_uuid}/{volume_name}" subvolume_roi = browser.get_subvolume_roi() # Relocate host to top of 'recent' list, and limit list to 10 items. try: i = recent_hosts.index(hostname) del recent_hosts[i] except ValueError: pass finally: recent_hosts.insert(0, hostname) recent_hosts = recent_hosts[:10] # Save pref preferences.set(group, recent_hosts_key, recent_hosts) recent_nodes[hostname] = node_uuid preferences.set(group, recent_nodes_key, recent_nodes) self.addLanes([UrlDatasetInfo(url=dvid_url, subvolume_roi=subvolume_roi)], roleIndex)
class IdealImagerDialog(QWidget): def __init__(self, conjugate_type, specsheets, cmd_fct=None, **kwargs): super().__init__(**kwargs) self.dlog_attrs = {} self.conjugate_type = conjugate_type self.conjugate_box = self.createConjugateBox(itype=self.conjugate_type) self.specsheet_dict = specsheets self.imager_stack = {} self.etendue_stack = {} self.imager_groupbox_stack = QStackedWidget() self.etendue_groupbox_stack = QStackedWidget() for key, specsheet in specsheets.items(): isgb = ImagerSpecGroupBox(self, specsheet) self.imager_stack[key] = isgb self.imager_groupbox_stack.addWidget(isgb) egb = EtendueGroupBox(self, key, specsheet) self.etendue_stack[key] = egb self.etendue_groupbox_stack.addWidget(egb) imager_groupbox = self.imager_stack[self.conjugate_type] self.imager_groupbox_stack.setCurrentWidget(imager_groupbox) etendue_groupbox = self.etendue_stack[self.conjugate_type] self.etendue_groupbox_stack.setCurrentWidget(etendue_groupbox) # construct the top level layout overallLayout = QVBoxLayout(self) mainLayout = QHBoxLayout() mainLayout.addWidget(self.conjugate_box) mainLayout.addWidget(self.imager_groupbox_stack) mainLayout.addWidget(self.etendue_groupbox_stack) self.button_box = self.createButtonBox(cmd_fct) overallLayout.addLayout(mainLayout) overallLayout.addWidget(self.button_box) self.setLayout(overallLayout) self.setWindowTitle("Optical Spec Sheet") def createButtonBox(self, cmd_fct): def clicked(button): command = button.text() specsheet = self.specsheet_dict[self.conjugate_type] if cmd_fct: cmd_fct(self, command, specsheet) else: print(button.text(), 'button pressed') buttonbox = QDialogButtonBox(qt.Horizontal, self) buttonbox.addButton('New', QDialogButtonBox.ApplyRole) buttonbox.addButton(QDialogButtonBox.Apply) buttonbox.addButton('Update', QDialogButtonBox.ApplyRole) buttonbox.addButton(QDialogButtonBox.Close) for b in buttonbox.buttons(): b.setAutoDefault(False) # buttonbox.setCenterButtons(True) buttonbox.clicked.connect(clicked) return buttonbox def createConjugateBox(self, itype='infinite'): conjugate_box = QGroupBox("Conjugates") layout = QVBoxLayout() infinite = QRadioButton('Infinite') self.dlog_attrs['infinite'] = infinite layout.addWidget(infinite) infinite.clicked.connect(lambda: self.change_conjugate('infinite')) finite = QRadioButton('Finite') self.dlog_attrs['finite'] = finite layout.addWidget(finite) finite.clicked.connect(lambda: self.change_conjugate('finite')) conjugate_box.setLayout(layout) self.dlog_attrs[itype].setChecked(True) return conjugate_box def change_conjugate(self, conj_type): print("change_conjugate:", conj_type) if self.conjugate_type is not conj_type: self.update_conjugate(conj_type) def update_conjugate(self, conj_type): prev_specsheet = self.specsheet_dict[self.conjugate_type] self.conjugate_type = conj_type new_specsheet = self.specsheet_dict[conj_type] new_partition, max_inputs = new_specsheet.partition_defined() if new_partition is None: if prev_specsheet.imager.f is not None: prev_f = prev_specsheet.imager.f new_specsheet.imager_inputs['f'] = prev_f new_imager_groupbox = self.imager_stack[conj_type] self.imager_groupbox_stack.setCurrentWidget(new_imager_groupbox) etendue_groupbox = self.etendue_stack[conj_type] self.etendue_groupbox_stack.setCurrentWidget(etendue_groupbox) self.update_values() def update_values(self): """ callback routine for any dialog value change """ conj_type = self.conjugate_type specsheet = self.specsheet_dict[conj_type] imager_groupbox = self.imager_stack[conj_type] etendue_groupbox = self.etendue_stack[conj_type] imager_inputs = imager_groupbox.specsheet.imager_inputs etendue_inputs = etendue_groupbox.specsheet.etendue_inputs imager, etendue_values = specsheet.generate_from_inputs( imager_inputs, etendue_inputs) specsheet.imager_inputs = imager_inputs specsheet.etendue_inputs = etendue_inputs imager_groupbox.update_values() etendue_groupbox.update_values() def update_checkboxes(self): """ callback routine for any dialog checkbox change """ imager_groupbox = self.imager_stack[self.conjugate_type] etendue_groupbox = self.etendue_stack[self.conjugate_type] imager_groupbox.update_checkboxes() etendue_groupbox.update_checkboxes()
class GUI(QMainWindow): def __init__(self): super(GUI, self).__init__() self.title = "MudShark v0.1" self.setWindowTitle(self.title) self.defWidth = 800 self.defHeight = 600 # self.setGeometry(100, 100, self.defWidth, self.defHeight) self.resize(self.defWidth, self.defHeight) self.host = "None" self.port = "None" self.log_name = "None" self.log_num = 0 self.mode = "None" self.connected = False self.client = None self.recSize = 0 self.rpw = 0 self.numRecs = 0 self.init_actions() self.modeChangeSignal = ModeChange() self.initUI() def initUI(self): # central widget contains a widget containing a vertical layout, # and that widget contains the stacked widget self.centralWidget = QWidget(self) self.centerVLayoutWidget = QWidget(self.centralWidget) self.centerVLayoutWidget.setGeometry(QRect(0, 0, 800, 550)) self.centralWidLayout = QVBoxLayout(self.centerVLayoutWidget) self.centralWidLayout.setContentsMargins(0, 0, 0, 0) # stacked widget is contained inside centralWidLayout self.stackedWidget = QStackedWidget(self.centerVLayoutWidget) # this is where we instantiate the different views inside stackedWidget self.initialView = InitialView(self) self.stackedWidget.addWidget(self.initialView) self.stackedWidget.setCurrentWidget(self.initialView) self.centralWidLayout.addWidget(self.stackedWidget) self.setCentralWidget(self.centralWidget) self.init_menu() self.init_statusbar() self.init_toolbar() self.show() def updateMode(self, mode): self.mode = mode self.modeChangeSignal.modeChange.connect(self.update_view) self.modeChangeSignal.modeChange.emit() def update_view(self): if self.mode == "None": self.stackedWidget.setCurrentWidget(self.initialView) elif self.mode == "Program log": self.stackedWidget.setCurrentWidget(self.programLogView) elif self.mode == "Retrieve log": self.logDet = client.engage_log(self.client, self.log_num, 0) self.recSize = self.logDet.record_size self.rpw = self.logDet.records_per_window self.numRecs = (self.logDet.max_records / self.logDet.records_per_window) self.records = client.retrieve_records(self.client, self.logDet.records_per_window, self.logDet.max_records, self.logDet.record_size) client.disengage_log(self.client, self.log_num) self.stackedWidget.setCurrentWidget(self.retrieveLogView) def init_actions(self): # exit action self.exitAct = QAction(QIcon('icons/exit.png'), '&Exit', self) self.exitAct.setShortcut('Ctrl+Q') self.exitAct.setStatusTip('Exit application') self.exitAct.setText("Exit") self.exitAct.triggered.connect(qApp.quit) # connect action self.connectAct = QAction(QIcon('icons/connect.png'), 'Connect', self) self.connectAct.setShortcut('Alt+C') self.connectAct.setStatusTip('Connect to a Modbus Meter') self.connectAct.setText("Connect") self.connectAct.triggered.connect(self.connect_popup) # disconnect action self.disconnectAct = QAction(QIcon('icons/disconnect.png'), 'Disconnect', self) self.disconnectAct.setShortcut('Ctrl+D') self.disconnectAct.setText("Disconnect") self.disconnectAct.setStatusTip('Disconnect from current' ' connection') self.disconnectAct.triggered.connect(self.disconnect) # export as action self.exportAsAct = QAction('Export As..', self) self.exportAsAct.setShortcut('Ctrl+e') self.exportAsAct.setText('Export As..') # help action self.helpAct = QAction('Help', self) self.helpAct.setShortcut('Ctrl+h') self.helpAct.setText('Help') # about the author action self.aboutAuthAct = QAction('About the Author', self) self.aboutAuthAct.setText('About the Author') def connect_popup(self): self.ConnectionWindow = cw.ConnectWindow(self) self.ConnectionWindow.show() def disconnect(self): self.host = "None" self.port = "None" self.log_name = "None" self.connected = False self.updateMode("None") self.update_statusbar() if self.client is not None: self.client.close() def init_statusbar(self): self.statusbar = QStatusBar(self) self.setStatusBar(self.statusbar) self.statusMessage = QLabel() statusString = ("Host: " + self.host + " | Port: " + self.port + " | Log: " + self.log_name + " | Mode: " + self.mode + " | Connected: " + str(self.connected)) self.statusMessage.setText(statusString) self.statusBar().addPermanentWidget(self.statusMessage) def update_statusbar(self): self.statusBar().removeWidget(self.statusMessage) statusString = ("Host: " + self.host + " | Port: " + self.port + " | Log: " + self.log_name + " | Mode: " + self.mode + " | Connected: " + str(self.connected)) self.statusMessage = QLabel() self.statusMessage.setText(statusString) self.statusBar().addPermanentWidget(self.statusMessage) def init_toolbar(self): self.toolbar = self.addToolBar("Toolbar") self.toolbar.addAction(self.connectAct) self.toolbar.addAction(self.disconnectAct) self.toolbar.addAction(self.exitAct) def init_menu(self): self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 800, 20)) self.menuFile = QMenu(self.menubar) self.menuFile.setTitle("File") self.menuAbout = QMenu(self.menubar) self.menuAbout.setTitle("About") self.setMenuBar(self.menubar) # File -> Connect self.menuFile.addAction(self.connectAct) # File -> Disconnect self.menuFile.addAction(self.disconnectAct) self.menuFile.addSeparator() # File -> Export As.. self.menuFile.addAction(self.exportAsAct) self.menuFile.addSeparator() # File -> Exit self.menuFile.addAction(self.exitAct) # About -> Help self.menuAbout.addAction(self.helpAct) # About -> About the Author self.menuAbout.addAction(self.aboutAuthAct) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuAbout.menuAction()) ################## # EVENT HANDLING # ################## def toggleMenu(self, state): if state: self.statusbar.show() else: self.statusbar.hide() def contextMenuEvent(self, event): cmenu = QMenu(self) quitAct = cmenu.addAction("Quit") action = cmenu.exec_(self.mapToGlobal(event.pos())) if action == quitAct: qApp.quit()
class Popup(QDialog): def __init__(self, schedule: Schedule, parent=None, edit_data=None): super(Popup, self).__init__(parent) self.schedule = schedule self.event_name, self.data = edit_data if edit_data is not None else ( None, None) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self.setWindowTitle("Add New Scheduled Notes" if edit_data is None else "Edit {} Details".format(self.event_name)) self.layout = QVBoxLayout() self.layout.setSpacing(0) self.form_layout = QFormLayout() self.form_layout.setContentsMargins(0, 0, 0, self.form_layout.verticalSpacing()) self.form_layout_widget = QWidget() self.form_layout_widget.setLayout(self.form_layout) #The amount of fields in the form that come before the block section (name, #blocks, start, end date, color) self.rows_before_blocks = 3 self.event_type = QPushButton() event_type_menu = QMenu() event_type_menu.addAction("Class") event_type_menu.addSection("Event") event_type_menu.addAction("One Time Event") event_type_menu.addAction("Recurring Event") event_type_menu.addAction("One Time Class Event") for action in event_type_menu.actions(): if not action.isSeparator(): action.triggered.connect( lambda state, x=action.text(): self.set_type(x)) self.event_type.setMenu(event_type_menu) self.form_layout.addRow("Type:", self.event_type) #Class Title self.name_edit = QLineEdit() self.form_layout.addRow("Name:", self.name_edit) #Color self.color_picker = QColorDialog() self.color_button = QPushButton("Pick Color") self.color_button.clicked.connect(self.color_picker.open) self.color_picker.currentColorChanged.connect(self.update_color) self.form_layout.addRow("Color Code:", self.color_button) # Initialize widgets to be added later self.start_date_model = DateTimePickerSeriesModel(self) self.class_start_date = DateTimePickerSeries(self.start_date_model, "MMM d yyyy") self.event_start_date = DateTimePickerSeries(self.start_date_model, "MMM d yyyy") self.end_date_model = DateTimePickerSeriesModel(self) self.class_end_date = DateTimePickerSeries(self.end_date_model, "MMM d yyyy") self.event_end_date = DateTimePickerSeries(self.end_date_model, "MMM d yyyy") self.event_date_model = DateTimePickerSeriesModel(self) self.class_event_date = DateTimePickerSeries(self.event_date_model, "MMM d yyyy hh:mm:AP") self.event_date = DateTimePickerSeries(self.event_date_model, "MMM d yyyy hh:mm:AP") # Blocks self.blocks = 1 self.spin_box = QSpinBox() self.spin_box.setValue(1) self.spin_box.setMinimum(1) self.spin_box.setMaximum(7) self.spin_box.valueChanged.connect(self.update_blocks) self.class_picker = QPushButton() class_picker_menu = QMenu() for class_name in self.schedule.schedule.keys(): if self.schedule.schedule[class_name]["type"] != "class": continue class_action = QAction(class_name, parent=class_picker_menu) class_action.triggered.connect(lambda state, x=class_action.text(): self.class_picker.setText(x)) class_picker_menu.addAction(class_action) class_picker_menu.aboutToShow.connect( lambda: class_picker_menu.setMinimumWidth(self.class_picker.width( ))) self.class_picker.setMenu(class_picker_menu) self.stack = QStackedWidget() self.stack.setContentsMargins(0, 0, 0, 0) class_layout = QFormLayout() class_layout.setContentsMargins(0, 0, 0, class_layout.verticalSpacing()) class_layout.addRow("Start Date:", self.class_start_date) class_layout.addRow("End Date:", self.class_end_date) class_layout.addRow("Weekly Blocks:", self.spin_box) class_layout.addRow("Block Time:", ClassTimePicker()) self.class_options = QWidget() self.class_options.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.class_options.setLayout(class_layout) recurring_event_layout = QFormLayout() recurring_event_layout.setContentsMargins( 0, 0, 0, recurring_event_layout.verticalSpacing()) recurring_event_layout.addRow("Start Date:", self.event_start_date) recurring_event_layout.addRow("End Date:", self.event_end_date) self.recurring_event_time_picker = ClassTimePicker() recurring_event_layout.addRow("Event Time:", self.recurring_event_time_picker) self.recurring_event_options = QWidget() self.recurring_event_options.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.recurring_event_options.setLayout(recurring_event_layout) one_time_event_layout = QFormLayout() one_time_event_layout.setContentsMargins( 0, 0, 0, one_time_event_layout.verticalSpacing()) one_time_event_layout.addRow("Event Date:", self.event_date) self.one_time_event_options = QWidget() self.one_time_event_options.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.one_time_event_options.setLayout(one_time_event_layout) class_event_layout = QFormLayout() class_event_layout.setContentsMargins( 0, 0, 0, class_event_layout.verticalSpacing()) class_event_layout.addRow("Class:", self.class_picker) class_event_layout.addRow("Event Date:", self.class_event_date) self.class_event_options = QWidget() self.class_event_options.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.class_event_options.setLayout(class_event_layout) self.stack.addWidget(self.class_event_options) self.stack.addWidget(self.one_time_event_options) self.stack.addWidget(self.recurring_event_options) self.stack.addWidget(self.class_options) if self.data is None: self.set_type("Class") self.layout.addWidget(self.form_layout_widget) self.layout.addWidget(self.stack) self.setLayout(self.layout) self.show_buttons() #Update Values if self.data is defined if self.data is not None: event_type = self.data["type"] self.set_type(camel_case(event_type)) # noinspection PyTypeChecker class_layout: QFormLayout = self.stack.currentWidget().layout() self.name_edit.setText(self.event_name) self.name_edit.setDisabled(True) self.color_picker.setCurrentColor(to_qcolor(self.data["color"])) if event_type in ["class", "recurring event"]: self.start_date_model.content = QDateTime( to_qdate(self.data["start"])) self.end_date_model.content = QDateTime( to_qdate(self.data["end"])) if event_type == "class": blocks = self.data["blocks"] self.update_blocks(len(blocks)) for i, row in enumerate( range(self.rows_before_blocks, class_layout.rowCount())): block = blocks[i] # noinspection PyTypeChecker block_widget: ClassTimePicker = class_layout.itemAt( row, QFormLayout.FieldRole).widget() block_widget.set_time(to_qtime(block["time"])) block_widget.set_day(block["day"]) if event_type == "recurring event": self.recurring_event_time_picker.set_day(self.data["day"]) self.recurring_event_time_picker.set_time( to_qtime(self.data["time"])) if event_type in ["one time event", "one time class event"]: date_time = QDateTime() date_time.setDate(to_qdate(self.data["date"])) date_time.setTime(to_qtime(self.data["time"])) self.event_date_model.content = date_time if event_type == "one time class event": self.class_picker.setText(self.data["class_name"]) def show_buttons(self): save_button = QDialogButtonBox.Save if self.data is None else QDialogButtonBox.Apply cancel_button = QDialogButtonBox.Cancel buttonBox = QDialogButtonBox(Qt.Horizontal) buttonBox.addButton(save_button).clicked.connect(self.accept) buttonBox.addButton(cancel_button).clicked.connect(self.reject) if self.data is not None: delete_button = buttonBox.addButton(QDialogButtonBox.Discard) delete_button.setText("Delete") delete_button.clicked.connect(self.delete_event) self.layout.addWidget(buttonBox) def set_type(self, event_type: str): if self.event_type.text() == event_type: return self.event_type.setText(event_type) self.stack.currentWidget().setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) if event_type == "Class": self.class_options.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.class_options.adjustSize() self.stack.setCurrentWidget(self.class_options) elif event_type == "Recurring Event": self.recurring_event_options.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.recurring_event_options.adjustSize() self.stack.setCurrentWidget(self.recurring_event_options) elif event_type == "One Time Event": self.one_time_event_options.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.one_time_event_options.adjustSize() self.stack.setCurrentWidget(self.one_time_event_options) elif event_type == "One Time Class Event": self.class_event_options.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.class_event_options.adjustSize() self.stack.setCurrentWidget(self.class_event_options) self.stack.adjustSize() max_width = 0 for i in range(self.form_layout.rowCount()): widget = self.form_layout.itemAt(i, QFormLayout.LabelRole).widget() widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) widget.adjustSize() max_width = max(widget.size().width(), max_width) # noinspection PyTypeChecker current_widget_layout: QFormLayout = self.stack.currentWidget().layout( ) for i in range(current_widget_layout.rowCount()): widget = current_widget_layout.itemAt( i, QFormLayout.LabelRole).widget() widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) widget.adjustSize() max_width = max(widget.size().width(), max_width) for i in range(self.form_layout.rowCount()): self.form_layout.itemAt( i, QFormLayout.LabelRole).widget().setMinimumWidth(max_width) for i in range(current_widget_layout.rowCount()): current_widget_layout.itemAt( i, QFormLayout.LabelRole).widget().setMinimumWidth(max_width) self.adjustSize() def update_color(self): self.color_button.setStyleSheet( "background-color: rgb({},{},{})".format( self.color_picker.currentColor().red(), self.color_picker.currentColor().green(), self.color_picker.currentColor().blue())) def update_blocks(self, value): if self.spin_box.value() != value: self.spin_box.setValue(value) return if self.blocks == value: return old_blocks = self.blocks self.blocks = value class_options_layout: QFormLayout = self.class_options.layout() if self.blocks > old_blocks: #Change label of block 1 if old_blocks == 1: class_options_layout.itemAt( self.rows_before_blocks, QFormLayout.LabelRole).widget().setText("Block 1 Time:") for i in range(1, self.blocks - old_blocks + 1): offset = self.rows_before_blocks + old_blocks + i - 1 widget = class_options_layout.itemAt(offset, QFormLayout.FieldRole) label = class_options_layout.itemAt(offset, QFormLayout.LabelRole) if widget is not None and label is not None: widget = widget.widget() label = label.widget() widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) widget.adjustSize() label.adjustSize() widget.show() label.show() else: picker = ClassTimePicker() picker.sizePolicy().setRetainSizeWhenHidden(False) class_options_layout.addRow( "Block {} Time:".format(old_blocks + i), picker) elif self.blocks < old_blocks: if self.blocks == 1: class_options_layout.itemAt( self.rows_before_blocks, QFormLayout.LabelRole).widget().setText("Block Time:") for i in range(old_blocks - self.blocks): offset = self.rows_before_blocks + old_blocks + i - 1 widget = class_options_layout.itemAt( offset, QFormLayout.FieldRole).widget() label = class_options_layout.itemAt( offset, QFormLayout.LabelRole).widget() widget.hide() label.hide() widget.adjustSize() label.adjustSize() self.class_options.adjustSize() self.stack.adjustSize() self.adjustSize() # self.class_options.adjustSize() # self.stack.adjustSize() # self.adjustSize() def get_name(self): return self.name_edit.text() def get_data(self): event_type = self.event_type.text() data = { "type": event_type.lower(), "name": self.get_name(), "color": { "r": self.color_picker.currentColor().red(), "g": self.color_picker.currentColor().green(), "b": self.color_picker.currentColor().blue(), } } if event_type == "Class": block_data = [] # noinspection PyTypeChecker class_layout: QFormLayout = self.stack.currentWidget().layout() for row in range(self.rows_before_blocks, class_layout.rowCount()): # noinspection PyTypeChecker block_widget: ClassTimePicker = class_layout.itemAt( row, QFormLayout.FieldRole).widget() if block_widget.isHidden(): continue time = block_widget.get_time() block_data.append({ "day": block_widget.day_picker.get_day(), "time": { "hour": time.hour(), "minute": time.minute() } }) data["blocks"] = block_data if event_type in ["Class", "Recurring Event"]: start_date = self.start_date_model.content.date() data["start"] = { "day": start_date.day(), "month": start_date.month(), "year": start_date.year() } end_date = self.end_date_model.content.date() data["end"] = { "day": end_date.day(), "month": end_date.month(), "year": end_date.year() } if event_type == "Recurring Event": data["day"] = self.recurring_event_time_picker.day_picker.get_day() time = self.recurring_event_time_picker.get_time() data["time"] = {"hour": time.hour(), "minute": time.minute()} if event_type == "One Time Class Event": data["class_name"] = self.class_picker.text() if event_type in ["One Time Event", "One Time Class Event"]: date_time = self.event_date_model.content date = date_time.date() time = date_time.time() data["date"] = { "day": date.day(), "month": date.month(), "year": date.year(), } data["time"] = {"hour": time.hour(), "minute": time.minute()} return data def delete_event(self): error = QMessageBox() error.setText("Are you sure you would like to delete this event?") error.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) result = error.exec_() if result == QMessageBox.Yes: self.schedule.delete_event(self.event_name) self.reject() def accept(self): event_type = self.event_type.text() if event_type == "": error = QMessageBox() error.setText("Please select a type for the event.") error.exec_() self.event_type.setFocus() return # Check Name if len(self.get_name()) == 0: error = QMessageBox() error.setText("Please enter a name for the event.") error.exec_() self.name_edit.setFocus() return if event_type in ["Class", "Recurring Event"]: # Check Start/End Date start_date = self.start_date_model.content.date() end_date = self.end_date_model.content.date() if start_date >= end_date: error = QMessageBox() error.setText("End date cannot {} start date.".format( "be equal to" if start_date == end_date else "come before")) error.exec_() if event_type == "Class": self.class_end_date.setFocus() else: self.event_end_date.setFocus() return if event_type == "Class": # Check Blocks # noinspection PyTypeChecker class_layout: QFormLayout = self.stack.currentWidget().layout() for row in range(self.rows_before_blocks, class_layout.rowCount()): block_widget = class_layout.itemAt( row, QFormLayout.FieldRole).widget() if block_widget.isHidden(): continue # Make sure a day is selected for each block if not block_widget.is_valid(): block_name = "the class block" if self.blocks == 1 else str.lower( class_layout.itemAt(row, QFormLayout.LabelRole). widget().text()).replace(" time:", "") error = QMessageBox() error.setText( "Please select a valid day for {}.".format( block_name)) error.exec_() return # Check for duplicate blocks for other in range(self.rows_before_blocks, class_layout.rowCount() - 1): if row == other: continue other_block_widget = class_layout.itemAt( other, QFormLayout.FieldRole).widget() same_time = block_widget.get_time( ) == other_block_widget.get_time() same_day = block_widget.day_picker.get_day( ) == other_block_widget.day_picker.get_day() if same_time and same_day: error = QMessageBox() error.setText( "Block {} and {} cannot have the same day and time." .format(row - self.rows_before_blocks + 1, other - self.rows_before_blocks + 1)) error.exec_() return if event_type == "Recurring Event": # Make sure a day is selected if not self.recurring_event_time_picker.is_valid(): error = QMessageBox() error.setText("Please select a valid day for this event.") error.exec_() self.recurring_event_time_picker.setFocus() return if event_type == "One Time Class Event": # Check Class if len(self.class_picker.text()) == 0: error = QMessageBox() error.setText("Please select a class for this event.") error.exec_() self.class_picker.setFocus() return # Valid name if self.get_name() in self.schedule.schedule.keys(): if self.data is None: error = QMessageBox() error.setText( "An event with this name already exists, would you like to overwrite it?" ) error.setStandardButtons(error.Apply | error.Cancel) result = error.exec_() if result == error.Apply: to_overwrite = self.schedule.schedule[self.get_name()] if to_overwrite["type"] == "class": if self.event_type.text() == "One Time Class Event": if self.class_picker.text() == self.get_name(): error = QMessageBox() error.setText( "Cannot overwrite a class with a one time class event for that class.\n" "Please select a different class.") error.setStandardButtons(QMessageBox.Close) error.exec_() return self.schedule.edit_event(self.get_data()) self.reject() elif result == error.Cancel: self.name_edit.setFocus() return self.schedule.edit_event(self.get_data()) self.reject() super(Popup, self).accept() def reject(self): super(Popup, self).reject()
class ScorePartsWidget(QSplitter): def __init__(self, parent): super(ScorePartsWidget, self).__init__(parent) self.typesLabel = QLabel() self.typesView = QTreeView( selectionMode=QTreeView.ExtendedSelection, selectionBehavior=QTreeView.SelectRows, animated=True, headerHidden=True) self.scoreLabel = QLabel() self.scoreView = widgets.treewidget.TreeWidget( selectionMode=QTreeView.ExtendedSelection, selectionBehavior=QTreeView.SelectRows, headerHidden=True, animated=True, dragDropMode=QTreeView.InternalMove) self.addButton = QPushButton(icon = icons.get("list-add")) self.removeButton = QPushButton(icon = icons.get("list-remove")) self.upButton = QToolButton(icon = icons.get("go-up")) self.downButton = QToolButton(icon = icons.get("go-down")) self.partSettings = QStackedWidget() w = QWidget() self.addWidget(w) layout = QVBoxLayout(spacing=0) w.setLayout(layout) layout.addWidget(self.typesLabel) layout.addWidget(self.typesView) layout.addWidget(self.addButton) w = QWidget() self.addWidget(w) layout = QVBoxLayout(spacing=0) w.setLayout(layout) layout.addWidget(self.scoreLabel) layout.addWidget(self.scoreView) box = QHBoxLayout(spacing=0) layout.addLayout(box) box.addWidget(self.removeButton) box.addWidget(self.upButton) box.addWidget(self.downButton) self.addWidget(self.partSettings) self.typesView.setModel(parts.model()) app.translateUI(self) # signal connections self.addButton.clicked.connect(self.slotAddButtonClicked) self.removeButton.clicked.connect(self.slotRemoveButtonClicked) self.typesView.doubleClicked.connect(self.slotDoubleClicked) self.scoreView.currentItemChanged.connect(self.slotCurrentItemChanged) self.upButton.clicked.connect(self.scoreView.moveSelectedChildrenUp) self.downButton.clicked.connect(self.scoreView.moveSelectedChildrenDown) def translateUI(self): bold = "<b>{0}</b>".format self.typesLabel.setText(bold(_("Available parts:"))) self.scoreLabel.setText(bold(_("Score:"))) self.addButton.setText(_("&Add")) self.removeButton.setText(_("&Remove")) self.upButton.setToolTip(_("Move up")) self.downButton.setToolTip(_("Move down")) def slotDoubleClicked(self, index): self.addParts([index]) def slotAddButtonClicked(self): self.addParts(self.typesView.selectedIndexes()) def addParts(self, indexes): """Adds the parts for the given indexes.""" # add to current if that is a container type currentItem = self.scoreView.currentItem() for index in indexes: category = index.internalPointer() if category: part = category.items[index.row()] box = QGroupBox(self.partSettings) self.partSettings.addWidget(box) # determine the parent: current or root if currentItem and issubclass(part, currentItem.part.accepts()): parent = currentItem parent.setExpanded(True) else: parent = self.scoreView item = PartItem(parent, part, box) def slotCurrentItemChanged(self, item): if isinstance(item, PartItem): self.partSettings.setCurrentWidget(item.box) def slotRemoveButtonClicked(self): self.scoreView.removeSelectedItems() def clear(self): """Called when the user clicks the clear button on this page.""" self.scoreView.clear() def rootPartItem(self): """Returns the invisibleRootItem(), representing the tree of parts in the score view.""" return self.scoreView.invisibleRootItem()
class CentralWidget(QWidget): # This signals is used by notificator databaseSaved = pyqtSignal('QString') querySaved = pyqtSignal('QString') def __init__(self): QWidget.__init__(self) box = QVBoxLayout(self) box.setContentsMargins(0, 0, 0, 0) box.setSpacing(0) self.stacked = QStackedWidget() box.addWidget(self.stacked) self.created = False self.__last_open_folder = None self.__recent_dbs = [] if PSetting.RECENT_DBS: self.__recent_dbs = PSetting.RECENT_DBS Pireal.load_service("central", self) @property def recent_databases(self): return self.__recent_dbs @recent_databases.setter def recent_databases(self, database_file): if database_file in PSetting.RECENT_DBS: PSetting.RECENT_DBS.remove(database_file) PSetting.RECENT_DBS.insert(0, database_file) self.__recent_dbs = PSetting.RECENT_DBS def create_database(self): """ Show a wizard widget to create a new database, only have one database open at time. """ if self.created: QMessageBox.information(self, self.tr("Information"), self.tr("You may only have one database" " open at time.")) DEBUG("Ya existe una base de datos abierta") return wizard = database_wizard.DatabaseWizard(self) wizard.wizardFinished.connect( self.__on_wizard_finished) # Hide menubar and toolbar pireal = Pireal.get_service("pireal") pireal.show_hide_menubar() pireal.show_hide_toolbar() # Add wizard widget to stacked self.add_widget(wizard) def __on_wizard_finished(self, data, wizard_widget): """ This slot execute when wizard to create a database is finished """ pireal = Pireal.get_service("pireal") if not data: # If it's canceled, remove wizard widget and return to Start Page self.remove_last_widget() else: # Create a new data base container db_container = database_container.DatabaseContainer() # Associate the file name with the PFile object pfile_object = pfile.File(data['filename']) # Associate PFile object with data base container # and add widget to stacked db_container.pfile = pfile_object self.add_widget(db_container) # Remove wizard self.stacked.removeWidget(wizard_widget) # Set window title pireal.change_title(file_manager.get_basename(data['filename'])) # Enable db actions pireal.set_enabled_db_actions(True) pireal.set_enabled_relation_actions(True) self.created = True DEBUG("Base de datos creada correctamente: '{}'".format( data['filename'])) # If data or not, show menubar and toolbar again pireal.show_hide_menubar() pireal.show_hide_toolbar() def open_database(self, filename=''): """ This function opens a database and set this on the UI """ # If not filename provide, then open dialog to select if self.created: QMessageBox.information(self, self.tr("Information"), self.tr("You may only have one database" " open at time.")) DEBUG("Ya existe una base de datos abierta") return if not filename: if self.__last_open_folder is None: directory = os.path.expanduser("~") else: directory = self.__last_open_folder filter_ = settings.SUPPORTED_FILES.split(';;')[0] filename, _ = QFileDialog.getOpenFileName(self, self.tr("Open Database"), directory, filter_) # If is canceled, return if not filename: return # Remember the folder self.__last_open_folder = file_manager.get_path(filename) DEBUG("Abriendo la base de datos: '{}'".format(filename)) # If filename provide try: # Read pdb file pfile_object = pfile.File(filename) db_data = pfile_object.read() # Create a dict to manipulate data more easy db_data = self.__sanitize_data(db_data) except Exception as reason: QMessageBox.information(self, self.tr("The file couldn't be open"), str(reason)) CRITICAL("Error al intentar abrir el archivo: {}".format(reason)) return # Create a database container widget db_container = database_container.DatabaseContainer() try: db_container.create_database(db_data) except Exception as reason: QMessageBox.information(self, self.tr("Error"), str(reason)) CRITICAL("Error al crear la base de datos: {}".format(reason)) return # Set the PFile object to the new database db_container.pfile = pfile_object # Add data base container to stacked self.add_widget(db_container) # Database name db_name = file_manager.get_basename(filename) # Update title with the new database name, and enable some actions pireal = Pireal.get_service("pireal") pireal.change_title(db_name) pireal.set_enabled_db_actions(True) pireal.set_enabled_relation_actions(True) # Add to recent databases self.recent_databases = filename self.created = True def open_query(self): filter_ = settings.SUPPORTED_FILES.split(';;')[1] filename, _ = QFileDialog.getOpenFileName(self, self.tr("Open Query"), os.path.expanduser("~"), filter_) if not filename: return # FIXME: mejorar éste y new_query self.new_query(filename) def save_query(self, editor=None): db = self.get_active_db() fname = db.save_query(editor) if fname: self.querySaved.emit(self.tr("Query saved: {}".format(fname))) def save_query_as(self): pass def __sanitize_data(self, data): """ This function converts the data into a dictionary for better handling then. The argument 'data' is the content of the database. """ # FIXME: controlar cuando al final de la línea hay una coma data_dict = {'tables': []} for line_count, line in enumerate(data.splitlines()): # Ignore blank lines if not line: continue if line.startswith('@'): # This line is a header tpoint = line.find(':') if tpoint == -1: raise Exception("Invalid syntax at line {}".format( line_count + 1)) table_name, line = line.split(':') table_name = table_name[1:].strip() table_dict = {} table_dict['name'] = table_name table_dict['header'] = line.split(',') table_dict['tuples'] = [] else: for l in csv.reader([line]): # Remove spaces l = list(map(str.strip, l)) # FIXME: this is necesary? if table_dict['name'] == table_name: table_dict['tuples'].append(l) if not table_dict['tuples']: data_dict['tables'].append(table_dict) return data_dict def remove_last_widget(self): """ Remove last widget from stacked """ widget = self.stacked.widget(self.stacked.count() - 1) self.stacked.removeWidget(widget) def close_database(self): """ Close the database and return to the main widget """ db = self.get_active_db() query_container = db.query_container if db.modified: msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle(self.tr("Save Changes?")) msgbox.setText(self.tr("The <b>{}</b> database has ben" " modified.<br>Do you want save " "your changes?".format( db.dbname()))) cancel_btn = msgbox.addButton(self.tr("Cancel"), QMessageBox.RejectRole) msgbox.addButton(self.tr("No"), QMessageBox.NoRole) yes_btn = msgbox.addButton(self.tr("Yes"), QMessageBox.YesRole) msgbox.exec_() r = msgbox.clickedButton() if r == cancel_btn: return if r == yes_btn: self.save_database() # Check if editor is modified query_widget = query_container.currentWidget() if query_widget is not None: weditor = query_widget.get_editor() if weditor is not None: # TODO: duplicate code, see tab widget if weditor.modified: msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle(self.tr("File modified")) msgbox.setText(self.tr("The file <b>{}</b> has unsaved " "changes. You want to keep " "them?".format( weditor.name))) cancel_btn = msgbox.addButton(self.tr("Cancel"), QMessageBox.RejectRole) msgbox.addButton(self.tr("No"), QMessageBox.NoRole) yes_btn = msgbox.addButton(self.tr("Yes"), QMessageBox.YesRole) msgbox.exec_() r = msgbox.clickedButton() if r == cancel_btn: return if r == yes_btn: self.save_query(weditor) self.stacked.removeWidget(db) pireal = Pireal.get_service("pireal") pireal.set_enabled_db_actions(False) pireal.set_enabled_relation_actions(False) pireal.set_enabled_query_actions(False) pireal.set_enabled_editor_actions(False) self.created = False DEBUG("Se cerró la base de datos: '{}'".format(db.dbname())) del db def new_query(self, filename=''): pireal = Pireal.get_service("pireal") db_container = self.get_active_db() db_container.new_query(filename) # Enable editor actions # FIXME: refactoring pireal.set_enabled_query_actions(True) zoom_in_action = Pireal.get_action("zoom_in") zoom_in_action.setEnabled(True) zoom_out_action = Pireal.get_action("zoom_out") zoom_out_action.setEnabled(True) paste_action = Pireal.get_action("paste_action") paste_action.setEnabled(True) comment_action = Pireal.get_action("comment") comment_action.setEnabled(True) uncomment_action = Pireal.get_action("uncomment") uncomment_action.setEnabled(True) def execute_queries(self): db_container = self.get_active_db() db_container.execute_queries() def execute_selection(self): db_container = self.get_active_db() db_container.execute_selection() def save_database(self): db = self.get_active_db() if not db.modified: return # Get relations dict relations = db.table_widget.relations # Generate content content = file_manager.generate_database(relations) db.pfile.save(content=content) filename = db.pfile.filename # Emit signal self.databaseSaved.emit( self.tr("Database saved: {}".format(filename))) db.modified = False def save_database_as(self): filter = settings.SUPPORTED_FILES.split(';;')[0] filename, _ = QFileDialog.getSaveFileName(self, self.tr("Save Database As"), settings.PIREAL_PROJECTS, filter) if not filename: return db = self.get_active_db() # Get relations relations = db.table_widget.relations # Content content = file_manager.generate_database(relations) db.pfile.save(content, filename) self.databaseSaved.emit( self.tr("Database saved: {}".format(db.pfile.filename))) db.modified = False def remove_relation(self): db = self.get_active_db() if db.delete_relation(): db.modified = True def create_new_relation(self): data = new_relation_dialog.create_relation() if data is not None: db = self.get_active_db() rela, rela_name = data # Add table db.table_widget.add_table(rela, rela_name) # Add item to lateral widget db.lateral_widget.add_item(rela_name, rela.count()) # Set modified db db.modified = True def edit_relation(self): db = self.get_active_db() lateral = db.lateral_widget selected_items = lateral.selectedItems() if selected_items: selected_relation = selected_items[0].text(0) relation_text = selected_relation.split()[0].strip() rela = db.table_widget.relations[relation_text] data = edit_relation_dialog.edit_relation(rela) if data is not None: # Update table db.table_widget.update_table(data) # Update relation db.table_widget.relations[relation_text] = data # Set modified db db.modified = True lateral.update_item(data.count()) def load_relation(self, filename=''): """ Load Relation file """ if not filename: if self.__last_open_folder is None: directory = os.path.expanduser("~") else: directory = self.__last_open_folder msg = self.tr("Open Relation File") filter_ = settings.SUPPORTED_FILES.split(';;')[-1] filenames = QFileDialog.getOpenFileNames(self, msg, directory, filter_)[0] if not filenames: return # Save folder self.__last_open_folder = file_manager.get_path(filenames[0]) db_container = self.get_active_db() if db_container.load_relation(filenames): db_container.modified = True def add_start_page(self): """ This function adds the Start Page to the stacked widget """ sp = start_page.StartPage() self.add_widget(sp) def show_settings(self): """ Show settings dialog on stacked """ preferences_dialog = preferences.Preferences(self) if isinstance(self.widget(1), preferences.Preferences): self.widget(1).close() else: self.stacked.insertWidget(1, preferences_dialog) self.stacked.setCurrentIndex(1) # Connect the closed signal preferences_dialog.settingsClosed.connect(self._settings_closed) def widget(self, index): """ Returns the widget at the given index """ return self.stacked.widget(index) def add_widget(self, widget): """ Appends and show the given widget to the Stacked """ index = self.stacked.addWidget(widget) self.stacked.setCurrentIndex(index) def _settings_closed(self): self.stacked.removeWidget(self.widget(1)) self.stacked.setCurrentWidget(self.stacked.currentWidget()) def get_active_db(self): """ Return an instance of DatabaseContainer widget if the stacked contains an DatabaseContainer in last index or None if it's not an instance of DatabaseContainer """ index = self.stacked.count() - 1 widget = self.widget(index) if isinstance(widget, database_container.DatabaseContainer): return widget return None def get_unsaved_queries(self): query_container = self.get_active_db().query_container return query_container.get_unsaved_queries() def undo_action(self): query_container = self.get_active_db().query_container query_container.undo() def redo_action(self): query_container = self.get_active_db().query_container query_container.redo() def cut_action(self): query_container = self.get_active_db().query_container query_container.cut() def copy_action(self): query_container = self.get_active_db().query_container query_container.copy() def paste_action(self): query_container = self.get_active_db().query_container query_container.paste() def zoom_in(self): query_container = self.get_active_db().query_container query_container.zoom_in() def zoom_out(self): query_container = self.get_active_db().query_container query_container.zoom_out() def comment(self): query_container = self.get_active_db().query_container query_container.comment() def uncomment(self): query_container = self.get_active_db().query_container query_container.uncomment()
class FontsColors(preferences.Page): def __init__(self, dialog): super(FontsColors, self).__init__(dialog) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.scheme = SchemeSelector(self) layout.addWidget(self.scheme) self.printScheme = QCheckBox() layout.addWidget(self.printScheme) hbox = QHBoxLayout() self.tree = QTreeWidget(self) self.tree.setHeaderHidden(True) self.tree.setAnimated(True) self.stack = QStackedWidget(self) hbox.addWidget(self.tree) hbox.addWidget(self.stack) layout.addLayout(hbox) hbox = QHBoxLayout() self.fontLabel = QLabel() self.fontChooser = QFontComboBox() self.fontSize = QDoubleSpinBox() self.fontSize.setRange(6.0, 32.0) self.fontSize.setSingleStep(0.5) self.fontSize.setDecimals(1) hbox.addWidget(self.fontLabel) hbox.addWidget(self.fontChooser, 1) hbox.addWidget(self.fontSize) layout.addLayout(hbox) # add the items to our list self.baseColorsItem = i = QTreeWidgetItem() self.tree.addTopLevelItem(i) self.defaultStylesItem = i = QTreeWidgetItem() self.tree.addTopLevelItem(i) self.defaultStyles = {} for name in textformats.defaultStyles: self.defaultStyles[name] = i = QTreeWidgetItem() self.defaultStylesItem.addChild(i) i.name = name self.defaultStylesItem.setExpanded(True) self.allStyles = {} for group, styles in ly.colorize.default_mapping(): i = QTreeWidgetItem() children = {} self.allStyles[group] = (i, children) self.tree.addTopLevelItem(i) i.group = group for name, base, clss in styles: j = QTreeWidgetItem() j.name = name j.base = base i.addChild(j) children[name] = j self.baseColorsWidget = BaseColors(self) self.customAttributesWidget = CustomAttributes(self) self.emptyWidget = QWidget(self) self.stack.addWidget(self.baseColorsWidget) self.stack.addWidget(self.customAttributesWidget) self.stack.addWidget(self.emptyWidget) self.tree.currentItemChanged.connect(self.currentItemChanged) self.tree.setCurrentItem(self.baseColorsItem) self.scheme.currentChanged.connect(self.currentSchemeChanged) self.scheme.changed.connect(self.changed) self.baseColorsWidget.changed.connect(self.baseColorsChanged) self.customAttributesWidget.changed.connect(self.customAttributesChanged) self.fontChooser.currentFontChanged.connect(self.fontChanged) self.fontSize.valueChanged.connect(self.fontChanged) self.printScheme.clicked.connect(self.printSchemeChanged) app.translateUI(self) def translateUI(self): self.printScheme.setText(_("Use this scheme for printing")) self.fontLabel.setText(_("Font:")) self.baseColorsItem.setText(0, _("Base Colors")) self.defaultStylesItem.setText(0, _("Default Styles")) self.defaultStyleNames = defaultStyleNames() self.allStyleNames = allStyleNames() for name in textformats.defaultStyles: self.defaultStyles[name].setText(0, self.defaultStyleNames[name]) for group, styles in ly.colorize.default_mapping(): try: n = self.allStyleNames[group][0] except KeyError: n = group self.allStyles[group][0].setText(0, n) for name, base, clss in styles: try: n = self.allStyleNames[group][1][name] except KeyError: n = name self.allStyles[group][1][name].setText(0, n) def currentItemChanged(self, item, previous): if item is self.baseColorsItem: self.stack.setCurrentWidget(self.baseColorsWidget) elif not item.parent(): self.stack.setCurrentWidget(self.emptyWidget) else: data = self.data[self.scheme.currentScheme()] w = self.customAttributesWidget self.stack.setCurrentWidget(w) toptext = None if item.parent() is self.defaultStylesItem: # default style w.setTitle(item.text(0)) w.setTristate(False) w.setTextFormat(data.defaultStyles[item.name]) else: # specific style of specific group group, name = item.parent().group, item.name w.setTitle("{0}: {1}".format(item.parent().text(0), item.text(0))) inherit = item.base if inherit: toptext = _("(Inherits: {name})").format(name=self.defaultStyleNames[inherit]) w.setTristate(bool(inherit)) w.setTextFormat(data.allStyles[group][name]) w.setTopText(toptext) def currentSchemeChanged(self): scheme = self.scheme.currentScheme() if scheme not in self.data: self.data[scheme] = textformats.TextFormatData(scheme) self.updateDisplay() if self.tree.currentItem(): self.currentItemChanged(self.tree.currentItem(), None) with qutil.signalsBlocked(self.printScheme): self.printScheme.setChecked(scheme == self._printScheme) def fontChanged(self): data = self.data[self.scheme.currentScheme()] data.font = self.fontChooser.currentFont() data.font.setPointSizeF(self.fontSize.value()) self.updateDisplay() self.changed.emit() def printSchemeChanged(self): if self.printScheme.isChecked(): self._printScheme = self.scheme.currentScheme() else: self._printScheme = None self.changed.emit() def addSchemeData(self, scheme, tfd): self.data[scheme] = tfd def currentSchemeData(self): return self.data[self.scheme.currentScheme()] def updateDisplay(self): data = self.data[self.scheme.currentScheme()] with qutil.signalsBlocked(self.fontChooser, self.fontSize): self.fontChooser.setCurrentFont(data.font) self.fontSize.setValue(data.font.pointSizeF()) with qutil.signalsBlocked(self): # update base colors for name in textformats.baseColors: self.baseColorsWidget.color[name].setColor(data.baseColors[name]) # update base colors for whole treewidget p = QApplication.palette() p.setColor(QPalette.Base, data.baseColors['background']) p.setColor(QPalette.Text, data.baseColors['text']) p.setColor(QPalette.Highlight, data.baseColors['selectionbackground']) p.setColor(QPalette.HighlightedText, data.baseColors['selectiontext']) self.tree.setPalette(p) def setItemTextFormat(item, f): font = QFont(data.font) if f.hasProperty(QTextFormat.ForegroundBrush): item.setForeground(0, f.foreground().color()) else: item.setForeground(0, data.baseColors['text']) if f.hasProperty(QTextFormat.BackgroundBrush): item.setBackground(0, f.background().color()) else: item.setBackground(0, QBrush()) font.setWeight(f.fontWeight()) font.setItalic(f.fontItalic()) font.setUnderline(f.fontUnderline()) item.setFont(0, font) # update looks of default styles for name in textformats.defaultStyles: setItemTextFormat(self.defaultStyles[name], data.defaultStyles[name]) # update looks of all the specific styles for group, styles in ly.colorize.default_mapping(): children = self.allStyles[group][1] for name, inherit, clss in styles: f = QTextCharFormat(data.defaultStyles[inherit]) if inherit else QTextCharFormat() f.merge(data.allStyles[group][name]) setItemTextFormat(children[name], f) def baseColorsChanged(self, name): # keep data up to date with base colors data = self.data[self.scheme.currentScheme()] data.baseColors[name] = self.baseColorsWidget.color[name].color() self.updateDisplay() self.changed.emit() def customAttributesChanged(self): item = self.tree.currentItem() if not item or not item.parent(): return data = self.data[self.scheme.currentScheme()] if item.parent() is self.defaultStylesItem: # a default style has been changed data.defaultStyles[item.name] = self.customAttributesWidget.textFormat() else: # a specific style has been changed group, name = item.parent().group, item.name data.allStyles[group][name] = self.customAttributesWidget.textFormat() self.updateDisplay() self.changed.emit() def import_(self, filename): from . import import_export import_export.importTheme(filename, self, self.scheme) def export(self, name, filename): from . import import_export try: import_export.exportTheme(self, name, filename) except (IOError, OSError) as e: QMessageBox.critical(self, _("Error"), _( "Can't write to destination:\n\n{url}\n\n{error}").format( url=filename, error=e.strerror)) def loadSettings(self): self.data = {} # holds all data with scheme as key self._printScheme = QSettings().value("printer_scheme", "default", str) self.scheme.loadSettings("editor_scheme", "editor_schemes") def saveSettings(self): self.scheme.saveSettings("editor_scheme", "editor_schemes", "fontscolors") for scheme in self.scheme.schemes(): if scheme in self.data: self.data[scheme].save(scheme) if self._printScheme: QSettings().setValue("printer_scheme", self._printScheme) else: QSettings().remove("printer_scheme")
class MainWindow(QMainWindow): screen = None width, height = None, None picture = None login_form = None user_page = None pages = None def __init__(self): super().__init__() if sys.platform != 'linux': my_app_id = 'InnoUI.DMD_project.ez_A_for_course.101' # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( my_app_id) self.setWindowIcon(QIcon("./images/main_logo.png")) size = QDesktopWidget().screenGeometry(-1) self.width = size.width() // 5 self.picture = QPixmap("./images/main_logo.png").scaledToWidth( self.width - self.width // 20) self.height = self.picture.height() + size.height() // 16 self.setFixedSize(self.width, self.height) self.setWindowTitle('DmD2') self.pages = QStackedWidget() qr = self.frameGeometry() center = QDesktopWidget().availableGeometry().center() qr.moveCenter(center) self.move(qr.topLeft()) self.init_ui() def login(self, login, password): try: backend.login(login, password) except AssertionError as exception: QToolTip.showText( self.mapToGlobal(self.login_form.geometry().center()), 'Invalid credentials') return self.user_page = levels[backend.user.user_info['role']](self, backend.user) self.pages.addWidget(self.user_page) self.pages.setCurrentWidget(self.user_page) self.repaint() self.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) self.resize(self.width * 2, self.height * 2) qr = self.frameGeometry() center = QDesktopWidget().availableGeometry().center() qr.moveCenter(center) self.move(qr.topLeft()) def logout(self): backend.logout() self.pages.setCurrentWidget(self.login_form) self.repaint() self.setFixedSize(self.width, self.height) qr = self.frameGeometry() center = QDesktopWidget().availableGeometry().center() qr.moveCenter(center) self.move(qr.topLeft()) self.pages.removeWidget(self.user_page) def init_ui(self): self.login_form = LoginForm(self.width, self.height, self.picture) self.user_page = BasePage(self) self.pages.addWidget(self.login_form) self.pages.setCurrentWidget(self.login_form) self.setCentralWidget(self.pages) self.login_form.button.clicked.connect( lambda: self.login(self.login_form.login_field.text(), self.login_form.password_field.text())) self.show()
class HMI(QtWidgets.QMainWindow, Ui_MainWindow): signal_send_message = pyqtSignal(str) signal_temp_select_category = pyqtSignal(str) signal_start_timer = pyqtSignal(int) def __init__(self, parent=None, ui_file=None, loglevel=logging.INFO, hmi_port=None, game_port=None, skip_userreg=False, skip_spinanimation=False): super(HMI, self).__init__(parent) if not IMPORT_UI_ONTHEFLY: self.setupUi(self) else: # used for on-the-fly compilation of ui xml if ui_file is None: raise Exception("Did not specify a .ui file") uic.loadUi(ui_file, self) self.logger = logs.build_logger(__name__, loglevel) self.loglevel = loglevel self.hmi_port = hmi_port self.game_port = game_port self.logger.debug("selected hmi_port=%s" % (self.hmi_port)) self.logger.debug("selected game_port=%s" % (self.game_port)) self.skip_spinanimation = skip_spinanimation self.setWindowTitle("Wheel of Jeopardy") self.setStyleSheet("background-color: gray;") self.sounds = { "Correct": QSound("Correct.wav"), "Incorrect": QSound("Incorrect.wav"), "Bankrupt": QSound("Bankrupt.wav"), "Double": QSound("Double.wav") } self.stack = QStackedWidget() for i in reversed(range(self.baseLayout.count())): self.baseLayout.takeAt(0) for i in reversed(range(self.verticalLayout.count())): self.verticalLayout.takeAt(0) self.verticalLayout.addWidget(self.stack) self.setCentralWidget(self.stack) self.mw = QVBoxLayout() self.mw.addLayout(self.contextLayout) self.mw.addLayout(self.bodyLayout) self.mw.addLayout(self.controlLayout) self.qw = QWidget() self.qw.setLayout(self.mw) self.mw.setStretchFactor(self.contextLayout, 1) self.mw.setStretchFactor(self.bodyLayout, 5) self.mw.setStretchFactor(self.controlLayout, 1) self.stack.raise_() self.stack.addWidget(self.qw) self.MSG_controller = messaging.HMIMessageController( loglevel=loglevel, msg_controller_name="HMILogic", listen_port=self.hmi_port, target_port=self.game_port) self.registration_wizard = wizard.MyWizard( ui_file="register_user_wizard.ui", loglevel=self.loglevel) self.stack.addWidget(self.registration_wizard) self.logic_controller = HMILogicController(loglevel=loglevel) self.logic_controller_thread = QThread(self) self.buttonSpin = HoverButton(self) self.buttonSpin.setText("Spin") self.buttonSpin.setAlignment(Qt.AlignCenter) self.buttonSpin.clicked.connect(self.logic_controller.askToSpin) self.controlLayout.addWidget(self.buttonSpin) # Pass messages received to the logic controller self.MSG_controller.signal_recieve_message.connect( self.logic_controller.processMessage) # Pass responses from the logic controller into the output of the message controller self.logic_controller.signal_send_message.connect( self.MSG_controller.send_message) self.signal_send_message.connect(self.MSG_controller.send_message) # Pass requests from the logic controller to update game stats to the HMI engine self.logic_controller.signal_update_game_stats.connect( self.updateGameStats) # Pass requests from the logic controller to update player stats to the HMI engine self.logic_controller.signal_update_player_data.connect( self.updatePlayer) # Pass requests from the logic controller to spin the wheel to the HMI engine self.logic_controller.signal_spin_wheel.connect(self.spinWheel) # Pass requests from the logic controller to alter UI to indicate the name of winner self.logic_controller.signal_display_winner.connect(self.displayWinner) # Pass requests from the logic controller to prompt a user to select a category self.logic_controller.signal_playerselect_category.connect( partial(self.selectCategory, target="player")) self.logic_controller.signal_opponentselect_category.connect( partial(self.selectCategory, target="opponents")) # temporarily, connect category stuff up self.signal_temp_select_category.connect( self.logic_controller.returnCategory) # Pass requests form the logic controller to ask HMI to update the board self.logic_controller.signal_update_board.connect(self.updateBoard) # Pass requests from the logic controller to ask HMI to update the wheel self.logic_controller.signal_update_wheel.connect(self.updateWheel) # Pass requests from the logic controller to ask HMI to diplay a question self.logic_controller.signal_display_question.connect( self.displayQuestion) # Pass requests from the logic controller to prompt the user to indicate the outcome of the question self.logic_controller.signal_determine_correctness.connect( self.selectOutcome) # Pass requests from the logic controller to ask HMI to display the answer to a question self.logic_controller.signal_display_answer.connect(self.displayAnswer) # Pass requests from the logic controller to ask HMI to adjust various items self.logic_controller.signal_lock_unlock.connect(self.setUIState) self.logic_controller.moveToThread(self.logic_controller_thread) #connect logic controller to wizard success/fail self.logic_controller.signal_feedback_registration_fail.connect( self.registration_wizard.pageUserEntry. signal_validation_response_failure) self.logic_controller.signal_feedback_registration_failmsg.connect( self.registration_wizard.setFeedback) self.logic_controller.signal_feedback_registration_success.connect( self.registration_wizard.pageUserEntry. signal_validation_response_success) self.registration_wizard.signal_submit_players.connect( self.logic_controller.notifyUserRegistration) #connect sounds self.logic_controller.signal_play_correct_sound.connect( self.playCorrect) self.logic_controller.signal_play_incorrect_sound.connect( self.playIncorrect) self.logic_controller.signal_play_bankrupt_sound.connect( self.playBankrupt) self.logic_controller.signal_play_double_sound.connect(self.playDouble) self.logic_controller.signal_determine_freeturn_spend.connect( self.determineFreeTurnSpend) #help from https://stackoverflow.com/questions/46174073/open-a-new-window-after-finish-button-has-been-clicked-on-qwizard-pyqt5?rq=1 self.registration_wizard.button( QtWidgets.QWizard.FinishButton).clicked.connect( self.shiftToComboWheelBoardScore) #self.logic_controller.signal_scene_change_to_wheel.connect(self.shiftToWheelScene) self.logic_controller.signal_scene_change_to_main.connect( self.shiftToComboWheelBoardScore) self.main_scorebar = ScoreBar(self) self.contextLayout.addWidget(self.main_scorebar) self.round_spin_frame = RoundSpinFrame(self) self.main_scorebar.resized.connect(self.round_spin_frame.setMaxHeight) self.contextLayout.addWidget(self.round_spin_frame) self.contextLayout.setStretchFactor(self.main_scorebar, 15) self.contextLayout.setStretchFactor(self.round_spin_frame, 2) self.use_textwheel = False self.use_qgraphics_wheel = False self.wheelinfo = dict() for each in range(0, 13): setattr(self, "label_wheel_%s" % str(each), QLabel()) getattr(self, "label_wheel_%s" % str(each)).setDisabled(True) if self.use_qgraphics_wheel: self.wheel_radius = 125 self.wheel_scene = WheelScene(parent=self, radius=self.wheel_radius) self.wheel_view = QtWidgets.QGraphicsView(parent=self) self.wheel_view.setScene(self.wheel_scene) self.wheel_view.setStyleSheet("background: transparent") self.wheel_view.setSceneRect(0, 0, self.wheel_radius * 2, self.wheel_radius * 2) self.wheel_view.rotate((360 / 12) / 2) self.wheel_group = self.wheel_scene.createItemGroup( self.wheel_scene.items()) self.wheel_view.show() self.bodyLayout.addWidget(self.wheel_view) self.bodyLayout.setStretchFactor(self.wheel_view, 6) else: self.wheel_gui = WheelPhoto(self) self.bodyLayout.addWidget(self.wheel_gui) self.bodyLayout.setContentsMargins(0, 0, 0, 0) self.bodyLayout.setStretchFactor(self.wheel_gui, 6) self.selectionTitle = QLabel(self.wheel_gui) self.selectionTitle.setText("") self.selectionTitle.setAlignment(Qt.AlignRight) self.selectionTitle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.selectionTitle.move( self.wheel_gui.frameRect().width() / 2 - self.selectionTitle.width() * 1.5, self.wheel_gui.geometry().height() / 2.1) self.selectionTitle.setFixedWidth(self.wheel_gui.rect().width() / 2.5) #self.bodyLayout.addWidget(self.tempButton) self.arrowPointer = ArrowPointer(self) self.bodyLayout.addWidget(self.arrowPointer) self.bodyLayout.setStretchFactor(self.arrowPointer, 1) self.board = Board(self) self.bodyLayout.addWidget(self.board) self.bodyLayout.setStretchFactor(self.board, 16) self.buttonFreeTurnSpend = HoverButton(self) self.buttonFreeTurnSpend.setText("Spend FreeTurn Token") self.buttonFreeTurnSpend.setAlignment(Qt.AlignCenter) self.buttonFreeTurnSpend.clicked.connect(self.renderSpinButton) self.controlLayout.addWidget(self.buttonFreeTurnSpend) self.buttonFreeTurnSpend.hide() self.buttonFreeTurnSkip = HoverButton(self) self.buttonFreeTurnSkip.setText("Skip") self.buttonFreeTurnSkip.setAlignment(Qt.AlignCenter) self.buttonFreeTurnSkip.clicked.connect(self.renderSpinButton) self.controlLayout.addWidget(self.buttonFreeTurnSkip) self.buttonFreeTurnSkip.hide() self.scene_question = questionanswer.MyQuestionScene( parent=self, ui_file="scene_question.ui", loglevel=self.loglevel, ) self.scene_question.signal_reveal.connect( self.logic_controller.notifyNeedAnswer) self.scene_question.signal_incorrect.connect( self.logic_controller.notifyUnsuccesfullOutcome) self.scene_question.signal_correct.connect( self.logic_controller.notifySuccesfullOutcome) self.scene_question.signal_skipfreeturn.connect( self.logic_controller.notifyFreeTurnSkip) self.buttonFreeTurnSkip.clicked.connect( self.logic_controller.notifyFreeTurnSkip) self.scene_question.signal_spendfreeturn.connect( self.logic_controller.notifyFreeTurnSpend) self.buttonFreeTurnSpend.clicked.connect( self.logic_controller.notifyFreeTurnSpend) self.scene_question.show() self.stack.addWidget(self.scene_question) self.timer_obj = MyTimer(loglevel=self.loglevel) self.timer_thread = QThread() self.timer_thread.start() self.timer_obj.moveToThread(self.timer_thread) self.signal_start_timer.connect(self.timer_obj.count_down) self.timer_obj.signal_update_timer.connect( self.scene_question.updateTimer) self.logger.debug("building connection to start timer") self.logic_controller.signal_start_timer.connect(self.startTimer) self.logger.debug("building connection to stop timer") self.logic_controller.signal_stop_timer.connect(self.stopTimer) self.logic_controller.signal_update_main_score_bar_player.connect( self.main_scorebar.updatePlayers) self.logic_controller.signal_retain_player_data.connect( self.retainPlayerData) self.logic_controller_thread.start() self.MSG_controller.start() self.wheel_resting_place = None self.registration_wizard.signal_close.connect(self.close) self.logic_controller_thread.start() self.MSG_controller.start() #self.main = self.takeCentralWidget() if not skip_userreg: self.stack.setCurrentWidget(self.registration_wizard) #self.setCentralWidget(self.registration_wizard) else: self.stack.setCurrentWidget(self.qw) #self.setCentralWidget(self.main) @pyqtSlot() def shiftToComboWheelBoardScore(self): self.logger.debug("Shifting focus to combo-wheel-board-score panel") self.stack.setCurrentWidget(self.qw) #self.setCentralWidget(self.main) self.setStyleSheet("background-color: gray;") @pyqtSlot(list) def selectCategory(self, categories, target="player"): """Prompt user or opponents to select a category""" if isinstance(categories, list): if len(categories) <= 0: raise Exception("Category List does not include a sane value") else: self.cat_select = catselect.MyCatSelect( ui_file="select_category.ui", loglevel=self.loglevel, categories=categories, audience=target) self.stack.addWidget(self.cat_select) self.stack.setCurrentWidget(self.cat_select) self.cat_select.signal_submit_category.connect( self.logic_controller.returnCategory) self.cat_select.signal_shift_scene.connect( self.shiftToComboWheelBoardScore) #self.main = self.takeCentralWidget() #self.setCentralWidget(self.cat_select) #self.cat_select.show() @pyqtSlot() def selectOutcome(self): """Prompt players to indicate whether a response was correct or incorrect""" pass @pyqtSlot(dict) def displayQuestion(self, question_dict): """Render provided question to display""" self.stack.setCurrentWidget(self.scene_question) self.scene_question.set_context(self.playerData) self.scene_question.set_category(question_dict['category']) self.scene_question.set_value(question_dict['score']) self.scene_question.set_question(question_dict['question']) self.scene_question.render_controls_reveal() #self.main = self.takeCentralWidget() #self.setCentralWidget(self.scene_question) self.logger.debug("shift scene to question/answer") self.logic_controller.issueAck("displayQuestion") @pyqtSlot(dict) def displayAnswer(self, question_dict): """Render provided question to display""" self.scene_question.set_answer(question_dict['answer']) self.scene_question.render_controls_correct_incorrect() @pyqtSlot(int) def spinWheel(self, destination): """ Make the Wheel Spin. Ensure it lands on Destination""" if self.use_qgraphics_wheel: i = 0 self.wheel_transform = QTransform() self.wheel_offset = self.wheel_group.boundingRect().center() self.wheel_transform.translate(self.wheel_offset.x(), self.wheel_offset.y()) self.wheel_transform.setRotateCropHalve(i) self.wheel_transform.translate(-self.wheel_offset.x(), -self.wheel_offset.y()) self.wheel_group.setTransform(self.wheel_transform) self.wheel_view.transform() QtTest.QTest.qWait(50) i += 1 else: data = None num_sectors = 0 if self.use_textwheel: for each in range(0, 12): if getattr(self, "label_wheel_" + str(each)).isEnabled(): num_sectors += 1 else: num_sectors = 12 if self.wheel_resting_place is None: self.wheel_resting_place = 0 last = self.wheel_resting_place def cycle(start_number: int, delay_ms: int, num_switches: int, sectors: int, target: int = None) -> None: number = start_number delay_ms = delay_ms / 5 if start_number > 0: last = start_number - 1 else: last = sectors - 1 for each in range(number, num_switches): each = each % sectors # betterspin.wav from # https://freesound.org/people/door15studio/sounds/244774/ QSound.play("betterspin.wav") rot_angle = 360 / 12 self.wheel_gui.setRotateCropHalve(rot_angle) self.selectionTitle.setText( getattr(self, "label_wheel_%s" % each).text()) if self.use_textwheel: if last is not None: getattr(self, "label_wheel_" + str(last)).setAlignment(Qt.AlignLeft) getattr(self, "label_wheel_" + str(each)).setAlignment( Qt.AlignRight) number = each last = each if number == target and target is not None: return number QtTest.QTest.qWait(delay_ms) return number if self.skip_spinanimation: if self.use_textwheel: for each in range(0, num_sectors): if each != int(destination): getattr(self, "label_wheel_" + str(each)).setAlignment(Qt.AlignLeft) else: getattr(self, "label_wheel_" + str(each)).setAlignment(Qt.AlignRight) else: pass else: self.wheel_resting_place = cycle(last, 170, num_sectors * 2, num_sectors) self.wheel_resting_place = cycle(self.wheel_resting_place, 190, num_sectors, num_sectors) self.wheel_resting_place = cycle(self.wheel_resting_place, 210, num_sectors, num_sectors) self.wheel_resting_place = cycle(self.wheel_resting_place, 250, num_sectors * 2, num_sectors) self.wheel_resting_place = cycle(self.wheel_resting_place, 350, num_sectors * 2, num_sectors, target=int(destination)) self.logic_controller.issueAck("spinWheel") @pyqtSlot(str, str, str, str) def updateGameStats(self, spinsExecuted, maxSpins, currentRound, totalRounds): self.logger.debug("enter updateGameStats") self.round_spin_frame.setSpinsOccurred(spinsExecuted) self.round_spin_frame.setSpinsMax(maxSpins) self.round_spin_frame.setRound(currentRound) self.round_spin_frame.setRoundTotal(totalRounds) @pyqtSlot(str, str, str, str, str) def updatePlayer(self, playerid, name, score, tokens, currentPlayer): # help with font: https://stackoverflow.com/questions/34398797/bold-font-in-label-with-setbold-method getattr(self, "player" + playerid + "Name").setText(name) highlight_font = QtGui.QFont() if currentPlayer == name: highlight_font.setBold(True) getattr(self, "player" + playerid + "Name").setFont(highlight_font) else: highlight_font.setBold(False) getattr(self, "player" + playerid + "Name").setFont(highlight_font) getattr(self, "player" + playerid + "Score").setText(score) getattr(self, "player" + playerid + "FT").setText(tokens) @pyqtSlot(list) def updateWheel(self, sector_list): for i, each in enumerate(sector_list): sector_alias = getattr(self, "label_wheel_" + str(i)) # TODO: This breaks the rules. hmi shouldn't know anything about the protocol if each == "bankrupt": sector_alias.setStyleSheet( 'background-color: black; color: white') elif each == "loseturn": sector_alias.setStyleSheet("") elif each == "accumulatefreeturn": sector_alias.setStyleSheet("") elif each == 'playerschoice': sector_alias.setStyleSheet("") elif each == "opponentschoice": sector_alias.setStyleSheet("") elif each == "doublescore": sector_alias.setStyleSheet("") #sector_alias.setStylesheet("background-color:#ff0000;") sector_alias.setText(each) num_sectors = len(sector_list) if num_sectors != 12: for each in range(num_sectors, 12): getattr(self, "label_wheel_" + str(each)).setDisabled(True) @pyqtSlot(list) def updateBoard(self, category_list): for xpos, each in enumerate(category_list, 1): valid_prices = each['valid_prices'] getattr(self.board, "label_board_col" + str(xpos) + "_row1").setText( str(each['name'])) #self.logger.debug("category_list=%s" % (category_list)) for ypos, score in enumerate(valid_prices, 2): #ypos == enumerate starts at 0 + (row1 is category row), so it starts at 2. therefore ypos + 2. ugly. row_alias = getattr( self.board, "label_board_col" + str(xpos) + "_row" + str(ypos)) if str(score) in each['questions']: getattr(self.board, "label_board_col" + str(xpos) + "_row" + str(ypos)).setText(str(score)) else: getattr(self.board, "label_board_col" + str(xpos) + "_row" + str(ypos)).setText("") @pyqtSlot(str) def displayWinner(self, playername): for i in reversed(range(self.bodyLayout.count())): self.bodyLayout.itemAt(i).widget().hide() self.winnerLayout = QtWidgets.QVBoxLayout() self.winnerLayout.setObjectName("winnerLayout") self.bodyLayout.addLayout(self.winnerLayout) self.winnerTitle = QLabel(self) self.winnerTitle.setAlignment(Qt.AlignCenter) self.winnerTitle.setText("WINNER IS") self.winnerTitle.setStyleSheet(''' font-family: Arial, Helvetica, sans-serif; background-color: rgb(6,12,233); font-size: 45px; color: #FFFFFF; font-weight: 700; text-decoration: none; font-style: normal; font-variant: normal; text-transform: uppercase; ''') self.winnerLabel = QLabel(self) self.winnerLabel.setAlignment(Qt.AlignCenter) self.winnerLabel.setText(playername) self.winnerLabel.setWordWrap(True) self.winnerLabel.setStyleSheet(''' font-family: Arial, Helvetica, sans-serif; background-color: rgb(6,12,233); font-size: 120px; color: #FFFFFF; font-weight: 700; text-decoration: none; font-style: normal; font-variant: normal; text-transform: uppercase; ''') self.winnerLayout.setStretchFactor(self.winnerTitle, 1) self.winnerLayout.setStretchFactor(self.winnerLabel, 5) self.winnerLayout.addWidget(self.winnerTitle) self.winnerLayout.addWidget(self.winnerLabel) for i in reversed(range(self.controlLayout.count())): self.controlLayout.itemAt(i).widget().hide() self.exitButton = HoverButton(self) self.exitButton.setText("Quit") self.exitButton.setAlignment(Qt.AlignCenter) self.exitButton.clicked.connect(self.close) self.controlLayout.addWidget(self.exitButton) @pyqtSlot(dict) def setUIState(self, state): if not isinstance(state, dict): raise Exception("was expecting state of type dict, received %s" % (str(type(state)))) if "lock" in state.keys(): lock = state['lock'] for each in lock: getattr(self, each).setDisabled(True) if "unlock" in state.keys(): unlock = state['unlock'] for each in unlock: getattr(self, each).setEnabled(True) if "clear_lcd" in state.keys(): clear = state['clear_lcd'] for each in clear: getattr(self, each).display("") if "clear_textbox" in state.keys(): clear = state['clear_textbox'] for each in clear: getattr(self, each).setText("") pass @pyqtSlot(int) def startTimer(self, i): self.logger.debug("Start") self.signal_start_timer.emit(i) @pyqtSlot() def stopTimer(self): self.timer_obj.stop() @pyqtSlot() def playSpin(self): self.sounds["Spin"].play() @pyqtSlot() def playCorrect(self): self.sounds["Correct"].play() @pyqtSlot() def playIncorrect(self): self.sounds["Incorrect"].play() @pyqtSlot() def playBankrupt(self): self.sounds["Bankrupt"].play() @pyqtSlot() def playDouble(self): self.sounds["Double"].play() @pyqtSlot() def determineFreeTurnSpend(self): self.scene_question.render_controls_freeturn() for i in reversed(range(self.controlLayout.count())): self.controlLayout.itemAt(i).widget().hide() self.buttonSpin.hide() self.buttonFreeTurnSkip.show() self.buttonFreeTurnSpend.show() def close(self): self.logger.debug("closing") self.MSG_controller.quit() super(HMI, self).close() @pyqtSlot(list) def retainPlayerData(self, playerData): self.playerData = playerData @pyqtSlot() def renderSpinButton(self): for i in reversed(range(self.controlLayout.count())): self.controlLayout.itemAt(i).widget().hide() self.buttonSpin.show()
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.gameConstructor = GameConstructor( self, MyAction("Back", self.action_quit_from_game)) self.gameParams = GameParams.DefaultValues.create() self.initializase_components() self.state_menu() self.show() def initializase_components(self): self.stckWidg_scene = QStackedWidget() self.setCentralWidget(self.stckWidg_scene) self.menuScene = QWid_MenuScene(self, [ MyAction("Play", self.action_play), MyAction("Rules", self.action_rules), MyAction("Settings", self.action_settings), MyAction("Quit", self.action_quit) ]) self.rulesScene = QWid_RulesScene(self, MyAction("Back", self.state_menu)) self.settingsScene = QWid_SettingsScene( self, MyAction("Back", self.action_quit_from_settings)) self.stckWidg_scene.addWidget(self.settingsScene) self.stckWidg_scene.addWidget(self.menuScene) self.stckWidg_scene.addWidget(self.rulesScene) def createGameScene(self): return self.gameConstructor.createGame(self.gameParams) #return QMWind_GameScene(self, MyAction("Back", self.action_quit_from_game), self.gamePrefs) def action_play(self): self.state_game() def action_settings(self): self.state_settings() def action_rules(self): self.state_rules() def action_quit(self): self.close() def action_quit_from_game(self): self.state_menu() self.stckWidg_scene.removeWidget(self.gameScene) def action_quit_from_settings(self): self.gameParams = self.settingsScene.getChangedGameParams() self.state_menu() def state_game(self): self.gameScene = self.createGameScene() self.stckWidg_scene.addWidget(self.gameScene) self.gameScene.onShow() self.stckWidg_scene.setCurrentWidget(self.gameScene) def state_menu(self): self.menuScene.onShow() self.stckWidg_scene.setCurrentWidget(self.menuScene) def state_rules(self): self.rulesScene.onShow() self.stckWidg_scene.setCurrentWidget(self.rulesScene) def state_settings(self): self.settingsScene.onShow(self.gameParams) self.stckWidg_scene.setCurrentWidget(self.settingsScene)
def initializeUi(self): #sets up the user interface and creates buttons layout = QVBoxLayout() main_title = QLabel("Trading Simulator For Beginners 101", self) secondary_label = QLabel("", self) font2(self, secondary_label, main_title) # setting fonts for main-title and secondary label LogButton = QPushButton( "Log-in", self) # creating Log-in,Sign-up and Exit Buttons SignButton = QPushButton("Sign-up", self) ExitButton = QPushButton("Exit", self) LogButton.setStyleSheet( "background-color : cyan" ) # making Log-in,Sign-up and Exit Buttons blue SignButton.setStyleSheet("background-color : cyan") ExitButton.setStyleSheet("background-color : cyan") LogButton.setFixedSize(131, 51) #setting size of buttons SignButton.setFixedSize(131, 51) ExitButton.setFixedSize(131, 51) #what happens when buttons clicked ExitButton.clicked.connect(self.close) # closes the program # what happens when buttons clicked # lambda function (sometimes called a mini function) # lambda allows us to take multiple arguments on the same line LogInObject = Log_In(self) #creating Log_In object LogButton.clicked.connect(lambda: self.setCentralWidget( stackedwidget)) #connecting Log_In Object to Sign-Up Button SignUpObject = Sign_Up(self) #creating Sign_Up object SignButton.clicked.connect(lambda: self.setCentralWidget( SignUpObject)) #connecting SignUp Object to Sign-Up Button layout.addWidget(main_title) layout.addWidget(secondary_label) layout.addStretch() layout.addWidget(LogButton) layout.addWidget(SignButton) layout.addWidget(ExitButton) layout.addStretch() main = QWidget() main.setLayout(layout) self.setCentralWidget(main) stackedwidget = QStackedWidget(self) DashboardWidget = Dashboard(self) PortfolioWidget = Portfolio(self) stackedwidget.addWidget(DashboardWidget) stackedwidget.addWidget(PortfolioWidget) stackedwidget.addWidget(LogInObject) stackedwidget.addWidget(SignUpObject) stackedwidget.setCurrentIndex(0) stackedwidget.setCurrentWidget(DashboardWidget) DashboardWidget.PortfolioButton.clicked.connect( lambda: stackedwidget.setCurrentIndex(1)) DashboardWidget.StockFinderButton.clicked.connect( lambda: stackedwidget.setCurrentIndex(2)) DashboardWidget.LearningCentreButton.clicked.connect( lambda: stackedwidget.setCurrentIndex(3))
class ViewSpace(QWidget): """A ViewSpace manages a stack of views, one of them is visible. The ViewSpace also has a statusbar, accessible in the status attribute. The viewChanged(View) signal is emitted when the current view for this ViewSpace changes. Also, when a ViewSpace is created (e.g. when a window is created or split), the app.viewSpaceCreated(space) signal is emitted. You can use the app.viewSpaceCreated() and the ViewSpace.viewChanged() signals to implement things on a per ViewSpace basis, e.g. in the statusbar of a ViewSpace. """ viewChanged = pyqtSignal(view_.View) def __init__(self, manager, parent=None): super(ViewSpace, self).__init__(parent) self.manager = weakref.ref(manager) self.views = [] layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self.stack = QStackedWidget(self) layout.addWidget(self.stack) self.status = ViewStatusBar(self) self.status.setEnabled(False) layout.addWidget(self.status) app.languageChanged.connect(self.updateStatusBar) app.viewSpaceCreated(self) def activeView(self): if self.views: return self.views[-1] def document(self): """Returns the currently active document in this space. If there are no views, returns None. """ if self.views: return self.views[-1].document() def showDocument(self, doc): """Shows the document, creating a View if necessary.""" if doc is self.document(): return cur = self.activeView() for view in self.views[:-1]: if doc is view.document(): self.views.remove(view) break else: view = view_.View(doc) self.stack.addWidget(view) self.views.append(view) if cur: self.disconnectView(cur) self.connectView(view) self.stack.setCurrentWidget(view) self.updateStatusBar() def removeDocument(self, doc): active = doc is self.document() if active: self.disconnectView(self.activeView()) for view in self.views: if doc is view.document(): self.views.remove(view) view.deleteLater() break else: return if active and self.views: self.connectView(self.views[-1]) self.stack.setCurrentWidget(self.views[-1]) self.updateStatusBar() def connectView(self, view): view.installEventFilter(self) view.cursorPositionChanged.connect(self.updateCursorPosition) view.modificationChanged.connect(self.updateModificationState) view.document().urlChanged.connect(self.updateDocumentName) self.viewChanged.emit(view) def disconnectView(self, view): view.removeEventFilter(self) view.cursorPositionChanged.disconnect(self.updateCursorPosition) view.modificationChanged.disconnect(self.updateModificationState) view.document().urlChanged.disconnect(self.updateDocumentName) def eventFilter(self, view, ev): if ev.type() == QEvent.FocusIn: self.setActiveViewSpace() return False def setActiveViewSpace(self): self.manager().setActiveViewSpace(self) def updateStatusBar(self): """Update all info in the statusbar, e.g. on document change.""" if self.views: self.updateCursorPosition() self.updateModificationState() self.updateDocumentName() def updateCursorPosition(self): cur = self.activeView().textCursor() line = cur.blockNumber() + 1 try: column = cur.positionInBlock() except AttributeError: # only in very recent PyQt5 column = cur.position() - cur.block().position() self.status.positionLabel.setText(_("Line: {line}, Col: {column}").format( line = line, column = column)) def updateModificationState(self): modified = self.document().isModified() pixmap = icons.get('document-save').pixmap(16) if modified else QPixmap() self.status.stateLabel.setPixmap(pixmap) def updateDocumentName(self): self.status.infoLabel.setText(self.document().documentName())
class AnalyzeTab(QWidget): analyzeDone = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject', 'PyQt_PyObject') def __init__(self): super().__init__() # Layouts self.mainLayout = QHBoxLayout() self.lVbox = QVBoxLayout() self.lHbox = QHBoxLayout() self.lHbox_top = QHBoxLayout() self.rVbox = QVBoxLayout() self.rHbox = QHBoxLayout() self.rVbox2 = QVBoxLayout() self.stack = QStackedWidget() self.stack_Vbox = QVBoxLayout() self.stack_Hbox1 = QHBoxLayout() self.stack_Hbox2 = QHBoxLayout() self.hSplit = QSplitter(Qt.Horizontal) self.hSplit.setFrameShape(QFrame.StyledPanel) self.vSplit = QSplitter(Qt.Vertical) self.vSplit.setFrameShape(QFrame.StyledPanel) self.mainLayout.addLayout(self.lVbox, 1) self.mainLayout.addLayout(self.rVbox, 3) # Setup file browser self.fileModel = QFileSystemModel() self.fileModel.setNameFilters(['*.wav']) self.fileModel.setRootPath(QDir.currentPath()) self.fileTree = QTreeView() self.fileTree.setModel(self.fileModel) self.fileTree.setRootIndex(self.fileModel.index(r'./')) self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection) self.fileTree.setColumnHidden(2, True) self.fileTree.setColumnHidden(1, True) self.rootDirEdit = QLineEdit(os.path.dirname(__file__)) self.rootDirEdit.returnPressed.connect(self.on_edit_root) self.browseBtn = QPushButton('Browse') self.browseBtn.clicked.connect(self.on_browse) self.lHbox_top.addWidget(self.rootDirEdit, 3) self.lHbox_top.addWidget(self.browseBtn, 1) # Setup Canvas self.canvas = PlotCanvas(self) self.analyzeDone.connect(self.canvas.plot) self._analyze = lambda _: self.analyze(self.fileTree.selectedIndexes()) self.analyzeBtn = QPushButton('Analyze') self.analyzeBtn.clicked.connect(self._analyze) ## BATCH ANALYSIS CONTROLS ## self.batchAnalyzeChk = QCheckBox('Batch Analysis') self.dataTable = QTableWidget() self.batchCtrlBox = QGroupBox("Batch Analysis") self.batchCtrlBox.setLayout(self.stack_Vbox) # Analysis Mode self.modeGroup = QButtonGroup() self.modeBox = QGroupBox('Analysis Mode') self.modeBox.setLayout(self.stack_Hbox1) self.stack_Vbox.addWidget(self.modeBox) self.wavAnalysisChk = QCheckBox('Wav analysis') self.wavAnalysisChk.setChecked(True) self.calibrationLocationBox = QComboBox() self.calibrationLocationBox.addItems([str(n) for n in range(1, 11)]) self.calibrationCurveChk = QCheckBox('Calibration Curve') self.calibrationCurveChk.toggled.connect( lambda state: self.calibrationLocationBox.setEnabled(state)) self.calibrationCurveChk.setChecked(False) self.stack_Hbox1.addWidget(self.wavAnalysisChk, 3) self.stack_Hbox1.addWidget(self.calibrationCurveChk, 3) self.stack_Hbox1.addWidget(QLabel('Location: '), 1) self.stack_Hbox1.addWidget(self.calibrationLocationBox, 1) self.stack_Vbox.addLayout(self.stack_Hbox1) self.modeGroup.addButton(self.wavAnalysisChk) self.modeGroup.addButton(self.calibrationCurveChk) self.modeGroup.setExclusive(True) # Outputs self.outputCtrlBox = QGroupBox('Outputs') self.outputCtrlBox.setLayout(self.stack_Hbox2) self.stack_Vbox.addWidget(self.outputCtrlBox) self.toCSVchk = QCheckBox('.csv') self.toJSONchk = QCheckBox('.json') self.toCSVchk.stateChanged.connect(lambda _: self.update_settings( 'output', 'toCSV', self.toCSVchk.isChecked())) self.toJSONchk.stateChanged.connect(lambda _: self.update_settings( 'output', 'toJSON', self.toJSONchk.isChecked())) self.stack_Hbox2.addWidget(self.toCSVchk) self.stack_Hbox2.addWidget(self.toJSONchk) self.stack_Vbox.addLayout(self.stack_Hbox2) self.stack.addWidget(self.dataTable) self.stack.addWidget(self.batchCtrlBox) self.stack.setCurrentWidget(self.dataTable) self.stack.show() self.batchAnalyzeChk.stateChanged.connect(self.toggle_stack) self.batchAnalyzeChk.setChecked(False) self.stack_Vbox.addStretch() ## PROCESSING CONTROLS ## self.processControls = QGroupBox('Signal Processing') self.tOffsetSlider = QSlider(Qt.Horizontal, ) self.tOffsetSlider.setMinimum(1) self.tOffsetSlider.setMaximum(100) self.tOffsetSlider.setValue(100) self.tOffsetSlider.setTickPosition(QSlider.TicksBelow) self.tOffsetSlider.setTickInterval(10) self.tOffsetSlider.valueChanged.connect( lambda val: self.update_settings('processing', 'tChop', val)) self.tOffsetLayout = QHBoxLayout() self.tOffsetSlider_Box = QGroupBox( f'Chop Signal - {self.tOffsetSlider.value()}%') self.tOffsetSlider.valueChanged.connect( lambda val: self.tOffsetSlider_Box.setTitle(f'Chop Signal - {val}%' )) self.tOffsetSlider_Box.setLayout(self.tOffsetLayout) self.tOffsetLayout.addWidget(self.tOffsetSlider) self.nFFTSlider = QSlider(Qt.Horizontal, ) self.nFFTSlider.setMinimum(1) self.nFFTSlider.setMaximum(16) self.nFFTSlider.setValue(1) self.nFFTSlider.setTickPosition(QSlider.TicksBelow) self.nFFTSlider.setTickInterval(2) self.nFFTSlider.valueChanged.connect( lambda val: self.update_settings('processing', 'detail', val)) self.nFFTLayout = QHBoxLayout() self.nFFTSlider.valueChanged.connect( lambda val: self.nFFTSlider_Box.setTitle(f'FFT Size - {val*65536}' )) self.nFFTSlider_Box = QGroupBox( f'FFT Size - {self.nFFTSlider.value()*65536}') self.nFFTSlider_Box.setLayout(self.nFFTLayout) self.nFFTLayout.addWidget(self.nFFTSlider) self.rVbox2.addWidget(self.tOffsetSlider_Box) self.rVbox2.addWidget(self.nFFTSlider_Box) self.processControls.setLayout(self.rVbox2) self.lVbox.addLayout(self.lHbox_top, 1) self.lVbox.addWidget(self.fileTree, 7) self.lVbox.addLayout(self.lHbox, 1) self.lHbox.addWidget(self.analyzeBtn, 2) self.lHbox.addWidget(self.batchAnalyzeChk, 1) self.vSplit.addWidget(self.canvas) self.vSplit.addWidget(self.hSplit) self.rVbox.addWidget(self.vSplit) self.hSplit.addWidget(self.stack) self.hSplit.addWidget(self.processControls) self.settings = { 'processing': { 'tChop': self.tOffsetSlider.value(), 'detail': self.nFFTSlider.value() }, 'output': { 'toCSV': self.toCSVchk.isChecked(), 'toJSON': self.toJSONchk.isChecked() } } self.setLayout(self.mainLayout) def on_browse(self): # Browse to file tree root directory options = QFileDialog.Options() path = QFileDialog.getExistingDirectory( self, caption="Choose root directory", options=options) self.rootDirEdit.setText(path) self.fileTree.setRootIndex(self.fileModel.index(path)) def on_edit_root(self): # Update the file tree root directory self.fileTree.setRootIndex( self.fileModel.index(self.rootDirEdit.text())) def update_settings(self, category, setting, value): # Update settings and reprocess FFT if in single analysis mode self.settings[category][setting] = value if category == 'processing' and self.fileTree.selectedIndexes(): self.analyze(self.fileTree.selectedIndexes()) def toggle_stack(self, state): if state == 2: self.stack.setCurrentWidget(self.batchCtrlBox) self.fileTree.setSelectionMode(QAbstractItemView.MultiSelection) else: self.stack.setCurrentWidget(self.dataTable) self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection) def analyze(self, filePaths): if self.batchAnalyzeChk.isChecked(): if self.wavAnalysisChk.isChecked(): self.batch_analyze_wav( [self.fileModel.filePath(path) for path in filePaths[::4]]) if self.calibrationCurveChk.isChecked(): self.generate_calibration_curve( [self.fileModel.filePath(path) for path in filePaths[::4]]) else: if os.path.isdir(self.fileModel.filePath( filePaths[0])) or len(filePaths) > 4: QMessageBox.information( self, 'Error', 'Please select only 1 file for single analysis.') return self.single_analyze_wav(self.fileModel.filePath(filePaths[0])) def single_analyze_wav(self, filePath): """ Do an FFT and find peaks on a single wav file :param filePath: file path to .wav file """ tChopped, vChopped, fVals,\ powerFFT, peakFreqs, peakAmps = Utils.AnalyzeFFT(filePath, tChop=self.settings['processing']['tChop'], detail=self.settings['processing']['detail']) self.analyzeDone.emit(tChopped, vChopped, fVals, powerFFT, peakFreqs, peakAmps, filePath) self.update_table(peakFreqs, peakAmps) def batch_analyze_wav(self, filePaths): """ Perform a batch analysis of many .wav files. Outputs FFTs and peaks in .csv or .json format :param filePaths: A list of folders containing the .wav files to be analyzed """ toCSV = self.settings['output']['toCSV'] toJSON = self.settings['output']['toJSON'] start = time.time() fileTotal = 0 for path in filePaths: if os.path.isdir(path): blockName = os.path.basename(path) print(f'Block: {blockName}') files = [ os.path.join(path, file) for file in os.listdir(path) if '.wav' in file ] fileTotal += len(files) if toCSV: if not os.path.exists(os.path.join(path, 'fft_results_csv')): os.makedirs(os.path.join(path, 'fft_results_csv')) resultFilePath = os.path.join(path, 'fft_results_csv') print('Processing FFTs...') with multiprocessing.Pool(processes=4) as pool: results = pool.starmap( Utils.AnalyzeFFT, zip(files, itertools.repeat(True), itertools.repeat(True))) results = [ result for result in results if result is not None ] peaks = [result[0] for result in results] ffts = [result[1] for result in results] print('Writing to .csv...') resultFileName = os.path.join(resultFilePath, f'{blockName}_Peaks.csv') peakFrames = pd.concat(peaks) peakFrames.to_csv(resultFileName, index=False, header=True) with concurrent.futures.ThreadPoolExecutor( max_workers=16) as executor: executor.map(self.multi_csv_write, ffts) if toJSON: if not os.path.exists( os.path.join(path, 'fft_results_json')): os.makedirs(os.path.join(path, 'fft_results_json')) print(os.path.join(path, 'fft_results_json')) print('Processing FFTs...') with multiprocessing.Pool(processes=4) as pool: results = pool.starmap( Utils.AnalyzeFFT, zip(files, itertools.repeat(True), itertools.repeat(False), itertools.repeat(True))) results = [ result for result in results if result is not None ] print('Writing to .json...') with concurrent.futures.ThreadPoolExecutor( max_workers=16) as executor: executor.map(self.multi_json_write, results) end = time.time() print( f'**Done!** {len(filePaths)} blocks with {fileTotal} files took {round(end-start, 1)}s' ) def generate_calibration_curve(self, filePaths): """ Attempt to fit an exponential function to a set of data points (x: Peak Frequency, y: Compressive strength) provided in JSON format. ex:{ "shape": "2-Hole", "testData": { "location": "1", "strength": 3.092453552, "peaks": [ { "frequency": 1134.5561082797967, "magnitude": 0.349102384777402 }] }, "waveData": [...], "freqData": [...] } Plot the curve, data points and give the function if successful. ** NOTE ** This function is still experimental and a bit buggy. Sometimes the scipy.optimize curve_fit won't converge with the initial guess given for the coeffecients. You're probably better off writing your own code. :param filePaths: A list of folders containing .jsons """ # Strike Location location = self.calibrationLocationBox.currentText() # Function to fit to the data exp_f = lambda x, a, b, c: a * np.exp(b * x) + c # Threaded method for opening all the .jsons and fitting calibCurve = ThreadedCalibrationCurve(filePaths, location, exp_f) progressDialog = QProgressDialog( f'Gettings samples for location: {location}', None, 0, len(filePaths), self) progressDialog.setModal(True) calibCurve.blocksSearched.connect(progressDialog.setValue) try: peakFreqs, strengths, popt, pcov, fitX = calibCurve.run() except Exception as e: QMessageBox.information(self, 'Error', e) return # Calculate R Squared residuals = strengths - exp_f(peakFreqs, *popt) ss_res = np.sum(residuals**2) ss_tot = np.sum((strengths - np.mean(strengths))**2) r_squared = 1 - (ss_res / ss_tot) # Plot Results fig = Figure() plt.scatter(peakFreqs, strengths) plt.plot(fitX, exp_f(fitX, *popt), '-k') ax = plt.gca() plt.text( 0.05, 0.9, f'y = {round(popt[0],3)}*exp({round(popt[1], 5)}x) + {round(popt[2], 3)}\n', ha='left', va='center', transform=ax.transAxes) plt.text(0.05, 0.85, f'R^2 = {round(r_squared,3)}', ha='left', va='center', transform=ax.transAxes) plt.title(f'Calibration Curve, Location: {location}') plt.xlabel('Frequency (Hz)') plt.ylabel('Compressive Strength (MPa)') plt.show() def multi_csv_write(self, frameTuple): frame = frameTuple[1] wavPath = frameTuple[0] resultFileDir = os.path.join(os.path.dirname(wavPath), 'fft_results_csv') resultFileName = os.path.basename(wavPath) + '_fft.csv' resultFilePath = os.path.join(resultFileDir, resultFileName) frame.to_csv(resultFilePath, index=False, header=True) def multi_json_write(self, results): data = results[0] wavPath = results[1] jsonFileDir = os.path.join(os.path.dirname(wavPath), 'fft_results_json') resultFileName = os.path.basename(wavPath) + '_fft.json' resultFilePath = os.path.join(jsonFileDir, resultFileName) # blockName = os.path.basename(os.path.dirname(wavPath)) # blockDir = os.path.join(jsonFileDir, blockName) # if not os.path.exists(blockDir): # os.makedirs(blockDir) # print(resultFilePath) with open(resultFilePath, 'w') as f: json.dump(data, f, indent=2) def update_table(self, peakFreqs, peakAmps): """ :param peakFreqs: :param peakAmps: :return: """ self.dataTable.setRowCount(2) self.dataTable.setColumnCount(len(peakFreqs) + 1) self.dataTable.setItem(0, 0, QTableWidgetItem("Frequencies: ")) self.dataTable.setItem(1, 0, QTableWidgetItem("Powers: ")) for col, freq in enumerate(peakFreqs, start=1): self.dataTable.setItem(0, col, QTableWidgetItem(str(round(freq)))) for col, power in enumerate(peakAmps, start=1): item = QTableWidgetItem(str(round(power, 3))) if power > 0.7: item.setBackground(QColor(239, 81, 28)) elif power >= 0.4: item.setBackground(QColor(232, 225, 34)) elif power < 0.4: item.setBackground(QColor(113, 232, 34)) self.dataTable.setItem(1, col, item)
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 RecordTab(QWidget): def __init__(self): super().__init__() self.processed_fields = {} self.state = 'Waiting' # Setup Layouts self.mainLayout = QVBoxLayout() self.hBox = QHBoxLayout() self.outputGrid = QGridLayout() self.lGrpBox = QGroupBox('Output Options') self.lGrpBox.setLayout(self.outputGrid) self.rGrpBox = QGroupBox('Input Device') self.rVBox = QVBoxLayout() self.rGrpBox.setLayout(self.rVBox) # Center self.recordBtn = QPushButton() # Microphone Stream and live microphone canvas self.tempFile = None self.stream = MicrophoneStream() self.stream.recordingDone.connect(self.on_recording_done) self.canvasStack = QStackedWidget() self.microphoneCanvas = MicrophoneCanvas(self.stream) self.recordBtn.clicked.connect(self.stream.toggle_recording) self.recordBtn.clicked.connect(self.on_recording_toggle) self.plotCanvas = PlotCanvas(self) self.canvasStack.addWidget(self.microphoneCanvas) self.canvasStack.addWidget(self.plotCanvas) self.canvasStack.setCurrentWidget(self.microphoneCanvas) self.mainLayout.addWidget(self.canvasStack, 5) # Output Options (LEFT) self.dbLabel = QLabel('Database:') self.dbPath = QLineEdit(os.path.dirname(__file__)) self.dbBrowse = QPushButton('Browse') self.dbBrowse.clicked.connect(self.on_browse_db) self.dbChk = QCheckBox() self.dbChk.clicked.connect(self.handle_chk_state) Utils.addWidgets_to_grid(self.outputGrid, [(self.dbLabel, 1, 1, 1, 1), (self.dbPath, 1, 2, 1, 1), (self.dbBrowse, 1, 3, 1, 1), (self.dbChk, 1, 4, 1, 1)]) self.savePathLabel = QLabel('Directory:') self.savePath = QLineEdit(os.path.dirname(__file__)) self.savePathBrowse = QPushButton('Browse') self.savePathBrowse.clicked.connect(self.on_browse_local_dir) self.savePathChk = QCheckBox() self.savePathChk.setChecked(True) self.savePathChk.clicked.connect(self.handle_chk_state) Utils.addWidgets_to_grid(self.outputGrid, [(self.savePathLabel, 2, 1, 1, 1), (self.savePath, 2, 2, 1, 1), (self.savePathBrowse, 2, 3, 1, 1), (self.savePathChk, 2, 4, 1, 1)]) self.saveNameLabel = QLabel('Name:') self.saveName = QLineEdit() Utils.addWidgets_to_grid(self.outputGrid, [(self.saveNameLabel, 3, 1, 1, 1), (self.saveName, 3, 2, 1, 2)]) self.deleteBtn = QPushButton('Cancel') self.deleteBtn.setEnabled(False) self.deleteBtn.clicked.connect(self.on_press_delete) self.saveBtn = QPushButton('Save') self.saveBtn.setEnabled(False) self.saveBtn.clicked.connect(self.on_press_save) Utils.addWidgets_to_grid(self.outputGrid, [(self.saveBtn, 4, 1, 1, 2), (self.deleteBtn, 4, 3, 1, 2)]) self.outputGrid.setColumnMinimumWidth(4, 20) # Sound Device Controls (RIGHT) self.inputDropDown = QComboBox() self.inputDevices = [ device['name'] for device in sounddevice.query_devices() if device['max_input_channels'] > 0 ] self.inputDropDown.addItems(self.inputDevices) activeDevice = sounddevice.query_devices( device=self.stream.inputStream.device) self.inputDropDown.setCurrentIndex( self.inputDevices.index(activeDevice['name'])) self.inputDropDown.currentIndexChanged.connect(self.on_input_changed) # Add widgets to layouts self.mainLayout.addLayout(self.hBox, 1) self.hBox.addWidget(self.lGrpBox, 5) self.hBox.addStretch(2) self.hBox.addWidget(self.recordBtn, 3) self.hBox.addStretch(1) self.hBox.addWidget(self.rGrpBox, 1) Utils.setAlignments(self.hBox, [(self.recordBtn, Qt.AlignCenter), (self.rGrpBox, Qt.AlignRight)]) self.rVBox.addWidget(self.inputDropDown) self.setLayout(self.mainLayout) # Style self.recordBtn.setIcon(QIcon(r'.\assets\record.png')) self.recordBtn.setIconSize(QSize(100, 100)) self.recordBtn.setStyleSheet('QPushButton{border: 1px solid;}') # Shortcuts self.recordShortCut = QShortcut(QKeySequence('Space'), self) self.recordShortCut.activated.connect(self.on_recording_toggle) self.recordShortCut.activated.connect(self.stream.toggle_recording) self.handle_chk_state() def handle_chk_state(self): self.dbBrowse.setEnabled(self.dbChk.isChecked()) self.dbPath.setEnabled(self.dbChk.isChecked()) self.savePathBrowse.setEnabled(self.savePathChk.isChecked()) self.savePath.setEnabled(self.savePathChk.isChecked()) self.saveName.setEnabled(self.savePathChk.isChecked()) if self.state == 'saving': if not self.dbChk.isChecked() and not self.savePathChk.isChecked(): self.saveBtn.setEnabled(False) else: self.saveBtn.setEnabled(True) def on_browse_local_dir(self): options = QFileDialog.Options() path = QFileDialog.getExistingDirectory( self, caption="Choose save directory", options=options) self.savePath.setText(path) def on_browse_db(self): options = QFileDialog.Options() path, _ = QFileDialog.getOpenFileName(self, "Open .db File", "", "Sqlite3 DB File (*.db)", options=options) self.dbPath.setText(path) @pyqtSlot(int) def on_input_changed(self, index): deviceName = self.inputDevices[index] for i, dev in enumerate(sounddevice.query_devices()): if dev['name'] == deviceName: device = i break else: device = sounddevice.default.device[0] print('Failed to set input device') self.stream.inputStream.stop() self.stream.inputStream = sounddevice.InputStream( samplerate=self.stream.inputStream.samplerate, device=device, channels=1, callback=self.stream.audio_callback) self.stream.inputStream.start() @pyqtSlot() def on_recording_toggle(self): if self.state == 'Waiting': self.on_recording_start() @pyqtSlot() def on_recording_start(self): self.inputDropDown.setEnabled(False) self.saveBtn.setEnabled(False) self.deleteBtn.setEnabled(False) self.recordBtn.setIcon(QIcon(r'.\assets\stopWithSquare.png')) self.state = 'recording' @pyqtSlot(tuple) def on_recording_done(self, tempFile): """ Triggered when the microphone stream has finished recording. Enables / Disables appropriate buttons and receives tempFile from the microphone stream. Also processes an FFT for display :param tempFile: (file handle: int, file path: str) temporary audio file generated by the microphone stream to store the audio while the user decides whether or not to save it""" self.state = 'saving' self.handle_chk_state() self.deleteBtn.setEnabled(True) self.recordBtn.setEnabled(False) self.recordShortCut.setEnabled(False) self.recordBtn.setIcon(QIcon(r'.\assets\record.png')) self.inputDropDown.setEnabled(True) self.tempFile = tempFile # Process FFT tChopped, vChopped, fVals, \ powerFFT, peakFreqs, peakAmps = Utils.AnalyzeFFT(tempFile[1], tChop=None) # Get these fields ready for a possible insertion into DB self.processed_fields['PCM'] = str(list(vChopped)) self.processed_fields['Date'] = str(datetime.datetime.now()) self.processed_fields['FFT_Processed'] = str(list(powerFFT)) self.processed_fields['Sample_Rate'] = str(self.stream.sampleRate) self.processed_fields['Hash'] = hashlib.sha256( str(list(powerFFT)).encode('utf-8')).hexdigest() self.processed_fields['Peaks_Processed'] = [] for freq, amp in zip(peakFreqs, peakAmps): self.processed_fields['Peaks_Processed'].append({ "Frequency": freq, "Amplitude": amp }) self.processed_fields['Peaks_Processed'].sort( reverse=True, key=lambda peak: peak['Amplitude']) self.processed_fields['Peaks_Processed'] = str( self.processed_fields['Peaks_Processed']) os.close(self.tempFile[0]) os.remove(self.tempFile[1]) # Make a new .wav file from the processed data self.tempFile = tempfile.mkstemp(prefix='temp_processed_', suffix='.wav', dir='') fileStream = soundfile.SoundFile(self.tempFile[1], mode='w', samplerate=self.stream.sampleRate, channels=max(self.stream.channels), subtype=self.stream.subtype) fileStream.write(vChopped) fileStream.close() self.plotCanvas.plot(tChopped, vChopped, fVals, powerFFT, peakFreqs, peakAmps, 'Impulse Recording') self.canvasStack.setCurrentWidget(self.plotCanvas) @pyqtSlot() def on_press_save(self): """ Triggered when the save button is pressed. Opens a save file dialog to permanently save the temporary audio file. Removes temporary file after.""" if self.dbChk.isChecked(): self.processed_fields['db'] = self.dbPath.text() self.dbForm = DBFormWindow(self.processed_fields, self) self.dbForm.show() if self.savePathChk.isChecked(): if self.savePath.text(): shutil.copy( self.tempFile[1], os.path.join(self.savePath.text(), self.saveName.text() + '.wav')) os.close(self.tempFile[0]) os.remove(self.tempFile[1]) QMessageBox.information( self, 'Saved', f'Saved to: {os.path.join(self.savePath.text(), self.saveName.text()+".wav")}' ) self.saveBtn.setEnabled(False) self.deleteBtn.setEnabled(False) self.recordBtn.setEnabled(True) self.recordBtn.setIcon(QIcon(r'.\assets\record.png')) self.recordShortCut.setEnabled(True) self.inputDropDown.setEnabled(True) self.canvasStack.setCurrentWidget(self.microphoneCanvas) self.state = 'Waiting' @pyqtSlot() def on_press_delete(self): """ Triggered when the delete button is pressed. Removes temporary audio file""" os.close(self.tempFile[0]) os.remove(self.tempFile[1]) self.recordBtn.setEnabled(True) self.recordBtn.setIcon(QIcon(r'.\assets\record.png')) self.deleteBtn.setEnabled(False) self.saveBtn.setEnabled(False) self.inputDropDown.setEnabled(True) self.canvasStack.setCurrentWidget(self.microphoneCanvas) self.state = 'Waiting'
class TabBarWindow(TabWindow): """Implementation which uses a separate QTabBar and QStackedWidget. The Tab bar is placed next to the menu bar to save real estate.""" def __init__(self, app, **kwargs): super().__init__(app, **kwargs) def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(640, 480) self.tabBar = QTabBar() self.verticalLayout = QVBoxLayout() self.verticalLayout.setContentsMargins(0, 0, 0, 0) self._setupActions() self._setupMenu() self.centralWidget = QWidget(self) self.setCentralWidget(self.centralWidget) self.stackedWidget = QStackedWidget() self.centralWidget.setLayout(self.verticalLayout) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.addWidget(self.menubar, 0, Qt.AlignTop) self.horizontalLayout.addWidget(self.tabBar, 0, Qt.AlignTop) self.verticalLayout.addLayout(self.horizontalLayout) self.verticalLayout.addWidget(self.stackedWidget) self.tabBar.currentChanged.connect(self.showWidget) self.tabBar.tabCloseRequested.connect(self.onTabCloseRequested) self.stackedWidget.currentChanged.connect(self.updateMenuBar) self.stackedWidget.widgetRemoved.connect(self.onRemovedWidget) self.tabBar.setTabsClosable(True) self.restoreGeometry() def addTab(self, page, title, switch=True): stack_index = self.stackedWidget.insertWidget(-1, page) tab_index = self.tabBar.addTab(title) if isinstance(page, DirectoriesDialog): self.tabBar.setTabButton(tab_index, QTabBar.RightSide, None) if switch: # switch to the added tab immediately upon creation self.setTabIndex(tab_index) self.stackedWidget.setCurrentWidget(page) return stack_index @pyqtSlot(int) def showWidget(self, index): if index >= 0 and index <= self.stackedWidget.count() - 1: self.stackedWidget.setCurrentIndex(index) # if not self.tabBar.isTabVisible(index): self.setTabVisible(index, True) def indexOfWidget(self, widget): # Warning: this may return -1 if widget is not a child of stackedwidget return self.stackedWidget.indexOf(widget) def setCurrentIndex(self, tab_index): # The signal will handle switching the stackwidget's widget self.setTabIndex(tab_index) # self.stackedWidget.setCurrentWidget(self.stackedWidget.widget(tab_index)) @pyqtSlot(int) def setTabIndex(self, index): if not index: return self.tabBar.setCurrentIndex(index) def setTabVisible(self, index, value): return self.tabBar.setTabVisible(index, value) @pyqtSlot(int) def onRemovedWidget(self, index): self.removeTab(index) @pyqtSlot(int) def removeTab(self, index): # No need to remove the widget here: # self.stackedWidget.removeWidget(self.stackedWidget.widget(index)) return self.tabBar.removeTab(index) @pyqtSlot(int) def removeWidget(self, widget): return self.stackedWidget.removeWidget(widget) def isTabVisible(self, index): return self.tabBar.isTabVisible(index) def getCurrentIndex(self): return self.stackedWidget.currentIndex() def getWidgetAtIndex(self, index): return self.stackedWidget.widget(index) def getCount(self): return self.stackedWidget.count() @pyqtSlot() def toggleTabBar(self): value = self.sender().isChecked() self.actionToggleTabs.setChecked(value) self.tabBar.setVisible(value) @pyqtSlot(int) def onTabCloseRequested(self, index): current_widget = self.getWidgetAtIndex(index) current_widget.close() self.stackedWidget.removeWidget(current_widget)
class UIWidget(QWidget): """ Class that holds all of the widgets for viewing """ def __init__(self): """ Initializes class and variables """ super(UIWidget, self).__init__() self.axiom = QLabel("Axiom") self.angle = QLabel("Angles(degrees)") self.iters = QLabel("Iterations") self.axiom_edit = CustomLineEdit() self.angle_edit = CustomLineEdit() self.iters_edit = CustomLineEdit() self.text_boxes = [self.axiom_edit, self.angle_edit, self.iters_edit] self.prod_plus = QPushButton("+", self) self.lsys_button = QPushButton("Generate L System", self) self.boxcount_button = QPushButton("Fractal Dim", self) self.widget = QWidget() self.scroll_area = QtWidgets.QScrollArea() self.layout_examples = QVBoxLayout(self.widget) self.layout_examples.setAlignment(Qt.AlignTop) self.prods = 1 self.prod_rules_edit = [] self.examples = [] self.minuses = None self.made_angle = False self.prod_percent = [] self.amount = 0 self.index = 0 self.frac_points = [] self.made_line = False self.prod_rules = [] self.verts = [] # This will store the vertices from generate_lsystem self.saved_lsystems = load_saved_lsystems() self.two_d = LSystem2DWidget() self.three_d = LSystem3DWidget() self.fractal_menu = FractalDimension(self) self.dims = QStackedWidget() self.dims.addWidget(self.two_d) self.dims.addWidget(self.three_d) self.dims.setCurrentWidget(self.two_d) self.graphix = self.two_d self.init_UI() self.alphabet = [ "F", "f", "G", "g", "H", "h", "^", "&", "-", "+", "[", "]", "|", "(", ")", ">", "<", " ", ] self.ctrl_char = [ "A", "B", "C", "D", "E", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", ] def init_UI(self): """ Creates and adds all widgets in the viewport and sets the layout """ # renames the window self.setWindowTitle("L-Systems Generator") self.layout = QGridLayout() self.init_buttons() self.init_text_boxes() self.add_widgets() self.setLayout(self.layout) self.setGeometry(500, 500, 500, 500) def init_text_boxes(self): """Creates textboxes for the UI """ self.prod_rules.append(QLabel("Production Rule " + str(self.prods))) # creates the text box for each label self.axiom_edit.returnPressed.connect(self.lsys_button.click) self.axiom_edit.clicked.connect(lambda: self.axiom_edit.reset_color()) self.prod_rules_edit.append(CustomLineEdit()) self.prod_rules_edit[0].clicked.connect( lambda: self.prod_rules_edit[0].reset_color() ) self.text_boxes.append(self.prod_rules_edit[-1]) self.prod_rules_edit[0].returnPressed.connect(self.lsys_button.click) self.prod_rules_edit[0].textChanged.connect(lambda: self.show_popup()) self.prod_percent.append(CustomLineEdit()) self.prod_percent[0].setFixedWidth(50) self.prod_percent[0].setText("100%") self.text_boxes.append(self.prod_percent[-1]) self.angle_edit.returnPressed.connect(self.lsys_button.click) self.angle_edit.clicked.connect(lambda: self.angle_edit.reset_color()) self.iters_edit.returnPressed.connect(self.lsys_button.click) self.iters_edit.clicked.connect(lambda: self.iters_edit.reset_color()) self.prod_plus.clicked.connect(self.more_prods) def init_buttons(self): """Creates buttons for the UI""" # makes the lsys generator button self.lsys_button.clicked.connect(self.on_lsys_button_clicked) self.lsys_button.setAutoDefault(True) self.boxcount_button.clicked.connect(self.on_boxcount_button_clicked) self.boxcount_button.setAutoDefault(True) self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll_area.setWidgetResizable(True) self.scroll_area.setFixedWidth(150) self.scroll_area.setWidget(self.widget) self.precons = ['SierpinksiTriangle', 'KochCurve', 'KochSnowflake', 'KochIsland', 'PeanoCurve', 'DragonCurve', 'HilbertCurve', 'TreeExample', 'IslandsandLakes'] for i, key in enumerate(self.saved_lsystems["two-d"]): self.examples.append(QPushButton()) if i < len(self.precons): self.examples[i].setIcon(QIcon('{}/lsystem/assets/images/{}.png'.format(os.getcwd(), self.precons[i]))) self.examples[i].setIconSize(QtCore.QSize(120, 100)) else: self.examples[i].setText(key) self.examples[i].setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.examples[i].customContextMenuRequested.connect(lambda state, x=key: self.del_example(str(x), "two-d")) self.examples[i].clicked.connect( lambda state, x=key: self.gen_example(str(x)) ) self.layout_examples.addWidget(self.examples[i]) def reload_presets(self): """pulls saved lsystems from file""" self.saved_lsystems = load_saved_lsystems() self.set_presets() def set_presets(self): """Shows preset L-Systems for the appropriate dimention""" if self.is_2d(): for widget in self.examples: self.layout_examples.removeWidget(widget) widget.deleteLater() widget=None self.examples = [] for i, key in enumerate(self.saved_lsystems["two-d"]): self.examples.append(QPushButton()) if i < len(self.precons): self.examples[i].setIcon(QIcon('{}/lsystem/assets/images/{}.png'.format(os.getcwd(), self.precons[i]))) self.examples[i].setIconSize(QtCore.QSize(120, 100)) else: self.examples[i].setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.examples[i].customContextMenuRequested.connect(lambda state, x=key: self.del_example(str(x),"two-d")) self.examples[i].setText(key) self.examples[i].clicked.connect( lambda state, x=key: self.gen_example(str(x)) ) self.layout_examples.addWidget(self.examples[i]) elif not self.is_2d(): for widget in self.examples: self.layout_examples.removeWidget(widget) widget.deleteLater() widget=None self.examples = [] for i, key in enumerate(self.saved_lsystems["three-d"]): self.examples.append(QPushButton()) self.examples[i].setText(key) self.examples[i].clicked.connect( lambda state, x=key: self.gen_example(str(x)) ) self.layout_examples.addWidget(self.examples[i]) if(i>1): # TODO - This is temporary until we have a manner of separting 3d precons from 2d precons self.examples[i].setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.examples[i].customContextMenuRequested.connect(lambda state, x=key: self.del_example(str(x),"three-d")) @QtCore.pyqtSlot() def on_lsys_button_clicked(self): """Generates the L-System""" self.gen_sys() def boxcount_2d(self): """Calculates the dimensionality of a 2D L-System""" self.fractal_menu.show() start_size = 8 num_sizes = 7 self.x_arr = [] self.y_arr = [] fract_avg = [] end_size = start_size * (2 ** num_sizes) temp_verts = copy.deepcopy(self.verts) fractal_dim = fractal_dim_calc(temp_verts, end_size, num_sizes) for i in range(num_sizes): self.x_arr.append(np.log2((start_size))) self.y_arr.append(fractal_dim[i]) fract_avg.append(np.polyfit(self.x_arr, self.y_arr, 1)[0]) print("(box width = 1/", start_size, ") FRACTAL AVG: ", fract_avg[-1]) start_size = start_size * 2 # y_arr = np.asarray(y_arr) # x_arr = np.asarray(x_arr) print("Made it this far") figi, ax = plt.subplots() line, = ax.plot(self.x_arr, self.y_arr, "bo", picker=5) ax.set_title( "Fractal dimension = {}".format(np.polyfit(self.x_arr, self.y_arr, 1)[0]) ) # np.average(fract_avg))) figi.canvas.mpl_connect('pick_event', self.onpick1) #figi.show() plt.show() print("AVERAGE: ", np.average(fract_avg)) def boxcount_3d(self): """Calculates the dimensionality of a 3D L-System""" mesh = self.graphix.mesh if(mesh is not None): calc_fractal_dim3D(mesh) def is_2d(self): """Returns true if 2D is loaded, false if 3D is loaded.""" if (self.dims.currentWidget().__class__.__name__ == 'LSystem3DWidget'): return False return True def on_boxcount_button_clicked(self): """Determines which type of dimension checking is done""" if(self.is_2d()): self.boxcount_2d() else: self.boxcount_3d() def onpick1(self, event): print("I am an EVENT1") #plt.close(fig=None) ind = event.ind[0]+1 for i in range(ind): if(len(self.x_arr)==2): break # always delete index 0 because array shifts left after delete self.x_arr = np.delete(self.x_arr, 0, axis=None) self.y_arr = np.delete(self.y_arr, 0, axis=None) print("The x array is: ", self.x_arr) fig1, ax = plt.subplots() ax.plot(self.x_arr, self.y_arr, "bo", picker=5) ax.set_title( "Fractal dimension = {}".format(np.polyfit(self.x_arr, self.y_arr, 1)[0]) ) # np.average(fract_avg))) fig1.canvas.mpl_connect('pick_event', self.onpick2) fig1.show() return True def onpick2(self, event): print("I am an EVENT2") #plt.close(fig=None) ind = event.ind[0] num_to_remove = len(self.x_arr)-ind for i in range(num_to_remove): if(len(self.x_arr)==2): break # always delete index -1 self.x_arr = np.delete(self.x_arr, -1, axis=None) self.y_arr = np.delete(self.y_arr, -1, axis=None) fig2, ax = plt.subplots() ax.plot(self.x_arr, self.y_arr, "bo", picker=5) ax.set_title( "Fractal dimension = {}".format(np.polyfit(self.x_arr, self.y_arr, 1)[0]) ) # np.average(fract_avg))) fig2.show() return True def add_widgets(self): """Adds widgets to window""" self.layout.addWidget(self.axiom, 1, 0) self.layout.addWidget(self.axiom_edit, 1, 1, 1, 10) self.layout.addWidget(self.prod_rules[0], 2, 0, 1, 1) self.layout.addWidget(self.prod_rules_edit[0], 2, 1, 1, 9) self.layout.addWidget(self.prod_percent[0], 2, 9) self.layout.addWidget(self.prod_plus, 2, 10, 1, 1) self.layout.addWidget(self.angle, 10, 0) self.layout.addWidget(self.angle_edit, 10, 1, 1, 10) self.layout.addWidget(self.iters, 13, 0) self.layout.addWidget(self.iters_edit, 13, 1, 1, 10) self.layout.addWidget(self.scroll_area, 14, 0, 1, 1) self.layout.addWidget(self.boxcount_button, 16, 0, 1, 1) self.layout.addWidget(self.dims, 14, 1, 5, -1) self.layout.addWidget(self.lsys_button, 20, 0, 1, -1) def show_popup(self): """Adds and removes extra textboxes as needed""" if self.is_2d(): self.reset_text_box_color() prod_rule = "" rules = "" self.amount = 0 for prod in self.prod_rules_edit: prod_rule += prod.text() temp = prod.text() temp = temp.replace(" ", "") temp = temp[:].split(":")[0] rules += temp rules += " " all_prod_rule = prod_rule + self.axiom_edit.text() if (")" in all_prod_rule or "(" in all_prod_rule) and self.made_angle is False: self.turn_angle = QLabel("Turning Angle") self.turn_angle_edit = CustomLineEdit() self.text_boxes.append(self.turn_angle_edit) self.turn_angle_edit.returnPressed.connect(self.lsys_button.click) self.turn_angle_edit.clicked.connect( lambda: self.turn_angle_edit.reset_color() ) self.layout.addWidget(self.turn_angle, 11, 0) self.layout.addWidget(self.turn_angle_edit, 11, 1, 1, 10) self.made_angle = True if ( self.made_angle is True and not "(" in all_prod_rule and not ")" in all_prod_rule and self.made_angle is True ): self.text_boxes.remove(self.turn_angle_edit) self.layout.removeWidget(self.turn_angle_edit) self.layout.removeWidget(self.turn_angle) self.turn_angle.deleteLater() self.turn_angle_edit.deleteLater() self.turn_angle_edit = None self.turn_angle = None self.made_angle = False if (">" in all_prod_rule or "<" in all_prod_rule) and self.made_line is False: self.line_scale = QLabel("Line Scale") self.line_scale_edit = CustomLineEdit() self.text_boxes.append(self.line_scale_edit) self.line_scale_edit.returnPressed.connect(self.lsys_button.click) self.line_scale_edit.clicked.connect( lambda: self.line_scale_edit.reset_color() ) self.layout.addWidget(self.line_scale, 12, 0) self.layout.addWidget(self.line_scale_edit, 12, 1, 1, 10) self.made_line = True if ( self.made_line is True and not "<" in all_prod_rule and not ">" in all_prod_rule and self.made_line is True ): self.text_boxes.remove(self.line_scale_edit) self.layout.removeWidget(self.line_scale_edit) self.layout.removeWidget(self.line_scale) self.line_scale.deleteLater() self.line_scale_edit.deleteLater() self.line_scale_edit = None self.line_scale = None self.made_line = False # Probably doesn't need self as a param, can just be static. def gen_rule_dict(self, prod_rules): """ Generates a rule dictionary from an array of production rules taken from the UI. formats production rules as {Symbol1: [[probability,replacement],...], Symbol2: [[probability,replacement]... ], ...} """ rules = {} for rule in prod_rules: rule = rule.text() rule = rule.replace(" ", "") prod = rule.split(":") rules[prod[0]] = [] for i, rule in enumerate(prod_rules): rule = rule.text() rule = rule.replace(" ", "") prod = rule.split(":") rules[prod[0]].append([float(self.prod_percent[i].text().split("%")[0])/100, prod[1]]) return rules def close_event(self): print("[ INFO ] Exiting...") exit() def more_prods(self): """ Adds textboxes for additional production rules, maxiumum 8.""" if self.prods < 8: self.prods = self.prods + 1 self.prod_rules.append(QLabel("Production Rule " + str(self.prods))) self.prod_rules_edit.append(CustomLineEdit()) self.prod_percent.append(CustomLineEdit()) self.text_boxes.append(self.prod_rules_edit[-1]) self.text_boxes.append(self.prod_percent[-1]) self.prod_percent[-1].setFixedWidth(50) self.prod_rules_edit[self.prods - 1].textChanged.connect( lambda: self.show_popup() ) self.prod_rules_edit[-1].returnPressed.connect(self.lsys_button.click) self.prod_rules_edit[-1].clicked.connect( lambda: self.prod_rules_edit[-1].reset_color() ) self.layout.addWidget(self.prod_rules[self.prods - 1], self.prods + 1, 0) self.layout.addWidget( self.prod_rules_edit[self.prods - 1], self.prods + 1, 1, 1, 9 ) self.layout.addWidget(self.prod_percent[self.prods - 1], self.prods + 1, 9) if self.minuses is not None: # remove last minueses self.layout.removeWidget(self.minuses) self.minuses.deleteLater() self.minuses = None self.minuses = QPushButton("-", self) self.minuses.clicked.connect(self.less_prods) self.layout.addWidget(self.minuses, self.prods + 1, 10, 1, 1) self.prod_percent[-1].setText("100%") def less_prods(self): """ Removes textboxes for production rules when less are needed, minimum 1.""" if self.prods > 1: self.text_boxes.remove(self.prod_rules_edit[-1]) self.text_boxes.remove(self.prod_percent[-1]) # remove last widget prodrules self.layout.removeWidget(self.prod_rules[-1]) self.prod_rules[-1].deleteLater() self.prod_rules.pop() # remove last widget prodrulesEdit self.layout.removeWidget(self.prod_rules_edit[-1]) self.prod_rules_edit[-1].deleteLater() self.prod_rules_edit.pop() # remove last percentage self.layout.removeWidget(self.prod_percent[-1]) self.prod_percent[-1].deleteLater() self.prod_percent.pop() # remove last percentage # for i in self.index: # for j in i: # if j == self.prods-1: # print("WE NEED TO DELETE") # print(i) # print("HELLO") # self.layout.removeWidget(self.prodPercent[-1]) # self.prodPercent[-1].deleteLater() # self.prodPercent.pop() # self.amount = self.amount - 1 # print(len(self.prodPercent)) # remove last minuses self.layout.removeWidget(self.minuses) self.minuses.deleteLater() self.minuses = None self.prods = self.prods - 1 if self.prods > 1: self.minuses = QPushButton("-", self) self.minuses.clicked.connect(self.less_prods) self.layout.addWidget(self.minuses, self.prods + 1, 10, 1, 1) def reset_input_boxes(self): """Resets textboxes to initial configuration, does not clear the fractal from the widget.""" while self.prods >1: self.less_prods() self.prod_rules_edit[-1].setText("") self.axiom_edit.setText("") self.prod_percent[0].setText("100%") self.angle_edit.setText("") self.iters_edit.setText("") def gen_sys(self): """Generates the L-System described by the production rules""" if input_check(self): axiom_input = self.axiom_edit.text() # prodInput = [self.prodrlesEdit.text()] #changed to array angle_input = self.angle_edit.text() if self.made_angle: turn_angle_input = self.turn_angle_edit.text() else: turn_angle_input = 0 if self.made_line: line_scale_input = self.line_scale_edit.text() else: line_scale_input = 1 iters_input = self.iters_edit.text() # Format input for use rules = self.gen_rule_dict(self.prod_rules_edit) # Generate rule grammar dictionary. grammar = { "rules": rules, "axiom": axiom_input, "iterations": int(iters_input), "angle": float(angle_input), "turnAngle": float(turn_angle_input), "lineScale": float(line_scale_input), } if (self.dims.currentWidget().__class__.__name__ == 'LSystem3DWidget'): self.mesh = generate_lsystem_3d(grammar) if self.mesh ==-1: print("[ ERROR ] Invalid input no vertices generated.") else: self.graphix.clear() self.graphix.add_mesh(self.mesh) else: self.verts = generate_lsystem_2d(grammar) if self.verts ==-1: print("[ ERROR ] Invalid input no vertices generated.") else: # Sets verts on graphics widget and draws self.graphix.clear() self.graphix.set_graph(self.verts) # for i in range(1,len(self.verts)): # self.graphix.set_graph(self.verts[i],1) #split = true self.graphix.update() self.graphix.reset_camera() def gen_example(self, example): """Loads preset L-Systems""" self.axiom_edit.reset_box() for prod in self.prod_rules_edit: prod.reset_box() self.angle_edit.reset_box() self.iters_edit.reset_box() if(self.is_2d()): grammar = get_saved_lsystem(example, self.saved_lsystems["two-d"]) else: grammar = get_saved_lsystem(example, self.saved_lsystems["three-d"]) self.axiom_edit.setText(grammar["axiom"]) num_rules = 0 for key in grammar["rules"]: if isinstance(grammar["rules"][key], list): num_rules += len(grammar["rules"][key]) else: num_rules += 1 while self.prods < num_rules: self.more_prods() while self.prods > num_rules: self.less_prods() which_rule = 0 for i, key in enumerate(grammar["rules"]): value = grammar["rules"][key] if isinstance(value, str): self.prod_rules_edit[which_rule].setText(key + ": " + value) which_rule+=1 else: for val in value: print(val) self.prod_rules_edit[which_rule].setText(key + ": " + val[0]) self.prod_percent[which_rule].setText(val[1]) which_rule+=1 self.angle_edit.setText(str(grammar["angle"])) if self.made_angle: self.turn_angle_edit.setText(str(grammar["turn_angle"])) if self.made_line: self.line_scale_edit.setText(str(grammar["line_scale"])) self.iters_edit.setText(str(grammar["iterations"])) self.gen_sys() def del_example(self, example, dim): remove_saved_lsystem(example, dim) print(example, " was deleted from disk") self.reload_presets() def reset_text_box_color(self): """resets the color of all textboxes""" for box in self.text_boxes: box.reset_color() def reset_zoom(self): """resets the zoom level in the display widget""" self.two_d.reset_zoom() self.three_d.reset_zoom() #built in function don't change to snake script def screenshot(self, parent_pos): """Takes a screenshot of the display window""" #rel pos is the upper left pointof the widget relative to window rel_pos = self.dims.pos() #shift the y down by the total height - height of the widget (height of text boxes) #1.1 factor added arbitrarily rel_pos.setY((self.height()-self.dims.height())*1.1) pos = rel_pos+parent_pos qfd = QFileDialog() filter = "Images (*.png *.xpm *.jpg)" filename, type = QFileDialog.getSaveFileName(self, "", "", filter) if filename: self.two_d.screenshot(filename,pos)
class CreateFunction(QDialog): """A dialog to allow the creation of functions using only one window and a QStackedWidget. For each function in functions, a dialog is created and displayed in the stacked widget.""" valschanged = pyqtSignal(object, name='valschanged') def __init__(self, prevfunc=None, selected_fields=None, parent=None, example=None, text=None): """tags is a list of the tags you want to show in the FunctionDialog. Each item should be in the form (DisplayName, tagname) as used in audioinfo. prevfunc is a Function object that is to be edited.""" QDialog.__init__(self, parent) self.setWindowTitle(translate('Functions Dialog', "Functions")) winsettings('createfunction', self) # Allow __selected field to be used. self.allowSelected = True self.realfuncs = [] # Get all the function from the functions module. for z, funcname in functions.functions.items(): if isinstance(funcname, PluginFunction): self.realfuncs.append(funcname) elif callable(funcname) and (not (funcname.__name__.startswith("__") or (funcname.__doc__ is None))): self.realfuncs.append(z) funcnames = [(Function(z).funcname, z) for z in self.realfuncs] funcnames.sort(key=lambda x: translate('Functions', x[0])) self.realfuncs = [z[1] for z in funcnames] self.vbox = QVBoxLayout() self.functions = QComboBox() self.functions.addItems( sorted([translate('Functions', x[0]) for x in funcnames])) self.vbox.addWidget(self.functions) self.stack = QStackedWidget() self.vbox.addWidget(self.stack) self.okcancel = OKCancel() self.stackWidgets = {} # Holds the created windows in the form self.functions.index: window self.setLayout(self.vbox) self.setMinimumHeight(self.sizeHint().height()) self.okcancel.ok.connect(self.okClicked) self.okcancel.cancel.connect(self.close) self.example = example self._text = text if not selected_fields: self.selectedFields = [] else: self.selectedFields = selected_fields self.exlabel = ScrollLabel('') if prevfunc is not None: index = self.functions.findText( translate('Functions', prevfunc.funcname)) if index >= 0: self.functions.setCurrentIndex(index) self.createWindow(index, prevfunc.tag, prevfunc.args) else: self.createWindow(0) self.functions.activated.connect(self.createWindow) self.vbox.addWidget(self.exlabel) self.vbox.addLayout(self.okcancel) self.setLayout(self.vbox) def createWindow(self, index, fields=None, args=None): """Creates a Function dialog in the stack window if it doesn't exist already.""" self.stack.setFrameStyle(QFrame.Box) if index not in self.stackWidgets: widget = FunctionDialog(self.realfuncs[index], self.selectedFields, args, fields, example=self.example, text=self._text) if args is None: widget.loadSettings() self.stackWidgets.update({index: widget}) self.stack.addWidget(widget) widget.updateExample.connect(self.updateExample) self.stack.setCurrentWidget(self.stackWidgets[index]) self.stackWidgets[index].showexample() self.controls = getattr(self.stackWidgets[index], 'controls', []) self.setMinimumHeight(self.sizeHint().height()) if self.sizeHint().width() > self.width(): self.setMinimumWidth(self.sizeHint().width()) def okClicked(self, close=True): w = self.stack.currentWidget() w.argValues() if not self.checkFields(w.func.tag): return if close: self.close() if w.func.tag: fields = gettaglist() new_fields = [z for z in w.func.tag if z not in fields] if new_fields: settaglist(sorted(new_fields + fields)) for widget in self.stackWidgets.values(): widget.saveSettings() self.saveSettings() self.valschanged.emit(w.func) def checkFields(self, fields): func = self.stack.currentWidget().func msg = translate('Actions', "Error: Using <b>__selected</b> in Actions is not allowed.") if not self.allowSelected and '__selected' in fields: QMessageBox.warning(self, 'puddletag', msg) return False elif func is not None and func not in functions.no_fields: msg = translate('Actions', "Please enter some fields to write to.") if not [_f for _f in fields if _f]: QMessageBox.information(self, 'puddletag', msg) return False return True def loadSettings(self): cparser = PuddleConfig() func_name = cparser.get('functions', 'last_used', '') if not func_name: return try: index = self.realfuncs.index(func_name) self.createWindow(index) self.functions.setCurrentIndex(index) except ValueError: return def saveSettings(self): cparser = PuddleConfig() funcname = self.realfuncs[self.functions.currentIndex()] cparser.set('functions', 'last_used', funcname) def updateExample(self, text): if not text: self.exlabel.setText('') else: self.exlabel.setText(displaytags(text))
class MainWindow(QMainWindow): def __init__(self, uci_page): super().__init__() self.uci_page = uci_page self.add_menu_bar() self.create_content_table() self.window_init() self.change_central_widget_event() self.dataset_data = pd.DataFrame() def window_init(self): self.setWindowTitle(APP_TITLE) self.show() def create_content_table(self): self.content_table = ContentTable(self, self.uci_page) self.central_widget = QStackedWidget() self.central_widget.addWidget(self.content_table) self.central_widget.setCurrentWidget(self.content_table) self.setCentralWidget(self.central_widget) def add_menu_bar(self): self.qmenubar = QMenuBar(self) self.add_file_bar_menus() self.add_visualizer_bar_menus() self.add_normilization_bar_menus() self.setMenuBar(self.qmenubar) def add_file_bar_menus(self): file_menu = self.qmenubar.addMenu('&File') file_menu.addAction(self.open_dataset_action()) file_menu.addAction(self.attr_choose_action()) file_menu.addAction(self.show_pandas_action()) file_menu.addAction(self.quit_app_action()) def add_visualizer_bar_menus(self): file_menu = self.qmenubar.addMenu('&Visualizer') file_menu.addAction(self.histogram_plot_action()) file_menu.addAction(self.andrew_plot_action()) file_menu.addAction(self.parallel_plot_action()) file_menu.addAction(self.radviz_plot_action()) file_menu.addAction(self.heatmap_plot_action()) file_menu.addAction(self.scatter_matrix_plot_action()) file_menu.addAction(self.pca_plot_action()) file_menu.addAction(self.lda_plot_action()) def add_normilization_bar_menus(self): file_menu = self.qmenubar.addMenu('&Normilization') file_menu.addAction(self.norm_by_max_action()) file_menu.addAction(self.norm_by_min_action()) file_menu.addAction(self.norm_by_mean_action()) file_menu.addAction(self.norm_by_minimax_action()) file_menu.addAction(self.mean_norm_action()) def set_dataset_file(self, filename): self.dataset_file = filename print(self.dataset_file) def handle_open_dataset_action(self): filename = import_dataset_action() column_file = os.path.dirname(filename) + "/column_names.txt" print(column_file) self.set_dataset_file(filename) if not file_is_exists(column_file): information_dialod(NOT_SUCCESS_IMPORT_MESSAGE) else: self.dataset_cols = ColumnImporter(column_file).get_columns() dataset_importer = DatasetImporter(self.dataset_file, self.dataset_cols) self.dataset_data = dataset_importer.get_dataset() self.bak_data = self.dataset_data if info_import_dataset_dialog(SUCCESS_IMPORT_MESSAGE): self.show_pandas_table() def handle_attr_choose_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: cols = Chooser(self.dataset_cols[1:], self).get_data() self.dataset_data = self.dataset_data[cols] def show_pandas_table(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: pd_table = QTableView() model = PandasModel(self.dataset_data) pd_table.setModel(model) self.central_widget.addWidget(pd_table) self.central_widget.setCurrentWidget(pd_table) def handle_hist_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_histogram(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_andrew_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_andrew_curves(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_parallel_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_parallel_coordinates(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_radviz_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_radviz(self.dataset_data) cur_widget = self.central_widget.currentWidget() self.dataset_data = self.bak_data if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_heatmap_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_heatmap(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_scatter_matrix_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_scatter_matrix(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_pca_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_pca(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def handle_lda_plot_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: self.plot_window = PlotWindow() self.plot_window.draw_lda(self.dataset_data) self.dataset_data = self.bak_data cur_widget = self.central_widget.currentWidget() if cur_widget != self.content_table: self.central_widget.removeWidget(cur_widget) self.central_widget.addWidget(self.plot_window) self.central_widget.setCurrentWidget(self.plot_window) def show_pandas_action(self): action = QAction('Show imported dataset', self) action.setShortcut('Ctrl+s') action.triggered.connect(self.show_pandas_table) return action def attr_choose_action(self): action = QAction('Choose Attributes', self) action.triggered.connect(self.handle_attr_choose_action) return action def quit_app_action(self): action = QAction('Exit', self) action.setShortcut('Ctrl+q') action.setStatusTip('Exit from visualizer') action.triggered.connect(self.close) return action def open_dataset_action(self): action = QAction('Import dataset', self) action.setShortcut('Ctrl+I') action.setStatusTip('Import dataset from folder with it') action.triggered.connect( lambda state: self.handle_open_dataset_action()) return action def histogram_plot_action(self): action = QAction('Plot Histogram', self) action.triggered.connect(self.handle_hist_plot_action) return action def andrew_plot_action(self): action = QAction("Plot Andrew's Curves", self) action.triggered.connect(self.handle_andrew_plot_action) return action def parallel_plot_action(self): action = QAction("Plot Parallel Coordinates", self) action.triggered.connect(self.handle_parallel_plot_action) return action def radviz_plot_action(self): action = QAction("Plot Radviz", self) action.triggered.connect(self.handle_radviz_plot_action) return action def heatmap_plot_action(self): action = QAction("Plot Heatmap", self) action.triggered.connect(self.handle_heatmap_plot_action) return action def scatter_matrix_plot_action(self): action = QAction("Plot Scatter Matrix", self) action.triggered.connect(self.handle_scatter_matrix_plot_action) return action def pca_plot_action(self): action = QAction("Plot PCA", self) action.triggered.connect(self.handle_pca_plot_action) return action def lda_plot_action(self): action = QAction("Plot LDA", self) action.triggered.connect(self.handle_lda_plot_action) return action def norm_by_max_action(self): action = QAction('Normilize data by max value', self) action.triggered.connect(self.handle_norm_by_max_action) return action def norm_by_min_action(self): action = QAction('Normilize data by min value', self) action.triggered.connect(self.handle_norm_by_min_action) return action def norm_by_mean_action(self): action = QAction('Normilize data by mean value', self) action.triggered.connect(self.handle_norm_by_mean_action) return action def norm_by_minimax_action(self): action = QAction('Normilize data by minimax method', self) action.triggered.connect(self.handle_norm_by_minimax_action) return action def mean_norm_action(self): action = QAction('Normilize data by mean method', self) action.triggered.connect(self.handle_norm_mean_action) return action def change_central_widget_event(self): shortcut = QShortcut(QKeySequence("Ctrl+n"), self) shortcut.activated.connect(self.handle_central_widget) def handle_norm_by_max_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_max_value() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("by max value")) def handle_norm_by_min_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_min_value() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("by min value")) def handle_norm_by_mean_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_mean_value() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("by mean value")) def handle_norm_by_minimax_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.normilize_by_minimax() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("minimax method")) def handle_norm_mean_action(self): if len(self.dataset_data.index) == 0: information_dialod(NO_DATASET_MESSAGE) else: norm_data = Norm(self.dataset_data) norm_data.mean_normilization() self.dataset_data = norm_data.get_normilized_data() information_dialod(NORMILIZED_BY_MESSAGE.format("mean method")) def handle_central_widget(self): cur_widget = self.central_widget.currentWidget() self.central_widget.removeWidget(cur_widget) self.central_widget.setCurrentWidget(self.content_table)
class MarkedHistogram(QWidget): """Histogram with color-indication markers MarkedHistogram shows a histogram of a data set and an optional label for the numeric range of the data set. Color markers can be placed on the histogram by the user and moved interactively, either with the mouse or by typing in a particular data value. A color button is used to control the color of the "current" marker (the one most recently selected with the mouse). Markers can either be vertical bars or squares. Vertical bars can only be moved left/right whereas squares can also be moved up/down. Squares are also connected from left to right by line segments. A row of associated widgets (such as the marker color button) is placed below the histogram. User-specified widgets can also be placed in this row with the add_custom_widget() method. Individual markers are grouped into HistogramMarkers instances, and several HistogramMarkers instances can be associated with a single histogram, though only one instance is active/shown at a time. MarkedHistogram has the following constructor options: [Options noted as init options can only be specified at widget creation. Others can be changed later via the corresponding property name.] color_button -- controls whether a color button is offered in the user interface for changing marker colors. default: True data_source -- either a string or a 3-tuple. If a string, then no histogram is displayed and instead the string is displayed in the histogram area as a text message. The first 2 components of a 3-tuple should be the minimum and maximum values of the histogram, The third component should either be an array of numbers (i.e. the histogram) or a callback function that takes as its single argument the number of bins to histogram into and that returns a histogram array. default: 'no data' layout -- [init option] how to organize the megawidget layout. Choices are 'single', 'top', and 'below'. 'single' should be used when you are using a single histogram in your GUI, or histograms that aren't arrayed vertically. 'top' and 'below' should be used for histograms that are laid out in a vertical array ('top' for the top-most one, and 'below' for all others). Certain repeated elements will be omitted in 'below' histograms (e.g. some widget labels). default: single max_label/min_label [init options] show the max/min histogram values as labels below the histogram on the right/left. If neither is True, then the range will be shown in a single label widget below the histogram. default: False redraw_delay -- amount of time (in seconds) to delay between needing to redraw and actually starting to redraw. Used to avoid multiple (possibly slow) redraws during a resize. default: 0.25 scaling -- how to scale the vertical height of the histogram. Either 'logarithmic' or 'linear'. default: logarithmic select_callback -- [init option] function to call when the "current" marker changes. The function receives 4 argments: previous active marker set/marker, new active marker set/marker. The marker set can be None to indicate no active marker set and the marker can be None if no marker was/is current. show_marker_help -- [init option] whether to show the help text over the histogram describing how to add/delete markers. default: True status_line -- function to use to output messages (such as warning when trying to add more markers than allowed). The function should take a single string argument. default: None value_label -- [init option] label to use next to the entry widget describing the current marker value. default: 'Value' value_width -- width of the current marker value entry widget. default: 7 Constructor options that begin with 'Markers_' specify default constructor options for HistogramMarkers objects created in the add_markers method (e.g. Markers_connect_color='red' will supply connect_color='red' to the HistogramMarkers constructor). Options for specific instances can still be provided to the add_ markers() method as keyword arguments (without the 'Markers_' prefix). """ def __init__(self, *args, color_button=True, data_source='no data', layout='single', max_label=False, min_label=False, redraw_delay=0.25, scaling='logarithmic', select_callback=None, show_marker_help=True, status_line=None, value_label='Value', value_width=7, **kw): # Get HistogramMarkers options and initialise base class self._histogram_markers_kw = markers_kw = {} for opt_name in list(kw.keys()): if opt_name.startswith('Markers_'): markers_kw[opt_name[8:]] = kw.pop(opt_name) super().__init__(*args, **kw) # initialize variables self._layout = layout self.status_line = status_line self._show_marker_help = show_marker_help self._active_markers = None self._markers = [] self._markable = False self._drag_marker = None self._scaling = scaling self._select_callback = select_callback if select_callback: self._prev_markers = None self._prev_marker = None overall_layout = QVBoxLayout() self.setLayout(overall_layout) # Create the add/delete marker help if show_marker_help and layout != 'below': self._marker_help = QLabel( "Ctrl-click on histogram to add or delete thresholds") self._marker_help.setAlignment(Qt.AlignCenter) overall_layout.addWidget(self._marker_help) else: self._marker_help = None # Create the data area class HistFrame(QFrame): def sizeHint(self): return QSize(300, 64) data_frame = QFrame() data_frame.setLineWidth(1) data_frame.setMidLineWidth(2) data_frame.setFrameStyle(data_frame.Panel | data_frame.Sunken) data_frame.setContentsMargins(0, 0, 0, 0) data_frame_layout = QHBoxLayout() data_frame.setLayout(data_frame_layout) self._data_widgets = QStackedWidget() data_frame_layout.addWidget(self._data_widgets) # Crate the histogram widget self._hist_scene = QGraphicsScene() self._hist_bars = self._hist_scene.createItemGroup([]) self._hist_view = QGraphicsView(self._hist_scene) self._hist_view.resizeEvent = self._redraw self._hist_scene.mousePressEvent = lambda event: self._add_or_delete_marker_cb(event) \ if event.modifiers() & mod_key_info("control")[0] else self._select_marker_cb(event) self._hist_scene.mouseMoveEvent = lambda event: self._move_marker_cb(event) \ if self._drag_marker else super().mouseMoveEvent(event) self._hist_scene.mouseReleaseEvent = self._button_up_cb self._redraw_timer = QTimer() self._redraw_timer.timeout.connect(self._redraw_cb) self._redraw_timer.start(1000 * redraw_delay) self._redraw_timer.stop() self._data_widgets.addWidget(self._hist_view) # Create the histogram replacement label self._no_histogram_label = QLabel() self._no_histogram_label.setAlignment(Qt.AlignCenter) self._data_widgets.addWidget(self._no_histogram_label) overall_layout.addWidget(self._data_widgets, stretch=1) # Create range label(s) self._widget_area = QWidget() self._widget_layout = QHBoxLayout() self._min_label = self._max_label = None if min_label or max_label: min_max_layout = QHBoxLayout() if min_label: self._min_label = QLabel() min_max_layout.addWidget(self._min_label, alignment=Qt.AlignLeft & Qt.AlignTop) if max_label: self._max_label = QLabel() min_max_layout.addWidget(self._max_label, alignment=Qt.AlignRight & Qt.AlignTop) overall_layout.addLayout(min_max_layout) else: self._range_label = QLabel() if layout == 'below': self._widget_layout.addWidget(self._range_label) else: lab = QLabel("Range") if layout == 'single': self._widget_layout.addWidget(lab, alignment=Qt.AlignRight) self._widget_layout.addWidget(self._range_label, alignment=Qt.AlignLeft) else: # layout == 'top' range_layout = QVBoxLayout() range_layout.addWidget(lab, alignment=Qt.AlignBottom) range_layout.addWidget(self._range_label, alignment=Qt.AlignTop) self._widget_layout.addLayout(range_layout) self._widget_area.setLayout(self._widget_layout) overall_layout.addWidget(self._widget_area) # Create value widget self._value_entry = QLineEdit() self._value_entry.setEnabled(False) self._value_entry.returnPressed.connect(self._set_value_cb) self.value_width = value_width if layout == 'below': self._widget_layout.addWidget(self._value_entry) else: lab = QLabel(value_label) if layout == 'single': self._widget_layout.addWidget(lab, alignment=Qt.AlignRight) self._widget_layout.addWidget(self._value_entry, alignment=Qt.AlignLeft) else: value_layout = QVBoxLayout() value_layout.addWidget(lab, alignment=Qt.AlignBottom) value_layout.addWidget(self._value_entry, alignment=Qt.AlignTop) self._widget_layout.addLayout(value_layout) # Create color button widget from .color_button import ColorButton self._color_button = cb = ColorButton() cb.color_changed.connect( lambda rgba8: self._color_button_cb([c / 255.0 for c in rgba8])) cb.setEnabled(False) self._color_button_label = cbl = QLabel("Color") if layout == 'below': self._widget_layout.addWidget(self._color_button) else: if layout == 'single': self._widget_layout.addWidget(cbl, alignment=Qt.AlignRight) self._widget_layout.addWidget(cb, alignment=Qt.AlignLeft) else: color_layout = QVBoxLayout() color_layout.addWidget(cbl, alignment=Qt.AlignBottom) color_layout.addWidget(cb, alignment=Qt.AlignTop) self._widget_layout.addLayout(color_layout) self._color_button_shown = True # Show the histogram or the no-data label self.data_source = data_source def activate(self, markers): """Make the given set of markers the currently active set Any previously-active set will be hidden. """ if markers is not None and markers not in self._markers: raise ValueError("activate() called with bad value") if markers == self._active_markers: return if self._active_markers is not None: self._active_markers._hide() elif self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(False) self._active_markers = markers if self._active_markers is not None: self._active_markers.shown = True self._set_sel_marker(self._active_markers._sel_marker) else: if self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(True) if self._select_callback: if self._prev_marker is not None: self._select_callback(self._prev_markers, self._prev_marker, None, None) self._prev_markers = None self._prev_marker = None def add_custom_widget(self, widget, left_side=True): self._widget_layout.addWidget(0 if left_side else -1, widget) def add_markers(self, activate=True, **kw): """Create and return a new set of markers. If 'activate' is true, the new set will also become the active set. Other keyword arguments will be passed to the HistogramMarkers constructor. """ final_kw = {k: v for k, v in self._histogram_markers_kw.items()} final_kw.update(kw) final_kw['histogram'] = self markers = HistogramMarkers(**final_kw) self._markers.append(markers) if activate: self.activate(markers) return markers @property def color_button(self): return self._color_button_shown @color_button.setter def color_button(self, show): if show == self._color_button_shown: return if self.layout != 'below': self._color_button_label.setHidden(not show) self._color_button.setHidden(not show) self._color_button_shown = show def current_marker_info(self): """Identify the marker currently selected by the user. Returns a HistogramMarkers instance and a marker. The instance will be None if no marker set has been activated. The marker will be None if no marker has been selected by the user. """ if self._active_markers is None: return None, None return self._active_markers, self._active_markers._sel_marker @property def data_source(self): return self._data_source @data_source.setter def data_source(self, data_source): self._data_source = data_source self._new_data() def delete_markers(self, markers): """Delete the given set of markers. If the markers were active, there will be no active set of markers afterward. """ if markers not in self._markers: raise ValueError("Bad value for delete()") if markers == self._active_markers: self.activate(None) self._markers.remove(markers) @property def layout(self): return self._layout @property def redraw_delay(self): return self._redraw_timer.interval() / 1000.0 @redraw_delay.setter def redraw_delay(self, secs): self._redraw_timer.setInterval(secs * 1000) @property def scaling(self): return self._scaling @scaling.setter def scaling(self, scaling): if self._scaling != scaling: self._scaling = scaling self._redraw_cb() def snapshot_data(self): info = { 'version': 1, 'draw_min': self._draw_min, 'draw_max': self._draw_max, 'markers': [markers.snapshot_data() for markers in self._markers], } if self._active_markers is None: info['active markers'] = None else: info['active markers'] = self._markers.index(self._active_markers) if self['color_button']: info['color well'] = self._color_button.color else: info['color well'] = None return info def snapshot_restore(self, data): self._draw_min = data['draw_min'] self._draw_max = data['draw_max'] if data['color well'] is not None: self._color_button.color = data['color well'] if len(data['markers']) != len(self._markers): # don't know how to deal with this situation return for markers, markers_data in zip(self._markers, data['markers']): markers.snapshot_restore(markers_data) if data['active markers'] is not None: self.activate(self._markers[data['active markers']]) self._set_sel_marker(self._active_markers._sel_marker) @property def value_width(self): return self._value_width @value_width.setter def value_width(self, vw): self._value_width = vw ve = self._value_entry fm = ve.fontMetrics() tm = ve.textMargins() cm = ve.contentsMargins() w = vw * fm.width('w') + tm.left() + tm.right() + cm.left() + cm.right( ) + 8 ve.setMaximumWidth(w) def _abs2rel(self, abs_xy): x, y = abs_xy rel_x = (x - self._min_val) / float(self._max_val - self._min_val) rel_y = y / float(self._ymax) return rel_x, rel_y def _abs_xy(self, scene_xy): scene_x, scene_y = scene_xy dy = min(max(self._bottom - scene_y, 0), self._hist_height - 1) if self.scaling == 'logarithmic': exp = dy / float(self._hist_height - 1) abs_y = (self._max_height + 1)**exp - 1 else: abs_y = self._max_height * dy / float(self._hist_height - 1) cx = scene_x - self._border num_bins = len(self._bins) if num_bins == self._hist_width: fract = cx / (num_bins - 1) abs_x = self._min_val + fract * (self._max_val - self._min_val) elif num_bins == 2: abs_x = self._min_val + (self._max_val - self._min_val) * ( 2 * cx / self._hist_width - 0.5) else: extra = self._hist_width / (2.0 * (num_bins - 1)) abs_x = self._min_val + (self._max_val - self._min_val) * ( cx - extra) / (self._hist_width - 2.0 * extra) abs_x = max(self._min_val, abs_x) abs_x = min(self._max_val, abs_x) return abs_x, abs_y def _add_or_delete_marker_cb(self, event=None): if self._active_markers is None: return marker = self._active_markers._pick_marker(event.scenePos()) if marker is None: max_marks = self._active_markers.max_marks if max_marks is not None and len( self._active_markers) >= max_marks: if self.status_line: self.status_line("Maximum of %d markers\n" % max_marks) return xy = self._abs_xy((event.scenePos().x(), event.scenePos().y())) if self._active_markers.coord_type == 'relative': xy = self._abs2rel(xy) sel_marker = self._active_markers._sel_marker if sel_marker: color = sel_marker.rgba else: color = self._active_markers.new_color marker = self._active_markers.append((xy, color)) self._set_sel_marker(marker, drag_start=event) else: min_marks = self._active_markers.min_marks if min_marks is not None and len( self._active_markers) <= min_marks: if self.status_line: self.status_line("Minimum of %d markers\n" % min_marks) return self._active_markers.remove(marker) self._set_sel_marker(None) def _button_up_cb(self, event=None): if self._drag_marker: self._drag_marker = None if self._active_markers.move_callback: self._active_markers.move_callback('end') def _scene_xy(self, abs_xy): # minimum is in the _center_ of the first bin, # likewise, maximum is in the center of the last bin abs_x, abs_y = abs_xy abs_y = max(0, abs_y) abs_y = min(self._max_height, abs_y) if self.scaling == 'logarithmic': import math abs_y = math.log(abs_y + 1) scene_y = self._bottom - (self._hist_height - 1) * (abs_y / self._max_height) abs_x = max(self._min_val, abs_x) abs_x = min(self._max_val, abs_x) num_bins = len(self._bins) if num_bins == self._hist_width: bin_width = (self._max_val - self._min_val) / float(num_bins - 1) left_edge = self._min_val - 0.5 * bin_width scene_x = int((abs_x - left_edge) / bin_width) else: # histogram is effectively one bin wider (two half-empty bins on each end) if num_bins == 1: scene_x = 0.5 * (self._hist_width - 1) else: extra = (self._max_val - self._min_val) / (2.0 * (num_bins - 1)) eff_min_val = self._min_val - extra eff_max_val = self._max_val + extra eff_range = float(eff_max_val - eff_min_val) scene_x = (self._hist_width - 1) * (abs_x - eff_min_val) / eff_range return self._border + scene_x, scene_y def _color_button_cb(self, rgba): m = self._active_markers._sel_marker if not m: if self.status_line: self.status_line("No marker selected") return m.rgba = rgba def _marker2abs(self, marker): if self._active_markers.coord_type == 'absolute': return marker.xy else: return self._rel2abs(marker.xy) def _move_cur_marker(self, x, yy=None): # # Don't allow dragging out of the scene box. # m = self._active_markers._sel_marker if x < self._min_val: x = self._min_val elif x > self._max_val: x = self._max_val if yy is None: y = m.xy[1] else: y = yy if y < 0: y = 0 elif y > self._ymax: y = self._ymax if self._active_markers.coord_type == 'absolute': m.xy = (x, y) else: m.xy = self._abs2rel((x, y)) if yy is None: m.xy = (m.xy[0], y) self._set_value_entry(x) self._active_markers._update_plot() if self._active_markers.move_callback: self._active_markers.move_callback(m) def _move_marker_cb(self, event): mouse_xy = self._abs_xy((event.scenePos().x(), event.scenePos().y())) dx = mouse_xy[0] - self._last_mouse_xy[0] dy = mouse_xy[1] - self._last_mouse_xy[1] self._last_mouse_xy = mouse_xy if event.modifiers() & mod_key_info("shift")[0]: dx *= .1 dy *= .1 m = self._drag_marker mxy = self._marker2abs(m) x, y = mxy[0] + dx, mxy[1] + dy self._move_cur_marker(x, y) def _new_data(self): ds = self.data_source if isinstance(ds, str): self._no_histogram_label.setText(ds) self._data_widgets.setCurrentWidget(self._no_histogram_label) if self._min_label: self._min_label.setText("") if self._max_label: self._max_label.setText("") if self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(True) self._widget_area.setHidden(True) else: self._data_widgets.setCurrentWidget(self._hist_view) if self.layout != 'below' and self._show_marker_help: self._marker_help.setHidden(False) self._widget_area.setHidden(False) self._draw_min = self._draw_max = None self._redraw_cb() def _redraw(self, event=None): self._markable = False self._redraw_timer.start() def _redraw_cb(self): self._redraw_timer.stop() ds = self.data_source if isinstance(ds, str): # displaying a text label right now return view = self._hist_view scene = self._hist_scene hist_size = view.viewport().size() self._hist_width, self._hist_height = hist_width, hist_height = hist_size.width( ), hist_size.height() self._min_val, self._max_val, self._bins = ds filled_range = self._max_val - self._min_val empty_ranges = [0, 0] if self._draw_min != None: empty_ranges[0] = self._min_val - self._draw_min self._min_val = self._draw_min if self._draw_max != None: empty_ranges[1] = self._draw_max - self._max_val self._max_val = self._draw_max if callable(self._bins): if empty_ranges[0] or empty_ranges[1]: full_range = filled_range + empty_ranges[0] + empty_ranges[1] filled_bins = self._bins( int(hist_width * filled_range / full_range)) left = [0] * int(hist_width * empty_ranges[0] / full_range) right = [0] * (hist_width - len(filled_bins) - len(left)) self._bins = left + filled_bins + right else: self._bins = self._bins(hist_width) elif empty_ranges[0] or empty_ranges[1]: full_range = filled_range + empty_ranges[0] + empty_ranges[1] left = [0] * int(len(self._bins) * empty_ranges[0] / full_range) right = [0] * int(len(self._bins) * empty_ranges[1] / full_range) self._bins = left + self._bins + right if self._min_label: self._min_label.setText(self._str_val(self._min_val)) if self._max_label: self._max_label.setText(self._str_val(self._max_val)) if not self._min_label and not self._max_label: self._range_label.setText( "%s - %s" % (self._str_val(self._min_val), self._str_val(self._max_val))) bars = self._hist_bars.childItems() for bar in bars: self._hist_bars.removeFromGroup(bar) self._hist_scene.removeItem(bar) self._ymax = max(self._bins) if self.scaling == 'logarithmic': from numpy import array, log, float32 self._bins = array(self._bins, float32) self._bins += 1.0 log(self._bins, self._bins) max_height = max(self._bins) self._max_height = max_height h_scale = float(hist_height - 1) / max_height self._border = border = 0 bottom = hist_height + border - 1 self._bottom = bottom num_bins = len(self._bins) if num_bins == hist_width: for b, n in enumerate(self._bins): x = border + b h = int(h_scale * n) line = self._hist_scene.addLine(x, bottom, x, bottom - h) self._hist_bars.addToGroup(line) line.setZValue(-1) # keep bars below markers else: x_scale = (hist_width - 1) / float(num_bins) for b, n in enumerate(self._bins): x1 = border + b * x_scale x2 = border + (b + 1) * x_scale h = int(h_scale * n) rect = self._hist_scene.addRect(x1, bottom - h, x2 - x1, h) self._hist_bars.addToGroup(rect) rect.setZValue(-1) # keep bars below markers self._markable = True if self._active_markers is not None: self._active_markers._update_plot() marker = self._active_markers._sel_marker if marker: self._set_value_entry(self._marker2abs(marker)[0]) self._hist_scene.setSceneRect(self._hist_scene.itemsBoundingRect()) def _rel2abs(self, rel_xy): x, y = rel_xy abs_x = self._min_val * (1 - x) + x * self._max_val abs_y = y * self._ymax return abs_x, abs_y def _select_marker_cb(self, event=None): if self._active_markers is not None: marker = self._active_markers._pick_marker(event.scenePos()) self._set_sel_marker(marker, drag_start=event) if marker is not None: return # show value where histogram clicked self._set_value_entry(self._abs_xy((event.scenePos().x(), 0))[0]) def _set_sel_marker(self, marker, drag_start=None): self._active_markers._sel_marker = marker if not marker: self._color_button.color = "gray" self._color_button.setEnabled(False) self._set_value_entry("") self._value_entry.setEnabled(False) else: self._color_button.setEnabled(True) self._color_button.color = marker.rgba self._value_entry.setEnabled(True) self._set_value_entry(self._marker2abs(marker)[0]) if self._select_callback: if marker is not None or self._prev_marker is not None: self._select_callback(self._prev_markers, self._prev_marker, self._active_markers, marker) self._prev_markers = self._active_markers self._prev_marker = marker if not drag_start: return self._drag_marker = marker if not marker: return self._last_mouse_xy = self._abs_xy( (drag_start.scenePos().x(), drag_start.scenePos().y())) if self._active_markers.move_callback: self._active_markers.move_callback('start') def _set_value_cb(self): try: v = eval(self._value_entry.text()) except Exception: raise ValueError("Invalid histogram value") if type(self._min_val) != type(v): v = type(self._min_val)(v) if v < self._min_val: self._draw_min = v self._redraw_cb() elif v > self._max_val: self._draw_max = v self._redraw_cb() self._move_cur_marker(v) def _set_value_entry(self, val): if isinstance(val, str): self._value_entry.setText(val) return if isinstance(self._min_val, int): val = int(val + 0.5) self._value_entry.setText("%g" % val) def _str_val(self, val): if isinstance(val, (int, bool)): return str(val) return "%g" % val
class TreeWindow(QMainWindow): """Class override for the main window. Contains main window views and controls. """ selectChanged = pyqtSignal() nodeModified = pyqtSignal(treenode.TreeNode) treeModified = pyqtSignal() winActivated = pyqtSignal(QMainWindow) winMinimized = pyqtSignal() winClosing = pyqtSignal(QMainWindow) def __init__(self, model, allActions, parent=None): """Initialize the main window. Arguments: model -- the initial data model allActions -- a dict containing the upper level actions parent -- the parent window, usually None """ super().__init__(parent) self.allActions = allActions.copy() self.allowCloseFlag = True self.winActions = {} self.toolbars = [] self.rightTabActList = [] self.setAttribute(Qt.WA_DeleteOnClose) self.setAcceptDrops(True) self.setStatusBar(QStatusBar()) self.setCaption() self.setupActions() self.setupMenus() self.setupToolbars() self.restoreToolbarPosition() self.treeView = treeview.TreeView(model, self.allActions) self.breadcrumbSplitter = QSplitter(Qt.Vertical) self.setCentralWidget(self.breadcrumbSplitter) self.breadcrumbView = breadcrumbview.BreadcrumbView(self.treeView) self.breadcrumbSplitter.addWidget(self.breadcrumbView) self.breadcrumbView.setVisible(globalref. genOptions['InitShowBreadcrumb']) self.treeSplitter = QSplitter() self.breadcrumbSplitter.addWidget(self.treeSplitter) self.treeStack = QStackedWidget() self.treeSplitter.addWidget(self.treeStack) self.treeStack.addWidget(self.treeView) self.treeView.shortcutEntered.connect(self.execShortcut) self.treeView.selectionModel().selectionChanged.connect(self. updateRightViews) self.treeFilterView = None self.rightTabs = QTabWidget() self.treeSplitter.addWidget(self.rightTabs) self.rightTabs.setTabPosition(QTabWidget.South) self.rightTabs.tabBar().setFocusPolicy(Qt.NoFocus) self.outputSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.outputSplitter, _('Data Output')) parentOutputView = outputview.OutputView(self.treeView, False) parentOutputView.highlighted[str].connect(self.statusBar().showMessage) self.outputSplitter.addWidget(parentOutputView) childOutputView = outputview.OutputView(self.treeView, True) childOutputView.highlighted[str].connect(self.statusBar().showMessage) self.outputSplitter.addWidget(childOutputView) self.editorSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.editorSplitter, _('Data Edit')) parentEditView = dataeditview.DataEditView(self.treeView, self.allActions, False) parentEditView.shortcutEntered.connect(self.execShortcut) parentEditView.focusOtherView.connect(self.focusNextView) parentEditView.inLinkSelectMode.connect(self.treeView. toggleNoMouseSelectMode) self.treeView.skippedMouseSelect.connect(parentEditView. internalLinkSelected) self.editorSplitter.addWidget(parentEditView) childEditView = dataeditview.DataEditView(self.treeView, self.allActions, True) childEditView.shortcutEntered.connect(self.execShortcut) childEditView.focusOtherView.connect(self.focusNextView) childEditView.inLinkSelectMode.connect(self.treeView. toggleNoMouseSelectMode) self.treeView.skippedMouseSelect.connect(childEditView. internalLinkSelected) parentEditView.hoverFocusActive.connect(childEditView.endEditor) childEditView.hoverFocusActive.connect(parentEditView.endEditor) parentEditView.inLinkSelectMode.connect(childEditView. updateInLinkSelectMode) childEditView.inLinkSelectMode.connect(parentEditView. updateInLinkSelectMode) self.editorSplitter.addWidget(childEditView) self.titleSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.titleSplitter, _('Title List')) parentTitleView = titlelistview.TitleListView(self.treeView, False) parentTitleView.shortcutEntered.connect(self.execShortcut) self.titleSplitter.addWidget(parentTitleView) childTitleView = titlelistview.TitleListView(self.treeView, True) childTitleView.shortcutEntered.connect(self.execShortcut) self.titleSplitter.addWidget(childTitleView) self.rightTabs.currentChanged.connect(self.updateRightViews) self.updateFonts() def setExternalSignals(self): """Connect widow object signals to signals in this object. In a separate method to refresh after local control change. """ self.treeView.selectionModel().selectionChanged.connect(self. selectChanged) for i in range(2): self.editorSplitter.widget(i).nodeModified.connect(self. nodeModified) self.titleSplitter.widget(i).nodeModified.connect(self. nodeModified) self.titleSplitter.widget(i).treeModified.connect(self. treeModified) def updateActions(self, allActions): """Use new actions for menus, etc. when the local control changes. Arguments: allActions -- a dict containing the upper level actions """ # remove submenu actions that are children of the window self.removeAction(self.allActions['DataNodeType']) self.removeAction(self.allActions['FormatFontSize']) self.allActions = allActions.copy() self.allActions.update(self.winActions) self.menuBar().clear() self.setupMenus() self.addToolbarCommands() self.treeView.allActions = self.allActions for i in range(2): self.editorSplitter.widget(i).allActions = self.allActions def updateTreeNode(self, node): """Update all spots for the given node in the tree view. Arguments: node -- the node to be updated """ for spot in node.spotRefs: self.treeView.update(spot.index(self.treeView.model())) self.treeView.resizeColumnToContents(0) self.breadcrumbView.updateContents() def updateTree(self): """Update the full tree view. """ self.treeView.scheduleDelayedItemsLayout() self.breadcrumbView.updateContents() def updateRightViews(self, *args, outputOnly=False): """Update all right-hand views and breadcrumb view. Arguments: *args -- dummy arguments to collect args from signals outputOnly -- only update output views (not edit views) """ if globalref.mainControl.activeControl: self.rightTabActList[self.rightTabs. currentIndex()].setChecked(True) self.breadcrumbView.updateContents() splitter = self.rightTabs.currentWidget() if not outputOnly or isinstance(splitter.widget(0), outputview.OutputView): for i in range(2): splitter.widget(i).updateContents() def refreshDataEditViews(self): """Refresh the data in non-selected cells in curreent data edit views. """ splitter = self.rightTabs.currentWidget() if isinstance(splitter.widget(0), dataeditview.DataEditView): for i in range(2): splitter.widget(i).updateUnselectedCells() def updateCommandsAvail(self): """Set window commands available based on node selections. """ self.allActions['ViewPrevSelect'].setEnabled(len(self.treeView. selectionModel(). prevSpots) > 1) self.allActions['ViewNextSelect'].setEnabled(len(self.treeView. selectionModel(). nextSpots) > 0) def updateWinGenOptions(self): """Update tree and data edit windows based on general option changes. """ self.treeView.updateTreeGenOptions() for i in range(2): self.editorSplitter.widget(i).setMouseTracking(globalref. genOptions['EditorOnHover']) def updateFonts(self): """Update custom fonts in views. """ treeFont = QTextDocument().defaultFont() treeFontName = globalref.miscOptions['TreeFont'] if treeFontName: treeFont.fromString(treeFontName) self.treeView.setFont(treeFont) self.treeView.updateTreeGenOptions() if self.treeFilterView: self.treeFilterView.setFont(treeFont) ouputFont = QTextDocument().defaultFont() ouputFontName = globalref.miscOptions['OutputFont'] if ouputFontName: ouputFont.fromString(ouputFontName) editorFont = QTextDocument().defaultFont() editorFontName = globalref.miscOptions['EditorFont'] if editorFontName: editorFont.fromString(editorFontName) for i in range(2): self.outputSplitter.widget(i).setFont(ouputFont) self.editorSplitter.widget(i).setFont(editorFont) self.titleSplitter.widget(i).setFont(editorFont) def resetTreeModel(self, model): """Change the model assigned to the tree view. Arguments: model -- the new model to assign """ self.treeView.resetModel(model) self.treeView.selectionModel().selectionChanged.connect(self. updateRightViews) def activateAndRaise(self): """Activate this window and raise it to the front. """ self.activateWindow() self.raise_() def setCaption(self, pathObj=None, modified=False): """Change the window caption title based on the file name and path. Arguments: pathObj - a path object for the current file """ modFlag = '*' if modified else '' if pathObj: caption = '{0}{1} [{2}] - TreeLine'.format(str(pathObj.name), modFlag, str(pathObj.parent)) else: caption = '- TreeLine' self.setWindowTitle(caption) def filterView(self): """Create, show and return a filter view. """ self.removeFilterView() self.treeFilterView = treeview.TreeFilterView(self.treeView, self.allActions) self.treeFilterView.shortcutEntered.connect(self.execShortcut) self.treeView.selectionModel().selectionChanged.connect(self. treeFilterView. updateFromSelectionModel) for i in range(2): editView = self.editorSplitter.widget(i) editView.inLinkSelectMode.connect(self.treeFilterView. toggleNoMouseSelectMode) self.treeFilterView.skippedMouseSelect.connect(editView. internalLinkSelected) self.treeStack.addWidget(self.treeFilterView) self.treeStack.setCurrentWidget(self.treeFilterView) return self.treeFilterView def removeFilterView(self): """Hide and delete the current filter view. """ if self.treeFilterView != None: # check for None since False if empty self.treeStack.removeWidget(self.treeFilterView) globalref.mainControl.currentStatusBar().removeWidget(self. treeFilterView. messageLabel) self.treeFilterView.messageLabel.deleteLater() self.treeFilterView = None def rightParentView(self): """Return the current right-hand parent view if visible (or None). """ view = self.rightTabs.currentWidget().widget(0) if not view.isVisible() or view.height() == 0 or view.width() == 0: return None return view def rightChildView(self): """Return the current right-hand parent view if visible (or None). """ view = self.rightTabs.currentWidget().widget(1) if not view.isVisible() or view.height() == 0 or view.width() == 0: return None return view def focusNextView(self, forward=True): """Focus the next pane in the tab focus series. Called by a signal from the data edit views. Tab sequences tend to skip views without this. Arguments: forward -- forward in tab series if True """ reason = (Qt.TabFocusReason if forward else Qt.BacktabFocusReason) rightParent = self.rightParentView() rightChild = self.rightChildView() if (self.sender().isChildView == forward or (forward and rightChild == None) or (not forward and rightParent == None)): self.treeView.setFocus(reason) elif forward: rightChild.setFocus(reason) else: rightParent.setFocus(reason) def execShortcut(self, key): """Execute an action based on a shortcut key signal from a view. Arguments: key -- the QKeySequence shortcut """ keyDict = {action.shortcut().toString(): action for action in self.allActions.values()} try: action = keyDict[key.toString()] except KeyError: return if action.isEnabled(): action.trigger() def setupActions(self): """Add the actions for contols at the window level. These actions only affect an individual window, they're independent in multiple windows of the same file. """ viewExpandBranchAct = QAction(_('&Expand Full Branch'), self, statusTip=_('Expand all children of the selected nodes')) viewExpandBranchAct.triggered.connect(self.viewExpandBranch) self.winActions['ViewExpandBranch'] = viewExpandBranchAct viewCollapseBranchAct = QAction(_('&Collapse Full Branch'), self, statusTip=_('Collapse all children of the selected nodes')) viewCollapseBranchAct.triggered.connect(self.viewCollapseBranch) self.winActions['ViewCollapseBranch'] = viewCollapseBranchAct viewPrevSelectAct = QAction(_('&Previous Selection'), self, statusTip=_('Return to the previous tree selection')) viewPrevSelectAct.triggered.connect(self.viewPrevSelect) self.winActions['ViewPrevSelect'] = viewPrevSelectAct viewNextSelectAct = QAction(_('&Next Selection'), self, statusTip=_('Go to the next tree selection in history')) viewNextSelectAct.triggered.connect(self.viewNextSelect) self.winActions['ViewNextSelect'] = viewNextSelectAct viewRightTabGrp = QActionGroup(self) viewOutputAct = QAction(_('Show Data &Output'), viewRightTabGrp, statusTip=_('Show data output in right view'), checkable=True) self.winActions['ViewDataOutput'] = viewOutputAct viewEditAct = QAction(_('Show Data &Editor'), viewRightTabGrp, statusTip=_('Show data editor in right view'), checkable=True) self.winActions['ViewDataEditor'] = viewEditAct viewTitleAct = QAction(_('Show &Title List'), viewRightTabGrp, statusTip=_('Show title list in right view'), checkable=True) self.winActions['ViewTitleList'] = viewTitleAct self.rightTabActList = [viewOutputAct, viewEditAct, viewTitleAct] viewRightTabGrp.triggered.connect(self.viewRightTab) viewBreadcrumbAct = QAction(_('Show &Breadcrumb View'), self, statusTip=_('Toggle showing breadcrumb ancestor view'), checkable=True) viewBreadcrumbAct.setChecked(globalref. genOptions['InitShowBreadcrumb']) viewBreadcrumbAct.triggered.connect(self.viewBreadcrumb) self.winActions['ViewBreadcrumb'] = viewBreadcrumbAct viewChildPaneAct = QAction(_('&Show Child Pane'), self, statusTip=_('Toggle showing right-hand child views'), checkable=True) viewChildPaneAct.setChecked(globalref.genOptions['InitShowChildPane']) viewChildPaneAct.triggered.connect(self.viewShowChildPane) self.winActions['ViewShowChildPane'] = viewChildPaneAct viewDescendAct = QAction(_('Show Output &Descendants'), self, statusTip=_('Toggle showing output view indented descendants'), checkable=True) viewDescendAct.setChecked(globalref.genOptions['InitShowDescendants']) viewDescendAct.triggered.connect(self.viewDescendants) self.winActions['ViewShowDescend'] = viewDescendAct winCloseAct = QAction(_('&Close Window'), self, statusTip=_('Close this window')) winCloseAct.triggered.connect(self.close) self.winActions['WinCloseWindow'] = winCloseAct incremSearchStartAct = QAction(_('Start Incremental Search'), self) incremSearchStartAct.triggered.connect(self.incremSearchStart) self.addAction(incremSearchStartAct) self.winActions['IncremSearchStart'] = incremSearchStartAct incremSearchNextAct = QAction(_('Next Incremental Search'), self) incremSearchNextAct.triggered.connect(self.incremSearchNext) self.addAction(incremSearchNextAct) self.winActions['IncremSearchNext'] = incremSearchNextAct incremSearchPrevAct = QAction(_('Previous Incremental Search'), self) incremSearchPrevAct.triggered.connect(self.incremSearchPrev) self.addAction(incremSearchPrevAct) self.winActions['IncremSearchPrev'] = incremSearchPrevAct for name, action in self.winActions.items(): icon = globalref.toolIcons.getIcon(name.lower()) if icon: action.setIcon(icon) key = globalref.keyboardOptions[name] if not key.isEmpty(): action.setShortcut(key) self.allActions.update(self.winActions) def setupToolbars(self): """Add toolbars based on option settings. """ for toolbar in self.toolbars: self.removeToolBar(toolbar) self.toolbars = [] numToolbars = globalref.toolbarOptions['ToolbarQuantity'] iconSize = globalref.toolbarOptions['ToolbarSize'] for num in range(numToolbars): name = 'Toolbar{:d}'.format(num) toolbar = self.addToolBar(name) toolbar.setObjectName(name) toolbar.setIconSize(QSize(iconSize, iconSize)) self.toolbars.append(toolbar) self.addToolbarCommands() def addToolbarCommands(self): """Add toolbar commands for current actions. """ for toolbar, commandList in zip(self.toolbars, globalref. toolbarOptions['ToolbarCommands']): toolbar.clear() for command in commandList.split(','): if command: try: toolbar.addAction(self.allActions[command]) except KeyError: pass else: toolbar.addSeparator() def setupMenus(self): """Add menu items for actions. """ self.fileMenu = self.menuBar().addMenu(_('&File')) self.fileMenu.aboutToShow.connect(self.loadRecentMenu) self.fileMenu.addAction(self.allActions['FileNew']) self.fileMenu.addAction(self.allActions['FileOpen']) self.fileMenu.addAction(self.allActions['FileOpenSample']) self.fileMenu.addAction(self.allActions['FileImport']) self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FileSave']) self.fileMenu.addAction(self.allActions['FileSaveAs']) self.fileMenu.addAction(self.allActions['FileExport']) self.fileMenu.addAction(self.allActions['FileProperties']) self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FilePrintSetup']) self.fileMenu.addAction(self.allActions['FilePrintPreview']) self.fileMenu.addAction(self.allActions['FilePrint']) self.fileMenu.addAction(self.allActions['FilePrintPdf']) self.fileMenu.addSeparator() self.recentFileSep = self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FileQuit']) editMenu = self.menuBar().addMenu(_('&Edit')) editMenu.addAction(self.allActions['EditUndo']) editMenu.addAction(self.allActions['EditRedo']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditCut']) editMenu.addAction(self.allActions['EditCopy']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPaste']) editMenu.addAction(self.allActions['EditPastePlain']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPasteChild']) editMenu.addAction(self.allActions['EditPasteBefore']) editMenu.addAction(self.allActions['EditPasteAfter']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPasteCloneChild']) editMenu.addAction(self.allActions['EditPasteCloneBefore']) editMenu.addAction(self.allActions['EditPasteCloneAfter']) nodeMenu = self.menuBar().addMenu(_('&Node')) nodeMenu.addAction(self.allActions['NodeRename']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeAddChild']) nodeMenu.addAction(self.allActions['NodeInsertBefore']) nodeMenu.addAction(self.allActions['NodeInsertAfter']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeDelete']) nodeMenu.addAction(self.allActions['NodeIndent']) nodeMenu.addAction(self.allActions['NodeUnindent']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeMoveUp']) nodeMenu.addAction(self.allActions['NodeMoveDown']) nodeMenu.addAction(self.allActions['NodeMoveFirst']) nodeMenu.addAction(self.allActions['NodeMoveLast']) dataMenu = self.menuBar().addMenu(_('&Data')) # add action's parent to get the sub-menu dataMenu.addMenu(self.allActions['DataNodeType'].parent()) # add the action to activate the shortcut key self.addAction(self.allActions['DataNodeType']) dataMenu.addAction(self.allActions['DataConfigType']) dataMenu.addAction(self.allActions['DataCopyType']) dataMenu.addAction(self.allActions['DataVisualConfig']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataSortNodes']) dataMenu.addAction(self.allActions['DataNumbering']) dataMenu.addAction(self.allActions['DataRegenRefs']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataCloneMatches']) dataMenu.addAction(self.allActions['DataDetachClones']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataFlatCategory']) dataMenu.addAction(self.allActions['DataAddCategory']) dataMenu.addAction(self.allActions['DataSwapCategory']) toolsMenu = self.menuBar().addMenu(_('&Tools')) toolsMenu.addAction(self.allActions['ToolsFindText']) toolsMenu.addAction(self.allActions['ToolsFindCondition']) toolsMenu.addAction(self.allActions['ToolsFindReplace']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsFilterText']) toolsMenu.addAction(self.allActions['ToolsFilterCondition']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsSpellCheck']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsGenOptions']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsShortcuts']) toolsMenu.addAction(self.allActions['ToolsToolbars']) toolsMenu.addAction(self.allActions['ToolsFonts']) toolsMenu.addAction(self.allActions['ToolsColors']) formatMenu = self.menuBar().addMenu(_('Fo&rmat')) formatMenu.addAction(self.allActions['FormatBoldFont']) formatMenu.addAction(self.allActions['FormatItalicFont']) formatMenu.addAction(self.allActions['FormatUnderlineFont']) formatMenu.addSeparator() # add action's parent to get the sub-menu formatMenu.addMenu(self.allActions['FormatFontSize'].parent()) # add the action to activate the shortcut key self.addAction(self.allActions['FormatFontSize']) formatMenu.addAction(self.allActions['FormatFontColor']) formatMenu.addSeparator() formatMenu.addAction(self.allActions['FormatExtLink']) formatMenu.addAction(self.allActions['FormatIntLink']) formatMenu.addSeparator() formatMenu.addAction(self.allActions['FormatSelectAll']) formatMenu.addAction(self.allActions['FormatClearFormat']) viewMenu = self.menuBar().addMenu(_('&View')) viewMenu.addAction(self.allActions['ViewExpandBranch']) viewMenu.addAction(self.allActions['ViewCollapseBranch']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewPrevSelect']) viewMenu.addAction(self.allActions['ViewNextSelect']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewDataOutput']) viewMenu.addAction(self.allActions['ViewDataEditor']) viewMenu.addAction(self.allActions['ViewTitleList']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewBreadcrumb']) viewMenu.addAction(self.allActions['ViewShowChildPane']) viewMenu.addAction(self.allActions['ViewShowDescend']) self.windowMenu = self.menuBar().addMenu(_('&Window')) self.windowMenu.aboutToShow.connect(self.loadWindowMenu) self.windowMenu.addAction(self.allActions['WinNewWindow']) self.windowMenu.addAction(self.allActions['WinCloseWindow']) self.windowMenu.addSeparator() helpMenu = self.menuBar().addMenu(_('&Help')) helpMenu.addAction(self.allActions['HelpBasic']) helpMenu.addAction(self.allActions['HelpFull']) helpMenu.addSeparator() helpMenu.addAction(self.allActions['HelpAbout']) def viewExpandBranch(self): """Expand all children of the selected spots. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectedSpots = self.treeView.selectionModel().selectedSpots() if not selectedSpots: selectedSpots = self.treeView.model().treeStructure.rootSpots() for spot in selectedSpots: self.treeView.expandBranch(spot) QApplication.restoreOverrideCursor() def viewCollapseBranch(self): """Collapse all children of the selected spots. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectedSpots = self.treeView.selectionModel().selectedSpots() if not selectedSpots: selectedSpots = self.treeView.model().treeStructure.rootSpots() for spot in selectedSpots: self.treeView.collapseBranch(spot) QApplication.restoreOverrideCursor() def viewPrevSelect(self): """Return to the previous tree selection. """ self.treeView.selectionModel().restorePrevSelect() def viewNextSelect(self): """Go to the next tree selection in history. """ self.treeView.selectionModel().restoreNextSelect() def viewRightTab(self, action): """Show the tab in the right-hand view given by action. Arguments: action -- the action triggered in the action group """ if action == self.allActions['ViewDataOutput']: self.rightTabs.setCurrentWidget(self.outputSplitter) elif action == self.allActions['ViewDataEditor']: self.rightTabs.setCurrentWidget(self.editorSplitter) else: self.rightTabs.setCurrentWidget(self.titleSplitter) def viewBreadcrumb(self, checked): """Enable or disable the display of the breadcrumb view. Arguments: checked -- True if to be shown, False if to be hidden """ self.breadcrumbView.setVisible(checked) if checked: self.updateRightViews() def viewShowChildPane(self, checked): """Enable or disable the display of children in a split pane. Arguments: checked -- True if to be shown, False if to be hidden """ for tabNum in range(3): for splitNum in range(2): view = self.rightTabs.widget(tabNum).widget(splitNum) view.hideChildView = not checked self.updateRightViews() def viewDescendants(self, checked): """Set the output view to show indented descendants if checked. Arguments: checked -- True if to be shown, False if to be hidden """ self.outputSplitter.widget(1).showDescendants = checked self.updateRightViews() def incremSearchStart(self): """Start an incremental title search. """ if not self.treeFilterView: self.treeView.setFocus() self.treeView.incremSearchStart() def incremSearchNext(self): """Go to the next match in an incremental title search. """ if not self.treeFilterView: self.treeView.incremSearchNext() def incremSearchPrev(self): """Go to the previous match in an incremental title search. """ if not self.treeFilterView: self.treeView.incremSearchPrev() def loadRecentMenu(self): """Load recent file items to file menu before showing. """ for action in self.fileMenu.actions(): text = action.text() if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': self.fileMenu.removeAction(action) self.fileMenu.insertActions(self.recentFileSep, globalref.mainControl.recentFiles. getActions()) def loadWindowMenu(self): """Load window list items to window menu before showing. """ for action in self.windowMenu.actions(): text = action.text() if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': self.windowMenu.removeAction(action) self.windowMenu.addActions(globalref.mainControl.windowActions()) def saveWindowGeom(self): """Save window geometry parameters to history options. """ contentsRect = self.geometry() frameRect = self.frameGeometry() globalref.histOptions.changeValue('WindowXSize', contentsRect.width()) globalref.histOptions.changeValue('WindowYSize', contentsRect.height()) globalref.histOptions.changeValue('WindowXPos', contentsRect.x()) globalref.histOptions.changeValue('WindowYPos', contentsRect.y()) globalref.histOptions.changeValue('WindowTopMargin', contentsRect.y() - frameRect.y()) globalref.histOptions.changeValue('WindowOtherMargin', contentsRect.x() - frameRect.x()) try: upperWidth, lowerWidth = self.breadcrumbSplitter.sizes() crumbPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('CrumbSplitPercent', crumbPercent) leftWidth, rightWidth = self.treeSplitter.sizes() treePercent = int(100 * leftWidth / (leftWidth + rightWidth)) globalref.histOptions.changeValue('TreeSplitPercent', treePercent) upperWidth, lowerWidth = self.outputSplitter.sizes() outputPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('OutputSplitPercent', outputPercent) upperWidth, lowerWidth = self.editorSplitter.sizes() editorPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('EditorSplitPercent', editorPercent) upperWidth, lowerWidth = self.titleSplitter.sizes() titlePercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('TitleSplitPercent', titlePercent) except ZeroDivisionError: pass # skip if splitter sizes were never set tabNum = self.rightTabs.currentIndex() globalref.histOptions.changeValue('ActiveRightView', tabNum) def restoreWindowGeom(self, offset=0): """Restore window geometry from history options. Arguments: offset -- number of pixels to offset window, down and to right """ rect = QRect(globalref.histOptions['WindowXPos'], globalref.histOptions['WindowYPos'], globalref.histOptions['WindowXSize'], globalref.histOptions['WindowYSize']) if rect.x() == -1000 and rect.y() == -1000: # let OS position window the first time self.resize(rect.size()) else: if offset: rect.adjust(offset, offset, offset, offset) availRect = QApplication.primaryScreen().availableVirtualGeometry() topMargin = globalref.histOptions['WindowTopMargin'] otherMargin = globalref.histOptions['WindowOtherMargin'] # remove frame space from available rect availRect.adjust(otherMargin, topMargin, -otherMargin, -otherMargin) finalRect = rect.intersected(availRect) if finalRect.isEmpty(): rect.moveTo(0, 0) finalRect = rect.intersected(availRect) if finalRect.isValid(): self.setGeometry(finalRect) crumbWidth = int(self.breadcrumbSplitter.width() / 100 * globalref.histOptions['CrumbSplitPercent']) self.breadcrumbSplitter.setSizes([crumbWidth, self.breadcrumbSplitter.width() - crumbWidth]) treeWidth = int(self.treeSplitter.width() / 100 * globalref.histOptions['TreeSplitPercent']) self.treeSplitter.setSizes([treeWidth, self.treeSplitter.width() - treeWidth]) outHeight = int(self.outputSplitter.height() / 100.0 * globalref.histOptions['OutputSplitPercent']) self.outputSplitter.setSizes([outHeight, self.outputSplitter.height() - outHeight]) editHeight = int(self.editorSplitter.height() / 100.0 * globalref.histOptions['EditorSplitPercent']) self.editorSplitter.setSizes([editHeight, self.editorSplitter.height() - editHeight]) titleHeight = int(self.titleSplitter.height() / 100.0 * globalref.histOptions['TitleSplitPercent']) self.titleSplitter.setSizes([titleHeight, self.titleSplitter.height() - titleHeight]) self.rightTabs.setCurrentIndex(globalref. histOptions['ActiveRightView']) def resetWindowGeom(self): """Set all stored window geometry values back to default settings. """ globalref.histOptions.resetToDefaults(['WindowXPos', 'WindowYPos', 'WindowXSize', 'WindowYSize', 'CrumbSplitPercent', 'TreeSplitPercent', 'OutputSplitPercent', 'EditorSplitPercent', 'TitleSplitPercent', 'ActiveRightView']) def saveToolbarPosition(self): """Save the toolbar position to the toolbar options. """ toolbarPos = base64.b64encode(self.saveState().data()).decode('ascii') globalref.toolbarOptions.changeValue('ToolbarPosition', toolbarPos) globalref.toolbarOptions.writeFile() def restoreToolbarPosition(self): """Restore the toolbar position from the toolbar options. """ toolbarPos = globalref.toolbarOptions['ToolbarPosition'] if toolbarPos: self.restoreState(base64.b64decode(bytes(toolbarPos, 'ascii'))) def dragEnterEvent(self, event): """Accept drags of files to this window. Arguments: event -- the drag event object """ if event.mimeData().hasUrls(): event.accept() def dropEvent(self, event): """Open a file dropped onto this window. Arguments: event -- the drop event object """ fileList = event.mimeData().urls() if fileList: path = pathlib.Path(fileList[0].toLocalFile()) globalref.mainControl.openFile(path, checkModified=True) def changeEvent(self, event): """Detect an activation of the main window and emit a signal. Arguments: event -- the change event object """ super().changeEvent(event) if (event.type() == QEvent.ActivationChange and QApplication.activeWindow() == self): self.winActivated.emit(self) elif (event.type() == QEvent.WindowStateChange and globalref.genOptions['MinToSysTray'] and self.isMinimized()): self.winMinimized.emit() def closeEvent(self, event): """Signal that the view is closing and close if the flag allows it. Also save window status if necessary. Arguments: event -- the close event object """ self.winClosing.emit(self) if self.allowCloseFlag: event.accept() else: event.ignore()
class App(QMainWindow): def __init__(self, version): super().__init__() self.workspace_path = "" self.workspace_structures = {} self.version = version self.init_ui() self.show() self.center() def init_ui(self): self.setWindowTitle("Trener Szpilek by Patryk Wertka") self.setWindowIcon(QtGui.QIcon(os.path.join("resources", "skull.ico"))) self.home_widget = HomeWidget(self) self.tag_structure_widget = TagStructuresWidget(self) self.prepare_training_widget = PrepareTrainingWidget(self) self.generate_doc_settings_widget: GenerateDocSettingsWidget = None self.stacked = QStackedWidget() self.setCentralWidget(self.stacked) self.stacked.addWidget(self.home_widget) self.stacked.addWidget(self.tag_structure_widget) self.stacked.addWidget(self.prepare_training_widget) def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def set_workspace_path(self, directory): self.workspace_path = directory def launch_tag_structures(self): self.tag_structure_widget.load_images_paths() self.stacked.setCurrentWidget(self.tag_structure_widget) def launch_training(self): self.prepare_training_widget.init() self.stacked.setCurrentWidget(self.prepare_training_widget) def go_home(self): self.stacked.setCurrentWidget(self.home_widget) def load_workspace_structures(self): self.workspace_structures = load_structures(self.workspace_path) def save_workspace_structures(self): save_structures(self.workspace_path, self.workspace_structures) def import_structures(self, directory): def normpath(p): return os.path.normcase(os.path.normpath(p)) if (normpath(directory) == normpath( os.path.join(self.workspace_path, "_trener_dane_"))): msg = QMessageBox() msg.setText("Niepotrzebna akcja") msg.setInformativeText( "Próbujesz zaimportować struktury z aktualnie wybranego folderu roboczego. Nie ma takiej potrzeby :)" ) msg.setWindowTitle("Niepotrzebny import") msg.exec_() return imported_structures = load_structures(data_directory=directory) self.workspace_structures = merged_structures_dicts( self.workspace_structures, imported_structures) self.save_workspace_structures() def generate_doc(self): self.generate_doc_settings_widget = GenerateDocSettingsWidget(self) self.generate_doc_settings_widget.show()
class MAIN_WINDOW(QWidget): def __init__(self): super().__init__() self.move(350, 150) self.setMinimumSize(640, 480) self.win_title = "FACE RECOGNITION" self.setWindowTitle(self.win_title) self.setStyleSheet(open("./assets/css/main.css").read()) # Main Layout of Widget self.super_layout = QStackedLayout() self.setLayout(self.super_layout) self._view() self._other_view() def _view(self): initial_layout = QGridLayout() register = QPushButton("Register") register.setIcon(QIcon("./assets/icons/add_users.png")) register.setIconSize(QSize(50, 50)) stud_det = QPushButton("Student") stud_det.setIcon(QIcon("./assets/icons/student.png")) stud_det.setIconSize(QSize(50, 50)) staf_det = QPushButton("Staff") staf_det.setIcon(QIcon("./assets/icons/staff.png")) staf_det.setIconSize(QSize(50, 50)) register.setObjectName("main_btn") stud_det.setObjectName("main_btn") staf_det.setObjectName("main_btn") initial_layout.addWidget(register, 0, 0, 1, 0) initial_layout.addWidget(stud_det, 1, 0) initial_layout.addWidget(staf_det, 1, 1) self.main_widget = QWidget() self.main_widget.setLayout(initial_layout) self.super_layout.addWidget(self.main_widget) register.clicked.connect( lambda: self._handle_open_register_view(self.super_layout)) stud_det.clicked.connect(self._handle_open_student_details_view) staf_det.clicked.connect(self._handle_open_staff_details_view) def _other_view(self): # For other screens aside Login Screen And Main View Screen self.stacked = QStackedWidget() self.main_grid_widget = QWidget() self.main_grid = QGridLayout() back_btn = QCommandLinkButton() back_btn.setIcon(QIcon("./assets/icons/back.png")) back_btn.setIconSize(QSize(30, 30)) self.main_grid_widget.setLayout(self.main_grid) self.main_grid.addWidget(back_btn, 0, 0) self.main_grid.addWidget(self.stacked, 1, 0, 1, 0) back_btn.clicked.connect(self._handle_go_back) back_btn.setMaximumWidth(45) def _handle_open_register_view(self, main_layout): self.setWindowTitle("REGISTER") self.reg_view = REGISTER_MAIN(main_layout) self.stacked.addWidget(self.reg_view.main_widget) self.stacked.setCurrentWidget(self.reg_view.main_widget) self.super_layout.addWidget(self.main_grid_widget) self.super_layout.setCurrentWidget(self.main_grid_widget) def _handle_open_student_details_view(self): self.ver_view = Student_Verify( self, self.super_layout, self.main_grid_widget, self.stacked, self._handle_go_back, ) def _handle_open_staff_details_view(self): self.ver_view = Staff_Verify( self, self.super_layout, self.main_grid_widget, self.stacked, self._handle_go_back, ) def _handle_go_back(self): self.super_layout.setCurrentIndex(0) self.setWindowTitle(self.win_title)
class DataSelectionGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget(self): return self def appletDrawer(self): return self._drawer def menus(self): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, imageIndex): if imageIndex is not None: self.laneSummaryTableView.selectRow(imageIndex) for detailWidget in self._detailViewerWidgets: detailWidget.selectRow(imageIndex) def stopAndCleanUp(self): self._cleaning_up = True for editor in list(self.volumeEditors.values()): self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget()) editor.stopAndCleanUp() self.volumeEditors.clear() def imageLaneAdded(self, laneIndex): if len(self.laneSummaryTableView.selectedIndexes()) == 0: self.laneSummaryTableView.selectRow(laneIndex) # We don't have any real work to do because this gui initiated the lane addition in the first place if self.guiMode != GuiMode.Batch: if (len(self.topLevelOperator.DatasetGroup) != laneIndex + 1): import warnings warnings.warn( "DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex)) def imageLaneRemoved(self, laneIndex, finalLength): # There's nothing to do here because the GUI already # handles operator resizes via slot callbacks. pass def allowLaneSelectionChange(self): return False ########################################### ########################################### class UserCancelledError(Exception): # This exception type is raised when the user cancels the # addition of dataset files in the middle of the process somewhere. # It isn't an error -- it's used for control flow. pass def __init__(self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False): """ Constructor. :param dataSelectionOperator: The top-level operator. Must be of type :py:class:`OpMultiLaneDataSelectionGroup`. :param serializer: The applet's serializer. Must be of type :py:class:`DataSelectionSerializer` :param instructionText: A string to display in the applet drawer. :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``. Currently, there is no difference between normal and batch mode. :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow. If ``None``, there is no maximum. """ super(DataSelectionGui, self).__init__() self._cleaning_up = False self.parentApplet = parentApplet self._max_lanes = max_lanes self._default_h5_volumes = {} self.show_axis_details = show_axis_details self._viewerControls = QWidget() self.topLevelOperator = dataSelectionOperator self.guiMode = guiMode self.serializer = serializer self.threadRouter = ThreadRouter(self) self._initCentralUic() self._initAppletDrawerUic(instructionText) self._viewerControlWidgetStack = QStackedWidget(self) def handleImageRemove(multislot, index, finalLength): # Remove the viewer for this dataset datasetSlot = self.topLevelOperator.DatasetGroup[index] if datasetSlot in list(self.volumeEditors.keys()): editor = self.volumeEditors[datasetSlot] self.viewerStack.removeWidget(editor) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget()) editor.stopAndCleanUp() self.topLevelOperator.DatasetGroup.notifyRemove( bind(handleImageRemove)) opWorkflow = self.topLevelOperator.parent assert hasattr(opWorkflow.shell, 'onSaveProjectActionTriggered'), \ "This class uses the IlastikShell.onSaveProjectActionTriggered function. Did you rename it?" def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0] + '/' uic.loadUi(localDir + "/dataSelection.ui", self) self._initTableViews() self._initViewerStack() self.splitter.setSizes([150, 850]) def _initAppletDrawerUic(self, instructionText): """ Load the ui file for the applet drawer, which we own. """ localDir = os.path.split(__file__)[0] + '/' self._drawer = uic.loadUi(localDir + "/dataSelectionDrawer.ui") self._drawer.instructionLabel.setText(instructionText) def _initTableViews(self): self.fileInfoTabWidget.setTabText(0, "Summary") self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator)) self.laneSummaryTableView.dataLaneSelected.connect(self.showDataset) self.laneSummaryTableView.addFilesRequested.connect(self.addFiles) self.laneSummaryTableView.addStackRequested.connect(self.addStack) self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked) # These two helper functions enable/disable an 'add files' button for a given role # based on the the max lane index for that role and the overall permitted max_lanes def _update_button_status(viewer, role_index): if self._max_lanes: viewer.setEnabled( self._findFirstEmptyLane(role_index) < self._max_lanes) def _handle_lane_added(button, role_index, lane_slot, lane_index): def _handle_role_slot_added(role_slot, added_slot_index, *args): if added_slot_index == role_index: role_slot.notifyReady( bind(_update_button_status, button, role_index)) role_slot.notifyUnready( bind(_update_button_status, button, role_index)) lane_slot[lane_index].notifyInserted(_handle_role_slot_added) self._retained = [] # Retain menus so they don't get deleted self._detailViewerWidgets = [] for roleIndex, role in enumerate( self.topLevelOperator.DatasetRoles.value): detailViewer = DatasetDetailedInfoTableView(self) detailViewer.setModel( DatasetDetailedInfoTableModel(self, self.topLevelOperator, roleIndex)) self._detailViewerWidgets.append(detailViewer) # Button detailViewer.addFilesRequested.connect( partial(self.addFiles, roleIndex)) detailViewer.addStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.addPrecomputedVolumeRequested.connect( partial(self.addPrecomputedVolume, roleIndex)) detailViewer.addRemoteVolumeRequested.connect( partial(self.addDvidVolume, roleIndex)) # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab self.topLevelOperator.DatasetGroup.notifyInserted( bind(_handle_lane_added, detailViewer, roleIndex)) self.topLevelOperator.DatasetGroup.notifyRemoved( bind(_update_button_status, detailViewer, roleIndex)) # While we're at it, do the same for the buttons in the summary table, too self.topLevelOperator.DatasetGroup.notifyInserted( bind(_handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex)) self.topLevelOperator.DatasetGroup.notifyRemoved( bind(_update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex)) # Context menu detailViewer.replaceWithFileRequested.connect( partial(self.handleReplaceFile, roleIndex)) detailViewer.replaceWithStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.editRequested.connect( partial(self.editDatasetInfo, roleIndex)) detailViewer.resetRequested.connect( partial(self.handleClearDatasets, roleIndex)) # Drag-and-drop detailViewer.addFilesRequestedDrop.connect( partial(self.addFileNames, roleIndex=roleIndex)) # Selection handling def showFirstSelectedDataset(_roleIndex, lanes): if lanes: self.showDataset(lanes[0], _roleIndex) detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex)) self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role) self.fileInfoTabWidget.currentChanged.connect(self.handleSwitchTabs) self.fileInfoTabWidget.setCurrentIndex(0) def handleSwitchTabs(self, tabIndex): if tabIndex < len(self._detailViewerWidgets): roleIndex = tabIndex # If summary tab is moved to the front, change this line. detailViewer = self._detailViewerWidgets[roleIndex] selectedLanes = detailViewer.selectedLanes if selectedLanes: self.showDataset(selectedLanes[0], roleIndex) def _initViewerStack(self): self.volumeEditors = {} self.viewerStack.addWidget(QWidget()) def handleRemoveLaneButtonClicked(self): """ The user clicked the "Remove" button. Remove the currently selected row(s) from both the GUI and the top-level operator. """ # Figure out which lanes to remove selectedIndexes = self.laneSummaryTableView.selectedIndexes() rows = set() for modelIndex in selectedIndexes: rows.add(modelIndex.row()) # Don't remove the last row, which is just buttons. rows.discard(self.laneSummaryTableView.model().rowCount() - 1) # Remove in reverse order so row numbers remain consistent for row in reversed(sorted(rows)): # Remove lanes from the operator. # The table model will notice the changes and update the rows accordingly. finalSize = len(self.topLevelOperator.DatasetGroup) - 1 self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize) @threadRouted def showDataset(self, laneIndex, roleIndex=None): if self._cleaning_up: return if laneIndex == -1: self.viewerStack.setCurrentIndex(0) return assert threading.current_thread().name == "MainThread" if laneIndex >= len(self.topLevelOperator.DatasetGroup): return datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex] # Create if necessary if datasetSlot not in list(self.volumeEditors.keys()): class DatasetViewer(LayerViewerGui): def moveToTop(self, roleIndex): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return datasetRoles = opLaneView.DatasetRoles.value if roleIndex >= len(datasetRoles): return roleName = datasetRoles[roleIndex] try: layerIndex = [l.name for l in self.layerstack].index(roleName) except ValueError: return else: self.layerstack.selectRow(layerIndex) self.layerstack.moveSelectedToTop() def setupLayers(self): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return [] layers = [] datasetRoles = opLaneView.DatasetRoles.value for roleIndex, slot in enumerate(opLaneView.ImageGroup): if slot.ready(): roleName = datasetRoles[roleIndex] layer = self.createStandardLayerFromSlot(slot) layer.name = roleName layers.append(layer) return layers opLaneView = self.topLevelOperator.getLane(laneIndex) layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.volumeEditors[datasetSlot] = layerViewer self.viewerStack.addWidget(layerViewer) self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget()) # Show the right one viewer = self.volumeEditors[datasetSlot] displayedRole = self.fileInfoTabWidget.currentIndex() viewer.moveToTop(displayedRole) self.viewerStack.setCurrentWidget(viewer) self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget()) def handleReplaceFile(self, roleIndex, startingLane): self.addFiles(roleIndex, startingLane) def addFiles(self, roleIndex, startingLane=None): """ The user clicked the "Add File" button. Ask him to choose a file (or several) and add them to both the GUI table and the top-level operator inputs. """ # Find the directory of the most recently opened image file mostRecentImageFile = PreferencesManager().get('DataSelection', 'recent image') mostRecentImageFile = str(mostRecentImageFile) if mostRecentImageFile is not None: defaultDirectory = os.path.split(mostRecentImageFile)[0] else: defaultDirectory = os.path.expanduser('~') # Launch the "Open File" dialog fileNames = self.getImageFileNamesToOpen(self, defaultDirectory) # If the user didn't cancel if len(fileNames) > 0: PreferencesManager().set('DataSelection', 'recent image', fileNames[0]) try: self.addFileNames(fileNames, roleIndex, startingLane) except Exception as ex: log_exception(logger) QMessageBox.critical(self, "Error loading file", str(ex)) @classmethod def getImageFileNamesToOpen(cls, parent_window, defaultDirectory): """ Launch an "Open File" dialog to ask the user for one or more image files. """ extensions = OpDataSelection.SupportedExtensions filter_strs = ["*." + x for x in extensions] filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs] filt_all_str = "Image files (" + ' '.join(filter_strs) + ')' fileNames = [] if ilastik_config.getboolean("ilastik", "debug"): # use Qt dialog in debug mode (more portable?) file_dialog = QFileDialog(parent_window, "Select Images") file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) # do not display file types associated with a filter # the line for "Image files" is too long otherwise file_dialog.setNameFilters([filt_all_str] + filters) #file_dialog.setNameFilterDetailsVisible(False) # select multiple files file_dialog.setFileMode(QFileDialog.ExistingFiles) file_dialog.setDirectory(defaultDirectory) if file_dialog.exec_(): fileNames = file_dialog.selectedFiles() else: # otherwise, use native dialog of the present platform fileNames, _filter = QFileDialog.getOpenFileNames( parent_window, "Select Images", defaultDirectory, filt_all_str) return fileNames def _findFirstEmptyLane(self, roleIndex): opTop = self.topLevelOperator # Determine the number of files this role already has # Search for the last valid value. firstNewLane = 0 for laneIndex, slot in reversed( list( zip(list(range(len(opTop.DatasetGroup))), opTop.DatasetGroup))): if slot[roleIndex].ready(): firstNewLane = laneIndex + 1 break return firstNewLane def addFileNames(self, fileNames, roleIndex, startingLane=None, rois=None): """ Add the given filenames to both the GUI table and the top-level operator inputs. If startingLane is None, the filenames will be *appended* to the role's list of files. If rois is provided, it must be a list of (start,stop) tuples (one for each fileName) """ # What lanes will we touch? startingLane, endingLane = self._determineLaneRange( fileNames, roleIndex, startingLane) if startingLane is None: # Something went wrong. return # If we're only adding new lanes, NOT modifying existing lanes... adding_only = startingLane == len(self.topLevelOperator) # Create a list of DatasetInfos try: infos = self._createDatasetInfos(roleIndex, fileNames, rois) except DataSelectionGui.UserCancelledError: return # If no exception was thrown so far, set up the operator now loaded_all = self._configureOpWithInfos(roleIndex, startingLane, endingLane, infos) if loaded_all: # Now check the resulting slots. # If they should be copied to the project file, say so. self._reconfigureDatasetLocations(roleIndex, startingLane, endingLane) self._checkDataFormatWarnings(roleIndex, startingLane, endingLane) # If we succeeded in adding all images, show the first one. self.showDataset(startingLane, roleIndex) # Notify the workflow that we just added some new lanes. if adding_only: workflow = self.parentApplet.topLevelOperator.parent workflow.handleNewLanesAdded() # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() self.updateInternalPathVisiblity() def _determineLaneRange(self, fileNames, roleIndex, startingLane=None): """ Determine which lanes should be configured if the user wants to add the given fileNames to the specified role, starting at startingLane. If startingLane is None, assume the user wants to APPEND the files to the role's slots. """ if startingLane is None or startingLane == -1: startingLane = len(self.topLevelOperator.DatasetGroup) endingLane = startingLane + len(fileNames) - 1 else: assert startingLane < len(self.topLevelOperator.DatasetGroup) max_files = len(self.topLevelOperator.DatasetGroup) - \ startingLane if len(fileNames) > max_files: msg = "You selected {num_selected} files for {num_slots} "\ "slots. To add new files use the 'Add new...' option "\ "in the context menu or the button in the last row."\ .format(num_selected=len(fileNames), num_slots=max_files) QMessageBox.critical(self, "Too many files", msg) return (None, None) endingLane = min(startingLane + len(fileNames) - 1, len(self.topLevelOperator.DatasetGroup)) if self._max_lanes and endingLane >= self._max_lanes: msg = "You may not add more than {} file(s) to this workflow. Please try again.".format( self._max_lanes) QMessageBox.critical(self, "Too many files", msg) return (None, None) return (startingLane, endingLane) def _createDatasetInfos(self, roleIndex, filePaths, rois): """ Create a list of DatasetInfos for the given filePaths and rois rois may be None, in which case it is ignored. """ if rois is None: rois = [None] * len(filePaths) assert len(rois) == len(filePaths) infos = [] for filePath, roi in zip(filePaths, rois): info = self._createDatasetInfo(roleIndex, filePath, roi) infos.append(info) return infos def _createDatasetInfo(self, roleIndex, filePath, roi): """ Create a DatasetInfo object for the given filePath and roi. roi may be None, in which case it is ignored. """ cwd = self.topLevelOperator.WorkingDirectory.value datasetInfo = DatasetInfo(filePath, cwd=cwd) datasetInfo.subvolume_roi = roi # (might be None) absPath, relPath = getPathVariants(filePath, cwd) # If the file is in a totally different tree from the cwd, # then leave the path as absolute. Otherwise, override with the relative path. if relPath is not None and len(os.path.commonprefix([cwd, absPath ])) > 1: datasetInfo.filePath = relPath h5Exts = ['.ilp', '.h5', '.hdf5'] if os.path.splitext(datasetInfo.filePath)[1] in h5Exts: datasetNames = self.getPossibleInternalPaths(absPath) if len(datasetNames) == 0: raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath) elif len(datasetNames) == 1: datasetInfo.filePath += str(datasetNames[0]) else: # If exactly one of the file's datasets matches a user's previous choice, use it. if roleIndex not in self._default_h5_volumes: self._default_h5_volumes[roleIndex] = set() previous_selections = self._default_h5_volumes[roleIndex] possible_auto_selections = previous_selections.intersection( datasetNames) if len(possible_auto_selections) == 1: datasetInfo.filePath += str( list(possible_auto_selections)[0]) else: # Ask the user which dataset to choose dlg = H5VolumeSelectionDlg(datasetNames, self) if dlg.exec_() == QDialog.Accepted: selected_index = dlg.combo.currentIndex() selected_dataset = str(datasetNames[selected_index]) datasetInfo.filePath += selected_dataset self._default_h5_volumes[roleIndex].add( selected_dataset) else: raise DataSelectionGui.UserCancelledError() # Allow labels by default if this gui isn't being used for batch data. datasetInfo.allowLabels = (self.guiMode == GuiMode.Normal) return datasetInfo def _configureOpWithInfos(self, roleIndex, startingLane, endingLane, infos): """ Attempt to configure the specified role and lanes of the top-level operator with the given DatasetInfos. Returns True if all lanes were configured successfully, or False if something went wrong. """ opTop = self.topLevelOperator originalSize = len(opTop.DatasetGroup) # Resize the slot if necessary if len(opTop.DatasetGroup) < endingLane + 1: opTop.DatasetGroup.resize(endingLane + 1) # Configure each subslot for laneIndex, info in zip(list(range(startingLane, endingLane + 1)), infos): try: self.topLevelOperator.DatasetGroup[laneIndex][ roleIndex].setValue(info) except DatasetConstraintError as ex: return_val = [False] # Give the user a chance to fix the problem self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex, return_val) if return_val[0]: # Successfully repaired graph. continue else: # Not successfully repaired. Roll back the changes opTop.DatasetGroup.resize(originalSize) return False except OpDataSelection.InvalidDimensionalityError as ex: opTop.DatasetGroup.resize(originalSize) QMessageBox.critical(self, "Dataset has different dimensionality", ex.message) return False except Exception as ex: msg = "Wasn't able to load your dataset into the workflow. See error log for details." log_exception(logger, msg) QMessageBox.critical(self, "Dataset Load Error", msg) opTop.DatasetGroup.resize(originalSize) return False return True def _reconfigureDatasetLocations(self, roleIndex, startingLane, endingLane): """ Check the metadata for the given slots. If the data is stored a format that is poorly optimized for 3D access, then configure it to be copied to the project file. Finally, save the project if we changed something. """ save_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane + 1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.prefer_2d and 'z' in output_slot.meta.axistags: shape = numpy.array(output_slot.meta.shape) total_volume = numpy.prod(shape) # Only copy to the project file if the total volume is reasonably small if total_volume < 0.5e9: info_slot = opTop.DatasetGroup[lane_index][roleIndex] info = info_slot.value info.location = DatasetInfo.Location.ProjectInternal info_slot.setValue(info, check_changed=False) save_needed = True if save_needed: logger.info( "Some of your data cannot be accessed efficiently in 3D in its current format." " It will now be copied to the project file.") opWorkflow = self.topLevelOperator.parent opWorkflow.shell.onSaveProjectActionTriggered() def _checkDataFormatWarnings(self, roleIndex, startingLane, endingLane): warn_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane + 1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.inefficient_format: warn_needed = True if warn_needed: QMessageBox.warning( self, "Inefficient Data Format", "Your data cannot be accessed efficiently in its current format. " "Check the console output for details.\n" "(For HDF5 files, be sure to enable chunking on your dataset.)" ) @threadRouted def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex, return_val=[False]): if ex.unfixable: msg = ("Can't use dataset:\n\n" + filename + "\n\n" + "because it violates a constraint of the {} component.\n\n". format(ex.appletName) + ex.message + "\n\n") QMessageBox.warning(self, "Can't use dataset", msg) return_val[0] = False else: assert isinstance(ex, DatasetConstraintError) accepted = True while isinstance(ex, DatasetConstraintError) and accepted: msg = ( f"Can't use given properties for dataset:\n\n{filename}\n\nbecause it violates a constraint of " f"the {ex.appletName} component.\n\n{ex.message}\n\nIf possible, fix this problem by adjusting " f"the applet settings and or the dataset properties in the next window(s)." ) QMessageBox.warning(self, "Dataset Needs Correction", msg) for dlg in ex.fixing_dialogs: dlg() accepted, ex = self.repairDatasetInfo(info, roleIndex, laneIndex) # The success of this is 'returned' via our special out-param # (We can't return a value from this method because it is @threadRouted. return_val[0] = accepted and ex is None # successfully repaired def repairDatasetInfo(self, info, roleIndex, laneIndex): """Open the dataset properties editor and return True if the new properties are acceptable.""" defaultInfos = {} defaultInfos[laneIndex] = info editorDlg = DatasetInfoEditorWidget( self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos, show_axis_details=self.show_axis_details) dlg_state, ex = editorDlg.exec_() return (dlg_state == QDialog.Accepted), ex @classmethod def getPossibleInternalPaths(cls, absPath, min_ndim=2, max_ndim=5): datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with h5py.File(absPath, 'r') as f: # Define a closure to collect all of the dataset names in the file. def accumulateDatasetPaths(name, val): if type(val) == h5py._hl.dataset.Dataset and min_ndim <= len( val.shape) <= max_ndim: datasetNames.append('/' + name) # Visit every group/dataset in the file f.visititems(accumulateDatasetPaths) return datasetNames def addStack(self, roleIndex, laneIndex): """ The user clicked the "Import Stack Files" button. """ stackDlg = StackFileSelectionWidget(self) stackDlg.exec_() if stackDlg.result() != QDialog.Accepted: return files = stackDlg.selectedFiles sequence_axis = stackDlg.sequence_axis if len(files) == 0: return cwd = self.topLevelOperator.WorkingDirectory.value info = DatasetInfo(os.path.pathsep.join(files), cwd=cwd) originalNumLanes = len(self.topLevelOperator.DatasetGroup) if laneIndex is None or laneIndex == -1: laneIndex = len(self.topLevelOperator.DatasetGroup) if len(self.topLevelOperator.DatasetGroup) < laneIndex + 1: self.topLevelOperator.DatasetGroup.resize(laneIndex + 1) def importStack(): self.parentApplet.busy = True self.parentApplet.appletStateUpdateRequested() # Serializer will update the operator for us, which will propagate to the GUI. try: self.serializer.importStackAsLocalDataset(info, sequence_axis) try: self.topLevelOperator.DatasetGroup[laneIndex][ roleIndex].setValue(info) except DatasetConstraintError as ex: # Give the user a chance to repair the problem. filename = files[0] + "\n...\n" + files[-1] return_val = [False] self.parentApplet.busy = False # required for possible fixing dialogs from DatasetConstraintError self.parentApplet.appletStateUpdateRequested() self.handleDatasetConstraintError(info, filename, ex, roleIndex, laneIndex, return_val) if not return_val[0]: # Not successfully repaired. Roll back the changes and give up. self.topLevelOperator.DatasetGroup.resize( originalNumLanes) finally: self.parentApplet.busy = False self.parentApplet.appletStateUpdateRequested() req = Request(importStack) req.notify_finished( lambda result: self.showDataset(laneIndex, roleIndex)) req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes)) req.submit() @threadRouted def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info): msg = "Failed to load stack due to the following error:\n{}".format( exc) msg += "\nAttempted stack files were:\n" msg += "\n".join(files) log_exception(logger, msg, exc_info) QMessageBox.critical(self, "Failed to load image stack", msg) self.topLevelOperator.DatasetGroup.resize(originalNumLanes) def handleClearDatasets(self, roleIndex, selectedRows): for row in selectedRows: self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect() # Remove all operators that no longer have any connected slots laneIndexes = list(range(len(self.topLevelOperator.DatasetGroup))) for laneIndex, multislot in reversed( list(zip(laneIndexes, self.topLevelOperator.DatasetGroup))): any_ready = False for slot in multislot: any_ready |= slot.ready() if not any_ready: self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup) - 1) # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() def editDatasetInfo(self, roleIndex, laneIndexes): editorDlg = DatasetInfoEditorWidget( self, self.topLevelOperator, roleIndex, laneIndexes, show_axis_details=self.show_axis_details) editorDlg.exec_() self.parentApplet.appletStateUpdateRequested() def updateInternalPathVisiblity(self): for view in self._detailViewerWidgets: model = view.model() view.setColumnHidden(DatasetDetailedInfoColumn.InternalID, not model.hasInternalPaths()) def addPrecomputedVolume(self, roleIndex, laneIndex): # add history... history = [] browser = PrecomputedVolumeBrowser(history=history, parent=self) if browser.exec_() == PrecomputedVolumeBrowser.Rejected: return precomputed_url = browser.selected_url self.addFileNames([precomputed_url], roleIndex, laneIndex) def addDvidVolume(self, roleIndex, laneIndex): recent_hosts_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Hosts") recent_hosts = recent_hosts_pref.get() if not recent_hosts: recent_hosts = ["localhost:8000"] recent_hosts = [ h for h in recent_hosts if h ] # There used to be a bug where empty strings could be saved. Filter those out. recent_nodes_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Nodes") recent_nodes = recent_nodes_pref.get() or {} from .dvidDataSelectionBrowser import DvidDataSelectionBrowser browser = DvidDataSelectionBrowser(recent_hosts, recent_nodes, parent=self) if browser.exec_() == DvidDataSelectionBrowser.Rejected: return if None in browser.get_selection(): QMessageBox.critical("Couldn't use your selection.") return rois = None hostname, repo_uuid, volume_name, node_uuid, typename = browser.get_selection( ) dvid_url = 'http://{hostname}/api/node/{node_uuid}/{volume_name}'.format( **locals()) subvolume_roi = browser.get_subvolume_roi() # Relocate host to top of 'recent' list, and limit list to 10 items. try: i = recent_hosts.index(hostname) del recent_hosts[i] except ValueError: pass finally: recent_hosts.insert(0, hostname) recent_hosts = recent_hosts[:10] # Save pref recent_hosts_pref.set(recent_hosts) recent_nodes[hostname] = node_uuid recent_nodes_pref.set(recent_nodes) if subvolume_roi is None: self.addFileNames([dvid_url], roleIndex, laneIndex) else: start, stop = subvolume_roi self.addFileNames([dvid_url], roleIndex, laneIndex, [(start, stop)])
class ITSDAQUploadGui(QMainWindow): def __init__(self, ITkPDSession=None, parent=None): super(ITSDAQUploadGui, self).__init__(parent) self.parent = parent self.ITkPDSession = ITkPDSession self.title = 'ATLAS ITkPD ITSDAQ Test Upload' self.geometry = (0, 0, 1200, 720) self.__setDefaults() self.__initUI() def __setDefaults(self): self.sctvar_folder_path = None self.ps_folder_path = None self.config_folder_path = None self.get_confirm = True self.upload_files = True self.recursion_depth = 0 self.file_regexes = None self.upload_these_files = None self.ResultsFolder = None self.ResultsFile = None self.files_with_regex = None self.file_index = 0 self.upload_status = (True, True) self.files_to_upload = [] ############################################################################################################################################ # Step 1 ################################################################################################################################### ############################################################################################################################################ def __exploreForDirectory(self, caption, directory='.'): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog options |= QFileDialog.ShowDirsOnly directory_path = QFileDialog.getExistingDirectory(parent=self, caption=caption, directory=directory, options=options) return directory_path def __get__sctvar_folder_path(self): self.input__sctvar_folder_path.setText( self.__exploreForDirectory('Explore for /$(SCTDAQ_VAR)/') + '/') def __get__ps_folder_path(self): self.input__ps_folder_path.setText( self.__exploreForDirectory('Explore for /$(SCTDAQ_VAR)/ps/') + '/') def __get__config_folder_path(self): self.input__config_folder_path.setText( self.__exploreForDirectory('Explore for /$(SCTDAQ_VAR)/config/') + '/') def __getStep1Args(self): if self.input__sctvar_folder_path.text() == '': QMessageBox.warning(self, 'Error', '/$(SCTDAQ_VAR)/ is not specified (None)') return if os.path.exists(self.input__sctvar_folder_path.text()): self.sctvar_folder_path = self.input__sctvar_folder_path.text() else: QMessageBox.warning( self, 'Error', '/$(SCTDAQ_VAR)/ path does not exist: %s' % self.input__sctvar_folder_path.text()) return if self.input__ps_folder_path.text() != '': if os.path.exists(self.input__ps_folder_path.text()): self.ps_folder_path = self.input__ps_folder_path.text() else: QMessageBox.warning( self, 'Error', '/$(SCTDAQ_VAR)/ps/ path does not exist: %s' % self.input__ps_folder_path.text()) return if self.input__config_folder_path.text() != '': if os.path.exists(self.input__config_folder_path.text()): self.config_folder_path = self.input__config_folder_path.text() else: QMessageBox.warning( self, 'Error', '/$(SCTDAQ_VAR)/config/ path does not exist: %s' % self.input__config_folder_path.text()) return self.recursion_depth = int(self.input__recursion_depth.text()) self.get_confirm = self.input__get_confirm.isChecked() self.upload_files = self.input__upload_files.isChecked() self.file_regexes = [ (r'' + regex) for sublist in self.input__file_regexes.text().split() for regex in sublist.split(',') if regex != '' ] self.__getAndPrepResultsFolder() self.__fillTableWithFiles() if len(self.ResultsFolder) != 0: self.confirm__file_indices.setEnabled(True) self.confirm__file_indices.setFocusPolicy(Qt.StrongFocus) def __getAndPrepResultsFolder(self): if self.ResultsFolder is None: self.ResultsFolder = ResultsFolder( sctvar_folder_path=self.sctvar_folder_path, ps_folder_path=self.ps_folder_path, config_folder_path=self.config_folder_path, enable_printing=False) else: self.ResultsFolder.reset( sctvar_folder_path=self.sctvar_folder_path, ps_folder_path=self.ps_folder_path, config_folder_path=self.config_folder_path, enable_printing=False) self.ResultsFolder.getFiles(depth=self.recursion_depth) self.ResultsFolder.filterFilesByRegex(regexes=self.file_regexes) ############################################################################################################################################ # Step 2 ################################################################################################################################### ############################################################################################################################################ def __fillTableWithFiles(self): self.table__files.setRowCount(len(self.ResultsFolder)) for i, results_file in enumerate(self.ResultsFolder): filename = QTableWidgetItem(str(results_file)) self.table__files.setItem(i, 0, filename) def __getIndicesFromLineEdit(self, QLineEdit_object, table_length): response = QLineEdit_object.text() if response == '': return [] elif response == 'all': return list(range(table_length)) else: response_split = [ item for sublist in response.split() for item in sublist.split(',') if item != '' ] indices = [[int(index) - 1] if '-' not in index else list( range( int(index.split('-')[0]) - 1, int(index.split('-')[1]) - 1)) for index in response_split] indices = [ index for sublist in indices for index in sublist if index < table_length and index >= 0 ] return sorted(list(set(indices))) def __getIndicesFromTable(self, QTableWidget_object): indices = QTableWidget_object.selectionModel().selectedRows() indices = [index.row() for index in sorted(indices)] return indices def __getStep2Args(self): try: indices = set( self.__getIndicesFromLineEdit(self.input__file_indices, len(self.ResultsFolder)) + self.__getIndicesFromTable(self.table__files)) except ValueError as e: QMessageBox.warning( self, 'Error', 'ValueError in QLineEdit index selection: %s -- please enter a list of positive, space/comma-separated integers or \"all\".' % e) return if len(indices) == 0: # https://stackoverflow.com/questions./1414781/prompt-on-exit-in-pyqt-application reply = QMessageBox.question( self, 'Message', 'No files selected -- do you want to quit?', QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: sys.exit(0) elif reply == QMessageBox.No: return self.files_with_regex = self.ResultsFolder.files self.ResultsFolder.filterFiles(indices) self.form__bottom.setCurrentWidget(self.form__step3) self.file_index = 0 self.__getStep3Args() ############################################################################################################################################ # Step 3 ################################################################################################################################### ############################################################################################################################################ def __fillTableWithTestsAndDisplayCurrentFile(self): self.ResultsFile = self.ResultsFolder[self.file_index] self.ResultsFile.enable_printing = False self.lineEdit__current_file.setText(str(self.ResultsFile)) self.ResultsFile.getTests() self.table__tests.setRowCount(len(self.ResultsFile)) for i, test in enumerate(self.ResultsFile.tests): self.table__tests.setItem( i, 0, QTableWidgetItem(test['JSON']['properties']['SERIAL_NUMBER'])) self.table__tests.setItem( i, 1, QTableWidgetItem(test['JSON']['testType'])) self.table__tests.setItem( i, 2, QTableWidgetItem(test['JSON']['date'] + '-' + test['JSON']['properties']['TIME'])) self.table__tests.setItem( i, 3, QTableWidgetItem(test['JSON']['runNumber'])) font = QFont() font.setBold(True) passed = QTableWidgetItem( 'YES' if test['JSON']['passed'] else 'NO') passed.setFont(font) if test['JSON']['passed']: passed.setBackground(QColor('green')) else: passed.setBackground(QColor('red')) self.table__tests.setItem(i, 4, passed) problems = QTableWidgetItem( 'YES' if test['JSON']['problems'] else 'NO') problems.setFont(font) if not test['JSON']['problems']: problems.setBackground(QColor('green')) else: problems.setBackground(QColor('red')) self.table__tests.setItem(i, 5, problems) def __getConfirm(self, test_index): self.form__bottom.setCurrentWidget(self.form__step4) if self.upload_files: files_to_upload = self.ResultsFile[i]['files_to_upload'] if files_to_upload == {}: pass else: for i, filetype in enumerate(files_to_upload.keys()): self.table__files_upload.setItem(i, 0, QTableWidgetItem(filetype)) if files_to_upload[filetype] is None: self.table__files_upload.setItem( i, 1, QTableWidgetItem('<NOT FOUND>')) else: self.table__files_upload.setItem( i, 1, QTableWidgetItem(files_to_upload[filetype])) def __getStep3Args(self): self.recursion_depth = int(self.input__recursion_depth_2.text()) self.get_confirm = self.input__get_confirm_2.isChecked() self.upload_files = self.input__upload_files_2.isChecked() self.__fillTableWithTestsAndDisplayCurrentFile() try: indices = set( self.__getIndicesFromLineEdit(self.input__test_indices, len(self.ResultsFile)) + self.__getIndicesFromTable(self.table__tests)) except ValueError as e: QMessageBox.warning( self, 'Error', 'ValueError in QLineEdit index selection: %s -- please enter a list of positive, space/comma-separated integers or \"all\".' % e) return for i in indices: try: print self.upload_files self.ResultsFile.finalizeTest(i, ITkPDSession=self.ITkPDSession, upload_files=self.upload_files, ResultsFolder=self.ResultsFolder, depth=self.recursion_depth, full_test=False) if self.get_confirm: self.__getConfirm(i) if self.upload_status == (False, False): QMessageBox.information( self, 'Cancelled', 'Cancelled upload of run number %s for component %s (%s).' % (self.ResultsFile[i]['JSON']['runNumber'], self.ResultsFile[i]['JSON']['properties'] ['SERIAL_NUMBER'], self.ResultsFile[i]['JSON']['component'])) continue if self.upload_status[0]: self.ResultsFile.uploadJSON( i, ITkPDSession=self.ITkPDSession) if self.upload_status[1]: filetypes = [ filetype for i, filetype in enumerate( self.ResultsFile[i]['files_to_upload'].keys()) if i in self.files_to_upload ] self.ResultsFile.uploadFiles( i, ITkPDSession=self.ITkPDSession, upload_files=self.upload_files, upload_these_files=True, filetypes=filetypes) QMessageBox.information( self, 'Success', 'Run number %s for component %s (%s) uploaded successfully!' % (self.ResultsFile[i]['JSON']['runNumber'], self. ResultsFile[i]['JSON']['properties']['SERIAL_NUMBER'], self.ResultsFile[i]['JSON']['component'])) self.upload_status = (True, True) self.files_to_upload = [] except ComponentNotFound: QMessageBox.warning( self, 'Error', 'Run number %s for component with serial number \"%s\" could not be found in the ITkPD -- skipping!' % (self.ResultsFile[i]['JSON']['runNumber'], self. ResultsFile[i]['JSON']['properties']['SERIAL_NUMBER'])) continue except RequestException as e: QMessageBox.warning( self, 'Error', 'Run number %s for component with serial number \"%s\" could not be finalized/uploaded due to requests exception: %s -- skipping!' % (self.ResultsFile[i]['JSON']['runNumber'], self. ResultsFile[i]['JSON']['properties']['SERIAL_NUMBER'], e)) continue def __set__confirm_upload(self): self.upload_status = (True, True) self.files_to_upload = self.__getIndicesFromTable( self.table__files_upload) self.dialog__confirm.accept() def __set__only_json(self): self.upload_status = (True, False) self.dialog__confirm.accept() def __set__only_files(self): self.upload_status = (False, True) self.files_to_upload = self.__getIndicesFromTable( self.table__files_upload) self.dialog__confirm.accept() def __set__cancel_upload(self): self.upload_status = (False, False) self.dialog__confirm.accept() def __getConfirm(self, test_index): qtRectangle = self.dialog__confirm.frameGeometry() centerPoint = self.frameGeometry().center() qtRectangle.moveCenter(centerPoint) self.dialog__confirm.move(qtRectangle.topLeft()) self.dialog__confirm.setWindowTitle( 'Confirm Upload: Run Number %s' % self.ResultsFile[test_index]['JSON']['runNumber']) self.body__json.setText( pp.pformat(self.ResultsFile[test_index]['JSON'])) if self.upload_files: self.button__only_json.setEnabled(True) self.button__only_json.setFocusPolicy(Qt.StrongFocus) self.button__only_files.setEnabled(True) self.button__only_files.setFocusPolicy(Qt.StrongFocus) files_to_upload = self.ResultsFile[test_index]['files_to_upload'] if files_to_upload == {}: pass else: self.table__files_upload.setRowCount( len(files_to_upload.keys())) for i, filetype in enumerate(files_to_upload.keys()): self.table__files_upload.setItem( i, 0, QTableWidgetItem(filetype)) if files_to_upload[filetype] is None: self.table__files_upload.setItem( i, 1, QTableWidgetItem('<NOT FOUND>')) else: self.table__files_upload.setItem( i, 1, QTableWidgetItem(files_to_upload[filetype])) else: self.button__only_json.setEnabled(False) self.button__only_json.setFocusPolicy(Qt.NoFocus) self.button__only_files.setEnabled(False) self.button__only_files.setFocusPolicy(Qt.NoFocus) self.dialog__confirm.exec_() def __next(self): self.file_index += 1 if self.file_index == len(self.ResultsFolder): reply = QMessageBox.question( self, 'Finished', 'All finished -- do you want to quit?', QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: sys.exit(0) elif reply == QMessageBox.No: self.__reset() else: self.table__tests.clearSelection() self.input__test_indices.setText('') self.__fillTableWithTestsAndDisplayCurrentFile() def __back(self): self.file_index -= 1 if self.file_index < 0: self.ResultsFolder.files = self.files_with_regex self.form__bottom.setCurrentWidget(self.form__step12) else: self.input__test_indices.setText('') self.__fillTableWithTestsAndDisplayCurrentFile() def __reset(self): self.__setDefaults() self.input__sctvar_folder_path.setText('') self.input__ps_folder_path.setText('') self.input__config_folder_path.setText('') self.input__recursion_depth.setValue(self.recursion_depth) self.input__get_confirm.setChecked(self.get_confirm) self.input__upload_files.setChecked(self.upload_files) self.input__file_regexes.setText('') self.table__files.setRowCount(0) self.input__file_indices.setText('') self.confirm__file_indices.setEnabled(False) self.confirm__file_indices.setFocusPolicy(Qt.NoFocus) self.form__bottom.setCurrentWidget(self.form__step12) ############################################################################################################################################ # -----------------------------------------------------------------------------------------------------------------------------------------# # RUN THE MAIN FUNCTION OF THE UI CLASS ---------------------------------------------------------------------------------------------------# # -----------------------------------------------------------------------------------------------------------------------------------------# ############################################################################################################################################ # Super messy, could probably be organized better # But since I am learning how to build this as I go along, this is probably okay for a first attempt def __initUI(self): self.setWindowTitle(self.title) self.setGeometry(*self.geometry) self.setFixedSize(self.size()) self.setWindowIcon(QIcon('../media/ATLAS-Logo-Square-B&W-RGB.png')) #-------------------------------------------------------------------------------------------------------------------# #-------------------------------------------------------------------------------------------------------------------# #-------------------------------------------------------------------------------------------------------------------# # NOTE TO MATTHEW: USE ME TO SET GLOBAL STYLESHEETS IN THE FUTURE !!! # # SEE: https://stackoverflow.com/questions/49140843/setting-background-of-qpushbutton-in-qmessagebox # self.setStyleSheet( 'QMessageBox QPushButton { font-size: 12pt; color: black; }') self.setStyleSheet('QMessageBox { font-size: 12pt; color: black; }') #-------------------------------------------------------------------------------------------------------------------# #-------------------------------------------------------------------------------------------------------------------# #-------------------------------------------------------------------------------------------------------------------# qtRectangle = self.frameGeometry() centerPoint = QDesktopWidget().availableGeometry().center() qtRectangle.moveCenter(centerPoint) self.move(qtRectangle.topLeft()) shortcut__close = QShortcut(QKeySequence('Ctrl+Q'), self) shortcut__close.activated.connect(lambda: sys.exit(0)) form__main = QWidget(self) grid__main = QGridLayout(form__main) grid__main.setAlignment(Qt.AlignTop) self.setCentralWidget(form__main) ############################################################################################################################################ # Top ###################################################################################################################################### ############################################################################################################################################ title = QLabel( 'ATLAS ITk Production Database -- ITSDAQ Test Upload GUI', self) title.setStyleSheet( 'font-size: 18pt; font: bold; color: black; qproperty-alignment: AlignLeft;' ) quit = QPushButton('Quit', self) quit.clicked.connect(lambda: sys.exit(0)) quit.setAutoDefault(True) quit.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Quit the GUI')) quit.setStyleSheet('font-size: 12pt; font: bold; color: black;') form__top = QWidget(self) grid__top = QGridLayout(form__top) grid__top.addWidget(title, 0, 0, 1, 7) grid__top.addWidget(quit, 0, 7, 1, 1) ############################################################################################################################################ # Step 1 ################################################################################################################################### ############################################################################################################################################ label__step1 = QLabel( 'Step #1: Inform the program of the location of ITSDAQ results files and then click \"Confirm\".' ) label__step1.setStyleSheet( 'font-size: 14pt; font: bold; color: black;') label__sctvar_folder_path = QLabel( 'Enter the /$(SCTDAQ_VAR)/(results/) path:') label__sctvar_folder_path.setStyleSheet( 'font-size: 12pt; color: black;') self.input__sctvar_folder_path = QLineEdit(self) self.input__sctvar_folder_path.setText(os.getenv('SCTDAQ_VAR', '')) self.input__sctvar_folder_path.setStyleSheet( 'font-size: 12pt; color: black;') explore__sctvar_folder_path = QPushButton('Explore', self) explore__sctvar_folder_path.clicked.connect( self.__get__sctvar_folder_path) explore__sctvar_folder_path.setAutoDefault(True) explore__sctvar_folder_path.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Explore for /$(SCTDAQ_VAR)/')) explore__sctvar_folder_path.setStyleSheet( 'font-size: 12pt; color: black;') label__ps_folder_path = QLabel( '(Optional) Enter the /$(SCTDAQ_VAR)/ps/ path:') label__ps_folder_path.setStyleSheet('font-size: 12pt; color: black;') self.input__ps_folder_path = QLineEdit(self) self.input__ps_folder_path.setStyleSheet( 'font-size: 12pt; color: black;') explore__ps_folder_path = QPushButton('Explore', self) explore__ps_folder_path.clicked.connect(self.__get__ps_folder_path) explore__ps_folder_path.setAutoDefault(True) explore__ps_folder_path.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Explore for /$(SCTDAQ_VAR)/ps/')) explore__ps_folder_path.setStyleSheet('font-size: 12pt; color: black;') label__config_folder_path = QLabel( '(Optional) Enter the /$(SCTDAQ_VAR)/config/ path:') label__config_folder_path.setStyleSheet( 'font-size: 12pt; color: black;') self.input__config_folder_path = QLineEdit(self) self.input__config_folder_path.setStyleSheet( 'font-size: 12pt; color: black;') explore__config_folder_path = QPushButton('Explore', self) explore__config_folder_path.clicked.connect( self.__get__config_folder_path) explore__config_folder_path.setAutoDefault(True) explore__config_folder_path.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Explore for /$(SCTDAQ_VAR)/config/')) explore__config_folder_path.setStyleSheet( 'font-size: 12pt; color: black;') label__recursion_depth = QLabel('Specify the search depth:') label__recursion_depth.setStyleSheet('font-size: 12pt; color: black;') self.input__recursion_depth = QSpinBox(self) self.input__recursion_depth.setValue(self.recursion_depth) self.input__recursion_depth.setStyleSheet( 'font-size: 12pt; color: black;') self.input__get_confirm = QCheckBox('Confirm each upload:', self) self.input__get_confirm.setChecked(self.get_confirm) self.input__get_confirm.setLayoutDirection(Qt.RightToLeft) self.input__get_confirm.setStyleSheet( 'font-size: 12pt; color: black; margin-left: 50%; margin-right: 50%;' ) self.input__upload_files = QCheckBox('Upload local files:', self) self.input__upload_files.setChecked(self.upload_files) self.input__upload_files.setLayoutDirection(Qt.RightToLeft) self.input__upload_files.setStyleSheet( 'font-size: 12pt; color: black; margin-left: 50%; margin-right: 50%;' ) label__file_regexes = QLabel('(Optional) Regex for filtering files:') label__file_regexes.setStyleSheet('font-size: 12pt; color: black;') self.input__file_regexes = QLineEdit(self) self.input__file_regexes.setStyleSheet( 'font-size: 12pt; color: black;') self.confirm__args = QPushButton('Confirm', self) self.confirm__args.clicked.connect(self.__getStep1Args) self.confirm__args.setAutoDefault(True) self.confirm__args.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Confirm search/upload options')) self.confirm__args.setStyleSheet( 'font-size: 12pt; font: bold; color: black;') form__step1 = QWidget(self) grid__step1 = QGridLayout(form__step1) grid__step1.addWidget(label__step1, 0, 0, 1, -1) grid__step1.addWidget(label__sctvar_folder_path, 1, 0, 1, 2) grid__step1.addWidget(self.input__sctvar_folder_path, 1, 2, 1, 5) grid__step1.addWidget(explore__sctvar_folder_path, 1, 7, 1, 1) grid__step1.addWidget(label__ps_folder_path, 2, 0, 1, 2) grid__step1.addWidget(self.input__ps_folder_path, 2, 2, 1, 5) grid__step1.addWidget(explore__ps_folder_path, 2, 7, 1, 1) grid__step1.addWidget(label__config_folder_path, 3, 0, 1, 2) grid__step1.addWidget(self.input__config_folder_path, 3, 2, 1, 5) grid__step1.addWidget(explore__config_folder_path, 3, 7, 1, 1) grid__step1.addWidget(label__recursion_depth, 4, 1, 1, 1) grid__step1.addWidget(self.input__recursion_depth, 4, 2, 1, 1) grid__step1.addWidget(self.input__get_confirm, 4, 3, 1, 2) grid__step1.addWidget(self.input__upload_files, 4, 5, 1, 2) grid__step1.addWidget(label__file_regexes, 5, 1, 1, 1) grid__step1.addWidget(self.input__file_regexes, 5, 2, 1, 5) grid__step1.addWidget(self.confirm__args, 5, 7, 1, 1) ############################################################################################################################################ # Step 2 ################################################################################################################################### ############################################################################################################################################ label__step2 = QLabel( 'Step #2: Select the results file(s) to be opened and scanned for tests and then click \"Confirm Files\".' ) label__step2.setStyleSheet( 'font-size: 14pt; font: bold; color: black;') self.table__files = QTableWidget(self) self.table__files.setRowCount(0) self.table__files.setColumnCount(1) self.table__files.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.table__files.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.table__files.setHorizontalHeaderLabels(['Filename']) self.table__files.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table__files.setSelectionBehavior(QAbstractItemView.SelectRows) self.table__files.setStyleSheet('font-size: 12pt; color: black;') header = self.table__files.horizontalHeader() header.setStyleSheet('font: bold;') header.setDefaultAlignment(Qt.AlignHCenter) header.setSectionResizeMode(QHeaderView.Stretch) # header.setSectionResizeMode(QHeaderView.ResizeToContents) # header.setStretchLastSection(True) header_vertical__files = self.table__files.verticalHeader() header_vertical__files.setSectionResizeMode(QHeaderView.Fixed) # header_vertical__tests.setDefaultSectionSize(24) # label__file_indices = QLabel('Manually select rows from the above table or enter a list of space/comma-separated indices below (hyphens denote ranges), \"all\" for all, or \"none\" for none:') label__file_indices = QLabel( 'Manually select rows from the above table or enter a list of space/comma-separated indices below (hyphens denote ranges) or \"all\" for all:' ) label__file_indices.setStyleSheet('font-size: 12pt; color: black;') self.input__file_indices = QLineEdit(self) self.input__file_indices.setStyleSheet( 'font-size: 12pt; color: black;') self.confirm__file_indices = QPushButton('Confirm Files', self) self.confirm__file_indices.clicked.connect(self.__getStep2Args) self.confirm__file_indices.setAutoDefault(True) self.confirm__file_indices.setEnabled(False) self.confirm__file_indices.setFocusPolicy(Qt.NoFocus) self.confirm__file_indices.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Confirm selected files')) self.confirm__file_indices.setStyleSheet( 'font-size: 12pt; font:bold; color: black;') form__step2 = QWidget(self) grid__step2 = QGridLayout(form__step2) grid__step2.addWidget(label__step2, 0, 0, 1, -1) grid__step2.addWidget(self.table__files, 1, 0, 1, -1) grid__step2.addWidget(label__file_indices, 2, 0, 1, -1) grid__step2.addWidget(self.input__file_indices, 3, 0, 1, 7) grid__step2.addWidget(self.confirm__file_indices, 3, 7, 1, 1) ############################################################################################################################################ # Step 3 ################################################################################################################################### ############################################################################################################################################ label__step3 = QLabel( 'Step #3: Select the test(s) to be uploaded and then click \"Confirm\".' ) label__step3.setStyleSheet( 'font-size: 14pt; font: bold; color: black;') label__reset_args = QLabel('(Optional) Reset search/upload arguments:') label__reset_args.setStyleSheet('font-size: 12pt; color: black;') # 2 := duplication of the above ones (else, the widgets don't reset to their original position when we use Reset) # A little hacky... label__recursion_depth_2 = QLabel('Specify the search depth:') label__recursion_depth_2.setStyleSheet( 'font-size: 12pt; color: black;') self.input__recursion_depth_2 = QSpinBox(self) self.input__recursion_depth_2.setValue(self.recursion_depth) self.input__recursion_depth_2.setStyleSheet( 'font-size: 12pt; color: black;') self.input__get_confirm_2 = QCheckBox('Confirm each upload:', self) self.input__get_confirm_2.setChecked(self.get_confirm) self.input__get_confirm_2.setLayoutDirection(Qt.RightToLeft) self.input__get_confirm_2.setStyleSheet( 'font-size: 12pt; color: black; margin-left: 50%; margin-right: 50%;' ) self.input__upload_files_2 = QCheckBox('Upload local files:', self) self.input__upload_files_2.setChecked(self.upload_files) self.input__upload_files_2.setLayoutDirection(Qt.RightToLeft) self.input__upload_files_2.setStyleSheet( 'font-size: 12pt; color: black; margin-left: 50%; margin-right: 50%;' ) label__current_file = QLabel('Currently looking in:') label__current_file.setStyleSheet('font-size: 12pt; color: black;') self.lineEdit__current_file = QLineEdit(self) self.lineEdit__current_file.setReadOnly(True) self.lineEdit__current_file.setStyleSheet( 'font-size: 12pt; color: black;') self.table__tests = QTableWidget(self) self.table__tests.setRowCount(0) self.table__tests.setColumnCount(6) self.table__tests.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.table__tests.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.table__tests.setHorizontalHeaderLabels([ 'Serial Number', 'Test type', 'Date-Time', 'Run Number', 'Passed', 'Problems' ]) self.table__tests.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table__tests.setSelectionBehavior(QAbstractItemView.SelectRows) self.table__tests.setStyleSheet('font-size: 12pt; color: black;') header__tests = self.table__tests.horizontalHeader() header__tests.setStyleSheet('font: bold;') header__tests.setDefaultAlignment(Qt.AlignHCenter) header__tests.setSectionResizeMode(QHeaderView.Stretch) header_vertical__tests = self.table__tests.verticalHeader() header_vertical__tests.setSectionResizeMode(QHeaderView.Fixed) self.input__test_indices = QLineEdit(self) self.input__test_indices.setStyleSheet( 'font-size: 12pt; color: black;') label__test_indices = QLabel( 'Manually select rows from the above table or enter a list of space/comma-separated indices below (hyphens denote ranges) or \"all\" for all:' ) label__test_indices.setStyleSheet('font-size: 12pt; color: black;') button__confirm_tests = QPushButton('Confirm Tests', self) button__confirm_tests.clicked.connect(self.__getStep3Args) button__confirm_tests.setAutoDefault(True) button__confirm_tests.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Confirm selected tests')) button__confirm_tests.setStyleSheet( 'font-size: 12pt; font: bold; color: black;') # DOES TAB GO TO NEXT BEFORE BACK B/C IT IS DEFINED FIRST? button__next = QPushButton('Next', self) button__next.clicked.connect(self.__next) button__next.setAutoDefault(True) button__next.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Move to the next file')) button__next.setStyleSheet('font-size: 12pt; color: black;') button__back = QPushButton('Previous', self) button__back.clicked.connect(self.__back) button__back.setAutoDefault(True) button__back.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Move to the previous file')) button__back.setStyleSheet('font-size: 12pt; color: black;') button__reset = QPushButton('Reset', self) button__reset.clicked.connect(self.__reset) button__reset.setAutoDefault(True) button__reset.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Go back to Steps 1+2')) button__reset.setStyleSheet('font-size: 12pt; color: black;') self.form__step3 = QWidget(self) grid__step3 = QGridLayout(self.form__step3) grid__step3.addWidget(label__step3, 0, 0, 1, -1) grid__step3.addWidget(label__reset_args, 1, 0, 1, -1) grid__step3.addWidget(label__recursion_depth_2, 2, 1, 1, 1) grid__step3.addWidget(self.input__recursion_depth_2, 2, 2, 1, 1) grid__step3.addWidget(self.input__get_confirm_2, 2, 3, 1, 2) grid__step3.addWidget(self.input__upload_files_2, 2, 5, 1, 2) grid__step3.addWidget(label__current_file, 3, 0, 1, 1) grid__step3.addWidget(self.lineEdit__current_file, 3, 1, 1, -1) grid__step3.addWidget(self.table__tests, 4, 0, 1, -1) grid__step3.addWidget(label__test_indices, 5, 0, 1, -1) grid__step3.addWidget(self.input__test_indices, 6, 0, 1, 6) grid__step3.addWidget(button__confirm_tests, 6, 6, 1, 1) grid__step3.addWidget(button__back, 7, 4, 1, 1) grid__step3.addWidget(button__next, 7, 5, 1, 1) grid__step3.addWidget(button__reset, 7, 6, 1, 1) ############################################################################################################################################ # Upload File Dialog ####################################################################################################################### ############################################################################################################################################ self.dialog__confirm = QDialog(self) self.dialog__confirm.setGeometry(0, 0, 1000, 500) self.dialog__confirm.setFixedSize(self.dialog__confirm.size()) shortcut__close2 = QShortcut(QKeySequence('Ctrl+Q'), self.dialog__confirm) shortcut__close2.activated.connect(lambda: sys.exit(0)) label__step4 = QLabel( '(Step #4): Confirm the JSON and files to be uploaded.') label__step4.setStyleSheet( 'font-size: 14pt; font: bold; color: black;') label__confirm_json = QLabel('The following JSON will be uploaded:') label__confirm_json.setStyleSheet('font-size: 12pt; color: black;') self.body__json = QTextEdit(self) self.body__json.setReadOnly(True) self.body__json.setStyleSheet('font-size: 12pt; color: black;') label__confirm_files = QLabel('The following files will be uploaded:') label__confirm_files.setStyleSheet('font-size: 12pt; color: black;') self.table__files_upload = QTableWidget(self) self.table__files_upload.setRowCount(0) self.table__files_upload.setColumnCount(2) self.table__files_upload.setVerticalScrollBarPolicy( Qt.ScrollBarAsNeeded) self.table__files_upload.setHorizontalScrollBarPolicy( Qt.ScrollBarAsNeeded) self.table__files_upload.setHorizontalHeaderLabels( ['Filetype', 'Filename']) self.table__files_upload.setEditTriggers( QAbstractItemView.NoEditTriggers) self.table__files_upload.setSelectionBehavior( QAbstractItemView.SelectRows) header__files_upload = self.table__files_upload.horizontalHeader() header__files_upload.setStyleSheet('font: bold;') header__files_upload.setDefaultAlignment(Qt.AlignHCenter) header__files_upload.setSectionResizeMode(QHeaderView.ResizeToContents) header__files_upload.setStretchLastSection(True) header_vertical__files_upload = self.table__files_upload.verticalHeader( ) header_vertical__files_upload.setSectionResizeMode(QHeaderView.Fixed) button__upload = QPushButton('Upload All', self) button__upload.clicked.connect(self.__set__confirm_upload) button__upload.setAutoDefault(True) button__upload.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Upload JSON + files')) button__upload.setStyleSheet( 'font-size: 12pt; font: bold; color: black;') self.button__only_json = QPushButton('Upload Only JSON', self) self.button__only_json.clicked.connect(self.__set__only_json) self.button__only_json.setAutoDefault(True) self.button__only_json.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Upload only JSON')) self.button__only_json.setStyleSheet('font-size: 12pt; color: black;') self.button__only_files = QPushButton('Upload Only Files', self) self.button__only_files.clicked.connect(self.__set__only_files) self.button__only_files.setAutoDefault(True) self.button__only_files.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Upload only files')) self.button__only_files.setStyleSheet('font-size: 12pt; color: black;') button__cancel = QPushButton('Cancel Upload', self) button__cancel.clicked.connect(self.__set__cancel_upload) button__cancel.setAutoDefault(True) button__cancel.setToolTip( '<span style=\"background-color:black;color:white;font:normal;font-size:12pt\">{0}</span>' .format('Cancel this upload')) button__cancel.setStyleSheet( 'font-size: 12pt; font: bold; color: black;') grid__confirm = QGridLayout(self.dialog__confirm) grid__confirm.addWidget(label__step4, 0, 0, 1, 6) grid__confirm.addWidget(label__confirm_json, 1, 0, 1, -1) grid__confirm.addWidget(self.body__json, 2, 0, 1, -1) grid__confirm.addWidget(label__confirm_files, 3, 0, 1, -1) grid__confirm.addWidget(self.table__files_upload, 4, 0, 1, -1) grid__confirm.addWidget(button__upload, 5, 1, 1, 1) grid__confirm.addWidget(self.button__only_json, 5, 2, 1, 1) grid__confirm.addWidget(self.button__only_files, 5, 3, 1, 1) grid__confirm.addWidget(button__cancel, 5, 4, 1, 1) ############################################################################################################################################ # Configure widget arrangement ############################################################################################################# ############################################################################################################################################ grid__main.addWidget(form__top, 0, 0) self.form__bottom = QStackedWidget(self) grid__main.addWidget(self.form__bottom, 1, 0) self.form__step12 = QWidget(self) grid__step12 = QGridLayout(self.form__step12) grid__step12.addWidget(form__step1, 0, 0) grid__step12.addWidget(form__step2, 1, 0) self.form__bottom.addWidget(self.form__step12) self.form__bottom.addWidget(self.form__step3) self.form__bottom.setCurrentWidget(self.form__step12) ############################################################################################################################################ #------------------------------------------------------------------------------------------------------------------------------------------# # LOGIN INTO THE GUI ----------------------------------------------------------------------------------------------------------------------# #------------------------------------------------------------------------------------------------------------------------------------------# ############################################################################################################################################ # Gui is opened through a self.parent.open() call in ITkPDLoginGui, following authentication try: ITkPDLoginGui(self.ITkPDSession, self) except ExpiredToken as e: QMessageBox.warning( self, 'Expired Token', 'ITkPD token has expired: expired at %s, current time is %s -- exitting.' % (e.expired_at, e.current_time)) sys, exit(1) except RequestException as e: QMessageBox.warning( self, 'Error', 'Unhandled requests exception raised: %s -- exitting.' % e, QMessageBox.Ok) sys.exit(1)
class object_classifier_app(QMainWindow): def __init__(self): super().__init__() # init the main window self.title = 'Epif Object Classifier' self.setWindowTitle(self.title) self.left = 10 self.top = 10 self.width = 1500 self.height = 750 self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.width, self.height) # Create the map canvas and set the canvas to be the main widget self.canvasframe = QFrame() self.canvasframe.setFrameShape(QFrame.StyledPanel) self.canvasframe.setFrameShadow(QFrame.Raised) self.setCentralWidget(self.canvasframe) self.canvas = map_canvas() self.canvas.show() self.framelayout = QVBoxLayout(self.canvasframe) self.framelayout.addWidget(self.canvas) # create map canvas actions and set icons actionZoomIn = QAction("Zoom in", self) actionZoomOut = QAction("Zoom out", self) actionPan = QAction("Pan", self) actionZoomIn.setCheckable(True) actionZoomOut.setCheckable(True) actionPan.setCheckable(True) actionPanPixmap = QIcon(QPixmap(':/icons/pan.png')) actionZoomInPixmap = QIcon(QPixmap(':/icons/Zoom-In-icon.png')) actionZoomOutPixmap = QPixmap(':/icons/Zoom-Out-icon.png') actionsave = QIcon(QPixmap(':/icons/save.png')) actionsaveas = QIcon(QPixmap(':/icons/saveas.png')) actionnew = QIcon(QPixmap(':/icons/newproject.png')) actionloadproject = QIcon(QPixmap(':/icons/openproject.png')) actionpolygonize = QIcon(QPixmap(':/icons/polygon.png')) actionrasterize = QIcon(QPixmap(':/icons/raster.png')) actionconfusion = QIcon(QPixmap(':/icons/matrix.png')) actionZoomIn.setIcon(QIcon(actionZoomInPixmap)) actionZoomOut.setIcon(QIcon(actionZoomOutPixmap)) actionPan.setIcon(QIcon(actionPanPixmap)) actionZoomIn.triggered.connect(self.zoomIn) actionZoomOut.triggered.connect(self.zoomOut) actionPan.triggered.connect(self.pan) # create map canvas tool bar self.toolbar = self.addToolBar("Canvas actions") self.toolbar.addAction(actionZoomIn) self.toolbar.addAction(actionZoomOut) self.toolbar.addAction(actionPan) # create the map tools self.toolPan = QgsMapToolPan(self.canvas) self.toolPan.setAction(actionPan) self.toolZoomIn = QgsMapToolZoom(self.canvas, False) # false = in self.toolZoomIn.setAction(actionZoomIn) self.toolZoomOut = QgsMapToolZoom(self.canvas, True) # true = out self.toolZoomOut.setAction(actionZoomOut) #init the file tree widget and set as a dock widget self.treewidget = filetree() self.tree = QDockWidget("Select Data", self) self.tree.setWidget(self.treewidget) self.tree.setFloating(False) self.addDockWidget(Qt.LeftDockWidgetArea, self.tree) # init a menue bar and its actions self.menuebar = QMenuBar() self.setMenuBar(self.menuebar) filemenue = self.menuebar.addMenu('Project') classificationmenue = self.menuebar.addMenu('Classification') toolsmenue = self.menuebar.addMenu('Tools') self.supervised = QAction('supervised') self.training = QAction("Training set", self) self.unsupervised = QAction('unsupernised') self.poligonize = QAction(actionpolygonize, 'Polygonize') self.rasterize = QAction(actionrasterize, 'Rasterize') self.confusiomatrix = QAction(actionconfusion, 'Confusionmatrix') self.newprojectaction = QAction(actionnew, 'New project') self.loadprojectaction = QAction(actionloadproject, 'Load project') self.saveprojectasaction = QAction(actionsaveas, 'Save project as') self.saveaction = QAction(actionsave, 'Save') classificationmenue.addAction(self.supervised) classificationmenue.addAction(self.unsupervised) classificationmenue.addAction(self.training) toolsmenue.addAction(self.poligonize) toolsmenue.addAction(self.rasterize) toolsmenue.addAction(self.confusiomatrix) filemenue.addAction(self.loadprojectaction) filemenue.addAction(self.saveprojectasaction) filemenue.addAction(self.newprojectaction) filemenue.addAction(self.saveaction) # create a layerpanel self.layerpanel = LayersPanel(self.canvas) self.layerpanelDock = QDockWidget("Layers", self) self.layerpanelDock.setObjectName("layers") self.layerpanelDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.layerpanelDock.setWidget(self.layerpanel) self.layerpanelDock.setContentsMargins(9, 9, 9, 9) self.addDockWidget(Qt.LeftDockWidgetArea, self.layerpanelDock) #set the supervised classification window widget self.supervised_classification = supervised_classification_ui() self.StackedWidget = QStackedWidget() self.StackedWidget.addWidget(self.supervised_classification.step1) self.StackedWidget.addWidget(self.supervised_classification.step2) self.StackedWidget.addWidget(self.supervised_classification.step3) self.StackedWidget.setCurrentWidget( self.supervised_classification.step1) self.StackedWidget_dock = QDockWidget('classification', self) self.StackedWidget_dock.setWidget(self.StackedWidget) self.addDockWidget(Qt.RightDockWidgetArea, self.StackedWidget_dock) # set the unsupervised classification window widget # set polygonize widget polygonizewidget = Polygonize_ui() polygonizewidget.legendwidget = self.layerpanel self.StackedWidget.addWidget(polygonizewidget) # set rasterize widget rastrizewidget = Rasterize_ui() rastrizewidget.legendwidget = self.layerpanel rastrizewidget.legendwidget = self.layerpanel self.StackedWidget.addWidget(rastrizewidget) # set confusion matrix widget confusionmatrixwidget = Confusion_matrix_ui() self.StackedWidget.addWidget(confusionmatrixwidget) self.StackedWidget.addWidget(confusionmatrixwidget.resultsgroup) self.threadpool = QThreadPool() self.massage = QMessageBox(text='Running') self.StackedWidget_dock.setVisible(False) # set the widget for creating training set self.trainniwidget = create_training_set(self.canvas) self.StackedWidget.addWidget(self.trainniwidget) # connect signals and slots self.poligonize.triggered.connect( lambda: self.StackedWidget.setCurrentWidget(polygonizewidget)) self.poligonize.triggered.connect( lambda: self.StackedWidget_dock.setWindowTitle('Polygonize')) self.rasterize.triggered.connect( lambda: self.StackedWidget.setCurrentWidget(rastrizewidget)) self.rasterize.triggered.connect( lambda: self.StackedWidget_dock.setWindowTitle('Rasterize')) self.confusiomatrix.triggered.connect( lambda: self.StackedWidget.setCurrentWidget(confusionmatrixwidget)) self.confusiomatrix.triggered.connect( lambda: self.StackedWidget_dock.setWindowTitle('Confusion Matrix')) confusionmatrixwidget.done.connect( lambda: self.StackedWidget.setCurrentWidget(confusionmatrixwidget. resultsgroup)) self.supervised.triggered.connect( lambda: self.StackedWidget_dock.setWindowTitle( 'Supervised classification')) self.supervised.triggered.connect( lambda: self.StackedWidget.setCurrentWidget( self.supervised_classification.step1)) self.supervised.triggered.connect( lambda: self.StackedWidget_dock.setVisible(True)) self.poligonize.triggered.connect( lambda: self.StackedWidget_dock.setVisible(True)) self.rasterize.triggered.connect( lambda: self.StackedWidget_dock.setVisible(True)) self.confusiomatrix.triggered.connect( lambda: self.StackedWidget_dock.setVisible(True)) self.training.triggered.connect( lambda: self.StackedWidget_dock.setVisible(True)) self.training.triggered.connect( lambda: self.StackedWidget.setCurrentWidget(self.trainniwidget)) self.training.triggered.connect( lambda: self.trainniwidget.create_layer()) self.supervised_classification.step1next.clicked.connect( lambda: self.StackedWidget.setCurrentWidget( self.supervised_classification.step2)) self.supervised_classification.step2back.clicked.connect( lambda: self.StackedWidget.setCurrentWidget( self.supervised_classification.step1)) self.supervised_classification.step2next.clicked.connect( lambda: self.StackedWidget.setCurrentWidget( self.supervised_classification.step3)) self.supervised_classification.step3back.clicked.connect( lambda: self.StackedWidget.setCurrentWidget( self.supervised_classification.step2)) self.loadprojectaction.triggered.connect(self.openproject) self.saveprojectasaction.triggered.connect(self.saveproject) self.newprojectaction.triggered.connect(self.newproject) self.saveaction.triggered.connect(self.save) def zoomIn(self): """canvas zoom in tool""" self.canvas.setMapTool(self.toolZoomIn) def zoomOut(self): """canvas zoom out tool""" self.canvas.setMapTool(self.toolZoomOut) def pan(self): """canvas pan tool""" self.canvas.setMapTool(self.toolPan) def saveproject(self): """save the project as...""" savedfile = QFileDialog.getSaveFileName(self, "Save project", ".", "(*.qgs)")[0] + '.qgs' try: self.layerpanel.project.setFileName(savedfile) self.layerpanel.project.write() except: pass def openproject(self): """open a dialog ro select a project file to open""" file = QFileDialog.getOpenFileName(self, "Open project", ".", "(*.qgs)")[0] fileinfo = QFileInfo(file) projectfile = fileinfo.filePath() print(projectfile) try: self.layerpanel.project.read(projectfile) except: pass def newproject(self): """create a new empty project""" self.layerpanel.project.clear() def save(self): """save the changes in the project, if the project is not associated with a project file, a dialog for creating a project will open""" write = self.layerpanel.project.write() if write is True: pass else: self.saveproject() def main(argv): """runing thr appplication""" # create Qt application app = QApplication(argv) # Initialize qgis libraries QgsApplication.setPrefixPath(qgis_prefix, True) QgsApplication.initQgis() # create main window wnd = object_classifier_app() # Move the app window to upper left wnd.move(100, 100) wnd.show() # run! retval = app.exec_() # exit QgsApplication.exitQgis() sys.exit(retval)
class RMS(QMainWindow): def __init__(self, cu, cu_instance): super().__init__() self.cuv = cu.version() self.database = DatabaseHandler( debug=True if self.cuv in DUMMY_IDS else False) self.drivers = {} self.setDefaultDrivers() self.comp_mode = COMP_MODE__TRAINING self.comp_duration = 0 self.tts = TTSHandler() self.tts.start() self.main_stack = QStackedWidget(self) self.qualifyingseq = QualifyingSeq(self) self.main_stack.addWidget(self.qualifyingseq) self.threadtranslation = ThreadTranslation() self.main_stack.addWidget(self.threadtranslation) self.idle = IdleMonitor(cu=cu, cu_instance=cu_instance) self.bridge = CUBridge(cu=cu, cu_instance=cu_instance, selected_drivers=self.drivers, tts=self.tts, threadtranslation=self.threadtranslation) self.start_signal = StartSignal(cu=cu, cu_instance=cu_instance) self.grid = Grid(parent=self) self.home = Home(parent=self, database=self.database) self.settings = Settings(parent=self, database=self.database) self.resultlist = ResultList(parent=self) self.main_stack.addWidget(self.home) self.main_stack.addWidget(self.grid) self.main_stack.addWidget(self.settings) self.main_stack.addWidget(self.resultlist) self.bridge.update_grid.connect(self.grid.driver_change) self.bridge.comp_state.connect(self.comp_state_update) self.bridge.comp_finished.connect(self.comp_finished_all) self.start_signal.ready_to_run.connect(self.startAfterSignal) self.start_signal.show_lights.connect(self.grid.showLight) self.idle.update_state.connect(self.show_state) self.bridge.update_state.connect(self.show_state) self.start_signal.update_state.connect(self.show_state) self.setCentralWidget(self.main_stack) self.initUI() def initUI(self): self.statusBar().showMessage('Ready') if self.cuv not in DUMMY_IDS: self.showMaximized() self.setWindowTitle('RMS') self.showHome() self.show() def setDefaultDrivers(self): self.drivers = {} driversjson = self.database.getConfigStr('DEFAULT_DRIVERS') if driversjson is not None: driversdb = json.loads(driversjson) for addr, driver in driversdb.items(): addrt = int(addr) self.drivers[addrt] = driver def startBridgeThread(self): if not self.bridge.isRunning(): self.bridge.stop = False self.bridge.start() def startIdleThread(self): if not self.idle.isRunning(): self.idle.stop = False self.idle.start() def startStartSignalThread(self): if not self.start_signal.isRunning(): self.start_signal.stop = False self.start_signal.start() def stopAllThreads(self): if self.bridge.isRunning(): self.bridge.stop = True self.bridge.wait() if self.idle.isRunning(): self.idle.stop = True self.idle.wait() if self.start_signal.isRunning(): self.start_signal.stop = True self.start_signal.wait() def showSettings(self): self.main_stack.setCurrentWidget(self.settings) self.stopAllThreads() self.startIdleThread() def showHome(self): tn = self.database.getConfigStr('TRACKNAME') if tn is not None and len(tn) > 0: self.home.headline.setText(tn + ' ' + self.tr('RMS')) else: self.home.headline.setText(self.tr('Carrera RMS')) self.home.buildCarList() for i in range(0, 6): try: n = self.drivers[i]['name'] c = self.drivers[i]['car'] self.home.setOk(i, True) self.home.setName(i, n) self.home.setCar(i, c) except KeyError: self.home.setOk(i, False) self.home.setName(i, '') self.home.setCar(i, '') self.main_stack.setCurrentWidget(self.home) self.stopAllThreads() self.startIdleThread() def showResultList(self, cu_drivers): self.stopAllThreads() self.resultlist.resetDrivers() self.resultlist.addDrivers(self.drivers, cu_drivers, self.grid.sort_mode) self.main_stack.setCurrentWidget(self.resultlist) def showGrid(self): self.grid.resetDrivers() seq_found = None for addr, driver in self.drivers.items(): if self.comp_mode in [ COMP_MODE__QUALIFYING_LAPS_SEQ, COMP_MODE__QUALIFYING_TIME_SEQ ]: if seq_found is None and \ driver['qualifying_cu_driver'] is None: self.grid.addDriver(addr, driver) seq_found = addr else: self.grid.addDriver(addr, driver) self.main_stack.setCurrentWidget(self.grid) self.stopAllThreads() def startQualifying(self, mode, duration): self.comp_mode = mode self.comp_duration = duration self.grid.sort_mode = SORT_MODE__LAPTIME self.showGrid() self.bridge.reset(self.drivers, mode) self.startStartSignalThread() def startRace(self, mode, duration): self.comp_mode = mode self.comp_duration = duration self.grid.sort_mode = SORT_MODE__LAPS self.showGrid() self.bridge.reset(self.drivers, mode) self.startStartSignalThread() def startTraining(self): self.comp_mode = COMP_MODE__TRAINING self.grid.sort_mode = SORT_MODE__LAPTIME self.showGrid() self.bridge.reset(self.drivers, self.comp_mode) self.startStartSignalThread() @pyqtSlot() def startAfterSignal(self): self.startBridgeThread() @pyqtSlot(int, list) def comp_finished_all(self, rtime, drivers): tdrivers = drivers self.stopAllThreads() if self.comp_mode in [ COMP_MODE__QUALIFYING_LAPS_SEQ, COMP_MODE__QUALIFYING_TIME_SEQ ]: seq_found = [] next = None for addr, driver in self.drivers.items(): if driver['qualifying_cu_driver'] is not None: seq_found.append(driver) if tdrivers[addr].time is not None: driver['qualifying_cu_driver'] = tdrivers[addr] elif next is None: next = driver if len(seq_found) == len(self.drivers): for addr, driver in self.drivers.items(): tdrivers[addr] = driver['qualifying_cu_driver'] self.showResultList(tdrivers) else: self.qualifyingseq.setDrivers(seq_found[-1], next) self.main_stack.setCurrentWidget(self.qualifyingseq) else: self.showResultList(drivers) @pyqtSlot(int, list) def comp_state_update(self, rtime, cu_drivers): if self.comp_mode == COMP_MODE__TRAINING: self.grid.training_state.showTime(rtime=rtime) elif self.comp_mode == COMP_MODE__RACE_LAPS: self.grid.race_state.handleUpdateLaps(rtime=rtime, laps=self.comp_duration, cu_drivers=cu_drivers) elif self.comp_mode == COMP_MODE__RACE_TIME: self.grid.race_state.handleUpdateTime(rtime=rtime, minutes=self.comp_duration, cu_drivers=cu_drivers) elif self.comp_mode == COMP_MODE__QUALIFYING_LAPS: self.grid.qualifying_state.handleUpdateLaps( rtime=rtime, laps=self.comp_duration, cu_drivers=cu_drivers) elif self.comp_mode == COMP_MODE__QUALIFYING_TIME: self.grid.qualifying_state.handleUpdateTime( rtime=rtime, minutes=self.comp_duration, cu_drivers=cu_drivers) elif self.comp_mode == COMP_MODE__QUALIFYING_LAPS_SEQ: self.grid.qualifying_state.handleUpdateLapsSeq( rtime=rtime, laps=self.comp_duration, cu_drivers=cu_drivers) elif self.comp_mode == COMP_MODE__QUALIFYING_TIME_SEQ: self.grid.qualifying_state.handleUpdateTimeSeq( rtime=rtime, minutes=self.comp_duration, cu_drivers=cu_drivers) @pyqtSlot(int) def show_state(self, mode): binMode = "{0:04b}".format(mode) fuelmode = '' pitlane = '' lapcounter = '' if binMode[2] == '1': fuelmode = self.tr('Real') elif binMode[3] == '1': fuelmode = self.tr('On') elif binMode[3] == '0': fuelmode = self.tr('Off') if binMode[1] == '1': pitlane = self.tr('Exists') else: pitlane = self.tr('Missing') if binMode[0] == '1': lapcounter = self.tr('Exists') else: lapcounter = self.tr('Missing') self.statusBar().showMessage( self.tr('CU version: ') + str(self.cuv) + self.tr(', Pitlane: ') + str(pitlane) + self.tr(', Fuelmode: ') + str(fuelmode) + self.tr(', Lapcounter: ') + str(lapcounter)) def closeEvent(self, event): result = QMessageBox.question( self, self.tr("Confirm Exit..."), self.tr("Are you sure you want to exit ?"), QMessageBox.Yes | QMessageBox.No) event.ignore() if result == QMessageBox.Yes: event.accept() self.stopAllThreads() self.tts.stop = True self.tts.wait()
class Grid(QWidget): def __init__(self, parent=None): super().__init__(parent) self.sort_mode = SORT_MODE__LAPS self.driver_ui = {} self.initUI() self.initDriverUI() def initUI(self): self.mainLayout = QGridLayout() self.mainLayout.setSpacing(10) self.mainLayout.setHorizontalSpacing(10) self.headerFont = QFont() self.headerFont.setPointSize(14) self.headerFont.setBold(True) self.labelArr = [self.tr('Pos'), self.tr('Driver'), self.tr('Total'), self.tr('Laps'), self.tr('Laptime'), self.tr('Best Lap'), self.tr('Fuel'), self.tr('Pits')] for index, label in enumerate(self.labelArr): self.headerLabel = QLabel(label) self.headerLabel.setFont(self.headerFont) self.mainLayout.addWidget(self.headerLabel, 0, index, Qt.AlignHCenter) self.mainLayout.setColumnStretch(1, 1) self.mainLayout.setColumnStretch(2, 1) self.mainLayout.setColumnStretch(3, 2) self.mainLayout.setColumnStretch(4, 3) self.mainLayout.setColumnStretch(5, 3) self.mainLayout.setColumnStretch(6, 2) self.mainLayout.setColumnStretch(7, 1) self.stateStack = QStackedWidget() self.start_signal = StartLights() self.false_start = FalseStart() self.training_state = TrainingState() self.qualifying_state = QualifyingState() self.race_state = RaceState() self.stateStack.addWidget(self.false_start) self.stateStack.addWidget(self.start_signal) self.stateStack.addWidget(self.training_state) self.stateStack.addWidget(self.qualifying_state) self.stateStack.addWidget(self.race_state) self.vml = QVBoxLayout() self.vml.addLayout(self.mainLayout) self.vml.addStretch(1) self.vml.addWidget(self.stateStack) self.setLayout(self.vml) def initDriverUI(self): self.posFont = QFont() self.posFont.setPointSize(35) self.posFont.setBold(True) self.nameFont = QFont() self.nameFont.setPointSize(20) self.nameFont.setBold(True) self.timeFont = QFont() self.timeFont.setPointSize(36) self.timeFont.setBold(True) self.timeFont.setStyleHint(QFont.TypeWriter) self.timeFont.setFamily('monospace') self.posCss = "QLabel{ border-radius: 10px; border-color: black; " \ + "border: 5px solid black; background-color: white}" self.nameCss = "QLabel{ border-radius: 10px; border-color: black; " \ + "border: 5px solid black; background-color: white; " \ + "font-size: 20pt}" self.lcdCss = "QLCDNumber{ border-radius: 10px; " \ + "background-color: black}" self.lcdColor = QColor(255, 0, 0) self.num_row = 1 def addDriver(self, addr, driver): driverPos = QLabel(str(driver['pos'])) driverPos.setStyleSheet(self.posCss) driverPos.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) driverPos.setFont(self.posFont) self.mainLayout.addWidget(driverPos, self.num_row, 0) name = QLabel( '<big><b>' + str(driver['name']) + '</b></big><br><small>' + str(driver['car']) + '</small>') name.setTextFormat(Qt.RichText) name.setStyleSheet(self.nameCss) self.mainLayout.addWidget(name, self.num_row, 1) total = QLabel('00:00') total.setStyleSheet(self.posCss) total.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) total.setFont(self.timeFont) self.mainLayout.addWidget(total, self.num_row, 2) laps = QLabel('0') laps.setStyleSheet(self.posCss) laps.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) laps.setFont(self.timeFont) self.mainLayout.addWidget(laps, self.num_row, 3) laptime = QLabel('00:00') laptime.setStyleSheet(self.posCss) laptime.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) laptime.setFont(self.timeFont) self.mainLayout.addWidget(laptime, self.num_row, 4) bestlaptime = QLabel('00:00') bestlaptime.setStyleSheet(self.posCss) bestlaptime.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) bestlaptime.setFont(self.timeFont) self.mainLayout.addWidget(bestlaptime, self.num_row, 5) fuelbar = QProgressBar() fuelbar.setOrientation(Qt.Horizontal) fuelbar.setStyleSheet( "QProgressBar{ color: white; background-color: black; border: " + "5px solid black; border-radius: 10px; text-align: center} " + "QProgressBar::chunk { background: qlineargradient(x1: 1, " + "y1: 0.5, x2: 0, y2: 0.5, stop: 0 #00AA00, stop: " + str(0.92 - (1 / (15))) + " #22FF22, stop: " + str(0.921 - (1 / (15))) + " #22FF22, stop: " + str(1.001 - (1 / (15))) + " red, stop: 1 #550000); }") fuelbar.setMinimum(0) fuelbar.setMaximum(15) fuelbar.setValue(15) fuelbar.setFont(self.timeFont) fuelbar.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) self.mainLayout.addWidget(fuelbar, self.num_row, 6) pits = QLabel('00') pits.setStyleSheet(self.posCss) pits.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) pits.setFont(self.timeFont) pits.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) self.mainLayout.addWidget(pits, self.num_row, 7) self.driver_ui[addr] = { 'pos': driverPos, 'name': name, 'total': total, 'laps': laps, 'laptime': laptime, 'bestlaptime': bestlaptime, 'fuelbar': fuelbar, 'pits': pits } self.num_row += 1 def resetDrivers(self): for addr, row in self.driver_ui.items(): for name, widget in row.items(): self.mainLayout.removeWidget(widget) widget.deleteLater() del widget self.driver_ui = {} self.num_row = 1 @pyqtSlot(list) def driver_change(self, cu_drivers): rank = [] for addr, driver in self.driver_ui.items(): rank.append(cu_drivers[addr]) if self.sort_mode == SORT_MODE__LAPS: rank.sort( key=lambda dr: (0, 0) if dr.bestlap is None else (-dr.laps, dr.time)) if self.sort_mode == SORT_MODE__LAPTIME: rank.sort(key=lambda dr: 0 if dr.bestlap is None else dr.bestlap) for addr, driver in self.driver_ui.items(): try: di = cu_drivers[addr] p = str(di.pits) if di.pit: p += self.tr(' (in)') self.updateDriver( addr=addr, pos=rank.index(di)+1, total=di.time, laps=di.laps, laptime=di.laptime, bestlaptime=di.bestlap, fuelbar=di.fuel, pits=p) except KeyError: pass except ValueError: pass def updateDriver(self, addr, pos=None, name=None, total=None, laps=None, laptime=None, bestlaptime=None, fuelbar=None, pits=None): try: r = self.driver_ui[addr] if pos is not None: r['pos'].setText(str(pos)) if name is not None: r['name'].setText(str(name)) if total is not None: r['total'].setText(str(formattime(total))) if laps is not None: r['laps'].setText(str(laps)) if laptime is not None: r['laptime'].setText(str(formattime(laptime, longfmt=False))) if bestlaptime is not None: r['bestlaptime'].setText(str(formattime(bestlaptime, longfmt=False))) if fuelbar is not None: r['fuelbar'].setValue(fuelbar) if pits is not None: r['pits'].setText(str(pits)) except KeyError: print('wrong addr', addr) @pyqtSlot(int) def showLight(self, number): self.stateStack.setCurrentWidget(self.start_signal) if number == 2: self.start_signal.lightOne.setOn() elif number == 3: self.start_signal.lightTwo.setOn() elif number == 4: self.start_signal.lightThree.setOn() elif number == 5: self.start_signal.lightFour.setOn() elif number == 6: self.start_signal.lightFive.setOn() elif number == 0: self.stateStack.setCurrentWidget(self.false_start) elif number == 100: self.start_signal.lightOne.setGreen() self.start_signal.lightTwo.setGreen() self.start_signal.lightThree.setGreen() self.start_signal.lightFour.setGreen() self.start_signal.lightFive.setGreen() cm = self.parent().parent().comp_mode if cm == COMP_MODE__TRAINING: self.stateStack.setCurrentWidget(self.training_state) elif cm == COMP_MODE__RACE_LAPS: self.stateStack.setCurrentWidget(self.race_state) elif cm == COMP_MODE__RACE_TIME: self.stateStack.setCurrentWidget(self.race_state) elif cm == COMP_MODE__QUALIFYING_LAPS: self.stateStack.setCurrentWidget(self.qualifying_state) elif cm == COMP_MODE__QUALIFYING_TIME: self.stateStack.setCurrentWidget(self.qualifying_state) elif cm == COMP_MODE__QUALIFYING_LAPS_SEQ: self.stateStack.setCurrentWidget(self.qualifying_state) elif cm == COMP_MODE__QUALIFYING_TIME_SEQ: self.stateStack.setCurrentWidget(self.qualifying_state) elif number == 101: self.start_signal.lightOne.setOff() self.start_signal.lightTwo.setOff() self.start_signal.lightThree.setOff() self.start_signal.lightFour.setOff() self.start_signal.lightFive.setOff()
class MainWindow(QMainWindow): """The main window of our application. It will contain a toolbar and a QStackedWidget to switch between page. :parameter plugin: A reference to the plugin used to access its methods such as the RPC. """ def __init__(self, plugin): super().__init__() self.plugin = plugin self.initUi() def createActions(self): """Creates the main actions of the page. Namely the menubar and toolbar actions. """ # MenuBar actions self.quit_action = QAction("&Quit", self) self.quit_action.setShortcut("Ctrl+Q") self.quit_action.setStatusTip("Exit the GUI without stopping lightningd") self.quit_action.triggered.connect(qApp.quit) self.minimize_action = QAction("&Minimize", self) self.minimize_action.setShortcut("Ctrl+M") self.minimize_action.triggered.connect(lambda: self.showMinimized()) self.restore_action = QAction("&Restore", self) self.restore_action.triggered.connect(lambda: self.show()) self.del_expired_invoices_action = QAction("&Delete expired invoices", self) self.del_expired_invoices_action.triggered.connect(lambda: self.plugin.rpc.delexpiredinvoice()) self.del_invoice_action = QAction("&Delete a specified unpaid invoice", self) self.del_invoice_action.triggered.connect(self.menuDelInvoice) self.get_address_p2sh_action = QAction("&Get a P2SH-embedded segwit address") self.get_address_p2sh_action.triggered.connect(self.getAddressP2sh) self.get_address_segwit_action = QAction("&Get a native segwit address") self.get_address_segwit_action.triggered.connect(self.getAddressBech) # ToolBar actions self.show_overview_action = QAction(QIcon(":/icons/overview"), "&Overview", self) self.show_overview_action.setToolTip("Show overview page") self.show_overview_action.setShortcut("Alt+1") self.show_overview_action.triggered.connect(self.showOverview) self.show_receivepay_action = QAction(QIcon(":/icons/receive"), "&Receive Payment", self) self.show_receivepay_action.setToolTip("Show receive payment page") self.show_receivepay_action.setShortcut("Alt+2") self.show_receivepay_action.triggered.connect(self.showReceive) self.show_sendpay_action = QAction(QIcon(":/icons/send"), "&Send Payment", self) self.show_sendpay_action.setToolTip("Show send payment page") self.show_sendpay_action.setShortcut("Alt+3") self.show_sendpay_action.triggered.connect(self.showSend) self.show_managechan_action = QAction(QIcon(":/icons/lightning"), "&Manage channels", self) self.show_managechan_action.setToolTip("Show channel management page") self.show_managechan_action.setShortcut("Alt+4") self.show_managechan_action.triggered.connect(self.showChannelsPage) def createMenu(self): """Creates the menu at the top of the window.""" self.menu = self.menuBar() file_menu = self.menu.addMenu("&File") file_menu.addAction(self.quit_action) window_menu = self.menu.addMenu("&Window") window_menu.addAction(self.minimize_action) window_menu.addAction(self.restore_action) invoice_menu = self.menu.addMenu("&Invoices") invoice_menu.addAction(self.del_expired_invoices_action) invoice_menu.addAction(self.del_invoice_action) bitcoin_menu = self.menu.addMenu("&Bitcoin") bitcoin_menu.addAction(self.get_address_segwit_action) bitcoin_menu.addAction(self.get_address_p2sh_action) def createPages(self): """Creates each of our pages, which are QWidget-inherited objects We pass a reference to the plugin to pages, so that they can interact with it (for now it's mainly for RPC). """ self.overview_page = OverviewPage(self.plugin) self.page_manager.addWidget(self.overview_page) self.receive_page = ReceivePage(self.plugin) self.page_manager.addWidget(self.receive_page) self.send_page = SendPage(self.plugin) self.page_manager.addWidget(self.send_page) self.channels_page = ChannelsPage(self.plugin) self.page_manager.addWidget(self.channels_page) def createPageManager(self): """Creates the QStackedWidget which we will use as the page manager""" self.page_manager = QStackedWidget(self) self.setCentralWidget(self.page_manager) def createToolbar(self): """Creates the toolbar used to navigate between pages""" self.toolbar = self.addToolBar("") self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu) self.toolbar.setMovable(False) self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.toolbar.addAction(self.show_overview_action) self.toolbar.addAction(self.show_receivepay_action) self.toolbar.addAction(self.show_sendpay_action) self.toolbar.addAction(self.show_managechan_action) def getAddressP2sh(self): """Shows a message box containing a P2SH-embedded segwit address""" address = self.plugin.rpc.newaddr(addresstype="p2sh-segwit") if address: QMessageBox.information(self, "Bitcoin address", address["p2sh-segwit"]) def getAddressBech(self): """Shows a message box containing a native segwit address (bech32)""" address = self.plugin.rpc.newaddr() if address: QMessageBox.information(self, "Bitcoin address", address["bech32"]) def initUi(self): """Initializes the default parameters for the window (title, position, size).""" self.setWindowTitle("lightning-qt") self.setWindowIcon(QIcon(":/icons/lightning")) self.resize(700, 500) geo = self.frameGeometry() geo.moveCenter(QDesktopWidget().availableGeometry().center()) self.move(geo.topLeft()) self.createActions() self.createMenu() self.createToolbar() self.createPageManager() self.createPages() def menuDelInvoice(self): """Shows a message which asks for an invoice label and delete this invoice""" label = QInputDialog.getText(self, "Delete an unpaid invoice", "Enter the label of the invoice you want to delete") if label[1]: result = self.plugin.rpc.delinvoice(label[0], 'unpaid') if result: QMessageBox.information(self, "Delete an unpaid invoice", "Succesfully deleted invoice") def showChannelsPage(self): """Set channelsPage as the current widget""" self.channels_page.clear() self.channels_page.populateChannels() self.page_manager.setCurrentWidget(self.channels_page) def showOverview(self): """Set overviewPage as the current widget""" self.overview_page.update() self.page_manager.setCurrentWidget(self.overview_page) def showReceive(self): """Set receivePage as the current widget""" self.page_manager.setCurrentWidget(self.receive_page) def showSend(self): """Set sendPage as the current widget""" self.page_manager.setCurrentWidget(self.send_page)
class MainGUI(QMainWindow): def __init__(self, logman, comment_mgr, val): logging.debug("MainGUI(): Instantiated") super(MainGUI, self).__init__() self.setWindowTitle('Traffic Annotation Workflow') self.setFixedSize(670,565) self.logman = logman self.comment_mgr = comment_mgr self.val = val self.project_sessions = ProjectSessions() self.cm = ConfigurationManager.get_instance() #shared data between widgets self.existingconfignames = {} self.logEnabled = '' self.closeConfirmed = '' self.newProject_pressed = False self.newPro = None #get project folder self.project_data_folder = self.cm.read_config_value("PROJECTS", "PROJECTS_BASE_PATH") self.createRequiredSubDirectories() self.at_start = True self.mainWidget = QWidget() self.setCentralWidget(self.mainWidget) mainlayout = QVBoxLayout() self.baseWidget = QWidget() #BaseWidget() self.annotateWidget = QWidget() self.resultsWidget = QWidget() self.projectTree = QtWidgets.QTreeWidget() self.baseWidgets = {} self.blankTreeContextMenu = {} quit = QAction("Quit", self) quit.triggered.connect(self.closeEvent) #Add tab widget - RES tabWidget = QtWidgets.QTabWidget() tabWidget.setGeometry(QtCore.QRect(0, 15, 668, 565)) tabWidget.setObjectName("tabWidget") #BaseWidget self.baseWidget.setWindowTitle("BaseWidget") self.baseWidget.setObjectName("BaseWidget") baseLayoutWidget = QtWidgets.QWidget() baseLayoutWidget.setObjectName("layoutWidget") self.baseOuterVertBox = QtWidgets.QVBoxLayout() self.baseOuterVertBox.setObjectName("outerVertBox") baseLayoutWidget.setLayout(self.baseOuterVertBox) self.baseWidget.setLayout(self.baseOuterVertBox) #Configuration window - RES ## windowBoxHLayout contains: ###projectTree (Left) ###basedataStackedWidget (Right) windowWidget = QtWidgets.QWidget() windowWidget.setObjectName("windowWidget") windowBoxHLayout = QtWidgets.QHBoxLayout() windowBoxHLayout.setObjectName("windowBoxHLayout") windowWidget.setLayout(windowBoxHLayout) self.projectTree.itemSelectionChanged.connect(self.onItemSelected) self.projectTree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.projectTree.customContextMenuRequested.connect(self.showContextMenu) self.projectTree.setEnabled(True) self.projectTree.setMaximumSize(200,521) self.projectTree.setObjectName("projectTree") self.projectTree.headerItem().setText(0, "Projects") self.projectTree.setSortingEnabled(False) windowBoxHLayout.addWidget(self.projectTree) self.basedataStackedWidget = QStackedWidget() self.basedataStackedWidget.setObjectName("basedataStackedWidget") windowBoxHLayout.addWidget(self.basedataStackedWidget) tabWidget.addTab(windowWidget, "Configuration") #ADD TAB WIDGET - RES self.initMenu() mainlayout = QVBoxLayout() mainlayout.addWidget(self.mainMenu) mainlayout.addWidget(tabWidget) self.mainWidget.setLayout(mainlayout) #load any saved projects self.load_saved() self.load_sessions() self.at_start = False logging.debug("MainWindow(): Complete") def createRequiredSubDirectories(self): logging.debug("MainApp:createRequiredSubDirectories() instantiated") if os.path.exists(self.project_data_folder) == False: try: os.makedirs(self.project_data_folder) except: logging.error("MainApp:createRequiredSubDirectories(): An error occured when trying to create project directories: ") exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_exception(exc_type, exc_value, exc_traceback) QMessageBox.error(self, "Create Error", "Could not create project subdirectories, quitting...", QMessageBox.Ok) exit() #RES Method def onItemSelected(self): logging.debug("MainApp:onItemSelected instantiated") # Get the selected item self.selectedItem = self.projectTree.currentItem() if self.selectedItem == None: logging.debug("MainApp:onItemSelected no configurations left") self.statusBar.showMessage("No configuration items selected or available.") return #Check if it's the case that an project name was selected parentSelectedItem = self.selectedItem.parent() if(parentSelectedItem == None): #A base widget was selected #logging.debug"PROJECT_WIDGET: " + str((self.baseWidgets[self.selectedItem.text(0)]["ProjectWidget"]))) self.basedataStackedWidget.setCurrentWidget(self.baseWidgets[self.selectedItem.text(0)]["ProjectWidget"]) else: #for children parentOfParent = parentSelectedItem.parent() #Check if it's the case that a Session Name was selected if(self.selectedItem.text(0)[0] == "S"): #logging.debug"SESSION_WIDGET: " + str(self.baseWidgets[parentSelectedItem.text(0)][self.selectedItem.text(0)]["SessionWidget"])) logging.debug("Setting right widget: " + str(self.baseWidgets[parentSelectedItem.text(0)][self.selectedItem.text(0)]["SessionWidget"])) self.basedataStackedWidget.setCurrentWidget(self.baseWidgets[parentSelectedItem.text(0)][self.selectedItem.text(0)]["SessionWidget"]) #Check if it's the case that a Annotate was selected elif(self.selectedItem.text(0)[0] == "A"): #logging.debug"ANNOTATE " + str(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["AnnotateWidget"])) logging.debug("Setting right widget: " + str(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["AnnotateWidget"])) self.basedataStackedWidget.setCurrentWidget(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["AnnotateWidget"]) #Check if it's the case that a Rules was selected elif(self.selectedItem.text(0)[0] == "R"): #logging.debug"RULES " + str(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["RulesWidget"])) logging.debug("Setting right widget: " + str(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["RulesWidget"])) self.basedataStackedWidget.setCurrentWidget(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["RulesWidget"]) #Check if it's the case that a Results was selected elif(self.selectedItem.text(0)[0] == "X"): #logging.debug"RESULTS " + str(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["ResultsWidget"])) logging.debug("Setting right widget: " + str(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["ResultsWidget"])) self.basedataStackedWidget.setCurrentWidget(self.baseWidgets[parentOfParent.text(0)][parentSelectedItem.text(0)]["ResultsWidget"]) def on_add_curation_clicked(self): logging.debug("on_add_curation_clicked(): Instantiated") selectedItem = self.projectTree.currentItem() self.nsd = NewSessionDialog(selectedItem.text(0)) projectSessions = [] for pot_session in self.baseWidgets[selectedItem.text(0)]: if pot_session.startswith("S:"): projectSessions.append(pot_session) if len(projectSessions) > 0: self.nsd.templateNameComboBox.addItems(projectSessions) self.nsd.templateNameComboBox.setEnabled(True) self.nsd.created.connect(self.session_created) self.nsd.setWindowModality(QtCore.Qt.ApplicationModal) self.nsd.show() def addSession(self, projectItem, sessionName): logging.debug("addSession(): Instantiated") projectName = projectItem.text(0) add_session_result = self.add_session_list(projectName, sessionName) if add_session_result == False: logging.error("An error occured when trying to add session to GUI: " + str(sessionName)) return else: sessionLabel = "S: " + sessionName #create tree widget item sessionItem = QtWidgets.QTreeWidgetItem(projectItem) sessionItem.setText(0,sessionLabel) sessionWidget = SessionWidget(sessionName) self.baseWidgets[projectName][sessionLabel] = {} #project name (Parent of Parent) + session name (parent of children) self.baseWidgets[projectName][sessionLabel]["SessionWidget"] = sessionWidget self.basedataStackedWidget.addWidget(sessionWidget) #create other widget items ##ANNOTATE annItem = QtWidgets.QTreeWidgetItem() annLabel = "A: " + "Annotate" annItem.setText(0, annLabel) sessionItem.addChild(annItem) self.annotateWidget = AnnotateWidget(self.project_data_folder, projectName, os.path.basename(self.existingconfignames[projectName]), sessionName, self.comment_mgr) #send project name for the corresponding directory self.baseWidgets[projectName][sessionLabel]["AnnotateWidget"] = self.annotateWidget #child self.basedataStackedWidget.addWidget(self.annotateWidget) ##RULES rulesItem = QtWidgets.QTreeWidgetItem() rulesLabel = "R: " + "Rules" rulesItem.setText(0, rulesLabel) sessionItem.addChild(rulesItem) #add the corresponding directory -- if it is already created, skip rulesDir = os.path.join(self.project_data_folder, projectName) rulesDir = os.path.join(rulesDir, ConfigurationManager.STRUCTURE_RULES_GEN_PATH) if os.path.exists(rulesDir) == False: os.mkdir(rulesDir) rulesWidget = RulesWidget(self.project_data_folder, projectName, os.path.basename(self.existingconfignames[projectName]), sessionName, rulesDir, self.comment_mgr, self.val) self.baseWidgets[projectName][sessionLabel]["RulesWidget"] = rulesWidget self.basedataStackedWidget.addWidget(rulesWidget) ##RESULTS resultsItem = QtWidgets.QTreeWidgetItem() resultsLabel = "X: " + "Results" resultsItem.setText(0, resultsLabel) sessionItem.addChild(resultsItem) #add the corresponding directory -- if it is already created, skip resultsDir = os.path.join(self.project_data_folder, projectName) resultsDir = os.path.join(resultsDir, "IDS-ALERTS") if os.path.exists(resultsDir) == False: os.mkdir(resultsDir) self.resultsWidget = ResultsWidget(self.project_data_folder, projectName, sessionName, resultsDir, self.val) self.baseWidgets[projectName][sessionLabel]["ResultsWidget"] = self.resultsWidget self.basedataStackedWidget.addWidget(self.resultsWidget) logging.debug("on_add_curation_clicked(): Completed") def on_export_clicked(self): #get project dir selectedItem = self.projectTree.currentItem() selectedItemName = selectedItem.text(0) project_folder = os.path.join(self.project_data_folder, selectedItemName) project_folder = os.path.abspath(project_folder) self.exportPro = ExportDialog(self, selectedItemName, project_folder).exec_() #self.exportPro.setWindowModality(QtCore.Qt.ApplicationModal) #self.exportPro.show() #RES METHOD def showContextMenu(self, position): logging.debug("MainApp:showContextMenu() instantiated: " + str(position)) if(self.projectTree.itemAt(position) == None): self.blankTreeContextMenu.popup(self.projectTree.mapToGlobal(position)) elif(self.projectTree.itemAt(position).parent() == None): self.projectContextMenu.popup(self.projectTree.mapToGlobal(position)) #RES METHOD def initMenu(self): logging.debug("MainApp:initMenu() instantiated") self.mainMenu = QMenuBar() self.fileMenu = self.mainMenu.addMenu("File") self.blankTreeContextMenu = QtWidgets.QMenu() self.newProjectContextMenu = QtWidgets.QMenu("New Project") self.blankTreeContextMenu.addMenu(self.newProjectContextMenu) self.fromCapture = self.newProjectContextMenu.addAction("Create Capture") self.fromCapture.triggered.connect(self.newFromCapture) self.fromPCAP = self.newProjectContextMenu.addAction("From PCAP") self.fromPCAP.triggered.connect(self.newFromPCAP) # Experiment context menu self.importContextMenu = QtWidgets.QMenu("Import Project") self.blankTreeContextMenu.addMenu(self.importContextMenu) self.fromFolderContextSubMenu = self.importContextMenu.addAction("From Folder") self.fromFolderContextSubMenu.triggered.connect(self.importFromFolder) self.fromZipContextSubMenu = self.importContextMenu.addAction("From Zip") self.fromZipContextSubMenu.triggered.connect(self.importFromZip) #Context menu project self.projectContextMenu = QtWidgets.QMenu() self.addCuration = self.projectContextMenu.addAction("Create curation session") self.addCuration.triggered.connect(self.on_add_curation_clicked) self.exportProject = self.projectContextMenu.addAction("Export project") self.exportProject.triggered.connect(self.on_export_clicked) self.quitAppMenuButton = QAction(QIcon(), "Quit", self) self.quitAppMenuButton.setShortcut("Ctrl+Q") self.quitAppMenuButton.setStatusTip("Quit App") self.quitAppMenuButton.triggered.connect(self.closeEvent) self.fileMenu.addAction(self.quitAppMenuButton) #Used to create a new project, this is where the prompt to write a name for the project is taken. def newFromCapture(self): logging.debug("MainApp:newFromCapture() instantiated") #Creating a custom widget to display what is needed for creating a new project: self.newPro = NewFromCollectDataDialog(self.logman, self.existingconfignames) #slots to receive data from the custom widget self.newPro.logEnabled.connect(self.log_enabled) self.newPro.created.connect(self.project_created) self.newPro.closeConfirmed.connect(self.close_confirmed) self.newProject_pressed = True self.newPro.setWindowModality(QtCore.Qt.ApplicationModal) self.newPro.show() def newFromPCAP(self): logging.debug("MainApp:newFromPCAP() instantiated") #Creating a custom widget to display what is needed for creating a new project: self.newCap = NewFromPCAPDialog(self.existingconfignames) #slot to receive data from the custom widget self.newCap.created.connect(self.project_created) self.newProject_pressed = True self.newCap.setWindowModality(QtCore.Qt.ApplicationModal) self.newCap.show() #Slot for when the user created the new project, path and configname @QtCore.pyqtSlot(str, dict, str) def project_created(self, configname, existingconfignames, path): #update project info with new info selected from widget self.existingconfignames = existingconfignames #create the new project with the updated information self.addProject(configname, self.existingconfignames[configname], path) @QtCore.pyqtSlot(str) def session_created(self, newsessionname): #call the function to add session to gui selectedProject = self.projectTree.currentItem() self.addSession(selectedProject, newsessionname) #Slot to let us know if the logging has started @QtCore.pyqtSlot(str) def log_enabled(self, status): self.logEnabled = status #Slot to let us know if the close has been confirmed or canceled @QtCore.pyqtSlot(str) def close_confirmed(self, status): self.closeConfirmed = status #Used to create a new project, and this is where the project will actually be populated def addProject(self, configname, projectPCAP, path): if configname in self.baseWidgets: logging.debug("Project already in tree: " + str(configname + "; skipping")) return #create the folders and files for new project if it's not already there: self.projectWidget = ProjectWidget(configname, projectPCAP, path) configTreeWidgetItem = QtWidgets.QTreeWidgetItem(self.projectTree) configTreeWidgetItem.setText(0,configname) self.projectWidget.addProjectItem(configname) #Add base info self.baseWidgets[configname] = {"BaseWidget": {}, "ProjectWidget": {}, "SessionWidget": {}, "AnnotateWidget": {}, "RulesWidget": {}, "ResultsWidget": {}} self.baseWidgets[configname]["BaseWidget"] = self.baseWidget self.basedataStackedWidget.addWidget(self.baseWidget) self.baseWidgets[configname]["ProjectWidget"] = self.projectWidget self.basedataStackedWidget.addWidget(self.projectWidget) self.basedataStackedWidget.addWidget(self.baseWidget) #add to list self.project_sessions.add_project(configname) def importFromZip(self): logging.debug("MainApp:importFromZip() instantiated") zip_file = QFileDialog() zip_file.setWindowTitle("Select File") zip_file.setFileMode(QtWidgets.QFileDialog.ExistingFile) zip_file.setNameFilter("Zip Files (*.zip)") filenames = zip_file.getOpenFileName() filename = filenames[0] if filename == "": logging.debug("File choose cancelled") return else: configname = os.path.basename(filename) configname = os.path.splitext(configname)[0] if configname in self.existingconfignames: QMessageBox.warning(self, "Name Exists", "A project with the same name already exists.", QMessageBox.Ok) return None else: #instance of package manage pack_mgr = PackageManager() self.populate_import(pack_mgr, configname, os.path.abspath(filename)) def importFromFolder(self, configname): logging.debug("MainApp:importFromFolder() instantiated") folder_chosen = str(QFileDialog.getExistingDirectory(self, "Select Directory to Store Data")) if folder_chosen == "": logging.debug("File choose cancelled") return if len(folder_chosen) > 0: baseNoExt = os.path.basename(folder_chosen) baseNoExt = os.path.splitext(baseNoExt)[0] configname = ''.join(e for e in baseNoExt if e.isalnum) if configname in self.existingconfignames: QMessageBox.warning(self, "Name Exists", "A project with the same name already exists.", QMessageBox.Ok) return None else: self.populate_import("dir", configname, folder_chosen) def populate_import(self, function, configname, from_file): importedProjectPath = os.path.join(self.project_data_folder, configname) #copy selected dir to new dir self.batch_thread = BatchThread() self.batch_thread.progress_signal.connect(self.update_progress_bar) self.batch_thread.completion_signal.connect(self.copy_dir_complete) if function == "dir": self.batch_thread.add_function(self.copy_dir, from_file, importedProjectPath) else: self.batch_thread.add_function(function.unzip, from_file, configname, self.project_data_folder) self.progress_dialog_overall = ProgressBarDialog(self, self.batch_thread.get_load_count()) self.batch_thread.start() self.progress_dialog_overall.show() def load_project_widgets(self): logging.debug("load_project_widgets(): instantiated") for name in self.existingconfignames: path = os.path.join(self.project_data_folder, name) projectPCAP = os.path.join(path, self.existingconfignames[name]) configname = name self.addProject(configname, projectPCAP, path) logging.debug("load_project_widgets(): Complete") def copy_dir(self, from_dir, to_dir): logging.debug("copy_dir(): copying selected directory") copy_tree(from_dir, to_dir) logging.debug("copy_dir(): copying complete") def copy_dir_complete(self): logging.debug("copy_dir_complete(): Instantiated") self.progress_dialog_overall.update_progress() self.progress_dialog_overall.hide() #Need to load projects (add to existing...) ##GET Project PCAP Name Here and also add to existingprojectnames self.load_saved() self.load_sessions() logging.debug("copy_dir_complete(): Complete") def load_saved(self): i = 0 #for each subdir, import the saved projects for (dirName, subdirlist, filelist) in os.walk(self.project_data_folder): for projectFolder in subdirlist: pcapSubdir = os.path.join(self.project_data_folder, projectFolder, ConfigurationManager.STRUCTURE_PCAP_SUBDIR) abspcapSubdir = os.path.abspath(pcapSubdir) if os.path.exists(abspcapSubdir): #go one level down #if this is the pcap directory, then check if a pcap exists #if so, this is a good project; create an entry in existingconfignames for (subdirName, subsubdirlist, subfilelist) in os.walk(abspcapSubdir): if len(subfilelist) == 1: filename = subfilelist[0] spt = os.path.splitext(filename) if spt[1] == ".pcap" or spt[1] == ".pcapng": self.existingconfignames[projectFolder] = filename break #once everything has been added, populate widget self.load_project_widgets() def load_sessions(self): for name in self.existingconfignames: #for already saved project project_path = os.path.join(self.project_data_folder, name) project_pcap_session = os.path.join(project_path, ConfigurationManager.STRUCTURE_PCAP_SUBDIR) if os.path.exists(project_pcap_session): paths, dirs, files = next(os.walk(project_pcap_session)) if len(dirs) > 0: self.traverse_sessions(name, project_pcap_session) def traverse_sessions(self, project_name, path): #if RULES dir exists in project folder, then sessions exists i = 0 for (dirName, subdirlist, filelist) in os.walk(path): for sessionName in subdirlist: if os.path.isfile(sessionName): #skip break elif self.add_session_list(project_name, sessionName) == True: self.add_session_widgets(project_name, self.existingconfignames[project_name], sessionName) def add_session_widgets(self, project_name, project_pcap_filename, sessionName): sessionLabel = "S: " + sessionName #create tree widget item selectedItem = self.projectTree.findItems(project_name, Qt.Qt.MatchContains) sessionItem = QtWidgets.QTreeWidgetItem(selectedItem[0]) sessionItem.setText(0,sessionLabel) sessionWidget = SessionWidget(sessionName) self.baseWidgets[project_name][sessionLabel] = {} #project name (Parent of Parent) + session name (parent of children) self.baseWidgets[project_name][sessionLabel]["SessionWidget"] = sessionWidget self.basedataStackedWidget.addWidget(sessionWidget) #create other widget items ##ANNOTATE annItem = QtWidgets.QTreeWidgetItem() annLabel = "A: " + "Annotate" annItem.setText(0, annLabel) sessionItem.addChild(annItem) self.annotateWidget = AnnotateWidget(self.project_data_folder, project_name, project_pcap_filename, sessionName, self.comment_mgr) #send project name for the corresponding directory self.baseWidgets[project_name][sessionLabel]["AnnotateWidget"] = self.annotateWidget #child self.basedataStackedWidget.addWidget(self.annotateWidget) ##RULES rulesItem = QtWidgets.QTreeWidgetItem() rulesLabel = "R: " + "Rules" rulesItem.setText(0, rulesLabel) sessionItem.addChild(rulesItem) #add the corresponding directory -- if it is already created, skip rulesDir = os.path.join(self.project_data_folder, project_name) rulesDir = os.path.join(rulesDir, ConfigurationManager.STRUCTURE_RULES_GEN_PATH) if os.path.exists(rulesDir) == False: os.mkdir(rulesDir) self.rulesWidget = RulesWidget(self.project_data_folder, project_name, project_pcap_filename, sessionName, rulesDir, self.comment_mgr, self.val) self.baseWidgets[project_name][sessionLabel]["RulesWidget"] = self.rulesWidget self.basedataStackedWidget.addWidget(self.rulesWidget) ##RESULTS resultsItem = QtWidgets.QTreeWidgetItem() resultsLabel = "X: " + "Results" resultsItem.setText(0, resultsLabel) sessionItem.addChild(resultsItem) #add the corresponding directory -- if it is already created, skip resultsDir = os.path.join(self.project_data_folder, project_name) resultsDir = os.path.join(resultsDir, "IDS-ALERTS") if os.path.exists(resultsDir) == False: os.mkdir(resultsDir) self.resultsWidget = ResultsWidget(self.project_data_folder, project_name, sessionName, resultsDir, self.val) self.baseWidgets[project_name][sessionLabel]["ResultsWidget"] = self.resultsWidget self.basedataStackedWidget.addWidget(self.resultsWidget) def add_session_list(self, project_name, project_session): #method returns true if session was successfully added success = self.project_sessions.add_project_session(project_name, project_session) if success == False: #self.project_sessions.print_d() return False else: #self.project_sessions.print_d() return True def update_progress_bar(self): logging.debug('update_progress_bar(): Instantiated') self.progress_dialog_overall.update_progress() logging.debug('update_progress_bar(): Complete') def closeEvent(self, event): logging.debug("closeEvent(): instantiated") self.quit_event = event if self.logEnabled == "TRUE": #This means that the new project widget is still running so call the close event #for that widget first to stop logger self.newPro.closeEvent(event) #Check if the close was confirmed or not if self.closeConfirmed == "TRUE": #after that's done, make sure to quit the app self.quit_event.accept() #self.close() qApp.quit() else: return else: close = QMessageBox.question(self, "QUIT", "Are you sure you want to quit?", QMessageBox.Yes | QMessageBox.No) if close == QMessageBox.Yes: #call the delete data function from new project, just to make sure #everything has been cleared out if self.newProject_pressed == True: if self.newPro != None: self.newPro.delete_data() qApp.quit() return elif close == QMessageBox.No and not type(self.quit_event) == bool: self.quit_event.ignore() pass return def quit_app(self): self.quit_event.accept() qApp.quit() return
class MainWindow(QMainWindow): def __init__(self, doc): QMainWindow.__init__(self, None) self.doc = doc self.app = doc.app self._setupUi() # Create base elements self.model = MainWindowModel(document=doc.model) self.model2view = {} self.alookup = Lookup(self, model=self.model.account_lookup) self.clookup = Lookup(self, model=self.model.completion_lookup) self.drsel = DateRangeSelector(mainwindow=self, view=self.dateRangeSelectorView) self.sfield = SearchField(model=self.model.search_field, view=self.searchLineEdit) self.importWindow = ImportWindow(self) self.csvOptionsWindow = CSVOptionsWindow(self) self.recentDocuments = Recent(self.app, 'recentDocuments') self.recentDocuments.addMenu(self.menuOpenRecent) self.model.view = self self.model.connect() self._updateUndoActions() self._bindSignals() def _setupUi(self): # has to take place *before* base elements creation self.setWindowTitle("moneyGuru") self.resize(700, 580) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) self.verticalLayout.setSpacing(0) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.topBar = QWidget(self.centralwidget) self.horizontalLayout_2 = QHBoxLayout(self.topBar) self.horizontalLayout_2.setContentsMargins(2, 0, 2, 0) spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem) self.dateRangeSelectorView = DateRangeSelectorView(self.topBar) self.dateRangeSelectorView.setMinimumSize(QSize(220, 0)) self.horizontalLayout_2.addWidget(self.dateRangeSelectorView) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem1) self.searchLineEdit = SearchEdit(self.topBar) self.searchLineEdit.setMaximumSize(QSize(240, 16777215)) self.horizontalLayout_2.addWidget(self.searchLineEdit) self.verticalLayout.addWidget(self.topBar) self.tabBar = QTabBar(self.centralwidget) self.tabBar.setMinimumSize(QSize(0, 20)) self.verticalLayout.addWidget(self.tabBar) self.mainView = QStackedWidget(self.centralwidget) self.verticalLayout.addWidget(self.mainView) # Bottom buttons & status label self.bottomBar = QWidget(self.centralwidget) self.horizontalLayout = QHBoxLayout(self.bottomBar) self.horizontalLayout.setContentsMargins(2, 2, 2, 2) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.newItemButton = QPushButton(self.bottomBar) buttonSizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) buttonSizePolicy.setHorizontalStretch(0) buttonSizePolicy.setVerticalStretch(0) buttonSizePolicy.setHeightForWidth(self.newItemButton.sizePolicy().hasHeightForWidth()) self.newItemButton.setSizePolicy(buttonSizePolicy) self.newItemButton.setIcon(QIcon(QPixmap(':/plus_8'))) self.horizontalLayout.addWidget(self.newItemButton) self.deleteItemButton = QPushButton(self.bottomBar) self.deleteItemButton.setSizePolicy(buttonSizePolicy) self.deleteItemButton.setIcon(QIcon(QPixmap(':/minus_8'))) self.horizontalLayout.addWidget(self.deleteItemButton) self.editItemButton = QPushButton(self.bottomBar) self.editItemButton.setSizePolicy(buttonSizePolicy) self.editItemButton.setIcon(QIcon(QPixmap(':/info_gray_12'))) self.horizontalLayout.addWidget(self.editItemButton) self.horizontalLayout.addItem(horizontalSpacer(size=20)) self.graphVisibilityButton = QPushButton() self.graphVisibilityButton.setSizePolicy(buttonSizePolicy) self.graphVisibilityButton.setIcon(QIcon(QPixmap(':/graph_visibility_on_16'))) self.horizontalLayout.addWidget(self.graphVisibilityButton) self.piechartVisibilityButton = QPushButton() self.piechartVisibilityButton.setSizePolicy(buttonSizePolicy) self.piechartVisibilityButton.setIcon(QIcon(QPixmap(':/piechart_visibility_on_16'))) self.horizontalLayout.addWidget(self.piechartVisibilityButton) self.columnsVisibilityButton = QPushButton() self.columnsVisibilityButton.setSizePolicy(buttonSizePolicy) self.columnsVisibilityButton.setIcon(QIcon(QPixmap(':/columns_16'))) self.horizontalLayout.addWidget(self.columnsVisibilityButton) self.statusLabel = QLabel(tr("Status")) self.statusLabel.setAlignment(Qt.AlignCenter) self.horizontalLayout.addWidget(self.statusLabel) self.verticalLayout.addWidget(self.bottomBar) self.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 700, 20)) self.menuFile = QMenu(tr("File")) self.menuOpenRecent = QMenu(tr("Open Recent")) self.menuView = QMenu(tr("View")) self.menuDateRange = QMenu(tr("Date Range")) self.menuEdit = QMenu(tr("Edit")) self.menuHelp = QMenu(tr("Help")) self.setMenuBar(self.menubar) self.actionOpenDocument = QAction(tr("Open..."), self) self.actionOpenDocument.setShortcut("Ctrl+O") self.actionShowNetWorth = QAction(tr("Net Worth"), self) self.actionShowNetWorth.setShortcut("Ctrl+1") self.actionShowNetWorth.setIcon(QIcon(QPixmap(':/balance_sheet_48'))) self.actionShowProfitLoss = QAction(escapeamp(tr("Profit & Loss")), self) self.actionShowProfitLoss.setShortcut("Ctrl+2") self.actionShowProfitLoss.setIcon(QIcon(QPixmap(':/income_statement_48'))) self.actionShowTransactions = QAction(tr("Transactions"), self) self.actionShowTransactions.setShortcut("Ctrl+3") self.actionShowTransactions.setIcon(QIcon(QPixmap(':/transaction_table_48'))) self.actionShowSelectedAccount = QAction(tr("Show Account"), self) self.actionShowSelectedAccount.setShortcut("Ctrl+]") self.actionNewItem = QAction(tr("New Item"), self) self.actionNewItem.setShortcut("Ctrl+N") self.actionDeleteItem = QAction(tr("Remove Selected"), self) self.actionEditItem = QAction(tr("Show Info"), self) self.actionEditItem.setShortcut("Ctrl+I") self.actionToggleGraph = QAction(tr("Toggle Graph"), self) self.actionToggleGraph.setShortcut("Ctrl+Alt+G") self.actionTogglePieChart = QAction(tr("Toggle Pie Chart"), self) self.actionTogglePieChart.setShortcut("Ctrl+Alt+P") self.actionMoveUp = QAction(tr("Move Up"), self) self.actionMoveUp.setShortcut("Ctrl++") self.actionMoveDown = QAction(tr("Move Down"), self) self.actionMoveDown.setShortcut("Ctrl+-") self.actionNavigateBack = QAction(tr("Go Back"), self) self.actionNavigateBack.setShortcut("Ctrl+[") self.actionNewAccountGroup = QAction(tr("New Account Group"), self) self.actionNewAccountGroup.setShortcut("Ctrl+Shift+N") self.actionShowNextView = QAction(tr("Next View"), self) self.actionShowNextView.setShortcut("Ctrl+Shift+]") self.actionShowPreviousView = QAction(tr("Previous View"), self) self.actionShowPreviousView.setShortcut("Ctrl+Shift+[") self.actionNewDocument = QAction(tr("New Document"), self) self.actionOpenExampleDocument = QAction(tr("Open Example Document"), self) self.actionOpenPluginFolder = QAction(tr("Open Plugin Folder"), self) self.actionImport = QAction(tr("Import..."), self) self.actionImport.setShortcut("Ctrl+Alt+I") self.actionExport = QAction(tr("Export..."), self) self.actionExport.setShortcut("Ctrl+Alt+E") self.actionSave = QAction(tr("Save"), self) self.actionSave.setShortcut("Ctrl+S") self.actionSaveAs = QAction(tr("Save As..."), self) self.actionSaveAs.setShortcut("Ctrl+Shift+S") self.actionAbout = QAction(tr("About moneyGuru"), self) self.actionToggleReconciliationMode = QAction(tr("Toggle Reconciliation Mode"), self) self.actionToggleReconciliationMode.setShortcut("Ctrl+Shift+R") self.actionToggleAccountExclusion = QAction(tr("Toggle Exclusion Status of Account"), self) self.actionToggleAccountExclusion.setShortcut("Ctrl+Shift+X") self.actionShowSchedules = QAction(tr("Schedules"), self) self.actionShowSchedules.setShortcut("Ctrl+4") self.actionShowSchedules.setIcon(QIcon(QPixmap(':/schedules_48'))) self.actionShowBudgets = QAction(tr("Budgets"), self) self.actionShowBudgets.setShortcut("Ctrl+5") self.actionShowBudgets.setIcon(QIcon(QPixmap(':/budget_48'))) self.actionReconcileSelected = QAction(tr("Reconcile Selection"), self) self.actionReconcileSelected.setShortcut("Ctrl+R") self.actionMakeScheduleFromSelected = QAction(tr("Make Schedule from Selected"), self) self.actionMakeScheduleFromSelected.setShortcut("Ctrl+M") self.actionShowPreferences = QAction(tr("Preferences..."), self) self.actionPrint = QAction(tr("Print..."), self) self.actionPrint.setShortcut("Ctrl+P") self.actionQuit = QAction(tr("Quit moneyGuru"), self) self.actionQuit.setShortcut("Ctrl+Q") self.actionUndo = QAction(tr("Undo"), self) self.actionUndo.setShortcut("Ctrl+Z") self.actionRedo = QAction(tr("Redo"), self) self.actionRedo.setShortcut("Ctrl+Y") self.actionShowHelp = QAction(tr("moneyGuru Help"), self) self.actionShowHelp.setShortcut("F1") self.actionCheckForUpdate = QAction(tr("Check for update"), self) self.actionOpenDebugLog = QAction(tr("Open Debug Log"), self) self.actionDuplicateTransaction = QAction(tr("Duplicate Transaction"), self) self.actionDuplicateTransaction.setShortcut("Ctrl+D") self.actionJumpToAccount = QAction(tr("Jump to Account..."), self) self.actionJumpToAccount.setShortcut("Ctrl+Shift+A") self.actionNewTab = QAction(tr("New Tab"), self) self.actionNewTab.setShortcut("Ctrl+T") self.actionCloseTab = QAction(tr("Close Tab"), self) self.actionCloseTab.setShortcut("Ctrl+W") self.menuFile.addAction(self.actionNewDocument) self.menuFile.addAction(self.actionNewTab) self.menuFile.addAction(self.actionOpenDocument) self.menuFile.addAction(self.menuOpenRecent.menuAction()) self.menuFile.addAction(self.actionOpenExampleDocument) self.menuFile.addAction(self.actionOpenPluginFolder) self.menuFile.addAction(self.actionImport) self.menuFile.addSeparator() self.menuFile.addAction(self.actionCloseTab) self.menuFile.addAction(self.actionSave) self.menuFile.addAction(self.actionSaveAs) self.menuFile.addAction(self.actionExport) self.menuFile.addAction(self.actionPrint) self.menuFile.addAction(self.actionQuit) self.menuView.addAction(self.actionShowNetWorth) self.menuView.addAction(self.actionShowProfitLoss) self.menuView.addAction(self.actionShowTransactions) self.menuView.addAction(self.actionShowSchedules) self.menuView.addAction(self.actionShowBudgets) self.menuView.addAction(self.actionShowPreviousView) self.menuView.addAction(self.actionShowNextView) self.menuView.addAction(self.menuDateRange.menuAction()) self.menuView.addAction(self.actionShowPreferences) self.menuView.addAction(self.actionToggleGraph) self.menuView.addAction(self.actionTogglePieChart) self.menuEdit.addAction(self.actionNewItem) self.menuEdit.addAction(self.actionNewAccountGroup) self.menuEdit.addAction(self.actionDeleteItem) self.menuEdit.addAction(self.actionEditItem) self.menuEdit.addSeparator() self.menuEdit.addAction(self.actionMoveUp) self.menuEdit.addAction(self.actionMoveDown) self.menuEdit.addAction(self.actionDuplicateTransaction) self.menuEdit.addAction(self.actionMakeScheduleFromSelected) self.menuEdit.addAction(self.actionReconcileSelected) self.menuEdit.addAction(self.actionToggleReconciliationMode) self.menuEdit.addAction(self.actionToggleAccountExclusion) self.menuEdit.addSeparator() self.menuEdit.addAction(self.actionShowSelectedAccount) self.menuEdit.addAction(self.actionNavigateBack) self.menuEdit.addAction(self.actionJumpToAccount) self.menuEdit.addSeparator() self.menuEdit.addAction(self.actionUndo) self.menuEdit.addAction(self.actionRedo) self.menuHelp.addAction(self.actionShowHelp) self.menuHelp.addAction(self.actionCheckForUpdate) self.menuHelp.addAction(self.actionOpenDebugLog) self.menuHelp.addAction(self.actionAbout) mainmenus = [self.menuFile, self.menuEdit, self.menuView, self.menuHelp] for menu in mainmenus: self.menubar.addAction(menu.menuAction()) setAccelKeys(menu) setAccelKeys(self.menubar) self.tabBar.setMovable(True) self.tabBar.setTabsClosable(True) self.tabBar.setExpanding(False) seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Right) self._shortcutNextTab = QShortcut(seq, self) seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Left) self._shortcutPrevTab = QShortcut(seq, self) # Linux setup if ISLINUX: self.actionCheckForUpdate.setVisible(False) # This only works on Windows def _bindSignals(self): self.newItemButton.clicked.connect(self.actionNewItem.trigger) self.deleteItemButton.clicked.connect(self.actionDeleteItem.trigger) self.editItemButton.clicked.connect(self.actionEditItem.trigger) self.graphVisibilityButton.clicked.connect(self.actionToggleGraph.trigger) self.piechartVisibilityButton.clicked.connect(self.actionTogglePieChart.trigger) self.columnsVisibilityButton.clicked.connect(self.columnsVisibilityButtonClicked) self.recentDocuments.mustOpenItem.connect(self.doc.open) self.doc.documentOpened.connect(self.recentDocuments.insertItem) self.doc.documentSavedAs.connect(self.recentDocuments.insertItem) self.doc.documentPathChanged.connect(self.documentPathChanged) self.tabBar.currentChanged.connect(self.currentTabChanged) self.tabBar.tabCloseRequested.connect(self.tabCloseRequested) self.tabBar.tabMoved.connect(self.tabMoved) # Views self.actionShowNetWorth.triggered.connect(self.showNetWorthTriggered) self.actionShowProfitLoss.triggered.connect(self.showProfitLossTriggered) self.actionShowTransactions.triggered.connect(self.showTransactionsTriggered) self.actionShowSchedules.triggered.connect(self.showSchedulesTriggered) self.actionShowBudgets.triggered.connect(self.showBudgetsTriggered) self.actionShowPreviousView.triggered.connect(self.showPreviousViewTriggered) self.actionShowNextView.triggered.connect(self.showNextViewTriggered) self.actionShowPreferences.triggered.connect(self.app.showPreferences) self.actionToggleGraph.triggered.connect(self.toggleGraphTriggered) self.actionTogglePieChart.triggered.connect(self.togglePieChartTriggered) # Document Edition self.actionNewItem.triggered.connect(self.newItemTriggered) self.actionNewAccountGroup.triggered.connect(self.newAccountGroupTriggered) self.actionDeleteItem.triggered.connect(self.deleteItemTriggered) self.actionEditItem.triggered.connect(self.editItemTriggered) self.actionMoveUp.triggered.connect(self.moveUpTriggered) self.actionMoveDown.triggered.connect(self.moveDownTriggered) self.actionDuplicateTransaction.triggered.connect(self.model.duplicate_item) self.actionUndo.triggered.connect(self.doc.model.undo) self.actionRedo.triggered.connect(self.doc.model.redo) # Open / Save / Import / Export / New self.actionNewDocument.triggered.connect(self.doc.new) self.actionOpenDocument.triggered.connect(self.doc.openDocument) self.actionOpenExampleDocument.triggered.connect(self.doc.openExampleDocument) self.actionOpenPluginFolder.triggered.connect(self.model.app.open_plugin_folder) self.actionImport.triggered.connect(self.importDocument) self.actionSave.triggered.connect(self.doc.save) self.actionSaveAs.triggered.connect(self.doc.saveAs) self.actionExport.triggered.connect(self.model.export) # Misc self.actionNewTab.triggered.connect(self.model.new_tab) self.actionCloseTab.triggered.connect(self.closeTabTriggered) self.actionShowSelectedAccount.triggered.connect(self.model.show_account) self.actionNavigateBack.triggered.connect(self.navigateBackTriggered) self.actionJumpToAccount.triggered.connect(self.jumpToAccountTriggered) self.actionMakeScheduleFromSelected.triggered.connect(self.makeScheduleFromSelectedTriggered) self.actionReconcileSelected.triggered.connect(self.reconcileSelectedTriggered) self.actionToggleReconciliationMode.triggered.connect(self.toggleReconciliationModeTriggered) self.actionToggleAccountExclusion.triggered.connect(self.toggleAccountExclusionTriggered) self.actionPrint.triggered.connect(self._print) self.actionShowHelp.triggered.connect(self.app.showHelp) self.actionCheckForUpdate.triggered.connect(self.checkForUpdateTriggered) self.actionAbout.triggered.connect(self.aboutTriggered) self.actionOpenDebugLog.triggered.connect(self.openDebugLogTriggered) self.actionQuit.triggered.connect(self.close) # Extra Shortcuts self._shortcutNextTab.activated.connect(self.showNextViewTriggered) self._shortcutPrevTab.activated.connect(self.showPreviousViewTriggered) # --- QWidget overrides def closeEvent(self, event): if self.doc.confirmDestructiveAction(): event.accept() else: event.ignore() # --- Private def _print(self): dialog = QPrintDialog(self) if dialog.exec_() != QPrintDialog.Accepted: return printer = dialog.printer() currentView = self.mainView.currentWidget() viewPrinter = ViewPrinter(printer, currentView) currentView.fitViewsForPrint(viewPrinter) viewPrinter.render() def _getViewforPane(self, pane_type, pane_view): if pane_view in self.model2view: view = self.model2view[pane_view] else: view = PANETYPE2VIEWCLASS[pane_type](model=pane_view, mainwindow=self) self.model2view[pane_view] = view self.mainView.addWidget(view) view.restoreSubviewsSize() return view def _setTabIndex(self, index): if not self.tabBar.count(): return self.tabBar.setCurrentIndex(index) self._updateActionsState() pane_type = self.model.pane_type(index) pane_view = self.model.pane_view(index) view = self._getViewforPane(pane_type, pane_view) self.mainView.setCurrentWidget(view) view.setFocus() def _activeView(self): paneIndex = self.model.current_pane_index return self.model.pane_view(paneIndex) def _updateActionsState(self): # Updates enable/disable checked/unchecked state of all actions. These state can change # under various conditions: main view change, date range type change and when reconciliation # mode is toggled # Determine what actions are enabled view = self._activeView() viewType = view.VIEW_TYPE isSheet = viewType in {PaneType.NetWorth, PaneType.Profit} isTransactionOrEntryTable = viewType in {PaneType.Transaction, PaneType.Account} canToggleReconciliation = viewType == PaneType.Account and view.can_toggle_reconciliation_mode newItemLabel = { PaneType.NetWorth: tr("New Account"), PaneType.Profit: tr("New Account"), PaneType.Transaction: tr("New Transaction"), PaneType.Account: tr("New Transaction"), PaneType.Schedule: tr("New Schedule"), PaneType.Budget: tr("New Budget"), PaneType.GeneralLedger: tr("New Transaction"), }.get(viewType, tr("New Item")) # XXX make "New Item" disabled self.actionNewItem.setText(newItemLabel) self.actionNewAccountGroup.setEnabled(isSheet) self.actionMoveDown.setEnabled(isTransactionOrEntryTable) self.actionMoveUp.setEnabled(isTransactionOrEntryTable) self.actionDuplicateTransaction.setEnabled(isTransactionOrEntryTable) self.actionMakeScheduleFromSelected.setEnabled(isTransactionOrEntryTable) self.actionReconcileSelected.setEnabled(viewType == PaneType.Account and view.reconciliation_mode) self.actionShowNextView.setEnabled(self.model.current_pane_index < self.model.pane_count-1) self.actionShowPreviousView.setEnabled(self.model.current_pane_index > 0) self.actionShowSelectedAccount.setEnabled(isSheet or isTransactionOrEntryTable) self.actionNavigateBack.setEnabled(viewType == PaneType.Account) self.actionToggleReconciliationMode.setEnabled(canToggleReconciliation) self.actionToggleAccountExclusion.setEnabled(isSheet) def _updateUndoActions(self): if self.doc.model.can_undo(): self.actionUndo.setEnabled(True) self.actionUndo.setText(tr("Undo {0}").format(self.doc.model.undo_description())) else: self.actionUndo.setEnabled(False) self.actionUndo.setText(tr("Undo")) if self.doc.model.can_redo(): self.actionRedo.setEnabled(True) self.actionRedo.setText(tr("Redo {0}").format(self.doc.model.redo_description())) else: self.actionRedo.setEnabled(False) self.actionRedo.setText(tr("Redo")) # --- Actions # Views def showNetWorthTriggered(self): self.model.select_pane_of_type(PaneType.NetWorth) def showProfitLossTriggered(self): self.model.select_pane_of_type(PaneType.Profit) def showTransactionsTriggered(self): self.model.select_pane_of_type(PaneType.Transaction) def showSchedulesTriggered(self): self.model.select_pane_of_type(PaneType.Schedule) def showBudgetsTriggered(self): self.model.select_pane_of_type(PaneType.Budget) def showPreviousViewTriggered(self): self.model.select_previous_view() def showNextViewTriggered(self): self.model.select_next_view() # Document Edition def newItemTriggered(self): self.model.new_item() def newAccountGroupTriggered(self): self.model.new_group() def deleteItemTriggered(self): self.model.delete_item() def editItemTriggered(self): self.model.edit_item() def moveUpTriggered(self): self.model.move_up() def moveDownTriggered(self): self.model.move_down() # Misc def closeTabTriggered(self): self.model.close_pane(self.model.current_pane_index) def navigateBackTriggered(self): self.model.navigate_back() def jumpToAccountTriggered(self): self.model.jump_to_account() def makeScheduleFromSelectedTriggered(self): self.model.make_schedule_from_selected() def reconcileSelectedTriggered(self): self._activeView().etable.toggle_reconciled() def toggleReconciliationModeTriggered(self): self._activeView().toggle_reconciliation_mode() self._updateActionsState() def toggleAccountExclusionTriggered(self): viewType = self.model.pane_type(self.model.current_pane_index) if viewType in {PaneType.NetWorth, PaneType.Profit}: self._activeView().sheet.toggle_excluded() def toggleGraphTriggered(self): self.model.toggle_area_visibility(PaneArea.BottomGraph) def togglePieChartTriggered(self): self.model.toggle_area_visibility(PaneArea.RightChart) def columnsVisibilityButtonClicked(self): items = self.model.column_menu_items() if not items: return menu = QMenu() for i, (display, marked) in enumerate(items): action = menu.addAction(display) action.setCheckable(True) action.setChecked(marked) action.setData(i) action.triggered.connect(self.columnsMenuItemWasClicked) self._columnMenuHolder = menu # we need to hold a reference to it while it popups button = self.columnsVisibilityButton menu.popup(button.parentWidget().mapToGlobal(button.geometry().topLeft())) def columnsMenuItemWasClicked(self): action = self.sender() if action is not None: index = action.data() self.model.toggle_column_menu_item(index) def checkForUpdateTriggered(self): QProcess.execute('updater.exe', ['/checknow']) def aboutTriggered(self): self.app.showAboutBox() def openDebugLogTriggered(self): debugLogPath = op.join(getAppData(), 'debug.log') url = QUrl.fromLocalFile(debugLogPath) QDesktopServices.openUrl(url) def importDocument(self): title = tr("Select a document to import") filters = tr("Supported files (*.moneyguru *.ofx *.qfx *.qif *.csv *.txt)") docpath, filetype = QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters) # There's a strange glitch under GNOME where, right after the dialog is gone, the main # window isn't the active window, but it will become active if we give it enough time. If we # start showing the import window before that happens, we'll end up with an import window # under the main window, which is bad. Therefore, we process events until this happens. We # do this in a big forloop instead of a while to avoid a possible infinite loop. for i in range(10000): if self.app.mainWindow.isActiveWindow(): break QApplication.processEvents() if docpath: try: self.model.parse_file_for_import(docpath) except FileFormatError as e: QMessageBox.warning(self.app.mainWindow, tr("Cannot import file"), str(e)) # --- Other Signals def currentTabChanged(self, index): self.model.current_pane_index = index self._setTabIndex(index) def documentPathChanged(self): if self.doc.documentPath: title = "moneyGuru ({})".format(self.doc.documentPath) else: title = "moneyGuru" self.setWindowTitle(title) def tabCloseRequested(self, index): self.model.close_pane(index) def tabMoved(self, fromIndex, toIndex): self.model.move_pane(fromIndex, toIndex) # --- model --> view def change_current_pane(self): self._setTabIndex(self.model.current_pane_index) def get_panel_view(self, model): if isinstance(model, CustomDateRangePanelModel): return CustomDateRangePanel(model, self) else: return ExportPanel(model, self) def refresh_panes(self): # Always remove the "new tab" tab if self.tabBar.count() > 0: self.tabBar.removeTab(self.tabBar.count()-1) while self.tabBar.count() < self.model.pane_count: self.tabBar.addTab('') for i in range(self.model.pane_count): pane_label = self.model.pane_label(i) pane_label = escapeamp(pane_label) self.tabBar.setTabText(i, pane_label) pane_type = self.model.pane_type(i) pane_view = self.model.pane_view(i) # Ensure that the view's "view" has been created and bound self._getViewforPane(pane_type, pane_view) iconname = PANETYPE2ICON.get(pane_type) icon = QIcon(QPixmap(':/{0}'.format(iconname))) if iconname else QIcon() self.tabBar.setTabIcon(i, icon) # It's important that we proceed with tab removal *after* we've completed tab initialization. # We're walking on eggshells here. refresh_panes() can be called in multiple situations, one # of them is during the opening of a document. When that happens when another document was # previously opened, all views' model are uninitalized and don't have their "view" attribute # set yet. If we proceed with the setCurrentIndex() call below before _getViewforPane() # could be called above, we get a crash. if self.tabBar.currentIndex() >= self.model.pane_count: # Normally, we don't touch the tabBar index here and wait for change_current_pane, # but when we remove tabs, it's possible that currentTabChanged end up being called and # then the tab selection is bugged. I tried disconnecting/reconnecting the signal, but # this is buggy. So when a selected tab is about to be removed and is out of bounds, # we change the selection to the last index in the model. We don't use # self.model.current_pane_index because in some cases, it's -1 and prevents this crash # preventer from preventing its crash. self.tabBar.setCurrentIndex(self.model.pane_count - 1) while self.tabBar.count() > self.model.pane_count: self.tabBar.removeTab(self.tabBar.count()-1) self.tabBar.setTabsClosable(self.model.pane_count > 1) # Add the "new tab" tab last_tab_index = self.tabBar.addTab('') self.tabBar.setTabEnabled(last_tab_index, False) newTabButton = QToolButton() newTabButton.setText("+") newTabButton.clicked.connect(self.model.new_tab) self.tabBar.setTabButton(last_tab_index, QTabBar.RightSide, newTabButton) def refresh_status_line(self): self.statusLabel.setText(self.model.status_line) def refresh_undo_actions(self): self._updateUndoActions() def restore_window_frame(self, frame): self.setGeometry(*frame) def save_window_frame(self): r = self.geometry() return (r.x(), r.y(), r.width(), r.height()) def show_message(self, msg): title = tr("Warning") QMessageBox.warning(self, title, msg) def update_area_visibility(self): hidden = self.model.hidden_areas graphimg = ':/graph_visibility_{}_16'.format('off' if PaneArea.BottomGraph in hidden else 'on') pieimg = ':/piechart_visibility_{}_16'.format('off' if PaneArea.RightChart in hidden else 'on') self.graphVisibilityButton.setIcon(QIcon(QPixmap(graphimg))) self.piechartVisibilityButton.setIcon(QIcon(QPixmap(pieimg))) def view_closed(self, index): self.tabBar.removeTab(index) self.tabBar.setTabsClosable(self.model.pane_count > 1)
class StringListDlg(QDialog): def __init__(self, parent=None): super(StringListDlg, self).__init__(parent) self.path = parent.path self.cfgPath = parent.cfgPath self.name = 'Run: ' self.stack = QStackedWidget(self) self.listWidget = QListWidget() self.listWidget.currentItemChanged.connect(self.setView) self.runconfig = { 'Default': { 'numCores': 60, 'writeDates': True, 'cluster': True, 'ignoreCacheCheck': True, 'tag': '', 'step': '', } } self.runConfigDlg = {} for s in self.runConfig: self.listWidget.addItem(s) self.runConfigDlg[s] = RefitDlg(name=s, format=self.runConfig[s], parent=self) self.stack.addWidget(self.runConfigDlg[s]) self.listWidget.setCurrentRow(0) buttonLayout = QVBoxLayout() # New self.newButton = QPushButton('New') self.newButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.newButton) self.newButton.clicked.connect(self.new) # Edit self.editButton = QPushButton('Edit') self.editButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.editButton) self.editButton.clicked.connect(self.edit) # Remove self.removeButton = QPushButton('Remove') self.removeButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.removeButton) self.removeButton.clicked.connect(self.remove) # Save self.saveButton = QPushButton('Save') self.saveButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.saveButton) self.saveButton.clicked.connect(self.save) # Save As self.saveAsButton = QPushButton('Save As') self.saveAsButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.saveAsButton) self.saveAsButton.clicked.connect(self.saveAs) # Revert self.revertButton = QPushButton('Revert') self.revertButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.revertButton) self.revertButton.clicked.connect(self.revert) # Run self.runButton = QPushButton('Run') self.runButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.runButton) self.runButton.clicked.connect(self.run) buttonLayout.addStretch() # Close self.closeButton = QPushButton('Close') self.closeButton.setFocusPolicy(Qt.NoFocus) buttonLayout.addWidget(self.closeButton) self.closeButton.clicked.connect(self.accept) layout = QHBoxLayout() layout.addWidget(self.stack) layout.addWidget(self.listWidget) layout.addLayout(buttonLayout) self.setLayout(layout) self.setWindowTitle("Edit {} List".format(self.name)) def setView(self, i): s = str(i.text()) self.stack.setCurrentWidget(self.runConfigDlg[s]) self.setWindowTitle(s) def new(self): row = self.listWidget.currentRow() title = "Add {}".format(self.name) string, ok = QInputDialog.getText(self, title, title) if ok and string: s = str(string).strip() if s == 'Default': return self.listWidget.insertItem(row, s) self.runConfigDlg[s] = RefitDlg(name=s, format=self.runConfig['Default'], parent=self) self.stack.addWidget(self.runConfigDlg[s]) self.runConfig[s] = self.runConfig['Default'] def edit(self): row = self.listWidget.currentRow() item = self.listWidget.item(row) old = str(item.text()).strip() if item is not None: title = "Edit {}".format(self.name) string, ok = QInputDialog.getText(self, title, title, QLineEdit.Normal, item.text()) if ok and string: new = str(string).strip() if new == 'Default': return item.setText(new) self.runConfigDlg[new] = self.runConfigDlg[old] self.runConfig[new] = self.runConfig[old] del self.runConfigDlg[old] del self.runConfig[old] def remove(self): row = self.listWidget.currentRow() item = self.listWidget.item(row) if str(item.text()) == 'Default': return if item is None: return reply = QMessageBox.question( self, "Remove {}".format(self.name), "Remove {} `{}'?".format(self.name, item.text()), QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: item = self.listWidget.takeItem(row) del item def run(self): s = self.listWidget.currentItem().text() s = str(s) arg = self.runConfigDlg[s].get() parentDir = os.path.split(self.path)[0] subprocess.Popen(arg, cwd=parentDir) self.runButton.setEnabled(False) def saveAs(self): row = self.listWidget.currentRow() if row < self.listWidget.count() - 1: item = self.listWidget.takeItem(row) self.listWidget.insertItem(row + 1, item) self.listWidget.setCurrentItem(item) def save(self): for x in self.runConfigDlg: self.runConfig[x] = self.runConfigDlg[x].get(Args='form') with closing(shelve.open(self.cfgPath, writeback=True)) as f: f['runConfig'] = self.runConfig def revert(self): row = self.listWidget.currentRow() if row < self.listWidget.count() - 1: item = self.listWidget.takeItem(row) self.listWidget.insertItem(row + 1, item) self.listWidget.setCurrentItem(item) def reject(self): self.accept() def accept(self): QDialog.accept(self)
class MainWindow(QMainWindow): def __init__(self, app): QMainWindow.__init__(self, None) self.documentPath = None self.doc = DocumentModel(app=app.model) self.app = app self._setupUi() # Create base elements self.model = MainWindowModel(document=self.doc) self.model2view = {} self.alookup = Lookup(self, model=self.model.account_lookup) self.clookup = Lookup(self, model=self.model.completion_lookup) self.drsel = DateRangeSelector(mainwindow=self, view=self.dateRangeSelectorView) self.sfield = SearchField(model=self.model.search_field, view=self.searchLineEdit) self.recentDocuments = Recent(self.app, 'recentDocuments') self.recentDocuments.addMenu(self.menuOpenRecent) self.doc.view = self self.model.view = self self._updateUndoActions() self._bindSignals() def _setupUi(self): # has to take place *before* base elements creation self.setWindowTitle("moneyGuru") self.resize(700, 580) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) self.verticalLayout.setSpacing(0) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.topBar = QWidget(self.centralwidget) self.horizontalLayout_2 = QHBoxLayout(self.topBar) self.horizontalLayout_2.setContentsMargins(2, 0, 2, 0) spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem) self.dateRangeSelectorView = DateRangeSelectorView(self.topBar) self.dateRangeSelectorView.setMinimumSize(QSize(220, 0)) self.horizontalLayout_2.addWidget(self.dateRangeSelectorView) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem1) self.searchLineEdit = SearchEdit(self.topBar) self.searchLineEdit.setMaximumSize(QSize(240, 16777215)) self.horizontalLayout_2.addWidget(self.searchLineEdit) self.verticalLayout.addWidget(self.topBar) self.tabBar = TabBarPlus(self.centralwidget) self.tabBar.setMinimumSize(QSize(0, 20)) self.verticalLayout.addWidget(self.tabBar) self.mainView = QStackedWidget(self.centralwidget) self.verticalLayout.addWidget(self.mainView) # Bottom buttons & status label self.bottomBar = QWidget(self.centralwidget) self.horizontalLayout = QHBoxLayout(self.bottomBar) self.horizontalLayout.setContentsMargins(2, 2, 2, 2) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.newItemButton = QPushButton(self.bottomBar) buttonSizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) buttonSizePolicy.setHorizontalStretch(0) buttonSizePolicy.setVerticalStretch(0) buttonSizePolicy.setHeightForWidth(self.newItemButton.sizePolicy().hasHeightForWidth()) self.newItemButton.setSizePolicy(buttonSizePolicy) self.newItemButton.setIcon(QIcon(QPixmap(':/plus_8'))) self.horizontalLayout.addWidget(self.newItemButton) self.deleteItemButton = QPushButton(self.bottomBar) self.deleteItemButton.setSizePolicy(buttonSizePolicy) self.deleteItemButton.setIcon(QIcon(QPixmap(':/minus_8'))) self.horizontalLayout.addWidget(self.deleteItemButton) self.editItemButton = QPushButton(self.bottomBar) self.editItemButton.setSizePolicy(buttonSizePolicy) self.editItemButton.setIcon(QIcon(QPixmap(':/info_gray_12'))) self.horizontalLayout.addWidget(self.editItemButton) self.horizontalLayout.addItem(horizontalSpacer(size=20)) self.graphVisibilityButton = QPushButton() self.graphVisibilityButton.setSizePolicy(buttonSizePolicy) self.graphVisibilityButton.setIcon(QIcon(QPixmap(':/graph_visibility_on_16'))) self.horizontalLayout.addWidget(self.graphVisibilityButton) self.piechartVisibilityButton = QPushButton() self.piechartVisibilityButton.setSizePolicy(buttonSizePolicy) self.piechartVisibilityButton.setIcon(QIcon(QPixmap(':/piechart_visibility_on_16'))) self.horizontalLayout.addWidget(self.piechartVisibilityButton) self.columnsVisibilityButton = QPushButton() self.columnsVisibilityButton.setSizePolicy(buttonSizePolicy) self.columnsVisibilityButton.setIcon(QIcon(QPixmap(':/columns_16'))) self.horizontalLayout.addWidget(self.columnsVisibilityButton) self.statusLabel = QLabel(tr("Status")) self.statusLabel.setAlignment(Qt.AlignCenter) self.horizontalLayout.addWidget(self.statusLabel) self.verticalLayout.addWidget(self.bottomBar) self.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 700, 20)) self.menuFile = QMenu(tr("File")) self.menuOpenRecent = QMenu(tr("Open Recent")) self.menuView = QMenu(tr("View")) self.menuDateRange = QMenu(tr("Date Range")) self.menuEdit = QMenu(tr("Edit")) self.menuHelp = QMenu(tr("Help")) self.setMenuBar(self.menubar) self.actionOpenDocument = QAction(tr("Open..."), self) self.actionOpenDocument.setShortcut("Ctrl+O") self.actionShowNetWorth = QAction(tr("Net Worth"), self) self.actionShowNetWorth.setShortcut("Ctrl+1") self.actionShowNetWorth.setIcon(QIcon(QPixmap(':/balance_sheet_48'))) self.actionShowProfitLoss = QAction(escapeamp(tr("Profit & Loss")), self) self.actionShowProfitLoss.setShortcut("Ctrl+2") self.actionShowProfitLoss.setIcon(QIcon(QPixmap(':/income_statement_48'))) self.actionShowTransactions = QAction(tr("Transactions"), self) self.actionShowTransactions.setShortcut("Ctrl+3") self.actionShowTransactions.setIcon(QIcon(QPixmap(':/transaction_table_48'))) self.actionShowSelectedAccount = QAction(tr("Show Account"), self) self.actionShowSelectedAccount.setShortcut("Ctrl+]") self.actionNewItem = QAction(tr("New Item"), self) self.actionNewItem.setShortcut("Ctrl+N") self.actionDeleteItem = QAction(tr("Remove Selected"), self) self.actionEditItem = QAction(tr("Show Info"), self) self.actionEditItem.setShortcut("Ctrl+I") self.actionToggleGraph = QAction(tr("Toggle Graph"), self) self.actionToggleGraph.setShortcut("Ctrl+Alt+G") self.actionTogglePieChart = QAction(tr("Toggle Pie Chart"), self) self.actionTogglePieChart.setShortcut("Ctrl+Alt+P") self.actionMoveUp = QAction(tr("Move Up"), self) self.actionMoveUp.setShortcut("Ctrl++") self.actionMoveDown = QAction(tr("Move Down"), self) self.actionMoveDown.setShortcut("Ctrl+-") self.actionNavigateBack = QAction(tr("Go Back"), self) self.actionNavigateBack.setShortcut("Ctrl+[") self.actionNewAccountGroup = QAction(tr("New Account Group"), self) self.actionNewAccountGroup.setShortcut("Ctrl+Shift+N") self.actionShowNextView = QAction(tr("Next View"), self) self.actionShowNextView.setShortcut("Ctrl+Shift+]") self.actionShowPreviousView = QAction(tr("Previous View"), self) self.actionShowPreviousView.setShortcut("Ctrl+Shift+[") self.actionNewDocument = QAction(tr("New Document"), self) self.actionImport = QAction(tr("Import..."), self) self.actionImport.setShortcut("Ctrl+Alt+I") self.actionExport = QAction(tr("Export..."), self) self.actionExport.setShortcut("Ctrl+Alt+E") self.actionSave = QAction(tr("Save"), self) self.actionSave.setShortcut("Ctrl+S") self.actionSaveAs = QAction(tr("Save As..."), self) self.actionSaveAs.setShortcut("Ctrl+Shift+S") self.actionAbout = QAction(tr("About moneyGuru"), self) self.actionToggleReconciliationMode = QAction(tr("Toggle Reconciliation Mode"), self) self.actionToggleReconciliationMode.setShortcut("Ctrl+Shift+R") self.actionToggleAccountExclusion = QAction(tr("Toggle Exclusion Status of Account"), self) self.actionToggleAccountExclusion.setShortcut("Ctrl+Shift+X") self.actionShowSchedules = QAction(tr("Schedules"), self) self.actionShowSchedules.setShortcut("Ctrl+4") self.actionShowSchedules.setIcon(QIcon(QPixmap(':/schedules_48'))) self.actionReconcileSelected = QAction(tr("Reconcile Selection"), self) self.actionReconcileSelected.setShortcut("Ctrl+R") self.actionMakeScheduleFromSelected = QAction(tr("Make Schedule from Selected"), self) self.actionMakeScheduleFromSelected.setShortcut("Ctrl+M") self.actionShowPreferences = QAction(tr("Preferences..."), self) self.actionPrint = QAction(tr("Print..."), self) self.actionPrint.setShortcut("Ctrl+P") self.actionQuit = QAction(tr("Quit moneyGuru"), self) self.actionQuit.setShortcut("Ctrl+Q") self.actionUndo = QAction(tr("Undo"), self) self.actionUndo.setShortcut("Ctrl+Z") self.actionRedo = QAction(tr("Redo"), self) self.actionRedo.setShortcut("Ctrl+Y") self.actionShowHelp = QAction(tr("moneyGuru Help"), self) self.actionShowHelp.setShortcut("F1") self.actionDuplicateTransaction = QAction(tr("Duplicate Transaction"), self) self.actionDuplicateTransaction.setShortcut("Ctrl+D") self.actionJumpToAccount = QAction(tr("Jump to Account..."), self) self.actionJumpToAccount.setShortcut("Ctrl+Shift+A") self.actionNewTab = QAction(tr("New Tab"), self) self.actionNewTab.setShortcut("Ctrl+T") self.actionCloseTab = QAction(tr("Close Tab"), self) self.actionCloseTab.setShortcut("Ctrl+W") self.menuFile.addAction(self.actionNewDocument) self.menuFile.addAction(self.actionNewTab) self.menuFile.addAction(self.actionOpenDocument) self.menuFile.addAction(self.menuOpenRecent.menuAction()) self.menuFile.addAction(self.actionImport) self.menuFile.addSeparator() self.menuFile.addAction(self.actionCloseTab) self.menuFile.addAction(self.actionSave) self.menuFile.addAction(self.actionSaveAs) self.menuFile.addAction(self.actionExport) self.menuFile.addAction(self.actionPrint) self.menuFile.addAction(self.actionQuit) self.menuView.addAction(self.actionShowNetWorth) self.menuView.addAction(self.actionShowProfitLoss) self.menuView.addAction(self.actionShowTransactions) self.menuView.addAction(self.actionShowSchedules) self.menuView.addAction(self.actionShowPreviousView) self.menuView.addAction(self.actionShowNextView) self.menuView.addAction(self.menuDateRange.menuAction()) self.menuView.addAction(self.actionShowPreferences) self.menuView.addAction(self.actionToggleGraph) self.menuView.addAction(self.actionTogglePieChart) self.menuEdit.addAction(self.actionNewItem) self.menuEdit.addAction(self.actionNewAccountGroup) self.menuEdit.addAction(self.actionDeleteItem) self.menuEdit.addAction(self.actionEditItem) self.menuEdit.addSeparator() self.menuEdit.addAction(self.actionMoveUp) self.menuEdit.addAction(self.actionMoveDown) self.menuEdit.addAction(self.actionDuplicateTransaction) self.menuEdit.addAction(self.actionMakeScheduleFromSelected) self.menuEdit.addAction(self.actionReconcileSelected) self.menuEdit.addAction(self.actionToggleReconciliationMode) self.menuEdit.addAction(self.actionToggleAccountExclusion) self.menuEdit.addSeparator() self.menuEdit.addAction(self.actionShowSelectedAccount) self.menuEdit.addAction(self.actionNavigateBack) self.menuEdit.addAction(self.actionJumpToAccount) self.menuEdit.addSeparator() self.menuEdit.addAction(self.actionUndo) self.menuEdit.addAction(self.actionRedo) self.menuHelp.addAction(self.actionShowHelp) self.menuHelp.addAction(self.actionAbout) mainmenus = [self.menuFile, self.menuEdit, self.menuView, self.menuHelp] for menu in mainmenus: self.menubar.addAction(menu.menuAction()) setAccelKeys(menu) setAccelKeys(self.menubar) self.tabBar.setMovable(True) self.tabBar.setTabsClosable(True) self.tabBar.setExpanding(False) seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Right) self._shortcutNextTab = QShortcut(seq, self) seq = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Left) self._shortcutPrevTab = QShortcut(seq, self) def _bindSignals(self): self.newItemButton.clicked.connect(self.actionNewItem.trigger) self.deleteItemButton.clicked.connect(self.actionDeleteItem.trigger) self.editItemButton.clicked.connect(self.actionEditItem.trigger) self.graphVisibilityButton.clicked.connect(self.actionToggleGraph.trigger) self.piechartVisibilityButton.clicked.connect(self.actionTogglePieChart.trigger) self.columnsVisibilityButton.clicked.connect(self.columnsVisibilityButtonClicked) self.recentDocuments.mustOpenItem.connect(self.open) self.tabBar.currentChanged.connect(self.currentTabChanged) self.tabBar.tabCloseRequested.connect(self.tabCloseRequested) self.tabBar.tabMoved.connect(self.tabMoved) self.tabBar.plusClicked.connect(self.model.new_tab) # Views self.actionShowNetWorth.triggered.connect(self.showNetWorthTriggered) self.actionShowProfitLoss.triggered.connect(self.showProfitLossTriggered) self.actionShowTransactions.triggered.connect(self.showTransactionsTriggered) self.actionShowSchedules.triggered.connect(self.showSchedulesTriggered) self.actionShowPreviousView.triggered.connect(self.showPreviousViewTriggered) self.actionShowNextView.triggered.connect(self.showNextViewTriggered) self.actionShowPreferences.triggered.connect(self.app.showPreferences) self.actionToggleGraph.triggered.connect(self.toggleGraphTriggered) self.actionTogglePieChart.triggered.connect(self.togglePieChartTriggered) # Document Edition self.actionNewItem.triggered.connect(self.newItemTriggered) self.actionNewAccountGroup.triggered.connect(self.newAccountGroupTriggered) self.actionDeleteItem.triggered.connect(self.deleteItemTriggered) self.actionEditItem.triggered.connect(self.editItemTriggered) self.actionMoveUp.triggered.connect(self.moveUpTriggered) self.actionMoveDown.triggered.connect(self.moveDownTriggered) self.actionDuplicateTransaction.triggered.connect(self.model.duplicate_item) self.actionUndo.triggered.connect(self.model.undo) self.actionRedo.triggered.connect(self.model.redo) # Open / Save / Import / Export / New self.actionNewDocument.triggered.connect(self.new) self.actionOpenDocument.triggered.connect(self.openDocument) self.actionImport.triggered.connect(self.importDocument) self.actionSave.triggered.connect(self.save) self.actionSaveAs.triggered.connect(self.saveAs) self.actionExport.triggered.connect(self.model.export) # Misc self.actionNewTab.triggered.connect(self.model.new_tab) self.actionCloseTab.triggered.connect(self.closeTabTriggered) self.actionShowSelectedAccount.triggered.connect(self.model.show_account) self.actionNavigateBack.triggered.connect(self.navigateBackTriggered) self.actionJumpToAccount.triggered.connect(self.jumpToAccountTriggered) self.actionMakeScheduleFromSelected.triggered.connect(self.makeScheduleFromSelectedTriggered) self.actionReconcileSelected.triggered.connect(self.reconcileSelectedTriggered) self.actionToggleReconciliationMode.triggered.connect(self.toggleReconciliationModeTriggered) self.actionToggleAccountExclusion.triggered.connect(self.toggleAccountExclusionTriggered) self.actionPrint.triggered.connect(self._print) self.actionShowHelp.triggered.connect(self.app.showHelp) self.actionAbout.triggered.connect(self.aboutTriggered) self.actionQuit.triggered.connect(self.close) # Extra Shortcuts self._shortcutNextTab.activated.connect(self.showNextViewTriggered) self._shortcutPrevTab.activated.connect(self.showPreviousViewTriggered) # --- QWidget overrides def closeEvent(self, event): if self.confirmDestructiveAction(): event.accept() else: event.ignore() # --- Private def _print(self): dialog = QPrintDialog(self) if dialog.exec_() != QPrintDialog.Accepted: return printer = dialog.printer() currentView = self.mainView.currentWidget() viewPrinter = ViewPrinter(printer, currentView) currentView.fitViewsForPrint(viewPrinter) viewPrinter.render() def _getViewforPane(self, pane_type, pane_view): if pane_view in self.model2view: view = self.model2view[pane_view] else: view = PANETYPE2VIEWCLASS[pane_type](model=pane_view, mainwindow=self) self.model2view[pane_view] = view self.mainView.addWidget(view) view.restoreSubviewsSize() return view def _setTabIndex(self, index): if not self.tabBar.count(): return self.tabBar.setCurrentIndex(index) self._updateActionsState() pane_type = self.model.pane_type(index) pane_view = self.model.pane_view(index) view = self._getViewforPane(pane_type, pane_view) self.mainView.setCurrentWidget(view) view.setFocus() def _activeView(self): paneIndex = self.model.current_pane_index return self.model.pane_view(paneIndex) def _updateActionsState(self): # Updates enable/disable checked/unchecked state of all actions. These state can change # under various conditions: main view change, date range type change and when reconciliation # mode is toggled # Determine what actions are enabled view = self._activeView() viewType = view.VIEW_TYPE isSheet = viewType in {PaneType.NetWorth, PaneType.Profit} isTransactionOrEntryTable = viewType in {PaneType.Transaction, PaneType.Account} canToggleReconciliation = viewType == PaneType.Account and view.can_toggle_reconciliation_mode newItemLabel = { PaneType.NetWorth: tr("New Account"), PaneType.Profit: tr("New Account"), PaneType.Transaction: tr("New Transaction"), PaneType.Account: tr("New Transaction"), PaneType.Schedule: tr("New Schedule"), PaneType.GeneralLedger: tr("New Transaction"), }.get(viewType, tr("New Item")) # XXX make "New Item" disabled self.actionNewItem.setText(newItemLabel) self.actionNewAccountGroup.setEnabled(isSheet) self.actionMoveDown.setEnabled(isTransactionOrEntryTable) self.actionMoveUp.setEnabled(isTransactionOrEntryTable) self.actionDuplicateTransaction.setEnabled(isTransactionOrEntryTable) self.actionMakeScheduleFromSelected.setEnabled(isTransactionOrEntryTable) self.actionReconcileSelected.setEnabled(viewType == PaneType.Account and view.reconciliation_mode) self.actionShowNextView.setEnabled(self.model.current_pane_index < self.model.pane_count-1) self.actionShowPreviousView.setEnabled(self.model.current_pane_index > 0) self.actionShowSelectedAccount.setEnabled(isSheet or isTransactionOrEntryTable) self.actionNavigateBack.setEnabled(viewType == PaneType.Account) self.actionToggleReconciliationMode.setEnabled(canToggleReconciliation) self.actionToggleAccountExclusion.setEnabled(isSheet) def _updateUndoActions(self): if self.doc.can_undo(): self.actionUndo.setEnabled(True) self.actionUndo.setText(tr("Undo {0}").format(self.doc.undo_description())) else: self.actionUndo.setEnabled(False) self.actionUndo.setText(tr("Undo")) if self.doc.can_redo(): self.actionRedo.setEnabled(True) self.actionRedo.setText(tr("Redo {0}").format(self.doc.redo_description())) else: self.actionRedo.setEnabled(False) self.actionRedo.setText(tr("Redo")) # --- Actions # Document open/save/close def confirmDestructiveAction(self): # Asks whether the user wants to continue before continuing with an action that will replace # the current document. Will save the document as needed. Returns True if the action can # continue. if not self.model.document.is_dirty(): return True title = tr("Unsaved Document") msg = tr("Do you want to save your changes before continuing?") buttons = QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard result = QMessageBox.question(self.app.mainWindow, title, msg, buttons) if result == QMessageBox.Save: self.save() if self.model.document.is_dirty(): # "save as" was cancelled return False else: return True elif result == QMessageBox.Cancel: return False elif result == QMessageBox.Discard: return True def new(self): if not self.confirmDestructiveAction(): return self.model.close() self.documentPath = None self.model.clear() self.documentPathChanged() def open(self, docpath, initial=False): # initial flag is true when open() is called at the document's initialization. When that's # the case, we need to create a new document when we fail opening this one. if not self.confirmDestructiveAction(): return self.model.close() try: self.model.load_from_xml(docpath) self.documentPath = docpath except FileFormatError as e: QMessageBox.warning(self.app.mainWindow, tr("Cannot load file"), str(e)) if initial: self.new() self.documentPathChanged() self.recentDocuments.insertItem(docpath) def openDocument(self): title = tr("Select a document to load") filters = tr("moneyGuru Documents (*.moneyguru)") docpath, filetype = QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters) if docpath: self.open(docpath) def save(self): if self.documentPath is not None: self.model.save_to_xml(self.documentPath) else: self.saveAs() def saveAs(self): title = tr("Save As") filters = tr("moneyGuru Documents (*.moneyguru)") docpath = QFileDialog.getSaveFileName(self.app.mainWindow, title, '', filters)[0] if docpath: if not docpath.endswith('.moneyguru'): docpath += '.moneyguru' self.model.save_to_xml(docpath) self.documentPath = docpath self.documentPathChanged() self.recentDocuments.insertItem(docpath) # Views def showNetWorthTriggered(self): self.model.select_pane_of_type(PaneType.NetWorth) def showProfitLossTriggered(self): self.model.select_pane_of_type(PaneType.Profit) def showTransactionsTriggered(self): self.model.select_pane_of_type(PaneType.Transaction) def showSchedulesTriggered(self): self.model.select_pane_of_type(PaneType.Schedule) def showPreviousViewTriggered(self): self.model.select_previous_view() def showNextViewTriggered(self): self.model.select_next_view() # Document Edition def newItemTriggered(self): self.model.new_item() def newAccountGroupTriggered(self): self.model.new_group() def deleteItemTriggered(self): self.model.delete_item() def editItemTriggered(self): self.model.edit_item() def moveUpTriggered(self): self.model.move_up() def moveDownTriggered(self): self.model.move_down() # Misc def closeTabTriggered(self): self.model.close_pane(self.model.current_pane_index) def navigateBackTriggered(self): self.model.navigate_back() def jumpToAccountTriggered(self): self.model.jump_to_account() def makeScheduleFromSelectedTriggered(self): self.model.make_schedule_from_selected() def reconcileSelectedTriggered(self): self._activeView().etable.toggle_reconciled() def toggleReconciliationModeTriggered(self): self._activeView().toggle_reconciliation_mode() self._updateActionsState() def toggleAccountExclusionTriggered(self): viewType = self.model.pane_type(self.model.current_pane_index) if viewType in {PaneType.NetWorth, PaneType.Profit}: self._activeView().sheet.toggle_excluded() def toggleGraphTriggered(self): self.model.toggle_area_visibility(PaneArea.BottomGraph) def togglePieChartTriggered(self): self.model.toggle_area_visibility(PaneArea.RightChart) def columnsVisibilityButtonClicked(self): items = self.model.column_menu_items() if not items: return menu = QMenu() for i, (display, marked) in enumerate(items): action = menu.addAction(display) action.setCheckable(True) action.setChecked(marked) action.setData(i) action.triggered.connect(self.columnsMenuItemWasClicked) self._columnMenuHolder = menu # we need to hold a reference to it while it popups button = self.columnsVisibilityButton menu.popup(button.parentWidget().mapToGlobal(button.geometry().topLeft())) def columnsMenuItemWasClicked(self): action = self.sender() if action is not None: index = action.data() self.model.toggle_column_menu_item(index) def aboutTriggered(self): self.app.showAboutBox() def importDocument(self): title = tr("Select a document to import") filters = tr("Supported files (*.moneyguru *.ofx *.qfx *.qif *.csv *.txt);;All files (*)") docpath, filetype = QFileDialog.getOpenFileName(self.app.mainWindow, title, '', filters) # There's a strange glitch under GNOME where, right after the dialog is gone, the main # window isn't the active window, but it will become active if we give it enough time. If we # start showing the import window before that happens, we'll end up with an import window # under the main window, which is bad. Therefore, we process events until this happens. We # do this in a big forloop instead of a while to avoid a possible infinite loop. for i in range(10000): if self.app.mainWindow.isActiveWindow(): break QApplication.processEvents() if docpath: try: self.model.parse_file_for_import(docpath) except FileFormatError as e: QMessageBox.warning(self.app.mainWindow, tr("Cannot import file"), str(e)) # --- Other Signals def currentTabChanged(self, index): self.model.current_pane_index = index self._setTabIndex(index) def documentPathChanged(self): if self.documentPath: title = "moneyGuru ({})".format(self.documentPath) else: title = "moneyGuru" self.setWindowTitle(title) def tabCloseRequested(self, index): self.model.close_pane(index) def tabMoved(self, fromIndex, toIndex): # We don't refresh panes because tabMoved is apparently now called *during* drag operations. # If we start a full pane refresh during a drag operation, we segfault. self.model.move_pane(fromIndex, toIndex, refresh_panes=False) # --- document model --> view def query_for_schedule_scope(self): if QApplication.keyboardModifiers() & Qt.ShiftModifier: return ScheduleScope.Global if not self.app.model.show_schedule_scope_dialog: return ScheduleScope.Local dialog = ScheduleScopeDialog(self) return dialog.queryForScope() # --- model --> view def change_current_pane(self): self._setTabIndex(self.model.current_pane_index) def get_panel_view(self, model): if isinstance(model, CustomDateRangePanelModel): return CustomDateRangePanel(model, self) elif isinstance(model, CSVOptionsModel): return CSVOptionsWindow(model, self) elif isinstance(model, ImportWindowModel): return ImportWindow(model, self, self.app.prefs) else: return ExportPanel(model, self) def refresh_panes(self): while self.tabBar.count() < self.model.pane_count: self.tabBar.addTab('') for i in range(self.model.pane_count): pane_label = self.model.pane_label(i) pane_label = escapeamp(pane_label) self.tabBar.setTabText(i, pane_label) pane_type = self.model.pane_type(i) pane_view = self.model.pane_view(i) # Ensure that the view's "view" has been created and bound self._getViewforPane(pane_type, pane_view) iconname = PANETYPE2ICON.get(pane_type) icon = QIcon(QPixmap(':/{0}'.format(iconname))) if iconname else QIcon() self.tabBar.setTabIcon(i, icon) # It's important that we proceed with tab removal *after* we've completed tab initialization. # We're walking on eggshells here. refresh_panes() can be called in multiple situations, one # of them is during the opening of a document. When that happens when another document was # previously opened, all views' model are uninitalized and don't have their "view" attribute # set yet. If we proceed with the setCurrentIndex() call below before _getViewforPane() # could be called above, we get a crash. if self.tabBar.currentIndex() >= self.model.pane_count: # Normally, we don't touch the tabBar index here and wait for change_current_pane, # but when we remove tabs, it's possible that currentTabChanged end up being called and # then the tab selection is bugged. I tried disconnecting/reconnecting the signal, but # this is buggy. So when a selected tab is about to be removed and is out of bounds, # we change the selection to the last index in the model. We don't use # self.model.current_pane_index because in some cases, it's -1 and prevents this crash # preventer from preventing its crash. self.tabBar.setCurrentIndex(self.model.pane_count - 1) while self.tabBar.count() > self.model.pane_count: self.tabBar.removeTab(self.tabBar.count()-1) self.tabBar.setTabsClosable(self.model.pane_count > 1) def refresh_status_line(self): self.statusLabel.setText(self.model.status_line) def refresh_undo_actions(self): self._updateUndoActions() def restore_window_frame(self, frame): self.setGeometry(*frame) def save_window_frame(self): r = self.geometry() return (r.x(), r.y(), r.width(), r.height()) def show_message(self, msg): title = tr("Warning") QMessageBox.warning(self, title, msg) def update_area_visibility(self): hidden = self.model.hidden_areas graphimg = ':/graph_visibility_{}_16'.format('off' if PaneArea.BottomGraph in hidden else 'on') pieimg = ':/piechart_visibility_{}_16'.format('off' if PaneArea.RightChart in hidden else 'on') self.graphVisibilityButton.setIcon(QIcon(QPixmap(graphimg))) self.piechartVisibilityButton.setIcon(QIcon(QPixmap(pieimg))) def view_closed(self, index): self.tabBar.removeTab(index) self.tabBar.setTabsClosable(self.model.pane_count > 1)
class MainWindow(QMainWindow, Ui_MainWindow): """Frontend main window""" rom_opened = pyqtSignal() rom_closed = pyqtSignal() file_open = pyqtSignal(str, str) file_opening = pyqtSignal(str) set_caption = pyqtSignal(str) state_changed = pyqtSignal(tuple) save_image = pyqtSignal(bool) info_dialog = pyqtSignal(str) archive_dialog = pyqtSignal(list) def __init__(self, optparse): """Constructor""" QMainWindow.__init__(self, None) self.setupUi(self) self.opts, self.args = optparse logview.setParent(self) logview.setWindowFlags(Qt.Dialog) self.statusbar_label = QLabel() self.statusbar_label.setIndent(2) self.statusbar_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self.statusbar.addPermanentWidget(self.statusbar_label, 1) self.update_status( self.tr("Welcome to M64Py version %s." % FRONTEND_VERSION)) self.sizes = { SIZE_1X: self.action1X, SIZE_2X: self.action2X, SIZE_3X: self.action3X } self.slots = {} self.view = None self.stack = None self.glwidget = None self.cheats = None self.maximized = False self.widgets_height = None self.settings = Settings(self) self.worker = Worker(self) self.vidext = bool(self.settings.get_int_safe("enable_vidext", 1)) self.create_state_slots() self.create_widgets() self.recent_files = RecentFiles(self) self.connect_signals() self.worker.init() def closeEvent(self, event): self.worker.quit() def changeEvent(self, event): if event.type() == QEvent.WindowStateChange: if event.oldState() == Qt.WindowMaximized: self.maximized = False elif event.oldState() == Qt.WindowNoState and \ self.windowState() == Qt.WindowMaximized: self.maximized = True def resizeEvent(self, event): event.ignore() size = event.size() width, height = size.width(), size.height() if self.widgets_height: width, height = size.width(), size.height() - self.widgets_height self.window_size_triggered((width, height)) else: width, height = size.width(), size.height() self.resize(width, height) def showEvent(self, event): if not self.widgets_height: width, height = self.settings.get_size_safe() menubar_height = self.menubar.size().height() statusbar_height = self.statusbar.size().height() self.widgets_height = menubar_height + statusbar_height self.resize(width, height + self.widgets_height) self.create_size_actions() self.center_widget() def window_size_triggered(self, size): width, height = size if self.vidext and self.worker.core.get_handle(): fullscreen = self.window().isFullScreen() # event.ignore() doesn't work on windows if not sys.platform == "win32": if not fullscreen and \ bool(self.settings.get_int_safe("keep_aspect", 1)): width, height = self.keep_aspect(size) self.worker.core.config.open_section("Video-General") self.worker.core.config.set_parameter("ScreenWidth", width) self.worker.core.config.set_parameter("ScreenHeight", height) if not fullscreen: video_size = (width << 16) + height else: video_size = (width << 16) + (height + self.widgets_height) if self.worker.state in (M64EMU_RUNNING, M64EMU_PAUSED): self.worker.core_state_set(M64CORE_VIDEO_SIZE, video_size) self.set_sizes((width, height)) self.settings.qset.setValue("size", (width, height)) self.resize(width, height + self.widgets_height) def set_sizes(self, size): """Sets 'Window Size' radio buttons on resize event.""" width, height = size if size in self.sizes.keys(): self.sizes[(width, height)].setChecked(True) else: for action in self.sizes.values(): action.setChecked(False) def keep_aspect(self, size): """Keeps 4:3 aspect ratio.""" width, height = size if self.maximized: return width, height fixed_ratio = 1.3333333333333333 current_ratio = float(width) / float(height) if fixed_ratio > current_ratio: w = int(width) h = int(width / fixed_ratio) else: h = int(height) w = int(height * fixed_ratio) return w, h def center_widget(self): """Centers widget on desktop.""" size = self.size() desktop = QApplication.desktop() width, height = size.width(), size.height() dwidth, dheight = desktop.width(), desktop.height() cw, ch = (dwidth / 2) - (width / 2), (dheight / 2) - (height / 2) self.move(cw, ch) def connect_signals(self): """Connects signals.""" self.rom_opened.connect(self.on_rom_opened) self.rom_closed.connect(self.on_rom_closed) self.file_open.connect(self.on_file_open) self.file_opening.connect(self.on_file_opening) self.set_caption.connect(self.on_set_caption) self.state_changed.connect(self.on_state_changed) self.save_image.connect(self.on_save_image) self.info_dialog.connect(self.on_info_dialog) self.archive_dialog.connect(self.on_archive_dialog) def create_widgets(self): """Creates central widgets.""" self.stack = QStackedWidget(self) self.setCentralWidget(self.stack) self.view = View(self) self.stack.addWidget(self.view) self.glwidget = GLWidget(self) self.worker.video.set_widget(self) self.stack.addWidget(self.glwidget) self.stack.setCurrentWidget(self.view) def create_state_slots(self): """Creates state slot actions.""" group = QActionGroup(self) group.setExclusive(True) for slot in range(10): self.slots[slot] = QAction(self) self.slots[slot].setCheckable(True) self.slots[slot].setText("Slot %d" % slot) self.slots[slot].setShortcut(QKeySequence(str(slot))) self.slots[slot].setActionGroup(group) self.menuStateSlot.addAction(self.slots[slot]) self.slots[0].setChecked(True) for slot, action in self.slots.items(): action.triggered.connect( lambda t, s=slot: self.worker.state_set_slot(s)) def create_size_actions(self): """Creates window size actions.""" group = QActionGroup(self) group.setExclusive(True) for num, size in enumerate(sorted(self.sizes.keys()), 1): width, height = size action = self.sizes[size] action.setActionGroup(group) w, h = width, height + self.widgets_height action.setText("%dX" % num) action.setToolTip("%sx%s" % (width, height)) action.triggered.connect(lambda t, wi=w, he=h: self.resize(wi, he)) def on_file_open(self, filepath=None, filename=None): """Opens ROM file.""" if not filepath: action = self.sender() filepath = action.data() self.worker.core_state_query(M64CORE_EMU_STATE) if self.worker.state in [M64EMU_RUNNING, M64EMU_PAUSED]: self.worker.stop() self.worker.set_filepath(filepath, filename) self.worker.start() self.raise_() def update_status(self, status): """Updates label in status bar.""" self.statusbar_label.setText(status) def on_set_caption(self, title): """Sets window title.""" self.setWindowTitle(title) def on_file_opening(self, filepath): """Updates status on file opening.""" self.update_status("Loading %s..." % (os.path.basename(filepath))) def on_save_image(self, title): """Saves snapshot or title image.""" self.worker.save_image(title) def on_info_dialog(self, info): """Shows info dialog.""" self.settings.show_page(0) self.settings.raise_() InfoDialog(self.settings, info) def on_archive_dialog(self, files): """Shows archive dialog.""" archive = ArchiveDialog(self, files) rval = archive.exec_() if rval == QDialog.Accepted: curr_item = archive.listWidget.currentItem() fname = curr_item.data(Qt.UserRole) self.worker.filename = fname def on_state_changed(self, states): """Toggles actions state.""" load, pause, action, cheats = states self.menuLoad.setEnabled(load) self.menuRecent.setEnabled(load) self.menuStateSlot.setEnabled(load) self.actionLoadState.setEnabled(action) self.actionSaveState.setEnabled(action) self.actionLoadFrom.setEnabled(action) self.actionSaveAs.setEnabled(action) self.actionSaveScreenshot.setEnabled(action) self.actionShowROMInfo.setEnabled(action) self.actionMute.setEnabled(action) self.actionStop.setEnabled(action) self.actionReset.setEnabled(action) self.actionSoftReset.setEnabled(action) self.actionLimitFPS.setEnabled(action) self.actionSlowDown.setEnabled(action) self.actionSpeedUp.setEnabled(action) self.actionFullscreen.setEnabled(action) self.actionCheats.setEnabled(cheats) self.actionPause.setEnabled(pause) self.actionPaths.setEnabled(not action) self.actionEmulator.setEnabled(not action) self.actionGraphics.setEnabled(not action) self.actionPlugins.setEnabled(not action) def on_rom_opened(self): if self.vidext: self.stack.setCurrentWidget(self.glwidget) self.glwidget.setFocus(True) if not self.cheats: self.cheats = Cheat(self) self.update_status(self.worker.core.rom_settings.goodname.decode()) QTimer.singleShot(2000, self.worker.toggle_actions) def on_rom_closed(self): if self.vidext and self.isFullScreen(): self.glwidget.toggle_fs.emit() self.stack.setCurrentWidget(self.view) self.actionMute.setChecked(False) self.actionPause.setChecked(False) self.actionLimitFPS.setChecked(True) self.on_set_caption("M64Py") self.update_status("ROM closed.") del self.cheats self.cheats = None @pyqtSlot() def on_actionManually_triggered(self): """Shows ROM file dialog.""" dialog = QFileDialog() dialog.setFileMode(QFileDialog.ExistingFile) last_dir = self.settings.qset.value("last_dir") file_path, _ = dialog.getOpenFileName( self, self.tr("Load ROM Image"), last_dir, "Nintendo64 ROM (%s);;All files (*)" % EXT_FILTER) if file_path: self.file_open.emit(file_path, None) last_dir = QFileInfo(file_path).path() self.settings.qset.setValue("last_dir", last_dir) @pyqtSlot() def on_actionFromList_triggered(self): """Shows ROM list.""" ROMList(self) @pyqtSlot() def on_actionShowROMInfo_triggered(self): """Shows ROM information.""" RomInfo(self) @pyqtSlot() def on_actionLoadState_triggered(self): """Loads state.""" self.worker.state_load() @pyqtSlot() def on_actionSaveState_triggered(self): """Saves state.""" self.worker.state_save() @pyqtSlot() def on_actionLoadFrom_triggered(self): """Loads state from file.""" dialog = QFileDialog() dialog.setFileMode(QFileDialog.ExistingFile) file_path, _ = dialog.getOpenFileName( self, self.tr("Load State From File"), os.path.join(self.worker.core.config.get_path("UserData"), "save"), "M64P/PJ64 Saves (*.st* *.m64p *.zip *.pj);;All files (*)") if file_path: self.worker.state_load(file_path) @pyqtSlot() def on_actionSaveAs_triggered(self): """Saves state to file.""" dialog = QFileDialog() file_path, file_filter = dialog.getSaveFileName( self, self.tr("Save State To File"), os.path.join(self.worker.core.config.get_path("UserData"), "save"), ";;".join([ save_filter for save_filter, save_ext in M64P_SAVES.values() ]), M64P_SAVES[M64SAV_M64P][0]) if file_path: for save_type, filters in M64P_SAVES.items(): save_filter, save_ext = filters if file_filter == save_filter: if not file_path.endswith(save_ext): file_path = "%s.%s" % (file_path, save_ext) self.worker.state_save(file_path, save_type) @pyqtSlot() def on_actionSaveScreenshot_triggered(self): """Saves screenshot.""" self.worker.save_screenshot() @pyqtSlot() def on_actionPause_triggered(self): """Toggles pause.""" self.worker.toggle_pause() @pyqtSlot() def on_actionMute_triggered(self): """Toggles mute.""" self.worker.toggle_mute() @pyqtSlot() def on_actionStop_triggered(self): """Stops emulator.""" self.worker.stop() @pyqtSlot() def on_actionReset_triggered(self): """Resets emulator.""" self.worker.reset() @pyqtSlot() def on_actionSoftReset_triggered(self): """Resets emulator.""" self.worker.reset(True) @pyqtSlot() def on_actionLimitFPS_triggered(self): """Toggles speed limit.""" self.worker.toggle_speed_limit() @pyqtSlot() def on_actionSlowDown_triggered(self): """Speeds down emulator.""" self.worker.speed_down() @pyqtSlot() def on_actionSpeedUp_triggered(self): """Speeds up emulator.""" self.worker.speed_up() @pyqtSlot() def on_actionCheats_triggered(self): """Shows cheat dialog.""" if self.cheats: self.cheats.show() @pyqtSlot() def on_actionFullscreen_triggered(self): """Toggles fullscreen.""" self.worker.toggle_fs() @pyqtSlot() def on_actionPythonConsole_triggered(self): """Shows python console.""" pythonconsoleforeground(self) @pyqtSlot() def on_actionPaths_triggered(self): """Shows paths settings.""" self.settings.show_page(0) @pyqtSlot() def on_actionEmulator_triggered(self): """Shows emulator settings.""" self.settings.show_page(1) @pyqtSlot() def on_actionGraphics_triggered(self): """Shows emulator settings.""" self.settings.show_page(2) @pyqtSlot() def on_actionPlugins_triggered(self): """Shows plugins settings.""" self.settings.show_page(3) @pyqtSlot() def on_actionAbout_triggered(self): """Shows about dialog.""" AboutDialog(self) @pyqtSlot() def on_actionLicense_triggered(self): """Shows license dialog.""" LicenseDialog(self) @pyqtSlot() def on_actionLog_triggered(self): """Shows log dialog.""" logview.show()
class MainWindow(QMainWindow, Ui_MainWindow): """Frontend main window""" rom_opened = pyqtSignal() rom_closed = pyqtSignal() file_open = pyqtSignal(str, str) file_opening = pyqtSignal(str) set_caption = pyqtSignal(str) state_changed = pyqtSignal(tuple) save_image = pyqtSignal(bool) info_dialog = pyqtSignal(str) archive_dialog = pyqtSignal(list) def __init__(self, optparse): """Constructor""" QMainWindow.__init__(self, None) self.setupUi(self) self.opts, self.args = optparse logview.setParent(self) logview.setWindowFlags(Qt.Dialog) self.statusbar_label = QLabel() self.statusbar_label.setIndent(2) self.statusbar_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self.statusbar.addPermanentWidget(self.statusbar_label, 1) self.update_status(self.tr( "Welcome to M64Py version %s." % FRONTEND_VERSION)) self.sizes = { SIZE_1X: self.action1X, SIZE_2X: self.action2X, SIZE_3X: self.action3X} self.slots = {} self.view = None self.stack = None self.glwidget = None self.cheats = None self.maximized = False self.widgets_height = None self.settings = Settings(self) self.worker = Worker(self) self.vidext = bool(self.settings.get_int_safe("enable_vidext", 1)) self.create_state_slots() self.create_widgets() self.recent_files = RecentFiles(self) self.connect_signals() self.worker.init() def closeEvent(self, event): self.worker.quit() def changeEvent(self, event): if event.type() == QEvent.WindowStateChange: if event.oldState() == Qt.WindowMaximized: self.maximized = False elif event.oldState() == Qt.WindowNoState and \ self.windowState() == Qt.WindowMaximized: self.maximized = True def resizeEvent(self, event): event.ignore() size = event.size() width, height = size.width(), size.height() if self.widgets_height: width, height = size.width(), size.height() - self.widgets_height self.window_size_triggered((width, height)) else: width, height = size.width(), size.height() self.resize(width, height) def showEvent(self, event): if not self.widgets_height: width, height = self.settings.get_size_safe() menubar_height = self.menubar.size().height() statusbar_height = self.statusbar.size().height() self.widgets_height = menubar_height + statusbar_height self.resize(width, height + self.widgets_height) self.create_size_actions() self.center_widget() def window_size_triggered(self, size): width, height = size if self.vidext and self.worker.core.get_handle(): fullscreen = self.window().isFullScreen() # event.ignore() doesn't work on windows if not sys.platform == "win32": if not fullscreen and \ bool(self.settings.get_int_safe("keep_aspect", 1)): width, height = self.keep_aspect(size) self.worker.core.config.open_section("Video-General") self.worker.core.config.set_parameter("ScreenWidth", width) self.worker.core.config.set_parameter("ScreenHeight", height) if not fullscreen: video_size = (width << 16) + height else: video_size = (width << 16) + (height + self.widgets_height) if self.worker.state in (M64EMU_RUNNING, M64EMU_PAUSED): self.worker.core_state_set(M64CORE_VIDEO_SIZE, video_size) self.set_sizes((width, height)) self.settings.qset.setValue("size", (width, height)) self.resize(width, height + self.widgets_height) def set_sizes(self, size): """Sets 'Window Size' radio buttons on resize event.""" width, height = size if size in self.sizes.keys(): self.sizes[(width, height)].setChecked(True) else: for action in self.sizes.values(): action.setChecked(False) def keep_aspect(self, size): """Keeps 4:3 aspect ratio.""" width, height = size if self.maximized: return width, height fixed_ratio = 1.3333333333333333 current_ratio = float(width)/float(height) if fixed_ratio > current_ratio: w = int(width) h = int(width/fixed_ratio) else: h = int(height) w = int(height*fixed_ratio) return w, h def center_widget(self): """Centers widget on desktop.""" size = self.size() desktop = QApplication.desktop() width, height = size.width(), size.height() dwidth, dheight = desktop.width(), desktop.height() cw, ch = (dwidth/2)-(width/2), (dheight/2)-(height/2) self.move(cw, ch) def connect_signals(self): """Connects signals.""" self.rom_opened.connect(self.on_rom_opened) self.rom_closed.connect(self.on_rom_closed) self.file_open.connect(self.on_file_open) self.file_opening.connect(self.on_file_opening) self.set_caption.connect(self.on_set_caption) self.state_changed.connect(self.on_state_changed) self.save_image.connect(self.on_save_image) self.info_dialog.connect(self.on_info_dialog) self.archive_dialog.connect(self.on_archive_dialog) def create_widgets(self): """Creates central widgets.""" self.stack = QStackedWidget(self) self.setCentralWidget(self.stack) self.view = View(self) self.stack.addWidget(self.view) self.glwidget = GLWidget(self) self.worker.video.set_widget(self) self.stack.addWidget(self.glwidget) self.stack.setCurrentWidget(self.view) def create_state_slots(self): """Creates state slot actions.""" group = QActionGroup(self) group.setExclusive(True) for slot in range(10): self.slots[slot] = QAction(self) self.slots[slot].setCheckable(True) self.slots[slot].setText("Slot %d" % slot) self.slots[slot].setShortcut(QKeySequence(str(slot))) self.slots[slot].setActionGroup(group) self.menuStateSlot.addAction(self.slots[slot]) self.slots[0].setChecked(True) for slot, action in self.slots.items(): action.triggered.connect(lambda t, s=slot: self.worker.state_set_slot(s)) def create_size_actions(self): """Creates window size actions.""" group = QActionGroup(self) group.setExclusive(True) for num, size in enumerate( sorted(self.sizes.keys()), 1): width, height = size action = self.sizes[size] action.setActionGroup(group) w, h = width, height+self.widgets_height action.setText("%dX" % num) action.setToolTip("%sx%s" % (width, height)) action.triggered.connect(lambda t, wi=w, he=h: self.resize(wi, he)) def on_file_open(self, filepath=None, filename=None): """Opens ROM file.""" if not filepath: action = self.sender() filepath = action.data() self.worker.core_state_query(M64CORE_EMU_STATE) if self.worker.state in [M64EMU_RUNNING, M64EMU_PAUSED]: self.worker.stop() self.worker.set_filepath(filepath, filename) self.worker.start() self.raise_() def update_status(self, status): """Updates label in status bar.""" self.statusbar_label.setText(status) def on_set_caption(self, title): """Sets window title.""" self.setWindowTitle(title) def on_file_opening(self, filepath): """Updates status on file opening.""" self.update_status("Loading %s..." % ( os.path.basename(filepath))) def on_save_image(self, title): """Saves snapshot or title image.""" self.worker.save_image(title) def on_info_dialog(self, info): """Shows info dialog.""" self.settings.show_page(0) self.settings.raise_() InfoDialog(self.settings, info) def on_archive_dialog(self, files): """Shows archive dialog.""" archive = ArchiveDialog(self, files) rval = archive.exec_() if rval == QDialog.Accepted: curr_item = archive.listWidget.currentItem() fname = curr_item.data(Qt.UserRole) self.worker.filename = fname def on_state_changed(self, states): """Toggles actions state.""" load, pause, action, cheats = states self.menuLoad.setEnabled(load) self.menuRecent.setEnabled(load) self.menuStateSlot.setEnabled(load) self.actionLoadState.setEnabled(action) self.actionSaveState.setEnabled(action) self.actionLoadFrom.setEnabled(action) self.actionSaveAs.setEnabled(action) self.actionSaveScreenshot.setEnabled(action) self.actionShowROMInfo.setEnabled(action) self.actionMute.setEnabled(action) self.actionStop.setEnabled(action) self.actionReset.setEnabled(action) self.actionSoftReset.setEnabled(action) self.actionLimitFPS.setEnabled(action) self.actionSlowDown.setEnabled(action) self.actionSpeedUp.setEnabled(action) self.actionFullscreen.setEnabled(action) self.actionCheats.setEnabled(cheats) self.actionPause.setEnabled(pause) self.actionPaths.setEnabled(not action) self.actionEmulator.setEnabled(not action) self.actionGraphics.setEnabled(not action) self.actionPlugins.setEnabled(not action) def on_rom_opened(self): if self.vidext: self.stack.setCurrentWidget(self.glwidget) self.glwidget.setFocus(True) if not self.cheats: self.cheats = Cheat(self) self.update_status(self.worker.core.rom_settings.goodname.decode()) QTimer.singleShot(2000, self.worker.toggle_actions) def on_rom_closed(self): if self.vidext and self.isFullScreen(): self.glwidget.toggle_fs.emit() self.stack.setCurrentWidget(self.view) self.actionMute.setChecked(False) self.actionPause.setChecked(False) self.actionLimitFPS.setChecked(True) self.on_set_caption("M64Py") self.update_status("ROM closed.") del self.cheats self.cheats = None @pyqtSlot() def on_actionManually_triggered(self): """Shows ROM file dialog.""" dialog = QFileDialog() dialog.setFileMode(QFileDialog.ExistingFile) last_dir = self.settings.qset.value("last_dir") file_path, _ = dialog.getOpenFileName( self, self.tr("Load ROM Image"), last_dir, "Nintendo64 ROM (%s);;All files (*)" % EXT_FILTER) if file_path: self.file_open.emit(file_path, None) last_dir = QFileInfo(file_path).path() self.settings.qset.setValue("last_dir", last_dir) @pyqtSlot() def on_actionFromList_triggered(self): """Shows ROM list.""" ROMList(self) @pyqtSlot() def on_actionShowROMInfo_triggered(self): """Shows ROM information.""" RomInfo(self) @pyqtSlot() def on_actionLoadState_triggered(self): """Loads state.""" self.worker.state_load() @pyqtSlot() def on_actionSaveState_triggered(self): """Saves state.""" self.worker.state_save() @pyqtSlot() def on_actionLoadFrom_triggered(self): """Loads state from file.""" dialog = QFileDialog() dialog.setFileMode(QFileDialog.ExistingFile) file_path, _ = dialog.getOpenFileName( self, self.tr("Load State From File"), os.path.join(self.worker.core.config.get_path("UserData"), "save"), "M64P/PJ64 Saves (*.st* *.m64p *.zip *.pj);;All files (*)") if file_path: self.worker.state_load(file_path) @pyqtSlot() def on_actionSaveAs_triggered(self): """Saves state to file.""" dialog = QFileDialog() file_path, file_filter = dialog.getSaveFileName( self, self.tr("Save State To File"), os.path.join(self.worker.core.config.get_path("UserData"), "save"), ";;".join([save_filter for save_filter, save_ext in M64P_SAVES.values()]), M64P_SAVES[M64SAV_M64P][0]) if file_path: for save_type, filters in M64P_SAVES.items(): save_filter, save_ext = filters if file_filter == save_filter: if not file_path.endswith(save_ext): file_path = "%s.%s" % (file_path, save_ext) self.worker.state_save(file_path, save_type) @pyqtSlot() def on_actionSaveScreenshot_triggered(self): """Saves screenshot.""" self.worker.save_screenshot() @pyqtSlot() def on_actionPause_triggered(self): """Toggles pause.""" self.worker.toggle_pause() @pyqtSlot() def on_actionMute_triggered(self): """Toggles mute.""" self.worker.toggle_mute() @pyqtSlot() def on_actionStop_triggered(self): """Stops emulator.""" self.worker.stop() @pyqtSlot() def on_actionReset_triggered(self): """Resets emulator.""" self.worker.reset() @pyqtSlot() def on_actionSoftReset_triggered(self): """Resets emulator.""" self.worker.reset(True) @pyqtSlot() def on_actionLimitFPS_triggered(self): """Toggles speed limit.""" self.worker.toggle_speed_limit() @pyqtSlot() def on_actionSlowDown_triggered(self): """Speeds down emulator.""" self.worker.speed_down() @pyqtSlot() def on_actionSpeedUp_triggered(self): """Speeds up emulator.""" self.worker.speed_up() @pyqtSlot() def on_actionCheats_triggered(self): """Shows cheat dialog.""" if self.cheats: self.cheats.show() @pyqtSlot() def on_actionFullscreen_triggered(self): """Toggles fullscreen.""" self.worker.toggle_fs() @pyqtSlot() def on_actionPaths_triggered(self): """Shows paths settings.""" self.settings.show_page(0) @pyqtSlot() def on_actionEmulator_triggered(self): """Shows emulator settings.""" self.settings.show_page(1) @pyqtSlot() def on_actionGraphics_triggered(self): """Shows emulator settings.""" self.settings.show_page(2) @pyqtSlot() def on_actionPlugins_triggered(self): """Shows plugins settings.""" self.settings.show_page(3) @pyqtSlot() def on_actionAbout_triggered(self): """Shows about dialog.""" AboutDialog(self) @pyqtSlot() def on_actionLicense_triggered(self): """Shows license dialog.""" LicenseDialog(self) @pyqtSlot() def on_actionLog_triggered(self): """Shows log dialog.""" logview.show()
class ViewSpace(QWidget): """A ViewSpace manages a stack of views, one of them is visible. The ViewSpace also has a statusbar, accessible in the status attribute. The viewChanged(View) signal is emitted when the current view for this ViewSpace changes. Also, when a ViewSpace is created (e.g. when a window is created or split), the app.viewSpaceCreated(space) signal is emitted. You can use the app.viewSpaceCreated() and the ViewSpace.viewChanged() signals to implement things on a per ViewSpace basis, e.g. in the statusbar of a ViewSpace. """ viewChanged = pyqtSignal(view_.View) def __init__(self, manager, parent=None): super(ViewSpace, self).__init__(parent) self.manager = weakref.ref(manager) self.views = [] layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self.stack = QStackedWidget(self) layout.addWidget(self.stack) self.status = ViewStatusBar(self) self.status.setEnabled(False) layout.addWidget(self.status) app.languageChanged.connect(self.updateStatusBar) app.viewSpaceCreated(self) def activeView(self): if self.views: return self.views[-1] def document(self): """Returns the currently active document in this space. If there are no views, returns None. """ if self.views: return self.views[-1].document() def showDocument(self, doc): """Shows the document, creating a View if necessary.""" if doc is self.document(): return cur = self.activeView() for view in self.views[:-1]: if doc is view.document(): self.views.remove(view) break else: view = view_.View(doc) self.stack.addWidget(view) self.views.append(view) if cur: self.disconnectView(cur) self.connectView(view) self.stack.setCurrentWidget(view) self.updateStatusBar() def removeDocument(self, doc): active = doc is self.document() if active: self.disconnectView(self.activeView()) for view in self.views: if doc is view.document(): self.views.remove(view) view.deleteLater() break else: return if active and self.views: self.connectView(self.views[-1]) self.stack.setCurrentWidget(self.views[-1]) self.updateStatusBar() def connectView(self, view): view.installEventFilter(self) view.cursorPositionChanged.connect(self.updateCursorPosition) view.modificationChanged.connect(self.updateModificationState) view.document().urlChanged.connect(self.updateDocumentName) self.viewChanged.emit(view) def disconnectView(self, view): view.removeEventFilter(self) view.cursorPositionChanged.disconnect(self.updateCursorPosition) view.modificationChanged.disconnect(self.updateModificationState) view.document().urlChanged.disconnect(self.updateDocumentName) def eventFilter(self, view, ev): if ev.type() == QEvent.FocusIn: self.setActiveViewSpace() return False def setActiveViewSpace(self): self.manager().setActiveViewSpace(self) def updateStatusBar(self): """Update all info in the statusbar, e.g. on document change.""" if self.views: self.updateCursorPosition() self.updateModificationState() self.updateDocumentName() def updateCursorPosition(self): cur = self.activeView().textCursor() line = cur.blockNumber() + 1 try: column = cur.positionInBlock() except AttributeError: # only in very recent PyQt5 column = cur.position() - cur.block().position() self.status.positionLabel.setText( _("Line: {line}, Col: {column}").format(line=line, column=column)) def updateModificationState(self): modified = self.document().isModified() pixmap = icons.get('document-save').pixmap( 16) if modified else QPixmap() self.status.stateLabel.setPixmap(pixmap) def updateDocumentName(self): self.status.infoLabel.setText(self.document().documentName())
class DataSelectionGui(QWidget): """ Manages all GUI elements in the data selection applet. This class itself is the central widget and also owns/manages the applet drawer widgets. """ ########################################### ### AppletGuiInterface Concrete Methods ### ########################################### def centralWidget( self ): return self def appletDrawer( self ): return self._drawer def menus( self ): return [] def viewerControlWidget(self): return self._viewerControlWidgetStack def setImageIndex(self, imageIndex): if imageIndex is not None: self.laneSummaryTableView.selectRow(imageIndex) for detailWidget in self._detailViewerWidgets: detailWidget.selectRow(imageIndex) def stopAndCleanUp(self): self._cleaning_up = True for editor in list(self.volumeEditors.values()): self.viewerStack.removeWidget( editor ) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() ) editor.stopAndCleanUp() self.volumeEditors.clear() def imageLaneAdded(self, laneIndex): if len(self.laneSummaryTableView.selectedIndexes()) == 0: self.laneSummaryTableView.selectRow(laneIndex) # We don't have any real work to do because this gui initiated the lane addition in the first place if self.guiMode != GuiMode.Batch: if(len(self.topLevelOperator.DatasetGroup) != laneIndex+1): import warnings warnings.warn("DataSelectionGui.imageLaneAdded(): length of dataset multislot out of sync with laneindex [%s != %s + 1]" % (len(self.topLevelOperator.DatasetGroup), laneIndex)) def imageLaneRemoved(self, laneIndex, finalLength): # There's nothing to do here because the GUI already # handles operator resizes via slot callbacks. pass def allowLaneSelectionChange(self): return False ########################################### ########################################### class UserCancelledError(Exception): # This exception type is raised when the user cancels the # addition of dataset files in the middle of the process somewhere. # It isn't an error -- it's used for control flow. pass def __init__(self, parentApplet, dataSelectionOperator, serializer, instructionText, guiMode=GuiMode.Normal, max_lanes=None, show_axis_details=False): """ Constructor. :param dataSelectionOperator: The top-level operator. Must be of type :py:class:`OpMultiLaneDataSelectionGroup`. :param serializer: The applet's serializer. Must be of type :py:class:`DataSelectionSerializer` :param instructionText: A string to display in the applet drawer. :param guiMode: Either ``GuiMode.Normal`` or ``GuiMode.Batch``. Currently, there is no difference between normal and batch mode. :param max_lanes: The maximum number of lanes that the user is permitted to add to this workflow. If ``None``, there is no maximum. """ super(DataSelectionGui, self).__init__() self._cleaning_up = False self.parentApplet = parentApplet self._max_lanes = max_lanes self._default_h5n5_volumes = {} self.show_axis_details = show_axis_details self._viewerControls = QWidget() self.topLevelOperator = dataSelectionOperator self.guiMode = guiMode self.serializer = serializer self.threadRouter = ThreadRouter(self) self._initCentralUic() self._initAppletDrawerUic(instructionText) self._viewerControlWidgetStack = QStackedWidget(self) def handleImageRemove(multislot, index, finalLength): # Remove the viewer for this dataset datasetSlot = self.topLevelOperator.DatasetGroup[index] if datasetSlot in list(self.volumeEditors.keys()): editor = self.volumeEditors[datasetSlot] self.viewerStack.removeWidget( editor ) self._viewerControlWidgetStack.removeWidget( editor.viewerControlWidget() ) editor.stopAndCleanUp() self.topLevelOperator.DatasetGroup.notifyRemove( bind( handleImageRemove ) ) opWorkflow = self.topLevelOperator.parent assert hasattr(opWorkflow.shell, 'onSaveProjectActionTriggered'), \ "This class uses the IlastikShell.onSaveProjectActionTriggered function. Did you rename it?" def _initCentralUic(self): """ Load the GUI from the ui file into this class and connect it with event handlers. """ # Load the ui file into this class (find it in our own directory) localDir = os.path.split(__file__)[0]+'/' uic.loadUi(localDir+"/dataSelection.ui", self) self._initTableViews() self._initViewerStack() self.splitter.setSizes( [150, 850] ) def _initAppletDrawerUic(self, instructionText): """ Load the ui file for the applet drawer, which we own. """ localDir = os.path.split(__file__)[0]+'/' self._drawer = uic.loadUi(localDir+"/dataSelectionDrawer.ui") self._drawer.instructionLabel.setText( instructionText ) def _initTableViews(self): self.fileInfoTabWidget.setTabText( 0, "Summary" ) self.laneSummaryTableView.setModel( DataLaneSummaryTableModel(self, self.topLevelOperator) ) self.laneSummaryTableView.dataLaneSelected.connect( self.showDataset ) self.laneSummaryTableView.addFilesRequested.connect( self.addFiles ) self.laneSummaryTableView.addStackRequested.connect( self.addStack ) self.laneSummaryTableView.removeLanesRequested.connect( self.handleRemoveLaneButtonClicked ) # These two helper functions enable/disable an 'add files' button for a given role # based on the the max lane index for that role and the overall permitted max_lanes def _update_button_status(viewer, role_index): if self._max_lanes: viewer.setEnabled( self._findFirstEmptyLane(role_index) < self._max_lanes ) def _handle_lane_added( button, role_index, lane_slot, lane_index ): def _handle_role_slot_added( role_slot, added_slot_index, *args ): if added_slot_index == role_index: role_slot.notifyReady( bind(_update_button_status, button, role_index) ) role_slot.notifyUnready( bind(_update_button_status, button, role_index) ) lane_slot[lane_index].notifyInserted( _handle_role_slot_added ) self._retained = [] # Retain menus so they don't get deleted self._detailViewerWidgets = [] for roleIndex, role in enumerate(self.topLevelOperator.DatasetRoles.value): detailViewer = DatasetDetailedInfoTableView(self) detailViewer.setModel(DatasetDetailedInfoTableModel(self, self.topLevelOperator, roleIndex)) self._detailViewerWidgets.append( detailViewer ) # Button detailViewer.addFilesRequested.connect( partial(self.addFiles, roleIndex)) detailViewer.addStackRequested.connect( partial(self.addStack, roleIndex)) detailViewer.addPrecomputedVolumeRequested.connect( partial(self.addPrecomputedVolume, roleIndex)) detailViewer.addRemoteVolumeRequested.connect( partial(self.addDvidVolume, roleIndex)) # Monitor changes to each lane so we can enable/disable the 'add lanes' button for each tab self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, detailViewer, roleIndex ) ) self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, detailViewer, roleIndex ) ) # While we're at it, do the same for the buttons in the summary table, too self.topLevelOperator.DatasetGroup.notifyInserted( bind( _handle_lane_added, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) ) self.topLevelOperator.DatasetGroup.notifyRemoved( bind( _update_button_status, self.laneSummaryTableView.addFilesButtons[roleIndex], roleIndex ) ) # Context menu detailViewer.replaceWithFileRequested.connect( partial(self.handleReplaceFile, roleIndex) ) detailViewer.replaceWithStackRequested.connect( partial(self.addStack, roleIndex) ) detailViewer.editRequested.connect( partial(self.editDatasetInfo, roleIndex) ) detailViewer.resetRequested.connect( partial(self.handleClearDatasets, roleIndex) ) # Drag-and-drop detailViewer.addFilesRequestedDrop.connect( partial( self.addFileNames, roleIndex=roleIndex ) ) # Selection handling def showFirstSelectedDataset( _roleIndex, lanes ): if lanes: self.showDataset( lanes[0], _roleIndex ) detailViewer.dataLaneSelected.connect( partial(showFirstSelectedDataset, roleIndex) ) self.fileInfoTabWidget.insertTab(roleIndex, detailViewer, role) self.fileInfoTabWidget.currentChanged.connect( self.handleSwitchTabs ) self.fileInfoTabWidget.setCurrentIndex(0) def handleSwitchTabs(self, tabIndex ): if tabIndex < len(self._detailViewerWidgets): roleIndex = tabIndex # If summary tab is moved to the front, change this line. detailViewer = self._detailViewerWidgets[roleIndex] selectedLanes = detailViewer.selectedLanes if selectedLanes: self.showDataset( selectedLanes[0], roleIndex ) def _initViewerStack(self): self.volumeEditors = {} self.viewerStack.addWidget( QWidget() ) def handleRemoveLaneButtonClicked(self): """ The user clicked the "Remove" button. Remove the currently selected row(s) from both the GUI and the top-level operator. """ # Figure out which lanes to remove selectedIndexes = self.laneSummaryTableView.selectedIndexes() rows = set() for modelIndex in selectedIndexes: rows.add( modelIndex.row() ) # Don't remove the last row, which is just buttons. rows.discard( self.laneSummaryTableView.model().rowCount()-1 ) # Remove in reverse order so row numbers remain consistent for row in reversed(sorted(rows)): # Remove lanes from the operator. # The table model will notice the changes and update the rows accordingly. finalSize = len(self.topLevelOperator.DatasetGroup) - 1 self.topLevelOperator.DatasetGroup.removeSlot(row, finalSize) @threadRouted def showDataset(self, laneIndex, roleIndex=None): if self._cleaning_up: return if laneIndex == -1: self.viewerStack.setCurrentIndex(0) return assert threading.current_thread().name == "MainThread" if laneIndex >= len(self.topLevelOperator.DatasetGroup): return datasetSlot = self.topLevelOperator.DatasetGroup[laneIndex] # Create if necessary if datasetSlot not in list(self.volumeEditors.keys()): class DatasetViewer(LayerViewerGui): def moveToTop(self, roleIndex): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return datasetRoles = opLaneView.DatasetRoles.value if roleIndex >= len(datasetRoles): return roleName = datasetRoles[roleIndex] try: layerIndex = [l.name for l in self.layerstack].index(roleName) except ValueError: return else: self.layerstack.selectRow(layerIndex) self.layerstack.moveSelectedToTop() def setupLayers(self): opLaneView = self.topLevelOperatorView if not opLaneView.DatasetRoles.ready(): return [] layers = [] datasetRoles = opLaneView.DatasetRoles.value for roleIndex, slot in enumerate(opLaneView.ImageGroup): if slot.ready(): roleName = datasetRoles[roleIndex] layer = self.createStandardLayerFromSlot(slot) layer.name = roleName layers.append(layer) return layers opLaneView = self.topLevelOperator.getLane(laneIndex) layerViewer = DatasetViewer(self.parentApplet, opLaneView, crosshair=False) # Maximize the x-y view by default. layerViewer.volumeEditorWidget.quadview.ensureMaximized(2) self.volumeEditors[datasetSlot] = layerViewer self.viewerStack.addWidget( layerViewer ) self._viewerControlWidgetStack.addWidget( layerViewer.viewerControlWidget() ) # Show the right one viewer = self.volumeEditors[datasetSlot] displayedRole = self.fileInfoTabWidget.currentIndex() viewer.moveToTop(displayedRole) self.viewerStack.setCurrentWidget( viewer ) self._viewerControlWidgetStack.setCurrentWidget( viewer.viewerControlWidget() ) def handleReplaceFile(self, roleIndex, startingLane): self.addFiles(roleIndex, startingLane) def addFiles(self, roleIndex, startingLane=None): """ The user clicked the "Add File" button. Ask him to choose a file (or several) and add them to both the GUI table and the top-level operator inputs. """ # Find the directory of the most recently opened image file mostRecentImageFile = PreferencesManager().get( 'DataSelection', 'recent image' ) mostRecentImageFile = str(mostRecentImageFile) if mostRecentImageFile is not None: defaultDirectory = os.path.split(mostRecentImageFile)[0] else: defaultDirectory = os.path.expanduser('~') # Launch the "Open File" dialog fileNames = self.getImageFileNamesToOpen(self, defaultDirectory) # If the user didn't cancel if len(fileNames) > 0: PreferencesManager().set('DataSelection', 'recent image', fileNames[0]) try: self.addFileNames(fileNames, roleIndex, startingLane) except Exception as ex: log_exception( logger ) QMessageBox.critical(self, "Error loading file", str(ex)) @classmethod def getImageFileNamesToOpen(cls, parent_window, defaultDirectory): """ Launch an "Open File" dialog to ask the user for one or more image files. """ extensions = OpDataSelection.SupportedExtensions filter_strs = ["*." + x for x in extensions] filters = ["{filt} ({filt})".format(filt=x) for x in filter_strs] filt_all_str = "Image files (" + ' '.join(filter_strs) + ')' fileNames = [] file_dialog = QFileDialog( parent_window, caption="Select Images", directory=defaultDirectory, filter=filt_all_str) if ilastik_config.getboolean("ilastik", "debug"): # use Qt dialog in debug mode (more portable?) file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) #file_dialog.setNameFilterDetailsVisible(False) # select multiple files file_dialog.setFileMode(QFileDialog.ExistingFiles) if not file_dialog.exec_(): return [] return cls.cleanFileList(file_dialog.selectedFiles()) @staticmethod def cleanFileList(fileList: typing.List[str]) -> typing.List[str]: fileNames = [pathlib.Path(selected_file) for selected_file in fileList] # For the n5 extension the attributes.json file has to be selected in the file dialog. # However we need just the *.n5 directory-file. for i, fileName in enumerate(fileNames): # On some OS's the open file dialog allows to return file names that do not exist assert fileName.exists(), \ f"The file '{fileName}' does not exist." if fileName.name.lower() == 'attributes.json' and any(p.suffix == ".n5" for p in fileName.parents): fileNames[i] = fileName.parent fileNames = [fileName.as_posix() for fileName in fileNames] return fileNames def _findFirstEmptyLane(self, roleIndex): opTop = self.topLevelOperator # Determine the number of files this role already has # Search for the last valid value. firstNewLane = 0 for laneIndex, slot in reversed(list(zip(list(range(len(opTop.DatasetGroup))), opTop.DatasetGroup))): if slot[roleIndex].ready(): firstNewLane = laneIndex+1 break return firstNewLane def addFileNames(self, fileNames, roleIndex, startingLane=None, rois=None): """ Add the given filenames to both the GUI table and the top-level operator inputs. If startingLane is None, the filenames will be *appended* to the role's list of files. If rois is provided, it must be a list of (start,stop) tuples (one for each fileName) """ # What lanes will we touch? startingLane, endingLane = self._determineLaneRange(fileNames, roleIndex, startingLane) if startingLane is None: # Something went wrong. return # If we're only adding new lanes, NOT modifying existing lanes... adding_only = startingLane == len(self.topLevelOperator) # Create a list of DatasetInfos try: infos = self._createDatasetInfos(roleIndex, fileNames, rois) except DataSelectionGui.UserCancelledError: return # If no exception was thrown so far, set up the operator now loaded_all = self._configureOpWithInfos(roleIndex, startingLane, endingLane, infos) if loaded_all: # Now check the resulting slots. # If they should be copied to the project file, say so. self._reconfigureDatasetLocations(roleIndex, startingLane, endingLane) self._checkDataFormatWarnings(roleIndex, startingLane, endingLane) # If we succeeded in adding all images, show the first one. self.showDataset(startingLane, roleIndex) # Notify the workflow that we just added some new lanes. if adding_only: workflow = self.parentApplet.topLevelOperator.parent workflow.handleNewLanesAdded() # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() self.updateInternalPathVisiblity() def _determineLaneRange(self, fileNames, roleIndex, startingLane=None): """ Determine which lanes should be configured if the user wants to add the given fileNames to the specified role, starting at startingLane. If startingLane is None, assume the user wants to APPEND the files to the role's slots. """ if startingLane is None or startingLane == -1: startingLane = len(self.topLevelOperator.DatasetGroup) endingLane = startingLane+len(fileNames)-1 else: assert startingLane < len(self.topLevelOperator.DatasetGroup) max_files = len(self.topLevelOperator.DatasetGroup) - \ startingLane if len(fileNames) > max_files: msg = "You selected {num_selected} files for {num_slots} "\ "slots. To add new files use the 'Add new...' option "\ "in the context menu or the button in the last row."\ .format(num_selected=len(fileNames), num_slots=max_files) QMessageBox.critical( self, "Too many files", msg ) return (None, None) endingLane = min(startingLane+len(fileNames)-1, len(self.topLevelOperator.DatasetGroup)) if self._max_lanes and endingLane >= self._max_lanes: msg = "You may not add more than {} file(s) to this workflow. Please try again.".format( self._max_lanes ) QMessageBox.critical( self, "Too many files", msg ) return (None, None) return (startingLane, endingLane) def _createDatasetInfos(self, roleIndex, filePaths, rois): """ Create a list of DatasetInfos for the given filePaths and rois rois may be None, in which case it is ignored. """ if rois is None: rois = [None]*len(filePaths) assert len(rois) == len(filePaths) infos = [] for filePath, roi in zip(filePaths, rois): info = self._createDatasetInfo(roleIndex, filePath, roi) infos.append(info) return infos def _createDatasetInfo(self, roleIndex, filePath, roi): """ Create a DatasetInfo object for the given filePath and roi. roi may be None, in which case it is ignored. """ cwd = self.topLevelOperator.WorkingDirectory.value datasetInfo = DatasetInfo(filePath, cwd=cwd) datasetInfo.subvolume_roi = roi # (might be None) absPath, relPath = getPathVariants(filePath, cwd) # If the file is in a totally different tree from the cwd, # then leave the path as absolute. Otherwise, override with the relative path. if relPath is not None and len(os.path.commonprefix([cwd, absPath])) > 1: datasetInfo.filePath = relPath h5Exts = ['.ilp', '.h5', '.hdf5'] n5Exts = ['.n5'] if os.path.splitext(datasetInfo.filePath)[1] in h5Exts + n5Exts: if os.path.splitext(datasetInfo.filePath)[1] in n5Exts: datasetNames = self.getPossibleN5InternalPaths( absPath ) else: datasetNames = self.getPossibleH5InternalPaths( absPath ) if len(datasetNames) == 0: raise RuntimeError("HDF5 file %s has no image datasets" % datasetInfo.filePath) elif len(datasetNames) == 1: datasetInfo.filePath += str(datasetNames[0]) else: # If exactly one of the file's datasets matches a user's previous choice, use it. if roleIndex not in self._default_h5n5_volumes: self._default_h5n5_volumes[roleIndex] = set() previous_selections = self._default_h5n5_volumes[roleIndex] possible_auto_selections = previous_selections.intersection(datasetNames) if len(possible_auto_selections) == 1: datasetInfo.filePath += str(list(possible_auto_selections)[0]) else: # Ask the user which dataset to choose dlg = H5N5VolumeSelectionDlg(datasetNames, self) if dlg.exec_() == QDialog.Accepted: selected_index = dlg.combo.currentIndex() selected_dataset = str(datasetNames[selected_index]) datasetInfo.filePath += selected_dataset self._default_h5n5_volumes[roleIndex].add(selected_dataset) else: raise DataSelectionGui.UserCancelledError() # Allow labels by default if this gui isn't being used for batch data. datasetInfo.allowLabels = ( self.guiMode == GuiMode.Normal ) return datasetInfo def _configureOpWithInfos(self, roleIndex, startingLane, endingLane, infos): """ Attempt to configure the specified role and lanes of the top-level operator with the given DatasetInfos. Returns True if all lanes were configured successfully, or False if something went wrong. """ opTop = self.topLevelOperator originalSize = len(opTop.DatasetGroup) # Resize the slot if necessary if len( opTop.DatasetGroup ) < endingLane+1: opTop.DatasetGroup.resize( endingLane+1 ) # Configure each subslot for laneIndex, info in zip(list(range(startingLane, endingLane+1)), infos): try: self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue( info ) except DatasetConstraintError as ex: return_val = [False] # Give the user a chance to fix the problem self.handleDatasetConstraintError(info, info.filePath, ex, roleIndex, laneIndex, return_val) if return_val[0]: # Successfully repaired graph. continue else: # Not successfully repaired. Roll back the changes self._opTopRemoveDset(originalSize, laneIndex, roleIndex) return False except OpDataSelection.InvalidDimensionalityError as ex: self._opTopRemoveDset(originalSize, laneIndex, roleIndex) QMessageBox.critical( self, "Dataset has different dimensionality", ex.message ) return False except Exception as ex: self._opTopRemoveDset(originalSize, laneIndex, roleIndex) msg = "Wasn't able to load your dataset into the workflow. See error log for details." log_exception( logger, msg ) QMessageBox.critical( self, "Dataset Load Error", msg ) return False return True def _opTopRemoveDset(self, laneNum, laneIndex, roleIndex): """ Removes a dataset in topLevelOperator and sets the number of lanes to laneNum :param laneNum: total number of lanes after the cleanup :param laneIndex: the lane index of the dataset which is to be removed :param roleIndex: role index of the dataset """ self.topLevelOperator.DatasetGroup.resize(laneNum) self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(None) def _reconfigureDatasetLocations(self, roleIndex, startingLane, endingLane): """ Check the metadata for the given slots. If the data is stored a format that is poorly optimized for 3D access, then configure it to be copied to the project file. Finally, save the project if we changed something. """ save_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane+1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.prefer_2d and 'z' in output_slot.meta.axistags: shape = numpy.array(output_slot.meta.shape) total_volume = numpy.prod(shape) # Only copy to the project file if the total volume is reasonably small if total_volume < 0.5e9: info_slot = opTop.DatasetGroup[lane_index][roleIndex] info = info_slot.value info.location = DatasetInfo.Location.ProjectInternal info_slot.setValue( info, check_changed=False ) save_needed = True if save_needed: logger.info("Some of your data cannot be accessed efficiently in 3D in its current format." " It will now be copied to the project file.") opWorkflow = self.topLevelOperator.parent opWorkflow.shell.onSaveProjectActionTriggered() def _checkDataFormatWarnings(self, roleIndex, startingLane, endingLane): warn_needed = False opTop = self.topLevelOperator for lane_index in range(startingLane, endingLane+1): output_slot = opTop.ImageGroup[lane_index][roleIndex] if output_slot.meta.inefficient_format: warn_needed = True if warn_needed: QMessageBox.warning( self, "Inefficient Data Format", "Your data cannot be accessed efficiently in its current format. " "Check the console output for details.\n" "(For HDF5 files, be sure to enable chunking on your dataset.)" ) @threadRouted def handleDatasetConstraintError(self, info, filename, ex, roleIndex, laneIndex, return_val=[False]): if ex.unfixable: msg = ( "Can't use dataset:\n\n" + filename + "\n\n" + "because it violates a constraint of the {} component.\n\n".format( ex.appletName ) + ex.message + "\n\n" ) QMessageBox.warning( self, "Can't use dataset", msg ) return_val[0] = False else: assert isinstance(ex, DatasetConstraintError) accepted = True while isinstance(ex, DatasetConstraintError) and accepted: msg = ( f"Can't use given properties for dataset:\n\n{filename}\n\nbecause it violates a constraint of " f"the {ex.appletName} component.\n\n{ex.message}\n\nIf possible, fix this problem by adjusting " f"the applet settings and or the dataset properties in the next window(s).") QMessageBox.warning(self, "Dataset Needs Correction", msg) for dlg in ex.fixing_dialogs: dlg() accepted, ex = self.repairDatasetInfo(info, roleIndex, laneIndex) # The success of this is 'returned' via our special out-param # (We can't return a value from this method because it is @threadRouted. return_val[0] = accepted and ex is None # successfully repaired def repairDatasetInfo(self, info, roleIndex, laneIndex): """Open the dataset properties editor and return True if the new properties are acceptable.""" defaultInfos = {} defaultInfos[laneIndex] = info editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, [laneIndex], defaultInfos, show_axis_details=self.show_axis_details) dlg_state, ex = editorDlg.exec_() return (dlg_state == QDialog.Accepted), ex @classmethod def getPossibleH5InternalPaths(cls, absPath, min_ndim=2, max_ndim=5): datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with h5py.File(absPath, 'r') as f: # Define a closure to collect all of the dataset names in the file. def accumulateDatasetPaths(name, val): if type(val) == h5py._hl.dataset.Dataset and min_ndim <= len(val.shape) <= max_ndim: datasetNames.append( '/' + name ) # Visit every group/dataset in the file f.visititems(accumulateDatasetPaths) return datasetNames @classmethod def getPossibleN5InternalPaths(cls, absPath, min_ndim=2, max_ndim=5): """ Returns the name of all datasets in the file with at least 2 axes. """ datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with z5py.N5File(absPath, mode='r+') as f: def accumulate_names(path, val): if isinstance(val, z5py.dataset.Dataset) and min_ndim <= len(val.shape) <= max_ndim: name = path.replace(absPath, '') # Need only the internal path here datasetNames.append(name) f.visititems(accumulate_names) return datasetNames def addStack(self, roleIndex, laneIndex): """ The user clicked the "Import Stack Files" button. """ stackDlg = StackFileSelectionWidget(self) stackDlg.exec_() if stackDlg.result() != QDialog.Accepted : return files = stackDlg.selectedFiles sequence_axis = stackDlg.sequence_axis if len(files) == 0: return cwd = self.topLevelOperator.WorkingDirectory.value info = DatasetInfo(os.path.pathsep.join(files), cwd=cwd) originalNumLanes = len(self.topLevelOperator.DatasetGroup) if laneIndex is None or laneIndex == -1: laneIndex = len(self.topLevelOperator.DatasetGroup) if len(self.topLevelOperator.DatasetGroup) < laneIndex+1: self.topLevelOperator.DatasetGroup.resize(laneIndex+1) def importStack(): self.parentApplet.busy = True self.parentApplet.appletStateUpdateRequested() # Serializer will update the operator for us, which will propagate to the GUI. try: self.serializer.importStackAsLocalDataset( info, sequence_axis ) try: self.topLevelOperator.DatasetGroup[laneIndex][roleIndex].setValue(info) except DatasetConstraintError as ex: # Give the user a chance to repair the problem. filename = files[0] + "\n...\n" + files[-1] return_val = [False] self.parentApplet.busy = False # required for possible fixing dialogs from DatasetConstraintError self.parentApplet.appletStateUpdateRequested() self.handleDatasetConstraintError( info, filename, ex, roleIndex, laneIndex, return_val ) if not return_val[0]: # Not successfully repaired. Roll back the changes and give up. self.topLevelOperator.DatasetGroup.resize(originalNumLanes) finally: self.parentApplet.busy = False self.parentApplet.appletStateUpdateRequested() req = Request( importStack ) req.notify_finished( lambda result: self.showDataset(laneIndex, roleIndex) ) req.notify_failed( partial(self.handleFailedStackLoad, files, originalNumLanes ) ) req.submit() @threadRouted def handleFailedStackLoad(self, files, originalNumLanes, exc, exc_info): msg = "Failed to load stack due to the following error:\n{}".format( exc ) msg += "\nAttempted stack files were:\n" msg += "\n".join(files) log_exception( logger, msg, exc_info ) QMessageBox.critical(self, "Failed to load image stack", msg) self.topLevelOperator.DatasetGroup.resize(originalNumLanes) def handleClearDatasets(self, roleIndex, selectedRows): for row in selectedRows: self.topLevelOperator.DatasetGroup[row][roleIndex].disconnect() # Remove all operators that no longer have any connected slots laneIndexes = list(range( len(self.topLevelOperator.DatasetGroup))) for laneIndex, multislot in reversed(list(zip(laneIndexes, self.topLevelOperator.DatasetGroup))): any_ready = False for slot in multislot: any_ready |= slot.ready() if not any_ready: self.topLevelOperator.DatasetGroup.removeSlot( laneIndex, len(self.topLevelOperator.DatasetGroup)-1 ) # Notify the workflow that something that could affect applet readyness has occurred. self.parentApplet.appletStateUpdateRequested() def editDatasetInfo(self, roleIndex, laneIndexes): editorDlg = DatasetInfoEditorWidget(self, self.topLevelOperator, roleIndex, laneIndexes, show_axis_details=self.show_axis_details) editorDlg.exec_() self.parentApplet.appletStateUpdateRequested() def updateInternalPathVisiblity(self): for view in self._detailViewerWidgets: model = view.model() view.setColumnHidden(DatasetDetailedInfoColumn.InternalID, not model.hasInternalPaths()) def addPrecomputedVolume(self, roleIndex, laneIndex): # add history... history = [] browser = PrecomputedVolumeBrowser(history=history, parent=self) if browser.exec_() == PrecomputedVolumeBrowser.Rejected: return precomputed_url = browser.selected_url self.addFileNames([precomputed_url], roleIndex, laneIndex) def addDvidVolume(self, roleIndex, laneIndex): recent_hosts_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Hosts") recent_hosts = recent_hosts_pref.get() if not recent_hosts: recent_hosts = ["localhost:8000"] recent_hosts = [h for h in recent_hosts if h] # There used to be a bug where empty strings could be saved. Filter those out. recent_nodes_pref = PreferencesManager.Setting("DataSelection", "Recent DVID Nodes") recent_nodes = recent_nodes_pref.get() or {} from .dvidDataSelectionBrowser import DvidDataSelectionBrowser browser = DvidDataSelectionBrowser(recent_hosts, recent_nodes, parent=self) if browser.exec_() == DvidDataSelectionBrowser.Rejected: return if None in browser.get_selection(): QMessageBox.critical("Couldn't use your selection.") return rois = None hostname, repo_uuid, volume_name, node_uuid, typename = browser.get_selection() dvid_url = 'http://{hostname}/api/node/{node_uuid}/{volume_name}'.format( **locals() ) subvolume_roi = browser.get_subvolume_roi() # Relocate host to top of 'recent' list, and limit list to 10 items. try: i = recent_hosts.index(hostname) del recent_hosts[i] except ValueError: pass finally: recent_hosts.insert(0, hostname) recent_hosts = recent_hosts[:10] # Save pref recent_hosts_pref.set(recent_hosts) recent_nodes[hostname] = node_uuid recent_nodes_pref.set(recent_nodes) if subvolume_roi is None: self.addFileNames([dvid_url], roleIndex, laneIndex) else: start, stop = subvolume_roi self.addFileNames([dvid_url], roleIndex, laneIndex, [(start, stop)])
class E5SideBar(QWidget): """ Class implementing a sidebar with a widget area, that is hidden or shown, if the current tab is clicked again. """ Version = 1 North = 0 East = 1 South = 2 West = 3 def __init__(self, orientation=None, delay=200, parent=None): """ Constructor @param orientation orientation of the sidebar widget (North, East, South, West) @param delay value for the expand/shrink delay in milliseconds (integer) @param parent parent widget (QWidget) """ super(E5SideBar, self).__init__(parent) self.__tabBar = QTabBar() self.__tabBar.setDrawBase(True) self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setUsesScrollButtons(True) self.__tabBar.setDrawBase(False) self.__stackedWidget = QStackedWidget(self) self.__stackedWidget.setContentsMargins(0, 0, 0, 0) self.__autoHideButton = QToolButton() self.__autoHideButton.setCheckable(True) self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) self.__autoHideButton.setChecked(True) self.__autoHideButton.setToolTip( self.tr("Deselect to activate automatic collapsing")) self.barLayout = QBoxLayout(QBoxLayout.LeftToRight) self.barLayout.setContentsMargins(0, 0, 0, 0) self.layout = QBoxLayout(QBoxLayout.TopToBottom) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.barLayout.addWidget(self.__autoHideButton) self.barLayout.addWidget(self.__tabBar) self.layout.addLayout(self.barLayout) self.layout.addWidget(self.__stackedWidget) self.setLayout(self.layout) # initialize the delay timer self.__actionMethod = None self.__delayTimer = QTimer(self) self.__delayTimer.setSingleShot(True) self.__delayTimer.setInterval(delay) self.__delayTimer.timeout.connect(self.__delayedAction) self.__minimized = False self.__minSize = 0 self.__maxSize = 0 self.__bigSize = QSize() self.splitter = None self.splitterSizes = [] self.__hasFocus = False # flag storing if this widget or any child has the focus self.__autoHide = False self.__tabBar.installEventFilter(self) self.__orientation = E5SideBar.North if orientation is None: orientation = E5SideBar.North self.setOrientation(orientation) self.__tabBar.currentChanged[int].connect( self.__stackedWidget.setCurrentIndex) e5App().focusChanged[QWidget, QWidget].connect(self.__appFocusChanged) self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled) def setSplitter(self, splitter): """ Public method to set the splitter managing the sidebar. @param splitter reference to the splitter (QSplitter) """ self.splitter = splitter self.splitter.splitterMoved.connect(self.__splitterMoved) self.splitter.setChildrenCollapsible(False) index = self.splitter.indexOf(self) self.splitter.setCollapsible(index, False) def __splitterMoved(self, pos, index): """ Private slot to react on splitter moves. @param pos new position of the splitter handle (integer) @param index index of the splitter handle (integer) """ if self.splitter: self.splitterSizes = self.splitter.sizes() def __delayedAction(self): """ Private slot to handle the firing of the delay timer. """ if self.__actionMethod is not None: self.__actionMethod() def setDelay(self, delay): """ Public method to set the delay value for the expand/shrink delay in milliseconds. @param delay value for the expand/shrink delay in milliseconds (integer) """ self.__delayTimer.setInterval(delay) def delay(self): """ Public method to get the delay value for the expand/shrink delay in milliseconds. @return value for the expand/shrink delay in milliseconds (integer) """ return self.__delayTimer.interval() def __cancelDelayTimer(self): """ Private method to cancel the current delay timer. """ self.__delayTimer.stop() self.__actionMethod = None def shrink(self): """ Public method to record a shrink request. """ self.__delayTimer.stop() self.__actionMethod = self.__shrinkIt self.__delayTimer.start() def __shrinkIt(self): """ Private method to shrink the sidebar. """ self.__minimized = True self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() if self.splitter: self.splitterSizes = self.splitter.sizes() self.__stackedWidget.hide() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.setFixedHeight(self.__tabBar.minimumSizeHint().height()) else: self.setFixedWidth(self.__tabBar.minimumSizeHint().width()) self.__actionMethod = None def expand(self): """ Public method to record a expand request. """ self.__delayTimer.stop() self.__actionMethod = self.__expandIt self.__delayTimer.start() def __expandIt(self): """ Private method to expand the sidebar. """ self.__minimized = False self.__stackedWidget.show() self.resize(self.__bigSize) if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = max(self.__minSize, self.minimumSizeHint().height()) self.setMinimumHeight(minSize) self.setMaximumHeight(self.__maxSize) else: minSize = max(self.__minSize, self.minimumSizeHint().width()) self.setMinimumWidth(minSize) self.setMaximumWidth(self.__maxSize) if self.splitter: self.splitter.setSizes(self.splitterSizes) self.__actionMethod = None def isMinimized(self): """ Public method to check the minimized state. @return flag indicating the minimized state (boolean) """ return self.__minimized def isAutoHiding(self): """ Public method to check, if the auto hide function is active. @return flag indicating the state of auto hiding (boolean) """ return self.__autoHide def eventFilter(self, obj, evt): """ Public method to handle some events for the tabbar. @param obj reference to the object (QObject) @param evt reference to the event object (QEvent) @return flag indicating, if the event was handled (boolean) """ if obj == self.__tabBar: if evt.type() == QEvent.MouseButtonPress: pos = evt.pos() for i in range(self.__tabBar.count()): if self.__tabBar.tabRect(i).contains(pos): break if i == self.__tabBar.currentIndex(): if self.isMinimized(): self.expand() else: self.shrink() return True elif self.isMinimized(): self.expand() elif evt.type() == QEvent.Wheel: if qVersion() >= "5.0.0": delta = evt.angleDelta().y() else: delta = evt.delta() if delta > 0: self.prevTab() else: self.nextTab() return True return QWidget.eventFilter(self, obj, evt) def addTab(self, widget, iconOrLabel, label=None): """ Public method to add a tab to the sidebar. @param widget reference to the widget to add (QWidget) @param iconOrLabel reference to the icon or the label text of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.addTab(iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.addTab(iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.addWidget(widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def insertTab(self, index, widget, iconOrLabel, label=None): """ Public method to insert a tab into the sidebar. @param index the index to insert the tab at (integer) @param widget reference to the widget to insert (QWidget) @param iconOrLabel reference to the icon or the labeltext of the tab (QIcon, string) @param label the labeltext of the tab (string) (only to be used, if the second parameter is a QIcon) """ if label: index = self.__tabBar.insertTab(index, iconOrLabel, label) self.__tabBar.setTabToolTip(index, label) else: index = self.__tabBar.insertTab(index, iconOrLabel) self.__tabBar.setTabToolTip(index, iconOrLabel) self.__stackedWidget.insertWidget(index, widget) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def removeTab(self, index): """ Public method to remove a tab. @param index the index of the tab to remove (integer) """ self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index)) self.__tabBar.removeTab(index) if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() else: self.__minSize = self.minimumSizeHint().width() def clear(self): """ Public method to remove all tabs. """ while self.count() > 0: self.removeTab(0) def prevTab(self): """ Public slot used to show the previous tab. """ ind = self.currentIndex() - 1 if ind == -1: ind = self.count() - 1 self.setCurrentIndex(ind) self.currentWidget().setFocus() def nextTab(self): """ Public slot used to show the next tab. """ ind = self.currentIndex() + 1 if ind == self.count(): ind = 0 self.setCurrentIndex(ind) self.currentWidget().setFocus() def count(self): """ Public method to get the number of tabs. @return number of tabs in the sidebar (integer) """ return self.__tabBar.count() def currentIndex(self): """ Public method to get the index of the current tab. @return index of the current tab (integer) """ return self.__stackedWidget.currentIndex() def setCurrentIndex(self, index): """ Public slot to set the current index. @param index the index to set as the current index (integer) """ self.__tabBar.setCurrentIndex(index) self.__stackedWidget.setCurrentIndex(index) if self.isMinimized(): self.expand() def currentWidget(self): """ Public method to get a reference to the current widget. @return reference to the current widget (QWidget) """ return self.__stackedWidget.currentWidget() def setCurrentWidget(self, widget): """ Public slot to set the current widget. @param widget reference to the widget to become the current widget (QWidget) """ self.__stackedWidget.setCurrentWidget(widget) self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex()) if self.isMinimized(): self.expand() def indexOf(self, widget): """ Public method to get the index of the given widget. @param widget reference to the widget to get the index of (QWidget) @return index of the given widget (integer) """ return self.__stackedWidget.indexOf(widget) def isTabEnabled(self, index): """ Public method to check, if a tab is enabled. @param index index of the tab to check (integer) @return flag indicating the enabled state (boolean) """ return self.__tabBar.isTabEnabled(index) def setTabEnabled(self, index, enabled): """ Public method to set the enabled state of a tab. @param index index of the tab to set (integer) @param enabled enabled state to set (boolean) """ self.__tabBar.setTabEnabled(index, enabled) def orientation(self): """ Public method to get the orientation of the sidebar. @return orientation of the sidebar (North, East, South, West) """ return self.__orientation def setOrientation(self, orient): """ Public method to set the orientation of the sidebar. @param orient orientation of the sidebar (North, East, South, West) """ if orient == E5SideBar.North: self.__tabBar.setShape(QTabBar.RoundedNorth) self.__tabBar.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.TopToBottom) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.East: self.__tabBar.setShape(QTabBar.RoundedEast) self.__tabBar.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.RightToLeft) self.layout.setAlignment(self.barLayout, Qt.AlignTop) elif orient == E5SideBar.South: self.__tabBar.setShape(QTabBar.RoundedSouth) self.__tabBar.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Preferred) self.barLayout.setDirection(QBoxLayout.LeftToRight) self.layout.setDirection(QBoxLayout.BottomToTop) self.layout.setAlignment(self.barLayout, Qt.AlignLeft) elif orient == E5SideBar.West: self.__tabBar.setShape(QTabBar.RoundedWest) self.__tabBar.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Expanding) self.barLayout.setDirection(QBoxLayout.TopToBottom) self.layout.setDirection(QBoxLayout.LeftToRight) self.layout.setAlignment(self.barLayout, Qt.AlignTop) self.__orientation = orient def tabIcon(self, index): """ Public method to get the icon of a tab. @param index index of the tab (integer) @return icon of the tab (QIcon) """ return self.__tabBar.tabIcon(index) def setTabIcon(self, index, icon): """ Public method to set the icon of a tab. @param index index of the tab (integer) @param icon icon to be set (QIcon) """ self.__tabBar.setTabIcon(index, icon) def tabText(self, index): """ Public method to get the text of a tab. @param index index of the tab (integer) @return text of the tab (string) """ return self.__tabBar.tabText(index) def setTabText(self, index, text): """ Public method to set the text of a tab. @param index index of the tab (integer) @param text text to set (string) """ self.__tabBar.setTabText(index, text) def tabToolTip(self, index): """ Public method to get the tooltip text of a tab. @param index index of the tab (integer) @return tooltip text of the tab (string) """ return self.__tabBar.tabToolTip(index) def setTabToolTip(self, index, tip): """ Public method to set the tooltip text of a tab. @param index index of the tab (integer) @param tip tooltip text to set (string) """ self.__tabBar.setTabToolTip(index, tip) def tabWhatsThis(self, index): """ Public method to get the WhatsThis text of a tab. @param index index of the tab (integer) @return WhatsThis text of the tab (string) """ return self.__tabBar.tabWhatsThis(index) def setTabWhatsThis(self, index, text): """ Public method to set the WhatsThis text of a tab. @param index index of the tab (integer) @param text WhatsThis text to set (string) """ self.__tabBar.setTabWhatsThis(index, text) def widget(self, index): """ Public method to get a reference to the widget associated with a tab. @param index index of the tab (integer) @return reference to the widget (QWidget) """ return self.__stackedWidget.widget(index) def saveState(self): """ Public method to save the state of the sidebar. @return saved state as a byte array (QByteArray) """ if len(self.splitterSizes) == 0: if self.splitter: self.splitterSizes = self.splitter.sizes() self.__bigSize = self.size() if self.__orientation in [E5SideBar.North, E5SideBar.South]: self.__minSize = self.minimumSizeHint().height() self.__maxSize = self.maximumHeight() else: self.__minSize = self.minimumSizeHint().width() self.__maxSize = self.maximumWidth() data = QByteArray() stream = QDataStream(data, QIODevice.WriteOnly) stream.setVersion(QDataStream.Qt_4_6) stream.writeUInt16(self.Version) stream.writeBool(self.__minimized) stream << self.__bigSize stream.writeUInt16(self.__minSize) stream.writeUInt16(self.__maxSize) stream.writeUInt16(len(self.splitterSizes)) for size in self.splitterSizes: stream.writeUInt16(size) stream.writeBool(self.__autoHide) return data def restoreState(self, state): """ Public method to restore the state of the sidebar. @param state byte array containing the saved state (QByteArray) @return flag indicating success (boolean) """ if state.isEmpty(): return False if self.__orientation in [E5SideBar.North, E5SideBar.South]: minSize = self.layout.minimumSize().height() maxSize = self.maximumHeight() else: minSize = self.layout.minimumSize().width() maxSize = self.maximumWidth() data = QByteArray(state) stream = QDataStream(data, QIODevice.ReadOnly) stream.setVersion(QDataStream.Qt_4_6) stream.readUInt16() # version minimized = stream.readBool() if minimized and not self.__minimized: self.shrink() stream >> self.__bigSize self.__minSize = max(stream.readUInt16(), minSize) self.__maxSize = max(stream.readUInt16(), maxSize) count = stream.readUInt16() self.splitterSizes = [] for i in range(count): self.splitterSizes.append(stream.readUInt16()) self.__autoHide = stream.readBool() self.__autoHideButton.setChecked(not self.__autoHide) if not minimized: self.expand() return True ####################################################################### ## methods below implement the autohide functionality ####################################################################### def __autoHideToggled(self, checked): """ Private slot to handle the toggling of the autohide button. @param checked flag indicating the checked state of the button (boolean) """ self.__autoHide = not checked if self.__autoHide: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOn.png")) else: self.__autoHideButton.setIcon( UI.PixmapCache.getIcon("autoHideOff.png")) def __appFocusChanged(self, old, now): """ Private slot to handle a change of the focus. @param old reference to the widget, that lost focus (QWidget or None) @param now reference to the widget having the focus (QWidget or None) """ self.__hasFocus = self.isAncestorOf(now) if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() elif self.__autoHide and self.__hasFocus and self.isMinimized(): self.expand() def enterEvent(self, event): """ Protected method to handle the mouse entering this widget. @param event reference to the event (QEvent) """ if self.__autoHide and self.isMinimized(): self.expand() else: self.__cancelDelayTimer() def leaveEvent(self, event): """ Protected method to handle the mouse leaving this widget. @param event reference to the event (QEvent) """ if self.__autoHide and not self.__hasFocus and not self.isMinimized(): self.shrink() else: self.__cancelDelayTimer() def shutdown(self): """ Public method to shut down the object. This method does some preparations so the object can be deleted properly. It disconnects from the focusChanged signal in order to avoid trouble later on. """ e5App().focusChanged[QWidget, QWidget].disconnect( self.__appFocusChanged)
class ConfigurationWidget(QWidget): """ Class implementing a dialog for the configuration of eric6. @signal preferencesChanged() emitted after settings have been changed @signal masterPasswordChanged(str, str) emitted after the master password has been changed with the old and the new password @signal accepted() emitted to indicate acceptance of the changes @signal rejected() emitted to indicate rejection of the changes """ preferencesChanged = pyqtSignal() masterPasswordChanged = pyqtSignal(str, str) accepted = pyqtSignal() rejected = pyqtSignal() DefaultMode = 0 HelpBrowserMode = 1 TrayStarterMode = 2 HexEditorMode = 3 def __init__(self, parent=None, fromEric=True, displayMode=DefaultMode, expandedEntries=[]): """ Constructor @param parent The parent widget of this dialog. (QWidget) @keyparam fromEric flag indicating a dialog generation from within the eric6 ide (boolean) @keyparam displayMode mode of the configuration dialog (DefaultMode, HelpBrowserMode, TrayStarterMode, HexEditorMode) @exception RuntimeError raised to indicate an invalid dialog mode @keyparam expandedEntries list of entries to be shown expanded (list of strings) """ assert displayMode in ( ConfigurationWidget.DefaultMode, ConfigurationWidget.HelpBrowserMode, ConfigurationWidget.TrayStarterMode, ConfigurationWidget.HexEditorMode, ) super(ConfigurationWidget, self).__init__(parent) self.fromEric = fromEric self.displayMode = displayMode self.__setupUi() self.itmDict = {} if not fromEric: from PluginManager.PluginManager import PluginManager try: self.pluginManager = e5App().getObject("PluginManager") except KeyError: self.pluginManager = PluginManager(self) e5App().registerObject("PluginManager", self.pluginManager) if displayMode == ConfigurationWidget.DefaultMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "applicationPage": [self.tr("Application"), "preferences-application.png", "ApplicationPage", None, None], "cooperationPage": [self.tr("Cooperation"), "preferences-cooperation.png", "CooperationPage", None, None], "corbaPage": [self.tr("CORBA"), "preferences-orbit.png", "CorbaPage", None, None], "emailPage": [self.tr("Email"), "preferences-mail_generic.png", "EmailPage", None, None], "graphicsPage": [self.tr("Graphics"), "preferences-graphics.png", "GraphicsPage", None, None], "hexEditorPage": [self.tr("Hex Editor"), "hexEditor.png", "HexEditorPage", None, None], "iconsPage": [self.tr("Icons"), "preferences-icons.png", "IconsPage", None, None], "ircPage": [self.tr("IRC"), "irc.png", "IrcPage", None, None], "logViewerPage": [self.tr("Log-Viewer"), "preferences-logviewer.png", "LogViewerPage", None, None], "mimeTypesPage": [self.tr("Mimetypes"), "preferences-mimetypes.png", "MimeTypesPage", None, None], "networkPage": [self.tr("Network"), "preferences-network.png", "NetworkPage", None, None], "notificationsPage": [self.tr("Notifications"), "preferences-notifications.png", "NotificationsPage", None, None], "pluginManagerPage": [self.tr("Plugin Manager"), "preferences-pluginmanager.png", "PluginManagerPage", None, None], "printerPage": [self.tr("Printer"), "preferences-printer.png", "PrinterPage", None, None], "pythonPage": [self.tr("Python"), "preferences-python.png", "PythonPage", None, None], "qtPage": [self.tr("Qt"), "preferences-qtlogo.png", "QtPage", None, None], "securityPage": [self.tr("Security"), "preferences-security.png", "SecurityPage", None, None], "shellPage": [self.tr("Shell"), "preferences-shell.png", "ShellPage", None, None], "tasksPage": [self.tr("Tasks"), "task.png", "TasksPage", None, None], "templatesPage": [self.tr("Templates"), "preferences-template.png", "TemplatesPage", None, None], "trayStarterPage": [self.tr("Tray Starter"), "erict.png", "TrayStarterPage", None, None], "vcsPage": [self.tr("Version Control Systems"), "preferences-vcs.png", "VcsPage", None, None], "0debuggerPage": [self.tr("Debugger"), "preferences-debugger.png", None, None, None], "debuggerGeneralPage": [self.tr("General"), "preferences-debugger.png", "DebuggerGeneralPage", "0debuggerPage", None], "debuggerPythonPage": [self.tr("Python"), "preferences-pyDebugger.png", "DebuggerPythonPage", "0debuggerPage", None], "debuggerPython3Page": [self.tr("Python3"), "preferences-pyDebugger.png", "DebuggerPython3Page", "0debuggerPage", None], "0editorPage": [self.tr("Editor"), "preferences-editor.png", None, None, None], "editorAPIsPage": [self.tr("APIs"), "preferences-api.png", "EditorAPIsPage", "0editorPage", None], "editorAutocompletionPage": [self.tr("Autocompletion"), "preferences-autocompletion.png", "EditorAutocompletionPage", "0editorPage", None], "editorAutocompletionQScintillaPage": [self.tr("QScintilla"), "qscintilla.png", "EditorAutocompletionQScintillaPage", "editorAutocompletionPage", None], "editorCalltipsPage": [self.tr("Calltips"), "preferences-calltips.png", "EditorCalltipsPage", "0editorPage", None], "editorCalltipsQScintillaPage": [self.tr("QScintilla"), "qscintilla.png", "EditorCalltipsQScintillaPage", "editorCalltipsPage", None], "editorGeneralPage": [self.tr("General"), "preferences-general.png", "EditorGeneralPage", "0editorPage", None], "editorFilePage": [self.tr("Filehandling"), "preferences-filehandling.png", "EditorFilePage", "0editorPage", None], "editorSearchPage": [self.tr("Searching"), "preferences-search.png", "EditorSearchPage", "0editorPage", None], "editorSpellCheckingPage": [self.tr("Spell checking"), "preferences-spellchecking.png", "EditorSpellCheckingPage", "0editorPage", None], "editorStylesPage": [self.tr("Style"), "preferences-styles.png", "EditorStylesPage", "0editorPage", None], "editorSyntaxPage": [self.tr("Code Checkers"), "preferences-debugger.png", "EditorSyntaxPage", "0editorPage", None], "editorTypingPage": [self.tr("Typing"), "preferences-typing.png", "EditorTypingPage", "0editorPage", None], "editorExportersPage": [self.tr("Exporters"), "preferences-exporters.png", "EditorExportersPage", "0editorPage", None], "1editorLexerPage": [self.tr("Highlighters"), "preferences-highlighting-styles.png", None, "0editorPage", None], "editorHighlightersPage": [self.tr("Filetype Associations"), "preferences-highlighter-association.png", "EditorHighlightersPage", "1editorLexerPage", None], "editorHighlightingStylesPage": [self.tr("Styles"), "preferences-highlighting-styles.png", "EditorHighlightingStylesPage", "1editorLexerPage", None], "editorKeywordsPage": [self.tr("Keywords"), "preferences-keywords.png", "EditorKeywordsPage", "1editorLexerPage", None], "editorPropertiesPage": [self.tr("Properties"), "preferences-properties.png", "EditorPropertiesPage", "1editorLexerPage", None], "1editorMouseClickHandlers": [self.tr("Mouse Click Handlers"), "preferences-mouse-click-handler.png", "EditorMouseClickHandlerPage", "0editorPage", None], "0helpPage": [self.tr("Help"), "preferences-help.png", None, None, None], "helpDocumentationPage": [self.tr("Help Documentation"), "preferences-helpdocumentation.png", "HelpDocumentationPage", "0helpPage", None], "helpViewersPage": [self.tr("Help Viewers"), "preferences-helpviewers.png", "HelpViewersPage", "0helpPage", None], "0projectPage": [self.tr("Project"), "preferences-project.png", None, None, None], "projectBrowserPage": [self.tr("Project Viewer"), "preferences-project.png", "ProjectBrowserPage", "0projectPage", None], "projectPage": [self.tr("Project"), "preferences-project.png", "ProjectPage", "0projectPage", None], "multiProjectPage": [self.tr("Multiproject"), "preferences-multiproject.png", "MultiProjectPage", "0projectPage", None], "0interfacePage": [self.tr("Interface"), "preferences-interface.png", None, None, None], "interfacePage": [self.tr("Interface"), "preferences-interface.png", "InterfacePage", "0interfacePage", None], "viewmanagerPage": [self.tr("Viewmanager"), "preferences-viewmanager.png", "ViewmanagerPage", "0interfacePage", None], } try: from PyQt5 import QtWebKit # __IGNORE_WARNING__ self.configItems.update({ "helpAppearancePage": [self.tr("Appearance"), "preferences-styles.png", "HelpAppearancePage", "0helpPage", None], "helpFlashCookieManagerPage": [self.tr("Flash Cookie Manager"), "flashCookie16.png", "HelpFlashCookieManagerPage", "0helpPage", None], "helpVirusTotalPage": [self.tr("VirusTotal Interface"), "virustotal.png", "HelpVirusTotalPage", "0helpPage", None], "helpWebBrowserPage": [self.tr("eric6 Web Browser"), "ericWeb.png", "HelpWebBrowserPage", "0helpPage", None], }) except ImportError: pass self.configItems.update( e5App().getObject("PluginManager").getPluginConfigData()) elif displayMode == ConfigurationWidget.HelpBrowserMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "interfacePage": [self.tr("Interface"), "preferences-interface.png", "HelpInterfacePage", None, None], "networkPage": [self.tr("Network"), "preferences-network.png", "NetworkPage", None, None], "printerPage": [self.tr("Printer"), "preferences-printer.png", "PrinterPage", None, None], "securityPage": [self.tr("Security"), "preferences-security.png", "SecurityPage", None, None], "0helpPage": [self.tr("Help"), "preferences-help.png", None, None, None], "helpDocumentationPage": [self.tr("Help Documentation"), "preferences-helpdocumentation.png", "HelpDocumentationPage", "0helpPage", None], } try: from PyQt5 import QtWebKit # __IGNORE_WARNING__ self.configItems.update({ "helpAppearancePage": [self.tr("Appearance"), "preferences-styles.png", "HelpAppearancePage", "0helpPage", None], "helpFlashCookieManagerPage": [self.tr("Flash Cookie Manager"), "flashCookie16.png", "HelpFlashCookieManagerPage", "0helpPage", None], "helpVirusTotalPage": [self.tr("VirusTotal Interface"), "virustotal.png", "HelpVirusTotalPage", "0helpPage", None], "helpWebBrowserPage": [self.tr("eric6 Web Browser"), "ericWeb.png", "HelpWebBrowserPage", "0helpPage", None], }) except ImportError: pass elif displayMode == ConfigurationWidget.TrayStarterMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "trayStarterPage": [self.tr("Tray Starter"), "erict.png", "TrayStarterPage", None, None], } elif displayMode == ConfigurationWidget.HexEditorMode: self.configItems = { # key : [display string, pixmap name, dialog module name or # page creation function, parent key, # reference to configuration page (must always be last)] # The dialog module must have the module function 'create' to # create the configuration page. This must have the method # 'save' to save the settings. "hexEditorPage": [self.tr("Hex Editor"), "hexEditor.png", "HexEditorPage", None, None], } else: raise RuntimeError("Illegal mode value: {0}".format(displayMode)) # generate the list entries self.__expandedEntries = [] for key in sorted(self.configItems.keys()): pageData = self.configItems[key] if pageData[3]: if pageData[3] in self.itmDict: pitm = self.itmDict[pageData[3]] # get the parent item else: continue else: pitm = self.configList self.itmDict[key] = ConfigurationPageItem(pitm, pageData[0], key, pageData[1]) self.itmDict[key].setData(0, Qt.UserRole, key) if (not self.fromEric or displayMode != ConfigurationWidget.DefaultMode or key in expandedEntries): self.itmDict[key].setExpanded(True) self.configList.sortByColumn(0, Qt.AscendingOrder) # set the initial size of the splitter self.configSplitter.setSizes([200, 600]) self.configList.itemActivated.connect(self.__showConfigurationPage) self.configList.itemClicked.connect(self.__showConfigurationPage) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.rejected) if displayMode in [ConfigurationWidget.HelpBrowserMode, ConfigurationWidget.TrayStarterMode, ConfigurationWidget.HexEditorMode]: self.configListSearch.hide() if displayMode not in [ConfigurationWidget.TrayStarterMode, ConfigurationWidget.HexEditorMode]: self.__initLexers() def accept(self): """ Public slot to accept the buttonBox accept signal. """ if not isMacPlatform(): wdg = self.focusWidget() if wdg == self.configList: return self.accepted.emit() def __setupUi(self): """ Private method to perform the general setup of the configuration widget. """ self.setObjectName("ConfigurationDialog") self.resize(900, 650) self.verticalLayout_2 = QVBoxLayout(self) self.verticalLayout_2.setSpacing(6) self.verticalLayout_2.setContentsMargins(6, 6, 6, 6) self.verticalLayout_2.setObjectName("verticalLayout_2") self.configSplitter = QSplitter(self) self.configSplitter.setOrientation(Qt.Horizontal) self.configSplitter.setObjectName("configSplitter") self.configListWidget = QWidget(self.configSplitter) self.leftVBoxLayout = QVBoxLayout(self.configListWidget) self.leftVBoxLayout.setContentsMargins(0, 0, 0, 0) self.leftVBoxLayout.setSpacing(0) self.leftVBoxLayout.setObjectName("leftVBoxLayout") self.configListSearch = E5ClearableLineEdit( self, self.tr("Enter search text...")) self.configListSearch.setObjectName("configListSearch") self.leftVBoxLayout.addWidget(self.configListSearch) self.configList = QTreeWidget() self.configList.setObjectName("configList") self.leftVBoxLayout.addWidget(self.configList) self.configListSearch.textChanged.connect(self.__searchTextChanged) self.scrollArea = QScrollArea(self.configSplitter) self.scrollArea.setFrameShape(QFrame.NoFrame) self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scrollArea.setWidgetResizable(False) self.scrollArea.setObjectName("scrollArea") self.configStack = QStackedWidget() self.configStack.setFrameShape(QFrame.Box) self.configStack.setFrameShadow(QFrame.Sunken) self.configStack.setObjectName("configStack") self.scrollArea.setWidget(self.configStack) self.emptyPage = QWidget() self.emptyPage.setGeometry(QRect(0, 0, 372, 591)) self.emptyPage.setObjectName("emptyPage") self.vboxlayout = QVBoxLayout(self.emptyPage) self.vboxlayout.setSpacing(6) self.vboxlayout.setContentsMargins(6, 6, 6, 6) self.vboxlayout.setObjectName("vboxlayout") spacerItem = QSpacerItem( 20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) self.vboxlayout.addItem(spacerItem) self.emptyPagePixmap = QLabel(self.emptyPage) self.emptyPagePixmap.setAlignment(Qt.AlignCenter) self.emptyPagePixmap.setObjectName("emptyPagePixmap") self.emptyPagePixmap.setPixmap( QPixmap(os.path.join(getConfig('ericPixDir'), 'eric.png'))) self.vboxlayout.addWidget(self.emptyPagePixmap) self.textLabel1 = QLabel(self.emptyPage) self.textLabel1.setAlignment(Qt.AlignCenter) self.textLabel1.setObjectName("textLabel1") self.vboxlayout.addWidget(self.textLabel1) spacerItem1 = QSpacerItem( 20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) self.vboxlayout.addItem(spacerItem1) self.configStack.addWidget(self.emptyPage) self.verticalLayout_2.addWidget(self.configSplitter) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons( QDialogButtonBox.Apply | QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.Reset) self.buttonBox.setObjectName("buttonBox") if not self.fromEric and \ self.displayMode == ConfigurationWidget.DefaultMode: self.buttonBox.button(QDialogButtonBox.Apply).hide() self.buttonBox.button(QDialogButtonBox.Apply).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Reset).setEnabled(False) self.verticalLayout_2.addWidget(self.buttonBox) self.setWindowTitle(self.tr("Preferences")) self.configList.header().hide() self.configList.header().setSortIndicator(0, Qt.AscendingOrder) self.configList.setSortingEnabled(True) self.textLabel1.setText( self.tr("Please select an entry of the list \n" "to display the configuration page.")) QMetaObject.connectSlotsByName(self) self.setTabOrder(self.configList, self.configStack) self.configStack.setCurrentWidget(self.emptyPage) self.configList.setFocus() def __searchTextChanged(self, text): """ Private slot to handle a change of the search text. @param text text to search for (string) """ self.__searchChildItems(self.configList.invisibleRootItem(), text) def __searchChildItems(self, parent, text): """ Private method to enable child items based on a search string. @param parent reference to the parent item (QTreeWidgetItem) @param text text to search for (string) @return flag indicating an enabled child item (boolean) """ childEnabled = False text = text.lower() for index in range(parent.childCount()): itm = parent.child(index) if itm.childCount() > 0: enable = self.__searchChildItems(itm, text) or \ text == "" or text in itm.text(0).lower() else: enable = text == "" or text in itm.text(0).lower() if enable: childEnabled = True itm.setDisabled(not enable) return childEnabled def __initLexers(self): """ Private method to initialize the dictionary of preferences lexers. """ import QScintilla.Lexers from .PreferencesLexer import PreferencesLexer, \ PreferencesLexerLanguageError self.lexers = {} for language in QScintilla.Lexers.getSupportedLanguages(): if language not in self.lexers: try: self.lexers[language] = PreferencesLexer(language, self) except PreferencesLexerLanguageError: pass def __importConfigurationPage(self, name): """ Private method to import a configuration page module. @param name name of the configuration page module (string) @return reference to the configuration page module """ modName = "Preferences.ConfigurationPages.{0}".format(name) try: mod = __import__(modName) components = modName.split('.') for comp in components[1:]: mod = getattr(mod, comp) return mod except ImportError: E5MessageBox.critical( self, self.tr("Configuration Page Error"), self.tr("""<p>The configuration page <b>{0}</b>""" """ could not be loaded.</p>""").format(name)) return None def __showConfigurationPage(self, itm, column): """ Private slot to show a selected configuration page. @param itm reference to the selected item (QTreeWidgetItem) @param column column that was selected (integer) (ignored) """ pageName = itm.getPageName() self.showConfigurationPageByName(pageName, setCurrent=False) def __initPage(self, pageData): """ Private method to initialize a configuration page. @param pageData data structure for the page to initialize @return reference to the initialized page """ page = None if isinstance(pageData[2], types.FunctionType): page = pageData[2](self) else: mod = self.__importConfigurationPage(pageData[2]) if mod: page = mod.create(self) if page is not None: self.configStack.addWidget(page) pageData[-1] = page try: page.setMode(self.displayMode) except AttributeError: pass return page def showConfigurationPageByName(self, pageName, setCurrent=True): """ Public slot to show a named configuration page. @param pageName name of the configuration page to show (string) @param setCurrent flag indicating to set the current item (boolean) """ if pageName == "empty" or pageName not in self.configItems: page = self.emptyPage else: pageData = self.configItems[pageName] if pageData[-1] is None and pageData[2] is not None: # the page was not loaded yet, create it page = self.__initPage(pageData) else: page = pageData[-1] if page is None: page = self.emptyPage elif setCurrent: items = self.configList.findItems( pageData[0], Qt.MatchFixedString | Qt.MatchRecursive) for item in items: if item.data(0, Qt.UserRole) == pageName: self.configList.setCurrentItem(item) self.configStack.setCurrentWidget(page) ssize = self.scrollArea.size() if self.scrollArea.horizontalScrollBar(): ssize.setHeight( ssize.height() - self.scrollArea.horizontalScrollBar().height() - 2) if self.scrollArea.verticalScrollBar(): ssize.setWidth( ssize.width() - self.scrollArea.verticalScrollBar().width() - 2) psize = page.minimumSizeHint() self.configStack.resize(max(ssize.width(), psize.width()), max(ssize.height(), psize.height())) if page != self.emptyPage: page.polishPage() self.buttonBox.button(QDialogButtonBox.Apply).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Reset).setEnabled(True) else: self.buttonBox.button(QDialogButtonBox.Apply).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Reset).setEnabled(False) # reset scrollbars for sb in [self.scrollArea.horizontalScrollBar(), self.scrollArea.verticalScrollBar()]: if sb: sb.setValue(0) self.__currentConfigurationPageName = pageName def getConfigurationPageName(self): """ Public method to get the page name of the current page. @return page name of the current page (string) """ return self.__currentConfigurationPageName def calledFromEric(self): """ Public method to check, if invoked from within eric. @return flag indicating invocation from within eric (boolean) """ return self.fromEric def getPage(self, pageName): """ Public method to get a reference to the named page. @param pageName name of the configuration page (string) @return reference to the page or None, indicating page was not loaded yet """ return self.configItems[pageName][-1] def getLexers(self): """ Public method to get a reference to the lexers dictionary. @return reference to the lexers dictionary """ return self.lexers def setPreferences(self): """ Public method called to store the selected values into the preferences storage. """ for key, pageData in list(self.configItems.items()): if pageData[-1]: pageData[-1].save() # page was loaded (and possibly modified) QApplication.processEvents() # ensure HMI is responsive def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Apply): self.on_applyButton_clicked() elif button == self.buttonBox.button(QDialogButtonBox.Reset): self.on_resetButton_clicked() @pyqtSlot() def on_applyButton_clicked(self): """ Private slot called to apply the settings of the current page. """ if self.configStack.currentWidget() != self.emptyPage: page = self.configStack.currentWidget() savedState = page.saveState() page.save() self.preferencesChanged.emit() if savedState is not None: page.setState(savedState) page.polishPage() @pyqtSlot() def on_resetButton_clicked(self): """ Private slot called to reset the settings of the current page. """ if self.configStack.currentWidget() != self.emptyPage: currentPage = self.configStack.currentWidget() savedState = currentPage.saveState() pageName = self.configList.currentItem().getPageName() self.configStack.removeWidget(currentPage) if pageName == "editorHighlightingStylesPage": self.__initLexers() self.configItems[pageName][-1] = None self.showConfigurationPageByName(pageName) if savedState is not None: self.configStack.currentWidget().setState(savedState) def getExpandedEntries(self): """ Public method to get a list of expanded entries. @return list of expanded entries (list of string) """ return self.__expandedEntries @pyqtSlot(QTreeWidgetItem) def on_configList_itemCollapsed(self, item): """ Private slot handling a list entry being collapsed. @param item reference to the collapsed item (QTreeWidgetItem) """ pageName = item.data(0, Qt.UserRole) if pageName in self.__expandedEntries: self.__expandedEntries.remove(pageName) @pyqtSlot(QTreeWidgetItem) def on_configList_itemExpanded(self, item): """ Private slot handling a list entry being expanded. @param item reference to the expanded item (QTreeWidgetItem) """ pageName = item.data(0, Qt.UserRole) if pageName not in self.__expandedEntries: self.__expandedEntries.append(pageName)