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()
Exemple #4
0
    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)
Exemple #6
0
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)
Exemple #7
0
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)
Exemple #8
0
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()
Exemple #11
0
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()
Exemple #12
0
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()
Exemple #13
0
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()
Exemple #15
0
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")
Exemple #16
0
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()
Exemple #18
0
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))
Exemple #20
0
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)
Exemple #22
0
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'
Exemple #24
0
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)
Exemple #26
0
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))
Exemple #27
0
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)
Exemple #28
0
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
Exemple #29
0
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()
Exemple #30
0
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)
Exemple #32
0
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)])
Exemple #33
0
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)
Exemple #34
0
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)
Exemple #35
0
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()
Exemple #36
0
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()
Exemple #37
0
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)
Exemple #38
0
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
Exemple #39
0
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)
Exemple #40
0
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)
Exemple #41
0
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)
Exemple #42
0
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()
Exemple #44
0
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())
Exemple #45
0
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)])
Exemple #46
0
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)
Exemple #47
-1
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)