Esempio n. 1
0
class MVCConfigurationGUI(MVCConfigurationBase):
    """
    GUI implementation of MVCConfigurationBase
    """
    def __init__(self, configuration):
        super().__init__(configuration)
        assertMainThread()
        srv = Services.getService("MainWindow")
        srv.aboutToClose.connect(self._aboutToClose)
        confMenu = srv.menuBar().addMenu("&Configuration")
        toolBar = srv.getToolBar()

        configuration.configNameChanged.connect(self._configNameChanged)
        configuration.dirtyChanged.connect(self._dirtyChanged)

        style = QApplication.style()
        self.actLoad = QAction(
            QIcon.fromTheme("document-open",
                            style.standardIcon(QStyle.SP_DialogOpenButton)),
            "Open config", self)
        self.actLoad.triggered.connect(self._execLoad)
        self.actSave = QAction(
            QIcon.fromTheme("document-save",
                            style.standardIcon(QStyle.SP_DialogSaveButton)),
            "Save config", self)
        self.actSave.triggered.connect(self._execSaveConfig)
        self.actSaveWithGuiState = QAction(
            QIcon.fromTheme("document-save",
                            style.standardIcon(QStyle.SP_DialogSaveButton)),
            "Save config sync gui state", self)
        self.actSaveWithGuiState.triggered.connect(
            self._execSaveConfigWithGuiState)
        self.actNew = QAction(
            QIcon.fromTheme("document-new",
                            style.standardIcon(QStyle.SP_FileIcon)),
            "New config", self)
        self.actNew.triggered.connect(self._execNew)

        self.actActivate = QAction(
            QIcon.fromTheme("arrow-up", style.standardIcon(QStyle.SP_ArrowUp)),
            "Initialize", self)
        self.actActivate.triggered.connect(self.activate)
        self.actDeactivate = QAction(
            QIcon.fromTheme("arrow-down",
                            style.standardIcon(QStyle.SP_ArrowDown)),
            "Deinitialize", self)
        self.actDeactivate.triggered.connect(self.deactivate)

        confMenu.addAction(self.actLoad)
        confMenu.addAction(self.actSave)
        confMenu.addAction(self.actSaveWithGuiState)
        confMenu.addAction(self.actNew)
        confMenu.addAction(self.actActivate)
        confMenu.addAction(self.actDeactivate)
        toolBar.addAction(self.actLoad)
        toolBar.addAction(self.actSave)
        toolBar.addAction(self.actNew)
        toolBar.addAction(self.actActivate)
        toolBar.addAction(self.actDeactivate)

        self.recentConfigs = [QAction() for i in range(10)]
        self.recentConfigs[0].setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
        confMenu.addSeparator()
        recentMenu = confMenu.addMenu("Recent")
        for a in self.recentConfigs:
            a.setVisible(False)
            a.triggered.connect(self._openRecent)
            recentMenu.addAction(a)

        self.mainWidget = srv.newDockWidget("Configuration", None,
                                            Qt.LeftDockWidgetArea)
        self.treeView = QTreeView(self.mainWidget)
        self.treeView.setHeaderHidden(False)
        self.treeView.setSelectionMode(QAbstractItemView.NoSelection)
        self.treeView.setEditTriggers(self.treeView.EditKeyPressed
                                      | self.treeView.AnyKeyPressed)
        self.treeView.setAllColumnsShowFocus(True)
        self.treeView.setExpandsOnDoubleClick(False)
        self.treeView.setDragEnabled(True)
        self.treeView.setDropIndicatorShown(True)
        self.treeView.setDragDropMode(QAbstractItemView.DragOnly)
        self.mainWidget.setWidget(self.treeView)
        self.treeView.setModel(self.model)
        self.treeView.header().setStretchLastSection(True)
        self.treeView.header().setSectionResizeMode(
            0, QHeaderView.ResizeToContents)
        self.treeView.doubleClicked.connect(self._onItemDoubleClicked)
        self.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.treeView.customContextMenuRequested.connect(
            self._execTreeViewContextMenu)
        # expand applications by default
        self.treeView.setExpanded(self.model.index(1, 0), True)
        self.delegate = PropertyDelegate(self.model, ITEM_ROLE,
                                         ConfigurationModel.PropertyContent,
                                         self)
        self.treeView.setItemDelegate(self.delegate)

        self.restoreState()
        srv.aboutToClose.connect(self.saveState)
        # a list of dock widgets displaying subgraphs
        self._graphViews = []
        # make sure that the graph views are closed when the config is closed
        self._configuration.subConfigRemoved.connect(self._subConfigRemoved)
        self._waitForActivated = None
        self._waitForOpenState = None

    def _execLoad(self):
        assertMainThread()
        if self._checkDirty():
            return
        fn, _ = QFileDialog.getOpenFileName(self.mainWidget,
                                            "Load configuration",
                                            self.cfgfile,
                                            filter="*.json")
        if fn is not None and fn != "":
            logger.debug("Loading config file %s", fn)
            try:
                self.loadConfig(fn)
            except Exception as e:  # pylint: disable=broad-except
                logger.exception("Error while loading configuration %s: %s",
                                 fn, str(e))
                QMessageBox.warning(self.mainWidget,
                                    "Error while loading configuration",
                                    str(e))

    def _openRecent(self):
        """
        Called when the user clicks on a recent config.

        :return:
        """
        if self._checkDirty():
            return
        action = self.sender()
        fn = action.data()
        try:
            self.loadConfig(fn)
        except Exception as e:  # pylint: disable=broad-except
            # catching general exception is wanted here.
            logger.exception("Error while loading configuration %s: %s", fn,
                             str(e))
            QMessageBox.warning(self.mainWidget,
                                "Error while loading configuration", str(e))

    def _checkDirty(self):
        if self._configuration.dirty():
            ans = QMessageBox.question(
                None,
                "Save changes?",
                "There are unsaved changes. Do you want to save them?",
                buttons=QMessageBox.Save | QMessageBox.Discard
                | QMessageBox.Cancel,
                defaultButton=QMessageBox.Save)
            if ans == QMessageBox.Save:
                self.saveConfig()
                return False
            if ans == QMessageBox.Cancel:
                return True
        return False

    def _aboutToClose(self, mainWindow):
        if self._checkDirty():
            mainWindow.ignoreCloseEvent()

    def _execNew(self):
        assertMainThread()
        if self._checkDirty():
            return
        fn, _ = QFileDialog.getSaveFileName(self.mainWidget,
                                            "New configuration",
                                            ".",
                                            filter="*.json")
        if fn is not None and fn != "":
            logger.debug("Creating config file %s", fn)
            self.newConfig(fn)

    def _execSaveConfig(self):
        if self.configuration().filename() is None:
            self._execSaveConfigAs()
        else:
            self.saveConfig()

    def _execSaveConfigWithGuiState(self):
        if self.configuration().filename() is None:
            self._execSaveConfigAs()
        else:
            self.saveConfigWithGuiState()

    def _execSaveConfigAs(self):
        """
        Opens a file dialog to get the save file name and calls saveConfig.

        :return:
        """
        assertMainThread()
        fn, _ = QFileDialog.getSaveFileName(self.mainWidget,
                                            "Save configuration as", ".",
                                            "*.json")
        if fn is not None and fn != "":
            self.saveConfigAs(fn)

    def _addGraphView(self, subConfig):
        g = subConfig.getGraph()
        # remove already deleted graph views from internal list
        valid_graphViews = []
        for gv in self._graphViews:
            if shiboken2.isValid(gv):  # pylint: disable=no-member
                valid_graphViews.append(gv)
        self._graphViews = valid_graphViews
        # check if graph view is already there
        for gv in self._graphViews:
            if gv.widget().scene().graph == g:
                logger.info("Graph view already exists.")
                return
        # create new graph view
        srv = Services.getService("MainWindow")
        graphDw = srv.newDockWidget("Graph (%s)" % (subConfig.getName()),
                                    parent=None,
                                    defaultArea=Qt.RightDockWidgetArea,
                                    allowedArea=Qt.RightDockWidgetArea
                                    | Qt.BottomDockWidgetArea)
        graphDw.setAttribute(Qt.WA_DeleteOnClose, True)
        assert isinstance(graphDw, QDockWidget)
        graphView = GraphEditorView(graphDw)
        graphView.setScene(GraphScene(subConfig.getGraph(), graphDw))
        graphDw.setWidget(graphView)
        self._graphViews.append(graphDw)
        graphDw.visibleChanged.connect(self._removeGraphViewFromList)

    def _subConfigRemoved(self, subConfigName, configType):
        g = self._configuration.subConfigByNameAndTye(subConfigName,
                                                      configType).getGraph()
        for gv in self._graphViews:
            if gv.widget().scene().graph == g:
                logger.debug("deleting graph view for subconfig %s",
                             subConfigName)
                gv.deleteLater()

    def _removeGraphViewFromList(self, visible):
        if visible:
            return
        gv = self.sender()
        try:
            self._graphViews.remove(gv)
            logger.debug("removed graphview from list")
        except ValueError:
            logger.debug("graphview not in list, ignored")

    def _execTreeViewContextMenu(self, point):
        index = self.treeView.indexAt(point)
        item = self.model.data(index, ITEM_ROLE)
        if isinstance(item, ConfigurationModel.SubConfigContent):
            m = QMenu()
            a1 = QAction("Edit graph")
            m.addAction(a1)
            a1.triggered.connect(lambda: self._addGraphView(item.subConfig))
            if self.model.isApplication(index):
                a2 = QAction("Select Application")
                a2.triggered.connect(lambda: self.changeActiveApp(
                    self.model.data(index, Qt.DisplayRole)))
                a3 = QAction("Init Application")
                a3.triggered.connect(lambda: self._changeActiveAppAndInit(
                    self.model.data(index, Qt.DisplayRole)))
                m.addActions([a2, a3])
                pbsrv = Services.getService("PlaybackControl")
                m2 = m.addMenu("Init and load sequence")
                m3 = m.addMenu("Init, load and play")
                s1 = []
                s2 = []
                for a in pbsrv.recentSeqs:
                    assert isinstance(a, QAction)
                    if a.isVisible():
                        # pylint: disable=cell-var-from-loop
                        # the below statements are tested and work
                        aseq = QAction(a.text())
                        aseq.triggered.connect(lambda arg1=a.data(
                        ), seq=a.data(): self._changeActiveAppInitAndLoad(
                            self.model.data(index, Qt.DisplayRole), seq, False)
                                               )
                        s1.append(aseq)
                        aseq = QAction(a.text())
                        aseq.triggered.connect(lambda arg1=a.data(
                        ), seq=a.data(): self._changeActiveAppInitAndLoad(
                            self.model.data(index, Qt.DisplayRole), seq, True))
                        # pylint: enable=cell-var-from-loop
                        s2.append(aseq)
                m2.addActions(s1)
                m3.addActions(s2)
            m.exec_(self.treeView.mapToGlobal(point))
            return
        if self.model.isSubConfigParent(
                index) == Configuration.CONFIG_TYPE_APPLICATION:
            m = QMenu()
            a = QAction("Add application")
            m.addAction(a)
            a = m.exec_(self.treeView.mapToGlobal(point))
            if a is not None:
                self._configuration.addNewApplication()
            return
        if self.model.isSubConfigParent(
                index) == Configuration.CONFIG_TYPE_COMPOSITE:
            m = QMenu()
            a = QAction("Add composite filter")
            m.addAction(a)
            a = m.exec_(self.treeView.mapToGlobal(point))
            if a is not None:
                self._configuration.addNewCompositeFilter()
            return

    def _configNameChanged(self, cfgfile):
        logger.debug("_configNameChanged: %s", cfgfile)
        assertMainThread()
        self.cfgfile = cfgfile
        self._dirtyChanged(self._configuration.dirty())
        foundIdx = None
        for i, a in enumerate(self.recentConfigs):
            if a.data() == cfgfile:
                foundIdx = i
        if foundIdx is None:
            foundIdx = len(self.recentConfigs) - 1
        for i in range(foundIdx, 0, -1):
            self.recentConfigs[i].setText(self.recentConfigs[i - 1].text())
            self.recentConfigs[i].setData(self.recentConfigs[i - 1].data())
            self.recentConfigs[i].setVisible(
                self.recentConfigs[i - 1].data() is not None)
        self.recentConfigs[0].setText(cfgfile)
        self.recentConfigs[0].setData(cfgfile)
        self.recentConfigs[0].setVisible(True)

    def _dirtyChanged(self, dirty):
        srv = Services.getService("MainWindow")
        if self.cfgfile is None:
            title = "nexxT: <unnamed>"
        else:
            title = "nexxT: " + self.cfgfile
        if dirty:
            title += " *"
        srv.setWindowTitle(title)

    def _onItemDoubleClicked(self, index):
        assertMainThread()
        if self.model.isApplication(index):
            app = self.model.data(index, Qt.DisplayRole)
            self.changeActiveApp(app)
        else:
            self.treeView.edit(index)

    def _changeActiveAppAndInit(self, app):
        """
        Call this slot to activate and init an application

        :param app: can be either an Application instance or the name of an application
        :return:
        """
        assertMainThread()
        if isinstance(app, str):
            app = self.configuration().applicationByName(app)
        currentApp = Application.activeApplication
        if currentApp is not None:
            currentApp = currentApp.getApplication()
        self._waitForActivated = app
        self.changeActiveApp(app.getName())

    def _changeActiveAppInitAndLoad(self, app, sequence, startPlay):
        self._waitForOpenState = (app, sequence, startPlay)
        self._changeActiveAppAndInit(app)

    def appActivated(self, name, app):  # pylint: disable=unused-argument
        """
        Called when the application is activated.

        :param name: the application name
        :param app: An ActiveApplication instance.
        :return:
        """
        assertMainThread()
        if app is not None:
            self.activeAppStateChange(app.getState())
            app.stateChanged.connect(self.activeAppStateChange)
            if self._waitForActivated == app.getApplication():
                MethodInvoker(self.activate, Qt.QueuedConnection)
        else:
            self.actActivate.setEnabled(False)
            self.actDeactivate.setEnabled(False)
        self._waitForActivated = None

    def _disconnectSingleShotPlay(self):
        assertMainThread()
        pbsrv = Services.getService("PlaybackControl")
        try:
            pbsrv.playbackPaused.disconnect(self._singleShotPlay)
        except RuntimeError:
            # we are already disconnected.
            pass

    def _singleShotPlay(self):
        assertMainThread()
        pbsrv = Services.getService("PlaybackControl")
        MethodInvoker(pbsrv.startPlayback, Qt.QueuedConnection)
        self._disconnectSingleShotPlay()

    def activeAppStateChange(self, newState):
        """
        Called when the active application changes its state.

        :param newState: the new application's state (see FilterState)
        :return:
        """
        assertMainThread()
        if newState == FilterState.CONSTRUCTED:
            self.actActivate.setEnabled(True)
        else:
            self.actActivate.setEnabled(False)
        if newState == FilterState.ACTIVE:
            if self._waitForOpenState is not None:
                app, pbfile, startPlay = self._waitForOpenState
                self._waitForOpenState = None
                if app == Application.activeApplication.getApplication(
                ).getName():
                    pbsrv = Services.getService("PlaybackControl")
                    if startPlay:
                        pbsrv.playbackPaused.connect(self._singleShotPlay)
                        QTimer.singleShot(2000, self._disconnectSingleShotPlay)
                    MethodInvoker(pbsrv.browser.setActive, Qt.QueuedConnection,
                                  pbfile)
            self.actDeactivate.setEnabled(True)
            self.actSaveWithGuiState.setEnabled(False)
        else:
            self.actDeactivate.setEnabled(False)
            self.actSaveWithGuiState.setEnabled(True)

    def restoreState(self):
        """
        Restore the state of the configuration gui service (namely the recently
        open config files). This is saved in QSettings because it is used
        across config files.

        :return:
        """
        logger.debug("restoring config state ...")
        settings = QSettings()
        v = settings.value("ConfigurationRecentFiles")
        if v is not None and isinstance(v, QByteArray):
            ds = QDataStream(v)
            recentFiles = ds.readQStringList()
            idx = 0
            for f in recentFiles:
                if f != "" and f is not None:
                    self.recentConfigs[idx].setData(f)
                    self.recentConfigs[idx].setText(f)
                    self.recentConfigs[idx].setVisible(True)
                    idx += 1
                    if idx >= len(self.recentConfigs):
                        break
        logger.debug("restoring config state done")

    def saveState(self):
        """
        Save the state of the configuration gui service (namely the recently
        open config files). This is saved in QSettings because it is used
        across config files.

        :return:
        """
        logger.debug("saving config state ...")
        settings = QSettings()
        b = QByteArray()
        ds = QDataStream(b, QIODevice.WriteOnly)
        l = [
            rc.data() for rc in self.recentConfigs
            if rc.isVisible() and rc.data() is not None and rc.data() != ""
        ]
        ds.writeQStringList(l)
        settings.setValue("ConfigurationRecentFiles", b)
        logger.debug("saving config state done (%s)", l)
Esempio n. 2
0
    def create_ui(self):
        """Setup main UI elements, dock widgets, UI-related elements, etc.
        """

        log.debug('Loading UI')

        # Undo Stack
        self.undo_stack = QUndoStack(self)
        self.undo_stack.setUndoLimit(100)

        # Object navigation history
        self.obj_history = deque([], config.MAX_OBJ_HISTORY)

        app = QApplication.instance()
        base_font = QFont()
        base_font.fromString(self.prefs['base_font'])
        app.setFont(base_font)

        # Object class table widget
        # classTable = QTableView(self)
        classTable = classtable.TableView(self)
        classTable.setObjectName("classTable")
        classTable.setAlternatingRowColors(True)
        classTable.setFrameShape(QFrame.StyledPanel)
        classTable_font = QFont()
        classTable_font.fromString(self.prefs['class_table_font'])
        classTable.setFont(classTable_font)
        fm = classTable.fontMetrics()
        classTable.setWordWrap(True)
        classTable.setEditTriggers(QAbstractItemView.EditKeyPressed |
                                   QAbstractItemView.DoubleClicked |
                                   QAbstractItemView.AnyKeyPressed |
                                   QAbstractItemView.SelectedClicked)
        # classTable.horizontalHeader().setMovable(True)
        # classTable.verticalHeader().setMovable(False)
        classTable.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
        classTable.verticalHeader().setSectionResizeMode(QHeaderView.Interactive)
        classTable.horizontalHeader().setDefaultSectionSize(self.prefs['default_column_width'])
        classTable.verticalHeader().setDefaultSectionSize(fm.height() + 0)
        classTable.setSelectionMode(QAbstractItemView.ExtendedSelection)
        classTable.setContextMenuPolicy(Qt.CustomContextMenu)
        classTable.customContextMenuRequested.connect(self.custom_table_context_menu)

        # Create table model and proxy layers for transposing and filtering
        self.classTableModel = classtable.IDFObjectTableModel(classTable)
        self.transposeableModel = classtable.TransposeProxyModel(self.classTableModel)
        self.transposeableModel.setSourceModel(self.classTableModel)
        self.sortableModel = classtable.SortFilterProxyModel(self.transposeableModel)
        self.sortableModel.setSourceModel(self.transposeableModel)

        # Assign model to table (enable sorting FIRST)
        # table.setSortingEnabled(True) # Disable for now, CRUD actions won't work!
        classTable.setModel(self.sortableModel)

        # Connect some signals
        selection_model = classTable.selectionModel()
        selection_model.selectionChanged.connect(self.table_selection_changed)
        scroll_bar = classTable.verticalScrollBar()
        scroll_bar.valueChanged.connect(self.scroll_changed)

        # These are currently broken
        # classTable.horizontalHeader().sectionMoved.connect(self.moveObject)
        # classTable.verticalHeader().sectionMoved.connect(self.moveObject)

        # Object class tree widget
        classTreeDockWidget = QDockWidget("Object Classes and Counts", self)
        classTreeDockWidget.setObjectName("classTreeDockWidget")
        classTreeDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas)

        classTree = QTreeView(classTreeDockWidget)
        classTree.setUniformRowHeights(True)
        classTree.setAllColumnsShowFocus(True)
        classTree.setRootIsDecorated(False)
        classTree.setExpandsOnDoubleClick(True)
        classTree.setIndentation(15)
        classTree.setAnimated(True)
        classTree_font = QFont()
        classTree_font.fromString(self.prefs['class_tree_font'])
        classTree.setFont(classTree_font)
        classTree.setAlternatingRowColors(True)
        classTree.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        palette = classTree.palette()
        palette.setColor(QPalette.Highlight, Qt.darkCyan)
        classTree.setPalette(palette)

        class_tree_window = QWidget(classTreeDockWidget)
        class_tree_dock_layout_v = QVBoxLayout()
        class_tree_dock_layout_h = QHBoxLayout()
        class_tree_dock_layout_v.setContentsMargins(0, 8, 0, 0)
        class_tree_dock_layout_h.setContentsMargins(0, 0, 0, 0)

        class_tree_filter_edit = QLineEdit(classTreeDockWidget)
        class_tree_filter_edit.setPlaceholderText("Filter Classes")
        class_tree_filter_edit.textChanged.connect(self.treeFilterRegExpChanged)

        class_tree_filter_cancel = QPushButton("Clear", classTreeDockWidget)
        class_tree_filter_cancel.setMaximumWidth(45)
        class_tree_filter_cancel.clicked.connect(self.clearTreeFilterClicked)

        class_tree_dock_layout_h.addWidget(class_tree_filter_edit)
        class_tree_dock_layout_h.addWidget(class_tree_filter_cancel)
        class_tree_dock_layout_v.addLayout(class_tree_dock_layout_h)
        class_tree_dock_layout_v.addWidget(classTree)
        class_tree_window.setLayout(class_tree_dock_layout_v)

        classTreeDockWidget.setWidget(class_tree_window)
        classTreeDockWidget.setContentsMargins(0,0,0,0)

        # Comments widget
        commentDockWidget = QDockWidget("Comments", self)
        commentDockWidget.setObjectName("commentDockWidget")
        commentDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas)
        commentView = UndoRedoTextEdit(commentDockWidget, self)
        commentView.setLineWrapMode(QTextEdit.FixedColumnWidth)
        commentView.setLineWrapColumnOrWidth(499)
        commentView.setFrameShape(QFrame.StyledPanel)
        commentView_font = QFont()
        commentView_font.fromString(self.prefs['comments_font'])
        commentView.setFont(commentView_font)
        commentDockWidget.setWidget(commentView)

        # Info and help widget
        infoDockWidget = QDockWidget("Info", self)
        infoDockWidget.setObjectName("infoDockWidget")
        infoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas)
        infoView = QTextEdit(infoDockWidget)
        infoView.setFrameShape(QFrame.StyledPanel)
        infoView.setReadOnly(True)
        infoDockWidget.setWidget(infoView)

        # Node list and jump menu widget
        refDockWidget = QDockWidget("Field References", self)
        refDockWidget.setObjectName("refDockWidget")
        refDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas)
        ref_model = reftree.ReferenceTreeModel(None, refDockWidget)
        refView = QTreeView(refDockWidget)
        refView.setModel(ref_model)
        refView.setUniformRowHeights(True)
        refView.setRootIsDecorated(False)
        refView.setIndentation(15)
        refView.setColumnWidth(0, 160)
        refView.setFrameShape(QFrame.StyledPanel)
        refDockWidget.setWidget(refView)
        refView.doubleClicked.connect(self.ref_tree_double_clicked)

        # Logging and debugging widget
        logDockWidget = QDockWidget("Log Viewer", self)
        logDockWidget.setObjectName("logDockWidget")
        logDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas)
        logView = QPlainTextEdit(logDockWidget)
        logView.setLineWrapMode(QPlainTextEdit.NoWrap)
        logView.setReadOnly(True)
        logView_font = QFont()
        logView_font.fromString(self.prefs['base_font'])
        logView.setFont(logView_font)
        logView.ensureCursorVisible()
        logDockWidget.setWidget(logView)

        # Undo view widget
        undoDockWidget = QDockWidget("Undo History", self)
        undoDockWidget.setObjectName("undoDockWidget")
        undoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas)
        undoView = QUndoView(self.undo_stack)
        undoDockWidget.setWidget(undoView)

        # Define corner docking behaviour
        self.setDockNestingEnabled(True)
        self.setCorner(Qt.TopLeftCorner,
                       Qt.LeftDockWidgetArea)
        self.setCorner(Qt.TopRightCorner,
                       Qt.RightDockWidgetArea)
        self.setCorner(Qt.BottomLeftCorner,
                       Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomRightCorner,
                       Qt.RightDockWidgetArea)

        # Assign main widget and dock widgets to QMainWindow
        self.setCentralWidget(classTable)
        self.addDockWidget(Qt.LeftDockWidgetArea, classTreeDockWidget)
        self.addDockWidget(Qt.RightDockWidgetArea, commentDockWidget)
        self.addDockWidget(Qt.RightDockWidgetArea, infoDockWidget)
        self.addDockWidget(Qt.RightDockWidgetArea, refDockWidget)
        self.addDockWidget(Qt.RightDockWidgetArea, logDockWidget)
        self.addDockWidget(Qt.RightDockWidgetArea, undoDockWidget)

        # Store widgets for access by other objects
        self.classTable = classTable
        self.commentView = commentView
        self.infoView = infoView
        self.classTree = classTree
        self.logView = logView
        self.undoView = undoView
        self.refView = refView
        self.filterTreeBox = class_tree_filter_edit

        # Store docks for access by other objects
        self.commentDockWidget = commentDockWidget
        self.infoDockWidget = infoDockWidget
        self.classTreeDockWidget = classTreeDockWidget
        self.logDockWidget = logDockWidget
        self.undoDockWidget = undoDockWidget
        self.refDockWidget = refDockWidget

        # Perform other UI-related initialization tasks
        self.center()
        self.setUnifiedTitleAndToolBarOnMac(True)
        self.setWindowIcon(QIcon(':/images/logo.png'))

        # Status bar setup
        self.statusBar().showMessage('Status: Ready')
        self.unitsLabel = QLabel()
        self.unitsLabel.setAlignment(Qt.AlignCenter)
        self.unitsLabel.setMinimumSize(self.unitsLabel.sizeHint())
        self.unitsLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        self.statusBar().addPermanentWidget(self.unitsLabel)
        self.pathLabel = QLabel()
        self.pathLabel.setAlignment(Qt.AlignCenter)
        self.pathLabel.setMinimumSize(self.pathLabel.sizeHint())
        self.pathLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        self.statusBar().addPermanentWidget(self.pathLabel)
        self.versionLabel = QLabel()
        self.versionLabel.setAlignment(Qt.AlignCenter)
        self.versionLabel.setMinimumSize(self.versionLabel.sizeHint())
        self.versionLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        self.statusBar().addPermanentWidget(self.versionLabel)
        self.progressBarIDF = QProgressBar()
        self.progressBarIDF.setAlignment(Qt.AlignCenter)
        self.progressBarIDF.setMaximumWidth(200)
        self.statusBar().addPermanentWidget(self.progressBarIDF)

        self.clipboard = QApplication.instance().clipboard()
        self.obj_clipboard = []

        self.setStyleSheet("""
            QToolTip {
               background-color: gray;
               color: white;
               border: black solid 1px
            } 
            # QMenu {
            #     background-color: rgbf(0.949020, 0.945098, 0.941176);
            #     color: rgb(255,255,255);
            # }
            # QMenu::item::selected {
            #     background-color: rgbf(0.949020, 0.945098, 0.941176);
            # }
            """)
Esempio n. 3
0
class CatsTreeWindow(PBDialog):
    """Extension of `PBDialog` that shows the categories tree"""
    def __init__(
        self,
        parent=None,
        askCats=False,
        askForBib=None,
        askForExp=None,
        expButton=True,
        previous=[],
        single=False,
        multipleRecords=False,
    ):
        """Initialize instance parameters and call the function that
        creates the layout.

        Parameters:
            parent (default None): the parent widget
            askCats (default False): if True, enable checkboxes for
                selection of categories
            askForBib (default None): the optional key which identifies
                in the database the bibtex entry for which categories
                are being selected
            askForExp (default None): the optional ID which identifies
                in the database the experiment for which categories
                are being selected
            expButton (default True): if True, add a button to accept
                the widget content and later ask for experiments
            previous (default []): the list of categories that
                must be selected at the beginning
            single (default False): if True, only allow the selection
                of a single category (the parent category, typically).
                Multiple checkboxes can be selected,
                but only the first one will be considered
            multipleRecords: used when dealing with categories
                corresponding to multiple records. Activate a tristate
                checkbox for the initial list of categories, which are
                typically not the same for all the elements in the list
        """
        PBDialog.__init__(self, parent)
        self.setWindowTitle(cwstr.cats)
        self.currLayout = QVBoxLayout(self)
        self.setLayout(self.currLayout)
        self.askCats = askCats
        self.askForBib = askForBib
        self.askForExp = askForExp
        self.expButton = expButton
        self.previous = previous
        self.single = single
        self.multipleRecords = multipleRecords
        self.result = False
        self.marked = []
        self.root_model = None
        self.proxyModel = None
        self.tree = None
        self.menu = None
        self.timer = None
        self.expsButton = None
        self.filterInput = None
        self.newCatButton = None
        self.acceptButton = None
        self.cancelButton = None

        self.setMinimumWidth(400)
        self.setMinimumHeight(600)

        self.createForm()

    def populateAskCat(self):
        """If selection of categories is allowed, add some information
        on the bibtex/experiment for which the categories are requested
        and a simple message, then create few required empty lists
        """
        if self.askCats:
            if self.askForBib is not None:
                try:
                    bibitem = pBDB.bibs.getByBibkey(self.askForBib,
                                                    saveQuery=False)[0]
                except IndexError:
                    pBGUILogger.warning(
                        cwstr.entryNotInDb % self.askForBib,
                        exc_info=True,
                    )
                    return
                try:
                    if bibitem["inspire"] != "" and bibitem[
                            "inspire"] is not None:
                        link = "<a href='%s'>%s</a>" % (
                            pBView.getLink(self.askForBib, "inspire"),
                            self.askForBib,
                        )
                    elif bibitem["arxiv"] != "" and bibitem[
                            "arxiv"] is not None:
                        link = "<a href='%s'>%s</a>" % (
                            pBView.getLink(self.askForBib, "arxiv"),
                            self.askForBib,
                        )
                    elif bibitem["doi"] != "" and bibitem["doi"] is not None:
                        link = "<a href='%s'>%s</a>" % (
                            pBView.getLink(self.askForBib, "doi"),
                            self.askForBib,
                        )
                    else:
                        link = self.askForBib
                    bibtext = PBLabel(
                        cwstr.markCatBibKAT %
                        (link, bibitem["author"], bibitem["title"]))
                except KeyError:
                    bibtext = PBLabel(cwstr.markCatBibK % (self.askForBib))
                self.currLayout.addWidget(bibtext)
            elif self.askForExp is not None:
                try:
                    expitem = pBDB.exps.getByID(self.askForExp)[0]
                except IndexError:
                    pBGUILogger.warning(
                        cwstr.expNotInDb % self.askForExp,
                        exc_info=True,
                    )
                    return
                try:
                    exptext = PBLabel(
                        cwstr.markCatExpINC %
                        (self.askForExp, expitem["name"], expitem["comments"]))
                except KeyError:
                    exptext = PBLabel(cwstr.markCatExpI % (self.askForExp))
                self.currLayout.addWidget(exptext)
            else:
                if self.single:
                    comment = PBLabel(cwstr.selectCat)
                else:
                    comment = PBLabel(cwstr.selectCats)
                self.currLayout.addWidget(comment)
            self.marked = []
            self.parent().selectedCats = []
        return True

    def onCancel(self):
        """Reject the dialog content and close the window"""
        self.result = False
        self.close()

    def onOk(self, exps=False):
        """Accept the dialog content
        (update the list of selected categories)
        and close the window.
        May set `self.result` to "Exps"
        for later opening of a new dialog to ask for experiments.

        Parameter:
            exps (default False): if True, set the result to "Exps",
                otherwise to "Ok"
        """
        self.parent().selectedCats = [
            idC for idC in self.root_model.selectedCats.keys()
            if self.root_model.selectedCats[idC] == True
        ]
        self.parent().previousUnchanged = [
            idC for idC in self.root_model.previousSaved.keys()
            if self.root_model.previousSaved[idC] == True
        ]

        if (self.single and len(self.parent().selectedCats) > 1
                and self.parent().selectedCats[0] == 0):
            self.parent().selectedCats.pop(0)
            self.parent().selectedCats = [self.parent().selectedCats[0]]
        self.result = "Exps" if exps else "Ok"
        self.close()

    def changeFilter(self, string):
        """When the filter `QLineEdit` is changed,
        update the `LeafFilterProxyModel` regexp filter

        Parameter:
            string: the new filter string
        """
        self.proxyModel.setFilterRegExp(str(string))
        self.tree.expandAll()

    def onAskExps(self):
        """Action to perform when the selection of categories
        will be folloed by the selection of experiments.
        Call `self.onOk` with `exps = True`.
        """
        self.onOk(exps=True)

    def onNewCat(self):
        """Action to perform when the creation
        of a new category is requested
        """
        editCategory(self, self.parent())

    def keyPressEvent(self, e):
        """Manage the key press events.
        Do nothing unless `Esc` is pressed:
        in this case close the dialog
        """
        if e.key() == Qt.Key_Escape:
            self.close()

    def createForm(self):
        """Create the dialog content, connect the model to the view
        and eventually add the buttons at the end
        """
        self.populateAskCat()

        catsTree = pBDB.cats.getHier()

        self.filterInput = QLineEdit("", self)
        self.filterInput.setPlaceholderText(cwstr.filterCat)
        self.filterInput.textChanged.connect(self.changeFilter)
        self.currLayout.addWidget(self.filterInput)
        self.filterInput.setFocus()

        self.tree = QTreeView(self)
        self.currLayout.addWidget(self.tree)
        self.tree.setMouseTracking(True)
        self.tree.entered.connect(self.handleItemEntered)
        self.tree.doubleClicked.connect(self.cellDoubleClick)
        self.tree.setExpandsOnDoubleClick(False)

        catsNamedTree = self._populateTree(catsTree[0], 0)

        self.root_model = CatsModel(
            pBDB.cats.getAll(),
            [catsNamedTree],
            self,
            self.previous,
            multipleRecords=self.multipleRecords,
        )
        self.proxyModel = LeafFilterProxyModel(self)
        self.proxyModel.setSourceModel(self.root_model)
        self.proxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.proxyModel.setFilterKeyColumn(-1)
        self.tree.setModel(self.proxyModel)

        self.tree.expandAll()

        self.tree.setHeaderHidden(True)
        # self.tree.doubleClicked.connect(self.askAndPerformAction)

        self.newCatButton = QPushButton(cwstr.addNew, self)
        self.newCatButton.clicked.connect(self.onNewCat)
        self.currLayout.addWidget(self.newCatButton)

        if self.askCats:
            self.acceptButton = QPushButton(cwstr.ok, self)
            self.acceptButton.clicked.connect(self.onOk)
            self.currLayout.addWidget(self.acceptButton)

            if self.expButton:
                self.expsButton = QPushButton(cwstr.askExp, self)
                self.expsButton.clicked.connect(self.onAskExps)
                self.currLayout.addWidget(self.expsButton)

            # cancel button
            self.cancelButton = QPushButton(cwstr.cancel, self)
            self.cancelButton.clicked.connect(self.onCancel)
            self.cancelButton.setAutoDefault(True)
            self.currLayout.addWidget(self.cancelButton)

    def _populateTree(self, children, idCat):
        """Read the list of categories recursively and
        populate the categories tree

        Parameters:
            children: the list of children categories
                of the currently considered one
            idCat: the id of the current category
        """
        name = pBDB.cats.getByID(idCat)[0]["name"]
        children_list = []
        for child in cats_alphabetical(children, pBDB):
            child_item = self._populateTree(children[child], child)
            children_list.append(child_item)
        return NamedElement(idCat, name, children_list)

    def handleItemEntered(self, index):
        """Process event when mouse enters an item and
        create a `QTooltip` which describes the category, with a timer

        Parameter:
            index: a `QModelIndex` instance
        """
        if index.isValid():
            row = index.row()
        else:
            return
        try:
            idString = self.proxyModel.sibling(row, 0, index).data()
        except AttributeError:
            pBLogger.debug("", exc_info=True)
            return
        try:
            idCat, catName = idString.split(": ")
        except AttributeError:
            pBLogger.debug("", exc_info=True)
            return
        idCat = idCat.strip()
        try:
            self.timer.stop()
            QToolTip.showText(QCursor.pos(), "", self.tree.viewport())
        except AttributeError:
            pass
        try:
            catData = pBDB.cats.getByID(idCat)[0]
        except IndexError:
            pBGUILogger.exception(cwstr.failedFind)
            return
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(lambda: QToolTip.showText(
            QCursor.pos(),
            cwstr.catId.format(idC=idCat, cat=catData["name"]) + cwstr.
            entriesCorrespondent.format(en=pBDB.catBib.countByCat(idCat)) +
            cwstr.expsAssociated.format(ex=pBDB.catExp.countByCat(idCat)),
            self.tree.viewport(),
            self.tree.visualRect(index),
            3000,
        ))
        self.timer.start(500)

    def contextMenuEvent(self, event):
        """Create a right click menu with few actions
        on the selected category

        Parameter:
            event: a `QEvent`
        """
        indexes = self.tree.selectedIndexes()
        try:
            index = indexes[0]
        except IndexError:
            pBLogger.debug(cwstr.clickMissingIndex)
            return
        if index.isValid():
            row = index.row()
        else:
            return
        try:
            idString = self.proxyModel.sibling(row, 0, index).data()
        except AttributeError:
            pBLogger.debug("", exc_info=True)
            return
        try:
            idCat, catName = idString.split(": ")
        except AttributeError:
            pBLogger.debug("", exc_info=True)
            return
        idCat = idCat.strip()

        menu = PBMenu()
        self.menu = menu
        titAction = QAction(cwstr.catDescr % catName)
        titAction.setDisabled(True)
        bibAction = QAction(cwstr.openEntryList)
        modAction = QAction(cwstr.modify)
        delAction = QAction(cwstr.delete)
        subAction = QAction(cwstr.addSub)
        menu.possibleActions = [
            titAction,
            None,
            bibAction,
            None,
            modAction,
            delAction,
            None,
            subAction,
        ]
        menu.fillMenu()

        action = menu.exec_(event.globalPos())

        if action == bibAction:
            self.parent().reloadMainContent(pBDB.bibs.getByCat(idCat))
        elif action == modAction:
            editCategory(self, self.parent(), idCat)
        elif action == delAction:
            deleteCategory(self, self.parent(), idCat, catName)
        elif action == subAction:
            editCategory(self, self.parent(), useParentCat=idCat)
        return

    def cellDoubleClick(self, index):
        """Process event when mouse double clicks an item.
        Opens a link if some columns

        Parameter:
            index: a `QModelIndex` instance
        """
        if index.isValid():
            row = index.row()
            col = index.column()
        else:
            return
        try:
            idString = self.proxyModel.sibling(row, 0, index).data()
        except AttributeError:
            pBLogger.debug("", exc_info=True)
            return
        try:
            idCat, catName = idString.split(": ")
        except AttributeError:
            pBLogger.debug("", exc_info=True)
            return
        idCat = idCat.strip()
        self.parent().reloadMainContent(pBDB.bibs.getByCat(idCat))
        return

    def recreateTable(self):
        """Delete the previous widgets and recreate them with new data"""
        self.cleanLayout()
        self.createForm()