コード例 #1
0
ファイル: gui.py プロジェクト: Jakoma02/pyCovering
class MainWindow(QMainWindow, Ui_MainWindow):
    """
    The main GUI window
    """
    HELP_URL = "https://www.github.com/jakoma02/pyCovering"

    model_type_changed = Signal()
    view_type_changed = Signal()
    model_changed = Signal(GeneralCoveringModel)
    view_changed = Signal(GeneralView)
    info_updated = Signal(GeneralCoveringModel, GeneralView)
    settings_changed = Signal()

    def __init__(self):
        QMainWindow.__init__(self)

        self.model = None
        self.view = None

        self.setupUi(self)
        self.create_action_groups()

        # A dict Action name -> GeneralView, so that we can set the
        # correct view upon view type action trigger
        self.action_views = dict()

        self.actionAbout_2.triggered.connect(self.show_about_dialog)
        self.actionDocumentation.triggered.connect(self.show_help)
        self.actionChange_dimensions.triggered.connect(
            self.show_dimensions_dialog)
        self.actionChange_tile_size.triggered.connect(
            self.show_block_size_dialog)

        self.actionGenerate.triggered.connect(self.start_covering)

        self.model_type_changed.connect(self.update_model_type)
        self.model_type_changed.connect(self.update_view_type_menu)
        self.model_type_changed.connect(self.update_constraints_menu)
        self.model_type_changed.connect(self.enable_model_menu_buttons)

        self.model_changed.connect(
            lambda _: self.info_updated.emit(self.model, self.view))

        self.view_type_changed.connect(self.update_view_type)

        self.view_changed.connect(
            lambda _: self.info_updated.emit(self.model, self.view))
        self.info_updated.connect(self.infoText.update)
        self.info_updated.connect(self.update_view)

        self.tiles_list_model = BlockListModel()
        self.tilesList.setModel(self.tiles_list_model)

        self.model_changed.connect(self.tiles_list_model.update_data)
        self.tiles_list_model.checkedChanged.connect(self.set_block_visibility)

        self.model_changed.emit(self.model)
        self.update_view_type_menu()

    def set_block_visibility(self, block, visible):
        """
        Update model visibility based on block list checkbox change
        """
        block.visible = visible
        self.model_changed.emit(self.model)

    def show_about_dialog(self):
        """
        Shows the "About" dialog
        """
        dialog = AboutDialog(self)
        dialog.open()

    def start_covering(self):
        """
        Starts covering, shows the corresponding dialog
        """
        if self.model is None:
            QMessageBox.warning(self, "No model", "No model selected!")
            return

        self.model.reset()

        self.thread = GenerateModelThread(self.model)
        dialog = CoveringDialog(self)

        dialog.rejected.connect(self.cancel_covering)

        self.thread.success.connect(dialog.accept)
        self.thread.success.connect(self.covering_success)
        self.thread.failed.connect(dialog.reject)
        self.thread.failed.connect(self.covering_failed)

        self.thread.done.connect(lambda: self.model_changed.emit(self.model))

        self.thread.start()
        dialog.open()

    def show_block_size_dialog(self):
        """
        Shows "Change block size" dialog
        """
        if self.model is None:
            QMessageBox.warning(self, "No model", "No model selected!")
            return

        curr_min = self.model.min_block_size
        curr_max = self.model.max_block_size

        dialog = BlockSizeDialog(self)
        dialog.sizesAccepted.connect(self.block_sizes_accepted)
        dialog.set_values(curr_min, curr_max)
        dialog.open()

    def show_dimensions_dialog(self):
        """
        Shows "Change dimensions" dialog
        """
        if self.model is None:
            QMessageBox.warning(self, "No model", "No model selected!")
            return

        if isinstance(self.model, TwoDCoveringModel):
            curr_width = self.model.width
            curr_height = self.model.height

            dialog = TwoDDimensionsDialog(self)
            dialog.set_values(curr_width, curr_height)

            dialog.dimensionsAccepted.connect(self.two_d_dimensions_accepted)
            dialog.show()

        elif isinstance(self.model, PyramidCoveringModel):
            curr_size = self.model.size

            dialog = PyramidDimensionsDialog(self)
            dialog.set_value(curr_size)

            dialog.dimensionsAccepted.connect(self.pyramid_dimensions_accepted)
            dialog.show()

    def two_d_dimensions_accepted(self, width, height):
        """
        Updates TwoDCoveringModel dimensions (after dialog confirmation)
        """
        assert isinstance(self.model, TwoDCoveringModel)

        self.model.set_size(width, height)
        self.model_changed.emit(self.model)

        self.message("Size updated")

    def pyramid_dimensions_accepted(self, size):
        """
        Updates PyramidCoveringModel dimensions (after dialog confirmation)
        """
        assert isinstance(self.model, PyramidCoveringModel)

        # PyLint doesn't know that this is a `PyramidCoveringModel`
        # and not a `TwoDCoveringModel`
        # pylint: disable=no-value-for-parameter
        self.model.set_size(size)
        self.model_changed.emit(self.model)

        self.message("Size updated")

    def block_sizes_accepted(self, min_val, max_val):
        """
        Updates covering model block size (after dialog confirmation)
        """
        assert self.model is not None

        self.model.set_block_size(min_val, max_val)
        self.model_changed.emit(self.model)

        self.message("Block size updated")

    @staticmethod
    def update_view(model, view):
        """
        Refreshes contents of given view
        """
        if view is None:
            return
        if model is not None and model.is_filled():
            view.show(model)
        else:
            view.close()

    def message(self, msg):
        """
        Shows a log message in the "Messages" window
        """
        self.messagesText.add_message(msg)

    def show_help(self):
        """
        Opens a webpage with help
        """
        webbrowser.open(self.HELP_URL)

    def create_action_groups(self):
        """
        Groups exclusive choice menu buttons in action groups.

        This should ideally be done in UI files, but Qt designer
        doesn't support it.
        """
        self.model_type_group = QActionGroup(self)
        self.model_type_group.addAction(self.action2D_Rectangle_2)
        self.model_type_group.addAction(self.actionPyramid_2)

        self.view_type_group = QActionGroup(self)

        self.model_type_group.triggered.connect(self.model_type_changed)
        self.view_type_group.triggered.connect(self.view_type_changed)

    def update_model_type(self):
        """
        Sets the current model after model type changed in menu
        """
        selected_model = self.model_type_group.checkedAction()

        if selected_model == self.action2D_Rectangle_2:
            model = TwoDCoveringModel(10, 10, 4, 4)
        elif selected_model == self.actionPyramid_2:
            model = PyramidCoveringModel(10, 4, 4)
        else:
            model = None

        self.model = model
        self.model_changed.emit(model)
        self.message("Model type updated")

    def enable_model_menu_buttons(self):
        """
        Enable menu buttons that are disabled at program start
        """
        self.actionChange_dimensions.setEnabled(True)
        self.actionChange_tile_size.setEnabled(True)
        self.actionGenerate.setEnabled(True)

    def update_view_type(self):
        """
        Sets the current view after view type changed in menu
        """
        if self.view is not None:
            self.view.close()

        selected_action = self.view_type_group.checkedAction()

        if selected_action is None:
            # Model was probably changed
            self.view = None
        else:
            action_name = selected_action.objectName()
            selected_view = self.action_views[action_name]

            self.view = selected_view()  # New instance of that view

            self.message("View type updated")

        self.view_changed.emit(self.view)

    def cancel_covering(self):
        """
        Stops ongoing covering
        """
        if self.thread.isRunning():
            # The thread is being terminated
            self.model.stop_covering()
            self.message("Covering terminated")

    def covering_success(self):
        """
        Prints a success log message (for now)
        """
        self.message("Covering successful")

    def covering_failed(self):
        """
        Prints a fail log message and shows an error window (for now)
        """
        self.message("Covering failed")
        QMessageBox.critical(self, "Failed", "Covering failed")

    def model_views(self, model):
        """
        Returns a list of tuples for all views
        for given mode as  (name, class)
        """

        if isinstance(model, TwoDCoveringModel):
            return [
                ("2D Print view", text_view_decorator(TwoDPrintView, self)),
                ("2D Visual view", parented_decorator(TwoDVisualView, self))
            ]

        if isinstance(model, PyramidCoveringModel):
            return [("Pyramid Print view",
                     text_view_decorator(PyramidPrintView, self)),
                    ("Pyramid Visual view", PyramidVisualView)]

        return []

    @staticmethod
    def model_constraints(model):
        """
        Returns a list of tuples for all constraint watchers
        for given mode as  (name, class)
        """

        if isinstance(model, TwoDCoveringModel):
            return [("Path blocks", PathConstraintWatcher)]

        if isinstance(model, PyramidCoveringModel):
            return [("Path blocks", PathConstraintWatcher),
                    ("Planar blocks", PlanarConstraintWatcher)]

        return []

    def update_view_type_menu(self):
        """
        Updates options for view type menu afted model type change
        """
        view_type_menu = self.menuType_2
        view_type_menu.clear()

        for action in self.view_type_group.actions():
            self.view_type_group.removeAction(action)

        all_views = self.model_views(self.model)

        if not all_views:
            # Likely no model selected
            view_type_menu.setEnabled(False)
            return

        view_type_menu.setEnabled(True)

        self.action_views.clear()

        for i, view_tuple in enumerate(all_views):
            name, view = view_tuple

            # As good as any, we just need to distinguish the actions
            action_name = f"Action{i}"

            action = QAction(self)
            action.setText(name)
            action.setCheckable(True)
            action.setObjectName(action_name)

            # So that we can later see which view should be activated
            self.action_views[action_name] = view
            view_type_menu.addAction(action)
            self.view_type_group.addAction(action)

        self.update_view_type()

    def watcher_set_active(self, constraint, value):
        """
        A slot, activate/deactivate constraint depending on value (True/False)
        """

        if value is True:
            self.model.add_constraint(constraint)
        else:
            self.model.remove_constraint(constraint)

        self.model_changed.emit(self.model)
        self.message("Constraint settings changed")

    def update_constraints_menu(self):
        """
        Updates options for model constraints after model type change
        """

        cstr_menu = self.menuConstraints
        cstr_menu.clear()

        all_constraints = self.model_constraints(self.model)

        for name, watcher in all_constraints:
            action = QAction(self)
            action.setText(name)
            action.setCheckable(True)

            action.toggled.connect(lambda val, watcher=watcher: self.
                                   watcher_set_active(watcher, val))

            cstr_menu.addAction(action)

        cstr_menu.setEnabled(True)

    def close(self):
        """
        While closing the window also closes the view
        """
        if self.view is not None:
            self.view.close()

        super().close()
コード例 #2
0
ファイル: PlaybackControl.py プロジェクト: pfrydlewicz/nexxT
class MVCPlaybackControlGUI(PlaybackControlConsole):
    """
    GUI implementation of MVCPlaybackControlBase
    """
    nameFiltersChanged = Signal("QStringList")

    def __init__(self, config):
        assertMainThread()
        super().__init__(config)

        # state
        self.preventSeek = False
        self.beginTime = None
        self.timeRatio = 1.0

        # gui
        srv = Services.getService("MainWindow")
        config.configLoaded.connect(self.restoreState)
        config.configAboutToSave.connect(self.saveState)
        self.config = config
        playbackMenu = srv.menuBar().addMenu("&Playback")

        style = QApplication.style()
        self.actStart = QAction(QIcon.fromTheme("media-playback-start", style.standardIcon(QStyle.SP_MediaPlay)),
                                "Start Playback", self)
        self.actPause = QAction(QIcon.fromTheme("media-playback-pause", style.standardIcon(QStyle.SP_MediaPause)),
                                "Pause Playback", self)
        self.actPause.setEnabled(False)
        self.actStepFwd = QAction(QIcon.fromTheme("media-seek-forward",
                                                  style.standardIcon(QStyle.SP_MediaSeekForward)),
                                  "Step Forward", self)
        self.actStepBwd = QAction(QIcon.fromTheme("media-seek-backward",
                                                  style.standardIcon(QStyle.SP_MediaSeekBackward)),
                                  "Step Backward", self)
        self.actSeekEnd = QAction(QIcon.fromTheme("media-skip-forward",
                                                  style.standardIcon(QStyle.SP_MediaSkipForward)),
                                  "Seek End", self)
        self.actSeekBegin = QAction(QIcon.fromTheme("media-skip-backward",
                                                    style.standardIcon(QStyle.SP_MediaSkipBackward)),
                                    "Seek Begin", self)
        self.actSetTimeFactor = {r : QAction("x 1/%d" % (1/r), self) if r < 1 else QAction("x %d" % r, self)
                                 for r in (1/8, 1/4, 1/2, 1, 2, 4, 8)}

        # pylint: disable=unnecessary-lambda
        # let's stay on the safe side and do not use emit as a slot...
        self.actStart.triggered.connect(lambda: self._startPlayback.emit())
        self.actPause.triggered.connect(lambda: self._pausePlayback.emit())
        self.actStepFwd.triggered.connect(lambda: self._stepForward.emit(self.selectedStream()))
        self.actStepBwd.triggered.connect(lambda: self._stepBackward.emit(self.selectedStream()))
        self.actSeekEnd.triggered.connect(lambda: self._seekEnd.emit())
        self.actSeekBegin.triggered.connect(lambda: self._seekBeginning.emit())
        # pylint: enable=unnecessary-lambda

        def setTimeFactor(newFactor):
            logger.debug("new time factor %f", newFactor)
            self._setTimeFactor.emit(newFactor)

        for r in self.actSetTimeFactor:
            logger.debug("adding action for time factor %f", r)
            self.actSetTimeFactor[r].triggered.connect(functools.partial(setTimeFactor, r))

        self.dockWidget = srv.newDockWidget("PlaybackControl", None, Qt.LeftDockWidgetArea)
        self.dockWidgetContents = QWidget(self.dockWidget)
        self.dockWidget.setWidget(self.dockWidgetContents)
        toolLayout = QBoxLayout(QBoxLayout.TopToBottom, self.dockWidgetContents)
        toolLayout.setContentsMargins(0, 0, 0, 0)
        toolBar = QToolBar()
        toolLayout.addWidget(toolBar)
        toolBar.addAction(self.actSeekBegin)
        toolBar.addAction(self.actStepBwd)
        toolBar.addAction(self.actStart)
        toolBar.addAction(self.actPause)
        toolBar.addAction(self.actStepFwd)
        toolBar.addAction(self.actSeekEnd)
        playbackMenu.addAction(self.actSeekBegin)
        playbackMenu.addAction(self.actStepBwd)
        playbackMenu.addAction(self.actStart)
        playbackMenu.addAction(self.actPause)
        playbackMenu.addAction(self.actStepFwd)
        playbackMenu.addAction(self.actSeekEnd)
        playbackMenu.addSeparator()
        for r in self.actSetTimeFactor:
            playbackMenu.addAction(self.actSetTimeFactor[r])
        self.timeRatioLabel = QLabel("x 1")
        self.timeRatioLabel.addActions(list(self.actSetTimeFactor.values()))
        self.timeRatioLabel.setContextMenuPolicy(Qt.ActionsContextMenu)
        toolBar.addSeparator()
        toolBar.addWidget(self.timeRatioLabel)
        contentsLayout = QGridLayout()
        toolLayout.addLayout(contentsLayout, 10)
        # now we add a position view
        self.positionSlider = QSlider(Qt.Horizontal, self.dockWidgetContents)
        self.beginLabel = QLabel(parent=self.dockWidgetContents)
        self.beginLabel.setAlignment(Qt.AlignLeft|Qt.AlignCenter)
        self.currentLabel = QLabel(parent=self.dockWidgetContents)
        self.currentLabel.setAlignment(Qt.AlignHCenter|Qt.AlignCenter)
        self.endLabel = QLabel(parent=self.dockWidgetContents)
        self.endLabel.setAlignment(Qt.AlignRight|Qt.AlignCenter)
        contentsLayout.addWidget(self.beginLabel, 0, 0, alignment=Qt.AlignLeft)
        contentsLayout.addWidget(self.currentLabel, 0, 1, alignment=Qt.AlignHCenter)
        contentsLayout.addWidget(self.endLabel, 0, 2, alignment=Qt.AlignRight)
        contentsLayout.addWidget(self.positionSlider, 1, 0, 1, 3)
        self.positionSlider.setTracking(False)
        self.positionSlider.valueChanged.connect(self.onSliderValueChanged, Qt.DirectConnection)
        self.positionSlider.sliderMoved.connect(self.displayPosition)

        # file browser
        self.browser = BrowserWidget(self.dockWidget)
        self.nameFiltersChanged.connect(self._onNameFiltersChanged, Qt.QueuedConnection)
        contentsLayout.addWidget(self.browser, 3, 0, 1, 3)
        contentsLayout.setRowStretch(3, 100)
        self.browser.activated.connect(self.browserActivated)

        self.actShowAllFiles = QAction("Show all files")
        self.actShowAllFiles.setCheckable(True)
        self.actShowAllFiles.setChecked(False)
        self.actShowAllFiles.toggled.connect(self._onShowAllFiles)
        playbackMenu.addSeparator()
        playbackMenu.addAction(self.actShowAllFiles)

        self.actGroupStream = QActionGroup(self)
        self.actGroupStream.setExclusionPolicy(QActionGroup.ExclusionPolicy.ExclusiveOptional)
        playbackMenu.addSeparator()
        self.actGroupStreamMenu = playbackMenu.addMenu("Step Stream")
        self._selectedStream = None

        self.recentSeqs = [QAction() for i in range(10)]
        playbackMenu.addSeparator()
        recentMenu = playbackMenu.addMenu("Recent")
        for a in self.recentSeqs:
            a.setVisible(False)
            a.triggered.connect(self.openRecent)
            recentMenu.addAction(a)

        self._supportedFeaturesChanged(set(), set())

    def __del__(self):
        logger.internal("deleting playback control")

    def _onNameFiltersChanged(self, nameFilt):
        self.browser.setFilter(nameFilt)

    def _onShowAllFiles(self, enabled):
        self.fileSystemModel.setNameFilterDisables(enabled)

    def _supportedFeaturesChanged(self, featureset, nameFilters):
        """
        overwritten from MVCPlaybackControlBase. This function is called
        from multiple threads, but not at the same time.

        :param featureset: the current featureset
        :return:
        """
        assertMainThread()
        self.featureset = featureset
        self.actStepFwd.setEnabled("stepForward" in featureset)
        self.actStepBwd.setEnabled("stepBackward" in featureset)
        self.actSeekBegin.setEnabled("seekBeginning" in featureset)
        self.actSeekEnd.setEnabled("seekEnd" in featureset)
        self.positionSlider.setEnabled("seekTime" in featureset)
        self.browser.setEnabled("setSequence" in featureset)
        self.timeRatioLabel.setEnabled("setTimeFactor" in featureset)
        for f in self.actSetTimeFactor:
            self.actSetTimeFactor[f].setEnabled("setTimeFactor" in featureset)
        self.timeRatioLabel.setEnabled("setTimeFactor" in featureset)
        self.timeRatioLabel.setEnabled("setTimeFactor" in featureset)
        self.timeRatioLabel.setEnabled("setTimeFactor" in featureset)
        self.timeRatioLabel.setEnabled("setTimeFactor" in featureset)
        if "startPlayback" not in featureset:
            self.actStart.setEnabled(False)
        if "pausePlayback" not in featureset:
            self.actPause.setEnabled(False)
        logger.debug("current feature set: %s", featureset)
        logger.debug("Setting name filters of browser: %s", list(nameFilters))
        self.nameFiltersChanged.emit(list(nameFilters))
        super()._supportedFeaturesChanged(featureset, nameFilters)

    def scrollToCurrent(self):
        """
        Scrolls to the current item in the browser

        :return:
        """
        assertMainThread()
        c = self.browser.current()
        if c is not None:
            self.browser.scrollTo(c)

    def _sequenceOpened(self, filename, begin, end, streams):
        """
        Notifies about an opened sequence.

        :param filename: the filename which has been opened
        :param begin: timestamp of sequence's first sample
        :param end: timestamp of sequence's last sample
        :param streams: list of streams in the sequence
        :return: None
        """
        assertMainThread()
        self.beginTime = begin
        self.preventSeek = True
        self.positionSlider.setRange(0, end.toMSecsSinceEpoch() - begin.toMSecsSinceEpoch())
        self.preventSeek = False
        self.beginLabel.setText(begin.toString("hh:mm:ss.zzz"))
        self.endLabel.setText(end.toString("hh:mm:ss.zzz"))
        self._currentTimestampChanged(begin)
        try:
            self.browser.blockSignals(True)
            self.browser.setActive(filename)
            self.browser.scrollTo(filename)
        finally:
            self.browser.blockSignals(False)
        self._selectedStream = None
        for a in self.actGroupStream.actions():
            logger.debug("Remove stream group action: %s", a.data())
            self.actGroupStream.removeAction(a)
        for stream in streams:
            act = QAction(stream, self.actGroupStream)
            act.triggered.connect(lambda cstream=stream: self.setSelectedStream(cstream))
            act.setCheckable(True)
            act.setChecked(False)
            logger.debug("Add stream group action: %s", act.data())
            self.actGroupStreamMenu.addAction(act)
        QTimer.singleShot(250, self.scrollToCurrent)
        super()._sequenceOpened(filename, begin, end, streams)

    def _currentTimestampChanged(self, currentTime):
        """
        Notifies about a changed timestamp

        :param currentTime: the new current timestamp
        :return: None
        """
        assertMainThread()
        if self.beginTime is None:
            self.currentLabel.setText("")
        else:
            sliderVal = currentTime.toMSecsSinceEpoch() - self.beginTime.toMSecsSinceEpoch()
            self.preventSeek = True
            self.positionSlider.setValue(sliderVal)
            self.preventSeek = False
            self.positionSlider.blockSignals(False)
            self.currentLabel.setEnabled(True)
            self.currentLabel.setText(currentTime.toString("hh:mm:ss.zzz"))
        super()._currentTimestampChanged(currentTime)

    def onSliderValueChanged(self, value):
        """
        Slot called whenever the slider value is changed.

        :param value: the new slider value
        :return:
        """
        assertMainThread()
        if self.beginTime is None or self.preventSeek:
            return
        if self.actStart.isEnabled():
            ts = QDateTime.fromMSecsSinceEpoch(self.beginTime.toMSecsSinceEpoch() + value, self.beginTime.timeSpec())
            self._seekTime.emit(ts)
        else:
            logger.warning("Can't seek while playing.")

    def displayPosition(self, value):
        """
        Slot called when the slider is moved. Displays the position without actually seeking to it.

        :param value: the new slider value.
        :return:
        """
        assertMainThread()
        if self.beginTime is None:
            return
        if self.positionSlider.isSliderDown():
            ts = QDateTime.fromMSecsSinceEpoch(self.beginTime.toMSecsSinceEpoch() + value, self.beginTime.timeSpec())
            self.currentLabel.setEnabled(False)
            self.currentLabel.setText(ts.toString("hh:mm:ss.zzz"))

    def _playbackStarted(self):
        """
        Notifies about starting playback

        :return: None
        """
        assertMainThread()
        self.actStart.setEnabled(False)
        if "pausePlayback" in self.featureset:
            self.actPause.setEnabled(True)
        super()._playbackStarted()

    def _playbackPaused(self):
        """
        Notifies about pause playback

        :return: None
        """
        assertMainThread()
        logger.debug("playbackPaused received")
        if "startPlayback" in self.featureset:
            self.actStart.setEnabled(True)
        self.actPause.setEnabled(False)
        super()._playbackPaused()

    def openRecent(self):
        """
        Called when the user clicks on a recent sequence.

        :return:
        """
        assertMainThread()
        action = self.sender()
        self.browser.setActive(action.data())

    def browserActivated(self, filename):
        """
        Called when the user activated a file.

        :param filename: the new filename
        :return:
        """
        assertMainThread()
        if filename is not None and Path(filename).is_file():
            foundIdx = None
            for i, a in enumerate(self.recentSeqs):
                if a.data() == filename:
                    foundIdx = i
            if foundIdx is None:
                foundIdx = len(self.recentSeqs)-1
            for i in range(foundIdx, 0, -1):
                self.recentSeqs[i].setText(self.recentSeqs[i-1].text())
                self.recentSeqs[i].setData(self.recentSeqs[i-1].data())
                logger.debug("%d data: %s", i, self.recentSeqs[i-1].data())
                self.recentSeqs[i].setVisible(self.recentSeqs[i-1].data() is not None)
            self.recentSeqs[0].setText(self.compressFileName(filename))
            self.recentSeqs[0].setData(filename)
            self.recentSeqs[0].setVisible(True)
            self._setSequence.emit(filename)

    def _timeRatioChanged(self, newRatio):
        """
        Notifies about a changed playback time ratio,

        :param newRatio the new playback ratio as a float
        :return: None
        """
        assertMainThread()
        self.timeRatio = newRatio
        logger.debug("new timeRatio: %f", newRatio)
        for r in [1/8, 1/4, 1/2, 1, 2, 4, 8]:
            if abs(newRatio / r - 1) < 0.01:
                self.timeRatioLabel.setText(("x 1/%d"%(1/r)) if r < 1 else ("x %d"%r))
                return
        self.timeRatioLabel.setText("%.2f" % newRatio)
        super()._timeRatioChanged(newRatio)

    def selectedStream(self):
        """
        Returns the user-selected stream (for forward/backward stepping)

        :return:
        """
        return self._selectedStream

    def setSelectedStream(self, stream):
        """
        Sets the user-selected stream (for forward/backward stepping)

        :param stream the stream name.
        :return:
        """
        self._selectedStream = stream

    def saveState(self):
        """
        Saves the state of the playback control

        :return:
        """
        assertMainThread()
        propertyCollection = self.config.guiState()
        showAllFiles = self.actShowAllFiles.isChecked()
        folder = self.browser.folder()
        logger.debug("Storing current folder: %s", folder)
        try:
            propertyCollection.setProperty("PlaybackControl_showAllFiles", int(showAllFiles))
            propertyCollection.setProperty("PlaybackControl_folder", folder)
            recentFiles = [a.data() for a in self.recentSeqs if a.data() is not None]
            propertyCollection.setProperty("PlaybackControl_recent", "|".join(recentFiles))
        except PropertyCollectionPropertyNotFound:
            pass

    def restoreState(self):
        """
        Restores the state of the playback control from the given property collection

        :param propertyCollection: a PropertyCollection instance
        :return:
        """
        assertMainThread()
        propertyCollection = self.config.guiState()
        propertyCollection.defineProperty("PlaybackControl_showAllFiles", 0, "show all files setting")
        showAllFiles = propertyCollection.getProperty("PlaybackControl_showAllFiles")
        self.actShowAllFiles.setChecked(bool(showAllFiles))
        propertyCollection.defineProperty("PlaybackControl_folder", "", "current folder name")
        folder = propertyCollection.getProperty("PlaybackControl_folder")
        if Path(folder).is_dir():
            logger.debug("Setting current file: %s", folder)
            self.browser.setFolder(folder)
        propertyCollection.defineProperty("PlaybackControl_recent", "", "recent opened sequences")
        recentFiles = propertyCollection.getProperty("PlaybackControl_recent")
        idx = 0
        for f in recentFiles.split("|"):
            if f != "" and Path(f).is_file():
                self.recentSeqs[idx].setData(f)
                self.recentSeqs[idx].setText(self.compressFileName(f))
                self.recentSeqs[idx].setVisible(True)
                idx += 1
                if idx >= len(self.recentSeqs):
                    break
        for a in self.recentSeqs[idx:]:
            a.setData(None)
            a.setText("")
            a.setVisible(False)

    @staticmethod
    def compressFileName(filename):
        """
        Compresses long path names with an ellipsis (...)

        :param filename: the original path name as a Path or string instance
        :return: the compressed path name as a string instance
        """
        p = Path(filename)
        parts = tuple(p.parts)
        if len(parts) >= 6:
            p = Path(*parts[:2]) / "..." /  Path(*parts[-2:])
        return str(p)
コード例 #3
0
    def __init__(
        self,
        document: Optional[vp.Document] = None,
        view_mode: ViewMode = ViewMode.PREVIEW,
        show_pen_up: bool = False,
        show_points: bool = False,
        parent=None,
    ):
        super().__init__(parent)

        self._settings = QSettings()
        self._settings.beginGroup("viewer")

        self.setWindowTitle("vpype viewer")
        self.setStyleSheet("""
        QToolButton:pressed {
            background-color: rgba(0, 0, 0, 0.2);
        }
        """)

        self._viewer_widget = QtViewerWidget(parent=self)

        # setup toolbar
        self._toolbar = QToolBar()
        self._icon_size = QSize(32, 32)
        self._toolbar.setIconSize(self._icon_size)

        view_mode_grp = QActionGroup(self._toolbar)
        if _DEBUG_ENABLED:
            act = view_mode_grp.addAction("None")
            act.setCheckable(True)
            act.setChecked(view_mode == ViewMode.NONE)
            act.triggered.connect(
                functools.partial(self.set_view_mode, ViewMode.NONE))
        act = view_mode_grp.addAction("Outline Mode")
        act.setCheckable(True)
        act.setChecked(view_mode == ViewMode.OUTLINE)
        act.triggered.connect(
            functools.partial(self.set_view_mode, ViewMode.OUTLINE))
        act = view_mode_grp.addAction("Outline Mode (Colorful)")
        act.setCheckable(True)
        act.setChecked(view_mode == ViewMode.OUTLINE_COLORFUL)
        act.triggered.connect(
            functools.partial(self.set_view_mode, ViewMode.OUTLINE_COLORFUL))
        act = view_mode_grp.addAction("Preview Mode")
        act.setCheckable(True)
        act.setChecked(view_mode == ViewMode.PREVIEW)
        act.triggered.connect(
            functools.partial(self.set_view_mode, ViewMode.PREVIEW))
        self.set_view_mode(view_mode)

        # VIEW MODE
        # view modes
        view_mode_btn = QToolButton()
        view_mode_menu = QMenu(view_mode_btn)
        act = view_mode_menu.addAction("View Mode:")
        act.setEnabled(False)
        view_mode_menu.addActions(view_mode_grp.actions())
        view_mode_menu.addSeparator()
        # show pen up
        act = view_mode_menu.addAction("Show Pen-Up Trajectories")
        act.setCheckable(True)
        act.setChecked(show_pen_up)
        act.toggled.connect(self.set_show_pen_up)
        self._viewer_widget.engine.show_pen_up = show_pen_up
        # show points
        act = view_mode_menu.addAction("Show Points")
        act.setCheckable(True)
        act.setChecked(show_points)
        act.toggled.connect(self.set_show_points)
        self._viewer_widget.engine.show_points = show_points
        # preview mode options
        view_mode_menu.addSeparator()
        act = view_mode_menu.addAction("Preview Mode Options:")
        act.setEnabled(False)
        # pen width
        pen_width_menu = view_mode_menu.addMenu("Pen Width")
        act_grp = PenWidthActionGroup(0.3, parent=pen_width_menu)
        act_grp.triggered.connect(self.set_pen_width_mm)
        pen_width_menu.addActions(act_grp.actions())
        self.set_pen_width_mm(0.3)
        # pen opacity
        pen_opacity_menu = view_mode_menu.addMenu("Pen Opacity")
        act_grp = PenOpacityActionGroup(0.8, parent=pen_opacity_menu)
        act_grp.triggered.connect(self.set_pen_opacity)
        pen_opacity_menu.addActions(act_grp.actions())
        self.set_pen_opacity(0.8)
        # debug view
        if _DEBUG_ENABLED:
            act = view_mode_menu.addAction("Debug View")
            act.setCheckable(True)
            act.toggled.connect(self.set_debug)
        # rulers
        view_mode_menu.addSeparator()
        act = view_mode_menu.addAction("Show Rulers")
        act.setCheckable(True)
        val = bool(self._settings.value("show_rulers", True))
        act.setChecked(val)
        act.toggled.connect(self.set_show_rulers)
        self._viewer_widget.engine.show_rulers = val
        # units
        units_menu = view_mode_menu.addMenu("Units")
        unit_action_grp = QActionGroup(units_menu)
        unit_type = UnitType(self._settings.value("unit_type",
                                                  UnitType.METRIC))
        act = unit_action_grp.addAction("Metric")
        act.setCheckable(True)
        act.setChecked(unit_type == UnitType.METRIC)
        act.setData(UnitType.METRIC)
        act = unit_action_grp.addAction("Imperial")
        act.setCheckable(True)
        act.setChecked(unit_type == UnitType.IMPERIAL)
        act.setData(UnitType.IMPERIAL)
        act = unit_action_grp.addAction("Pixel")
        act.setCheckable(True)
        act.setChecked(unit_type == UnitType.PIXELS)
        act.setData(UnitType.PIXELS)
        unit_action_grp.triggered.connect(self.set_unit_type)
        units_menu.addActions(unit_action_grp.actions())
        self._viewer_widget.engine.unit_type = unit_type

        view_mode_btn.setMenu(view_mode_menu)
        view_mode_btn.setIcon(load_icon("eye-outline.svg"))
        view_mode_btn.setText("View")
        view_mode_btn.setPopupMode(QToolButton.InstantPopup)
        view_mode_btn.setStyleSheet(
            "QToolButton::menu-indicator { image: none; }")
        self._toolbar.addWidget(view_mode_btn)

        # LAYER VISIBILITY
        self._layer_visibility_btn = QToolButton()
        self._layer_visibility_btn.setIcon(
            load_icon("layers-triple-outline.svg"))
        self._layer_visibility_btn.setText("Layer")
        self._layer_visibility_btn.setMenu(QMenu(self._layer_visibility_btn))
        self._layer_visibility_btn.setPopupMode(QToolButton.InstantPopup)
        self._layer_visibility_btn.setStyleSheet(
            "QToolButton::menu-indicator { image: none; }")
        self._toolbar.addWidget(self._layer_visibility_btn)

        # FIT TO PAGE
        fit_act = self._toolbar.addAction(load_icon("fit-to-page-outline.svg"),
                                          "Fit")
        fit_act.triggered.connect(self._viewer_widget.engine.fit_to_viewport)

        # RULER
        # TODO: not implemented yet
        # self._toolbar.addAction(load_icon("ruler-square.svg"), "Units")

        # MOUSE COORDINATES>
        self._mouse_coord_lbl = QLabel("")
        self._mouse_coord_lbl.setMargin(6)
        self._mouse_coord_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
        self._mouse_coord_lbl.setSizePolicy(QSizePolicy.Expanding,
                                            QSizePolicy.Minimum)
        self._toolbar.addWidget(self._mouse_coord_lbl)
        # noinspection PyUnresolvedReferences
        self._viewer_widget.mouse_coords.connect(
            self.set_mouse_coords)  # type: ignore

        # setup horizontal layout for optional side widgets
        self._hlayout = QHBoxLayout()
        self._hlayout.setSpacing(0)
        self._hlayout.setMargin(0)
        self._hlayout.addWidget(self._viewer_widget)
        widget = QWidget()
        widget.setLayout(self._hlayout)

        # setup global vertical layout
        layout = QVBoxLayout()
        layout.setSpacing(0)
        layout.setMargin(0)
        layout.addWidget(self._toolbar)
        layout.addWidget(widget)
        self.setLayout(layout)

        if document is not None:
            self.set_document(document)
コード例 #4
0
class ProfileManager(QObject):
    profile_changed = Signal(Profile)

    def __init__(self, menu, parent=None):
        super().__init__(parent)
        self.menu = menu
        actions = self.menu.actions()
        self.sep = actions[-2]
        self.parent = parent
        self.profiles = []
        self.actGroup = None
        self.active_profile = None
        self.load()
        QApplication.instance().aboutToQuit.connect(self.save)

    def load(self):
        settings = QSettings()
        settings.beginGroup('profiles')
        groups = settings.childGroups()
        self.profiles = [
            Profile(p,
                    settings.value(f'{p}/path'), settings.value(f'{p}/mask'),
                    settings.value(f'{p}/pattern')) for p in groups
        ]
        settings.endGroup()
        self.actGroup = QActionGroup(self.parent)
        self.actGroup.triggered.connect(self.set_active_profile)
        active = settings.value('active_profile')
        self.active_profile = self.get_profile(active)
        if len(self.profiles) > 0:
            for name in self.names():
                action = self.do_add_action(name)
                if name == active:
                    action.setChecked(True)

    def save(self):
        settings = QSettings()
        settings.beginGroup('profiles')
        settings.remove('')
        for p in self.profiles:
            settings.setValue(f'{p.name}/path', p.path)
            settings.setValue(f'{p.name}/mask', p.mask)
            settings.setValue(f'{p.name}/pattern', p.pattern)
        settings.endGroup()
        if self.active_profile is not None:
            settings.setValue('active_profile', self.active_profile.name)

    def names(self):
        for p in self.profiles:
            yield p.name

    def add_action(self, name, path, mask, pattern):
        if name in self.names():
            app = QApplication.instance()
            QMessageBox.warning(
                self.parent, app.applicationName(),
                app.translate('profile_manager',
                              '{} already exists').format(name))
        else:
            self.profiles.append(Profile(name, path, mask, pattern))
            self.do_add_action(name)

    def do_add_action(self, name):
        action = QAction(name, self.menu)
        self.menu.insertAction(self.sep, action)
        action.setCheckable(True)
        self.actGroup.addAction(action)
        return action

    def add_from_dialog(self, dialog):
        self.add_action(dialog.get_name(), dialog.get_path(),
                        dialog.get_mask(), dialog.get_pattern())

    def get_profile(self, name):
        for p in self.profiles:
            if name == p.name:
                return p
        return None

    def set_active_profile(self):
        action = self.actGroup.checkedAction()
        self.active_profile = self.get_profile(action.text()) if action \
            is not None else None
        self.profile_changed.emit(self.active_profile)

    def reset_profiles(self, profiles):
        self.clear_menu()
        self.profiles = profiles
        if len(profiles) > 0:
            if self.active_profile.name not in (p.name for p in profiles):
                active = profiles[0].name
            else:
                active = self.active_profile.name
            for name in self.names():
                action = self.do_add_action(name)
                if name == active:
                    action.setChecked(True)
        else:
            active = None
        self.active_profile = self.get_profile(active)
        self.profile_changed.emit(self.active_profile)

    def clear_menu(self):
        while len(self.actGroup.actions()) > 0:
            self.actGroup.removeAction(self.actGroup.actions()[0])
コード例 #5
0
class TreeContextMenu(QMenu):
    def __init__(self, view, ui, menu_name: str = _('Baum Kontextmenü')):
        """ Context menu of tree views

        :param modules.itemview.tree_view.KnechtTreeView view: tree view
        :param KnechtWindow ui: main window ui class
        :param str menu_name: name of the menu
        """
        super(TreeContextMenu, self).__init__(menu_name, view)
        self.view, self.ui, self.status_bar = view, ui, ui.statusBar()

        self.edit_menu = self.ui.main_menu.edit_menu
        self.create_menu = CreateMenu(self)
        self.tree_menu = TreeMenu(self, ui)

        self.send_dg_action = QAction(IconRsc.get_icon('paperplane'),
                                      _('Senden an DeltaGen'), self)
        dg_tip_1 = _(
            'Selektierte Bauminhalte als Variantenschaltung mit vorherigem Reset an DeltaGen senden.'
        )
        self.send_dg_action.setToolTip(dg_tip_1)
        self.send_dg_action.setStatusTip(dg_tip_1)
        self.send_dg_action.triggered.connect(self.send_to_deltagen)
        self.addAction(self.send_dg_action)
        self.addSeparator()

        self.send_ave_action = QAction(IconRsc.get_icon('paperplane'),
                                       _('Senden an AVE'), self)
        dg_tip_1 = _(
            'Selektierte Bauminhalte als Konfiguration an laufende AVE Instanz senden.'
        )
        self.send_ave_action.setToolTip(dg_tip_1)
        self.send_ave_action.setStatusTip(dg_tip_1)
        self.send_ave_action.triggered.connect(self.send_to_ave)
        self.addAction(self.send_ave_action)
        self.addSeparator()

        self.send_dg_short = QAction(IconRsc.get_icon('paperplane'),
                                     _('Ohne Reset an DeltaGen senden'), self)
        dg_tip_2 = _(
            'Selektierte Bauminhalte ohne einen Reset an DeltaGen senden.')
        self.send_dg_short.setToolTip(dg_tip_2)
        self.send_dg_short.setStatusTip(dg_tip_2)
        self.send_dg_short.triggered.connect(self.send_to_deltagen_wo_reset)
        self.addAction(self.send_dg_short)
        self.addSeparator()

        # -- PR-String Actions
        copy_pr = QAction(IconRsc.get_icon('options'),
                          _('PR String in Zwischenablage kopieren'), self)
        copy_pr.triggered.connect(self.copy_strings_to_clipboard)
        self.addAction(copy_pr)
        copy_li = QAction(IconRsc.get_icon('assignment'),
                          _('Linc String in Zwischenablage kopieren'), self)
        copy_li.triggered.connect(self.copy_linc_string_to_clipboard)
        self.addAction(copy_li)
        self.addSeparator()

        # -- PlmXml Actions
        self.show_plmxml_scn = QAction(IconRsc.get_icon('dog'),
                                       'PlmXml Schnuffi', self)
        self.show_plmxml_scn.triggered.connect(self.show_plmxml_scene)
        self.addAction(self.show_plmxml_scn)
        self.addSeparator()

        # ---- Create preset from selected actions ----
        self.addActions([
            self.create_menu.user_preset_from_selected,
            self.create_menu.render_preset_from_selected
        ])
        self.addSeparator()

        # ---- Prepare Context Menus & Actions ----
        # ---- Add main menu > edit -----
        self.addMenu(self.edit_menu)
        # ---- Add main menu > tree -----
        self.addMenu(self.tree_menu)
        # ---- Add main menu > create -----
        self.addMenu(self.create_menu)

        self.addSeparator()

        self.remove_row_action = QAction(
            IconRsc.get_icon('trash-a'),
            _('Selektierte Zeilen entfernen\tEntf'), self)
        self.remove_row_action.triggered.connect(
            self.edit_menu.remove_rows_action.trigger)
        self.addAction(self.remove_row_action)

        self.addSeparator()

        # ---- Developer Actions -----
        self.dev_actions = QActionGroup(self)
        cake = QAction(IconRsc.get_icon('layer'), '--- The cake was a lie ---',
                       self.dev_actions)

        show_id_action = QAction(IconRsc.get_icon('options'),
                                 'Show ID columns', self.dev_actions)
        show_id_action.triggered.connect(self.show_id_columns)

        hide_id_action = QAction(IconRsc.get_icon('options-neg'),
                                 _('Hide ID columns'), self.dev_actions)
        hide_id_action.triggered.connect(self.hide_id_columns)

        list_tab_widgets = QAction(IconRsc.get_icon('navicon'),
                                   'List Tab Widgets', self.dev_actions)
        list_tab_widgets.triggered.connect(self.list_tab_widgets)

        report_action = QAction('Report Item attributes to log',
                                self.dev_actions)
        report_action.setShortcut(QKeySequence('Ctrl+B'))
        report_action.triggered.connect(self.report_current)

        log_level = QAction(IconRsc.get_icon('sort'), 'Enable DEBUG log level',
                            self.dev_actions)
        log_level.triggered.connect(self.ui.app.set_debug_log_level)

        produce_exception = QAction(IconRsc.get_icon('warn'),
                                    'Produce Exception', self.dev_actions)
        produce_exception.triggered.connect(self.ui.app.produce_exception)

        open_dir = QAction(IconRsc.get_icon('folder'),
                           'Open Settings Directoy', self.dev_actions)
        open_dir.triggered.connect(self.open_settings_dir)

        notify = QAction(IconRsc.get_icon('eye-disabled'),
                         'Show tray notification', self.dev_actions)
        notify.triggered.connect(self.noclick_tray_notification)

        notify_click = QAction(IconRsc.get_icon('eye'),
                               'Show click tray notification',
                               self.dev_actions)
        notify_click.triggered.connect(self.click_tray_notification)

        overlay_btn_msg = QAction(IconRsc.get_icon('check_box'),
                                  'Show overlay confirm message',
                                  self.dev_actions)
        overlay_btn_msg.triggered.connect(self.overlay_confirm_message)

        overlay_msg = QAction(IconRsc.get_icon('check_box_empty'),
                              'Show regular overlay message', self.dev_actions)
        overlay_msg.triggered.connect(self.overlay_message)

        overlay_imm_msg = QAction(IconRsc.get_icon('reset'),
                                  'Show immediate overlay message',
                                  self.dev_actions)
        overlay_imm_msg.triggered.connect(self.overlay_message_immediate)

        restart = QAction(IconRsc.get_icon('reset'), 'Restart',
                          self.dev_actions)
        restart.triggered.connect(self.restart_app)

        reorder = QAction(IconRsc.get_icon('sort'),
                          'Rewrite item order whole tree', self.dev_actions)
        reorder.triggered.connect(self.reorder_tree)

        self.addActions(self.dev_actions.actions())
        self.dev_actions.setVisible(False)

        self.aboutToShow.connect(self.update_actions)

        # Install context menu event filter
        self.view.installEventFilter(self)

    def eventFilter(self, obj, event):
        if obj is not self.view:
            return False

        if event.type() == QtCore.QEvent.ContextMenu:
            self.dev_actions.setVisible(False)

            # Hold Control and Shift to display dev context
            if event.modifiers(
            ) == QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier:
                self.dev_actions.setVisible(True)

            self.popup(event.globalPos())
            return True

        return False

    def send_to_deltagen(self):
        variants = self.view.editor.collect.collect_current_index()
        self.ui.app.send_dg.send_variants(variants, self.view)

    def send_to_ave(self):
        variants = self.view.editor.collect.collect_current_index()
        variants.ave = True
        self.ui.app.send_dg.send_variants(variants, self.view)

    def send_to_deltagen_wo_reset(self):
        variants = self.view.editor.collect.collect_current_index(
            collect_reset=False)
        self.ui.app.send_dg.send_variants(variants, self.view)

    def copy_strings_to_clipboard(self):
        variants = self.view.editor.collect.collect_current_index(
            collect_reset=False)

        pr_string = ''
        for variant in variants.variants:
            pr_string += f'{variant.name} {variant.value};'

        self.ui.app.clipboard().setText(pr_string)

    def copy_linc_string_to_clipboard(self):
        variants = self.view.editor.collect.collect_current_index(
            collect_reset=False)

        pr_string = ''
        for variant in variants.variants:
            pr_string += f'+{variant.name}'

        self.ui.app.clipboard().setText(pr_string)

    def _get_plmxml_item(self, variants: KnechtVariantList) -> Optional[Path]:
        if variants.plm_xml_path:
            return Path(variants.plm_xml_path)

        index, src_model = self.view.editor.get_current_selection()
        current_item = src_model.get_item(index)

        if current_item.userType != Kg.plmxml_item:
            return
        else:
            plmxml_file = Path(current_item.data(Kg.VALUE))
            if not path_exists(plmxml_file):
                return

        return plmxml_file

    def show_plmxml_scene(self):
        variants = self.view.editor.collect.collect_current_index()
        plmxml_file = self._get_plmxml_item(variants)
        if not plmxml_file:
            return
        plmxml_scene_page = KnechtPlmXmlScene(self.ui, plmxml_file, variants)

        GenericTabWidget(self.ui, plmxml_scene_page)

    def hide_id_columns(self):
        self.view.hideColumn(Kg.REF)
        self.view.hideColumn(Kg.ID)

    def show_id_columns(self):
        self.view.showColumn(Kg.REF)
        self.view.showColumn(Kg.ID)

    def list_tab_widgets(self):
        self.ui.view_mgr.log_tabs()

    def report_current(self):
        self.view.editor.report_current()

    def open_settings_dir(self):
        settings_dir = Path(get_settings_dir())

        if path_exists(settings_dir):
            q = QUrl.fromLocalFile(settings_dir.as_posix())
            QDesktopServices.openUrl(q)

    def noclick_tray_notification(self):
        self.ui.show_tray_notification(
            title='Test Notification',
            message='Clicking the message should hopefully emit nothing.')

    def click_tray_notification(self):
        def test_callback():
            LOGGER.info('Test notification click callback activated.')
            self.ui.msg('Message triggered by notification messageClicked.',
                        4000)

        self.ui.show_tray_notification(
            title='Test Notification',
            message='Clicking the message should trigger a overlay message.',
            clicked_callback=test_callback)

    def restart_app(self):
        restart_knecht_app(self.ui)

    def reorder_tree(self):
        for idx, _ in self.view.editor.iterator.iterate_view():
            self.view.editor.iterator.order_items(idx)

    def overlay_message(self):
        self.view.info_overlay.display(
            'Message in queue for a duration of 5000ms', 5000)

    def overlay_message_immediate(self):
        self.view.info_overlay.display(
            'Immediate message for a duration of 6000ms', 6000, True)

    def overlay_confirm_message(self):
        buttons = (('Buttontext 1', None), ('Buttontext 2', None))
        self.view.info_overlay.display_confirm(
            'Test Message to confirm something. '
            'Lenghty information ahead! This message ends '
            'with this sentence.', buttons)

    def update_actions(self):
        src_model = self.view.model().sourceModel()
        if src_model.id_mgr.has_recursive_items():
            self.send_dg_action.setEnabled(False)
            self.send_dg_short.setEnabled(False)
        else:
            self.send_dg_action.setEnabled(True)
            self.send_dg_short.setEnabled(True)

        self.create_menu.update_current_view()

        if self.view.is_render_view:
            self.send_dg_action.setEnabled(False)
            self.send_dg_short.setEnabled(False)

        self.show_plmxml_scn.setEnabled(False)
        index, src_model = self.view.editor.get_current_selection()
        current_item = src_model.get_item(index)
        if current_item.userType in (Kg.plmxml_item, Kg.preset):
            self.show_plmxml_scn.setEnabled(True)
コード例 #6
0
class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowIcon(QIcon(":/icons/apps/16/tabulator.svg"))

        self._recentDocuments = []
        self._actionRecentDocuments = []
        self._keyboardShortcutsDialog = None

        self._preferences = Preferences()
        self._preferences.loadSettings()

        self._createActions()
        self._createMenus()
        self._createToolBars()

        self._loadSettings()

        self._updateActions()
        self._updateActionFullScreen()
        self._updateMenuOpenRecent()

        # Central widget
        self._documentArea = QMdiArea()
        self._documentArea.setViewMode(QMdiArea.TabbedView)
        self._documentArea.setTabsMovable(True)
        self._documentArea.setTabsClosable(True)
        self.setCentralWidget(self._documentArea)
        self._documentArea.subWindowActivated.connect(self._onDocumentWindowActivated)


    def closeEvent(self, event):

        if True:
            # Store application properties and preferences
            self._saveSettings()
            self._preferences.saveSettings()

            event.accept()
        else:
            event.ignore()


    def _loadSettings(self):

        settings = QSettings()

        # Recent documents
        size = settings.beginReadArray("RecentDocuments")
        for idx in range(size-1, -1, -1):
            settings.setArrayIndex(idx)
            canonicalName = QFileInfo(settings.value("Document")).canonicalFilePath()
            self._updateRecentDocuments(canonicalName)
        settings.endArray()

        # Application properties: Geometry
        geometry = settings.value("Application/Geometry", QByteArray()) if self._preferences.restoreApplicationGeometry() else QByteArray()
        if not geometry.isEmpty():
            self.restoreGeometry(geometry)
        else:
            availableGeometry = self.screen().availableGeometry()
            self.resize(availableGeometry.width() * 2/3, availableGeometry.height() * 2/3)
            self.move((availableGeometry.width() - self.width()) / 2, (availableGeometry.height() - self.height()) / 2)

        # Application properties: State
        state = settings.value("Application/State", QByteArray()) if self._preferences.restoreApplicationState() else QByteArray()
        if not state.isEmpty():
            self.restoreState(state)
        else:
            self._toolbarApplication.setVisible(True)
            self._toolbarDocument.setVisible(True)
            self._toolbarEdit.setVisible(True)
            self._toolbarTools.setVisible(True)
            self._toolbarView.setVisible(False)
            self._toolbarHelp.setVisible(False)


    def _saveSettings(self):

        settings = QSettings()

        # Recent documents
        if not self._preferences.restoreRecentDocuments():
            self._recentDocuments.clear()
        settings.remove("RecentDocuments")
        settings.beginWriteArray("RecentDocuments")
        for idx in range(len(self._recentDocuments)):
            settings.setArrayIndex(idx)
            settings.setValue("Document", self._recentDocuments[idx])
        settings.endArray()

        # Application properties: Geometry
        geometry = self.saveGeometry() if self._preferences.restoreApplicationGeometry() else QByteArray()
        settings.setValue("Application/Geometry", geometry)

        # Application properties: State
        state = self.saveState() if self._preferences.restoreApplicationState() else QByteArray()
        settings.setValue("Application/State", state)


    def _createActions(self):

        #
        # Actions: Application

        self._actionAbout = QAction(self.tr("About {0}").format(QApplication.applicationName()), self)
        self._actionAbout.setObjectName("actionAbout")
        self._actionAbout.setIcon(QIcon(":/icons/apps/16/tabulator.svg"))
        self._actionAbout.setIconText(self.tr("About"))
        self._actionAbout.setToolTip(self.tr("Brief description of the application"))
        self._actionAbout.triggered.connect(self._onActionAboutTriggered)

        self._actionColophon = QAction(self.tr("Colophon"), self)
        self._actionColophon.setObjectName("actionColophon")
        self._actionColophon.setToolTip(self.tr("Lengthy description of the application"))
        self._actionColophon.triggered.connect(self._onActionColophonTriggered)

        self._actionPreferences = QAction(self.tr("Preferences…"), self)
        self._actionPreferences.setObjectName("actionPreferences")
        self._actionPreferences.setIcon(QIcon.fromTheme("configure", QIcon(":/icons/actions/16/application-configure.svg")))
        self._actionPreferences.setToolTip(self.tr("Customize the appearance and behavior of the application"))
        self._actionPreferences.triggered.connect(self._onActionPreferencesTriggered)

        self._actionQuit = QAction(self.tr("Quit"), self)
        self._actionQuit.setObjectName("actionQuit")
        self._actionQuit.setIcon(QIcon.fromTheme("application-exit", QIcon(":/icons/actions/16/application-exit.svg")))
        self._actionQuit.setShortcut(QKeySequence.Quit)
        self._actionQuit.setToolTip(self.tr("Quit the application"))
        self._actionQuit.triggered.connect(self.close)

        #
        # Actions: Document

        self._actionNew = QAction(self.tr("New"), self)
        self._actionNew.setObjectName("actionNew")
        self._actionNew.setIcon(QIcon.fromTheme("document-new", QIcon(":/icons/actions/16/document-new.svg")))
        self._actionNew.setShortcut(QKeySequence.New)
        self._actionNew.setToolTip(self.tr("Create new document"))
        self._actionNew.triggered.connect(self._onActionNewTriggered)

        self._actionOpen = QAction(self.tr("Open…"), self)
        self._actionOpen.setObjectName("actionOpen")
        self._actionOpen.setIcon(QIcon.fromTheme("document-open", QIcon(":/icons/actions/16/document-open.svg")))
        self._actionOpen.setShortcut(QKeySequence.Open)
        self._actionOpen.setToolTip(self.tr("Open an existing document"))
        self._actionOpen.triggered.connect(self._onActionOpenTriggered)

        self._actionOpenRecentClear = QAction(self.tr("Clear List"), self)
        self._actionOpenRecentClear.setObjectName("actionOpenRecentClear")
        self._actionOpenRecentClear.setToolTip(self.tr("Clear document list"))
        self._actionOpenRecentClear.triggered.connect(self._onActionOpenRecentClearTriggered)

        self._actionSave = QAction(self.tr("Save"), self)
        self._actionSave.setObjectName("actionSave")
        self._actionSave.setIcon(QIcon.fromTheme("document-save", QIcon(":/icons/actions/16/document-save.svg")))
        self._actionSave.setShortcut(QKeySequence.Save)
        self._actionSave.setToolTip(self.tr("Save document"))
        self._actionSave.triggered.connect(self._onActionSaveTriggered)

        self._actionSaveAs = QAction(self.tr("Save As…"), self)
        self._actionSaveAs.setObjectName("actionSaveAs")
        self._actionSaveAs.setIcon(QIcon.fromTheme("document-save-as", QIcon(":/icons/actions/16/document-save-as.svg")))
        self._actionSaveAs.setShortcut(QKeySequence.SaveAs)
        self._actionSaveAs.setToolTip(self.tr("Save document under a new name"))
        self._actionSaveAs.triggered.connect(self._onActionSaveAsTriggered)

        self._actionSaveAsDelimiterColon = QAction(self.tr("Colon"), self)
        self._actionSaveAsDelimiterColon.setObjectName("actionSaveAsDelimiterColon")
        self._actionSaveAsDelimiterColon.setCheckable(True)
        self._actionSaveAsDelimiterColon.setToolTip(self.tr("Save document with colon as delimiter under a new name"))
        self._actionSaveAsDelimiterColon.setData("colon")
        self._actionSaveAsDelimiterColon.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("colon") )

        self._actionSaveAsDelimiterComma = QAction(self.tr("Comma"), self)
        self._actionSaveAsDelimiterComma.setObjectName("actionSaveAsDelimiterComma")
        self._actionSaveAsDelimiterComma.setCheckable(True)
        self._actionSaveAsDelimiterComma.setToolTip(self.tr("Save document with comma as delimiter under a new name"))
        self._actionSaveAsDelimiterComma.setData("comma")
        self._actionSaveAsDelimiterComma.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("comma") )

        self._actionSaveAsDelimiterSemicolon = QAction(self.tr("Semicolon"), self)
        self._actionSaveAsDelimiterSemicolon.setObjectName("actionSaveAsDelimiterSemicolon")
        self._actionSaveAsDelimiterSemicolon.setCheckable(True)
        self._actionSaveAsDelimiterSemicolon.setToolTip(self.tr("Save document with semicolon as delimiter under a new name"))
        self._actionSaveAsDelimiterSemicolon.setData("semicolon")
        self._actionSaveAsDelimiterSemicolon.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("semicolon") )

        self._actionSaveAsDelimiterTab = QAction(self.tr("Tab"), self)
        self._actionSaveAsDelimiterTab.setObjectName("actionSaveAsDelimiterTab")
        self._actionSaveAsDelimiterTab.setCheckable(True)
        self._actionSaveAsDelimiterTab.setToolTip(self.tr("Save document with tab as delimiter under a new name"))
        self._actionSaveAsDelimiterTab.setData("tab")
        self._actionSaveAsDelimiterTab.triggered.connect(lambda: self._onActionSaveAsDelimiterTriggered("tab") )

        self._actionSaveAsDelimiter = QActionGroup(self)
        self._actionSaveAsDelimiter.setObjectName("actionSaveAsDelimiter")
        self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterColon)
        self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterComma)
        self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterSemicolon)
        self._actionSaveAsDelimiter.addAction(self._actionSaveAsDelimiterTab)

        self._actionSaveCopyAs = QAction(self.tr("Save Copy As…"), self)
        self._actionSaveCopyAs.setObjectName("actionSaveCopyAs")
        self._actionSaveCopyAs.setIcon(QIcon.fromTheme("document-save-as", QIcon(":/icons/actions/16/document-save-as.svg")))
        self._actionSaveCopyAs.setToolTip(self.tr("Save copy of document under a new name"))
        self._actionSaveCopyAs.triggered.connect(self._onActionSaveCopyAsTriggered)

        self._actionSaveAll = QAction(self.tr("Save All"), self)
        self._actionSaveAll.setObjectName("actionSaveAll")
        self._actionSaveAll.setIcon(QIcon.fromTheme("document-save-all", QIcon(":/icons/actions/16/document-save-all.svg")))
        self._actionSaveAll.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_L))
        self._actionSaveAll.setToolTip(self.tr("Save all documents"))
        self._actionSaveAll.triggered.connect(self._onActionSaveAllTriggered)

        self._actionClose = QAction(self.tr("Close"), self)
        self._actionClose.setObjectName("actionClose")
        self._actionClose.setIcon(QIcon.fromTheme("document-close", QIcon(":/icons/actions/16/document-close.svg")))
        self._actionClose.setShortcut(QKeySequence.Close)
        self._actionClose.setToolTip(self.tr("Close document"))
        self._actionClose.triggered.connect(self._onActionCloseTriggered)

        self._actionCloseOther = QAction(self.tr("Close Other"), self)
        self._actionCloseOther.setObjectName("actionCloseOther")
        self._actionCloseOther.setToolTip(self.tr("Close all other documents"))
        self._actionCloseOther.triggered.connect(self._onActionCloseOtherTriggered)

        self._actionCloseAll = QAction(self.tr("Close All"), self)
        self._actionCloseAll.setObjectName("actionCloseAll")
        self._actionCloseAll.setShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_W))
        self._actionCloseAll.setToolTip(self.tr("Close all documents"))
        self._actionCloseAll.triggered.connect(self._onActionCloseAllTriggered)

        #
        # Actions: View

        self._actionFullScreen = QAction(self)
        self._actionFullScreen.setObjectName("actionFullScreen")
        self._actionFullScreen.setIconText(self.tr("Full Screen"))
        self._actionFullScreen.setCheckable(True)
        self._actionFullScreen.setShortcuts([QKeySequence(Qt.Key_F11), QKeySequence.FullScreen])
        self._actionFullScreen.triggered.connect(self._onActionFullScreenTriggered)

        self._actionTitlebarFullPath = QAction(self.tr("Show Path in Titlebar"), self)
        self._actionTitlebarFullPath.setObjectName("actionTitlebarFullPath")
        self._actionTitlebarFullPath.setCheckable(True)
        self._actionTitlebarFullPath.setChecked(True)
        self._actionTitlebarFullPath.setToolTip(self.tr("Display the full path of the document in the titlebar"))
        self._actionTitlebarFullPath.triggered.connect(self._onActionTitlebarFullPathTriggered)

        self._actionToolbarApplication = QAction(self.tr("Show Application Toolbar"), self)
        self._actionToolbarApplication.setObjectName("actionToolbarApplication")
        self._actionToolbarApplication.setCheckable(True)
        self._actionToolbarApplication.setToolTip(self.tr("Display the Application toolbar"))
        self._actionToolbarApplication.toggled.connect(lambda checked: self._toolbarApplication.setVisible(checked))

        self._actionToolbarDocument = QAction(self.tr("Show Document Toolbar"), self)
        self._actionToolbarDocument.setObjectName("actionToolbarDocument")
        self._actionToolbarDocument.setCheckable(True)
        self._actionToolbarDocument.setToolTip(self.tr("Display the Document toolbar"))
        self._actionToolbarDocument.toggled.connect(lambda checked: self._toolbarDocument.setVisible(checked))

        self._actionToolbarEdit = QAction(self.tr("Show Edit Toolbar"), self)
        self._actionToolbarEdit.setObjectName("actionToolbarEdit")
        self._actionToolbarEdit.setCheckable(True)
        self._actionToolbarEdit.setToolTip(self.tr("Display the Edit toolbar"))
        self._actionToolbarEdit.toggled.connect(lambda checked: self._toolbarEdit.setVisible(checked))

        self._actionToolbarTools = QAction(self.tr("Show Tools Toolbar"), self)
        self._actionToolbarTools.setObjectName("actionToolbarTools")
        self._actionToolbarTools.setCheckable(True)
        self._actionToolbarTools.setToolTip(self.tr("Display the Tools toolbar"))
        self._actionToolbarTools.toggled.connect(lambda checked: self._toolbarTools.setVisible(checked))

        self._actionToolbarView = QAction(self.tr("Show View Toolbar"), self)
        self._actionToolbarView.setObjectName("actionToolbarView")
        self._actionToolbarView.setCheckable(True)
        self._actionToolbarView.setToolTip(self.tr("Display the View toolbar"))
        self._actionToolbarView.toggled.connect(lambda checked: self._toolbarView.setVisible(checked))

        self._actionToolbarHelp = QAction(self.tr("Show Help Toolbar"), self)
        self._actionToolbarHelp.setObjectName("actionToolbarHelp")
        self._actionToolbarHelp.setCheckable(True)
        self._actionToolbarHelp.setToolTip(self.tr("Display the Help toolbar"))
        self._actionToolbarHelp.toggled.connect(lambda checked: self._toolbarHelp.setVisible(checked))

        #
        # Actions: Help

        self._actionKeyboardShortcuts = QAction(self.tr("Keyboard Shortcuts"), self)
        self._actionKeyboardShortcuts.setObjectName("actionKeyboardShortcuts")
        self._actionKeyboardShortcuts.setIcon(QIcon.fromTheme("help-keyboard-shortcuts", QIcon(":/icons/actions/16/help-keyboard-shortcuts.svg")))
        self._actionKeyboardShortcuts.setIconText(self.tr("Shortcuts"))
        self._actionKeyboardShortcuts.setToolTip(self.tr("List of all keyboard shortcuts"))
        self._actionKeyboardShortcuts.triggered.connect(self._onActionKeyboardShortcutsTriggered)


    def _createMenus(self):

        # Menu: Application
        menuApplication = self.menuBar().addMenu(self.tr("Application"))
        menuApplication.setObjectName("menuApplication")
        menuApplication.addAction(self._actionAbout)
        menuApplication.addAction(self._actionColophon)
        menuApplication.addSeparator()
        menuApplication.addAction(self._actionPreferences)
        menuApplication.addSeparator()
        menuApplication.addAction(self._actionQuit)

        #
        # Menu: Document

        self._menuOpenRecent = QMenu(self.tr("Open Recent"), self)
        self._menuOpenRecent.setObjectName("menuOpenRecent")
        self._menuOpenRecent.setIcon(QIcon.fromTheme("document-open-recent", QIcon(":/icons/actions/16/document-open-recent.svg")))
        self._menuOpenRecent.setToolTip(self.tr("Open a document which was recently opened"))

        self._menuSaveAsDelimiter = QMenu(self.tr("Save As with Delimiter…"), self)
        self._menuSaveAsDelimiter.setObjectName("menuSaveAsDelimiter")
        self._menuSaveAsDelimiter.setIcon(QIcon.fromTheme("document-save-as", QIcon(":/icons/actions/16/document-save-as.svg")))
        self._menuSaveAsDelimiter.setToolTip(self.tr("Save document with specific delimiter under a new name"))
        self._menuSaveAsDelimiter.addActions(self._actionSaveAsDelimiter.actions())

        menuDocument = self.menuBar().addMenu(self.tr("Document"))
        menuDocument.setObjectName("menuDocument")
        menuDocument.addAction(self._actionNew)
        menuDocument.addSeparator()
        menuDocument.addAction(self._actionOpen)
        menuDocument.addMenu(self._menuOpenRecent)
        menuDocument.addSeparator()
        menuDocument.addAction(self._actionSave)
        menuDocument.addAction(self._actionSaveAs)
        menuDocument.addMenu(self._menuSaveAsDelimiter)
        menuDocument.addAction(self._actionSaveCopyAs)
        menuDocument.addAction(self._actionSaveAll)
        menuDocument.addSeparator()
        menuDocument.addAction(self._actionClose)
        menuDocument.addAction(self._actionCloseOther)
        menuDocument.addAction(self._actionCloseAll)

        # Menu: Edit
        menuEdit = self.menuBar().addMenu(self.tr("Edit"))
        menuEdit.setObjectName("menuEdit")

        # Menu: Tools
        menuTools = self.menuBar().addMenu(self.tr("Tools"))
        menuTools.setObjectName("menuTools")

        # Menu: View
        menuView = self.menuBar().addMenu(self.tr("View"))
        menuView.setObjectName("menuView")
        menuView.addAction(self._actionFullScreen)
        menuView.addSeparator()
        menuView.addAction(self._actionTitlebarFullPath)
        menuView.addSeparator()
        menuView.addAction(self._actionToolbarApplication)
        menuView.addAction(self._actionToolbarDocument)
        menuView.addAction(self._actionToolbarEdit)
        menuView.addAction(self._actionToolbarTools)
        menuView.addAction(self._actionToolbarView)
        menuView.addAction(self._actionToolbarHelp)

        # Menu: Help
        menuHelp = self.menuBar().addMenu(self.tr("Help"))
        menuHelp.setObjectName("menuHelp")
        menuHelp.addAction(self._actionKeyboardShortcuts)


    def _createToolBars(self):

        # Toolbar: Application
        self._toolbarApplication = self.addToolBar(self.tr("Application Toolbar"))
        self._toolbarApplication.setObjectName("toolbarApplication")
        self._toolbarApplication.addAction(self._actionAbout)
        self._toolbarApplication.addAction(self._actionPreferences)
        self._toolbarApplication.addSeparator()
        self._toolbarApplication.addAction(self._actionQuit)
        self._toolbarApplication.visibilityChanged.connect(lambda visible: self._actionToolbarApplication.setChecked(visible))

        # Toolbar: Document
        self._toolbarDocument = self.addToolBar(self.tr("Document Toolbar"))
        self._toolbarDocument.setObjectName("toolbarDocument")
        self._toolbarDocument.addAction(self._actionNew)
        self._toolbarDocument.addAction(self._actionOpen)
        self._toolbarDocument.addSeparator()
        self._toolbarDocument.addAction(self._actionSave)
        self._toolbarDocument.addAction(self._actionSaveAs)
        self._toolbarDocument.addSeparator()
        self._toolbarDocument.addAction(self._actionClose)
        self._toolbarDocument.visibilityChanged.connect(lambda visible: self._actionToolbarDocument.setChecked(visible))

        # Toolbar: Edit
        self._toolbarEdit = self.addToolBar(self.tr("Edit Toolbar"))
        self._toolbarEdit.setObjectName("toolbarEdit")
        self._toolbarEdit.visibilityChanged.connect(lambda visible: self._actionToolbarEdit.setChecked(visible))

        # Toolbar: Tools
        self._toolbarTools = self.addToolBar(self.tr("Tools Toolbar"))
        self._toolbarTools.setObjectName("toolbarTools")
        self._toolbarTools.visibilityChanged.connect(lambda visible: self._actionToolbarTools.setChecked(visible))

        # Toolbar: View
        self._toolbarView = self.addToolBar(self.tr("View Toolbar"))
        self._toolbarView.setObjectName("toolbarView")
        self._toolbarView.addAction(self._actionFullScreen)
        self._toolbarView.visibilityChanged.connect(lambda visible: self._actionToolbarView.setChecked(visible))

        # Toolbar: Help
        self._toolbarHelp = self.addToolBar(self.tr("Help Toolbar"))
        self._toolbarHelp.setObjectName("toolbarHelp")
        self._toolbarHelp.addAction(self._actionKeyboardShortcuts)
        self._toolbarHelp.visibilityChanged.connect(lambda visible: self._actionToolbarHelp.setChecked(visible))


    def _updateActions(self, subWindowCount=0):

        hasDocument = subWindowCount >= 1
        hasDocuments = subWindowCount >= 2

        # Actions: Document
        self._actionSave.setEnabled(hasDocument)
        self._actionSaveAs.setEnabled(hasDocument)
        self._menuSaveAsDelimiter.setEnabled(hasDocument)
        self._actionSaveCopyAs.setEnabled(hasDocument)
        self._actionSaveAll.setEnabled(hasDocument)
        self._actionClose.setEnabled(hasDocument)
        self._actionCloseOther.setEnabled(hasDocuments)
        self._actionCloseAll.setEnabled(hasDocument)


    def _updateActionFullScreen(self):

        if not self.isFullScreen():
            self._actionFullScreen.setText(self.tr("Full Screen Mode"))
            self._actionFullScreen.setIcon(QIcon.fromTheme("view-fullscreen", QIcon(":/icons/actions/16/view-fullscreen.svg")))
            self._actionFullScreen.setChecked(False)
            self._actionFullScreen.setToolTip(self.tr("Display the window in full screen"))
        else:
            self._actionFullScreen.setText(self.tr("Exit Full Screen Mode"))
            self._actionFullScreen.setIcon(QIcon.fromTheme("view-restore", QIcon(":/icons/actions/16/view-restore.svg")))
            self._actionFullScreen.setChecked(True)
            self._actionFullScreen.setToolTip(self.tr("Exit the full screen mode"))


    def _updateActionRecentDocuments(self):

        # Add items to the list, if necessary
        for idx in range(len(self._actionRecentDocuments)+1, self._preferences.maximumRecentDocuments()+1):

            actionRecentDocument = QAction(self)
            actionRecentDocument.setObjectName(f"actionRecentDocument_{idx}")
            actionRecentDocument.triggered.connect(lambda data=actionRecentDocument.data(): self._onActionOpenRecentDocumentTriggered(data))

            self._actionRecentDocuments.append(actionRecentDocument)

        # Remove items from the list, if necessary
        while len(self._actionRecentDocuments) > self._preferences.maximumRecentDocuments():
            self._actionRecentDocuments.pop()

        # Update items
        for idx in range(len(self._actionRecentDocuments)):
            text = None
            data = None
            show = False

            if idx < len(self._recentDocuments):
                text = self.tr("{0} [{1}]").format(QFileInfo(self._recentDocuments[idx]).fileName(), self._recentDocuments[idx])
                data = self._recentDocuments[idx]
                show = True

            self._actionRecentDocuments[idx].setText(text)
            self._actionRecentDocuments[idx].setData(data)
            self._actionRecentDocuments[idx].setVisible(show)


    def _updateMenuOpenRecent(self):

        self._menuOpenRecent.clear()

        if self._preferences.maximumRecentDocuments() > 0:
            # Document list wanted; show the menu
            self._menuOpenRecent.menuAction().setVisible(True)

            if len(self._recentDocuments) > 0:
                # Document list has items; enable the menu
                self._menuOpenRecent.setEnabled(True)

                self._menuOpenRecent.addActions(self._actionRecentDocuments)
                self._menuOpenRecent.addSeparator()
                self._menuOpenRecent.addAction(self._actionOpenRecentClear)
            else:
                # Document list is empty; disable the menu
                self._menuOpenRecent.setEnabled(False)
        else:
            # No document list wanted; hide the menu
            self._menuOpenRecent.menuAction().setVisible(False)


    def _updateTitleBar(self):

        title = None

        document = self._activeDocument()
        if document:
            title = document.canonicalName() if self._actionTitlebarFullPath.isChecked() and document.canonicalName() else document.documentTitle()

        self.setWindowTitle(title)


    def _onActionAboutTriggered(self):

        dialog = AboutDialog(self)
        dialog.exec_()


    def _onActionColophonTriggered(self):

        dialog = ColophonDialog(self)
        dialog.exec_()


    def _onActionPreferencesTriggered(self):

        dialog = PreferencesDialog(self)
        dialog.setPreferences(self._preferences)
        dialog.exec_()

        self._preferences = dialog.preferences()

        self._updateRecentDocuments(None)
        self._updateMenuOpenRecent()


    def _onActionNewTriggered(self):

        self._loadDocument("")


    def _onActionOpenTriggered(self):

        fileNames = QFileDialog.getOpenFileNames(self, self.tr("Open Document"),
                        QStandardPaths.writableLocation(QStandardPaths.HomeLocation),
                        self.tr("CSV Files (*.csv);;All Files (*.*)"))[0]

        for fileName in fileNames:
            self._openDocument(fileName)


    def _onActionOpenRecentDocumentTriggered(self, canonicalName):
        pass

#        self.openDocument(canonicalName)


    def _onActionOpenRecentClearTriggered(self):

        self._recentDocuments.clear()

        self._updateRecentDocuments(None)
        self._updateMenuOpenRecent()


    def _onActionSaveTriggered(self):
        pass


    def _onActionSaveAsTriggered(self):
        pass


    def _onActionSaveAsDelimiterTriggered(self, delimiter):
        pass


    def _onActionSaveCopyAsTriggered(self):
        pass


    def _onActionSaveAllTriggered(self):
        pass


    def _onActionCloseTriggered(self):

        self._documentArea.closeActiveSubWindow()


    def _onActionCloseOtherTriggered(self):

        for subWindow in self._documentArea.subWindowList():
            if subWindow != self._documentArea.activeSubWindow():
                subWindow.close()


    def _onActionCloseAllTriggered(self):

        self._documentArea.closeAllSubWindows()


    def _onActionFullScreenTriggered(self):

        if not self.isFullScreen():
            self.setWindowState(self.windowState() | Qt.WindowFullScreen)
        else:
            self.setWindowState(self.windowState() & ~Qt.WindowFullScreen)

        self._updateActionFullScreen()


    def _onActionTitlebarFullPathTriggered(self):

        self._updateTitleBar()


    def _onActionKeyboardShortcutsTriggered(self):

        if not self._keyboardShortcutsDialog:
            self._keyboardShortcutsDialog = KeyboardShortcutsDialog(self)

        self._keyboardShortcutsDialog.show()
        self._keyboardShortcutsDialog.raise_()
        self._keyboardShortcutsDialog.activateWindow()


    def _onDocumentWindowActivated(self, subWindow):

        # Update the application window
        self._updateActions(len(self._documentArea.subWindowList()))
        self._updateTitleBar()

        if not subWindow:
            return


    def _onDocumentAboutToClose(self, canonicalName):

        # Workaround to show subwindows always maximized
        for subWindow in self._documentArea.subWindowList():
            if not subWindow.isMaximized():
                subWindow.showMaximized()

        # Update menu items without the emitter
        self._updateActions(len(self._documentArea.subWindowList()) - 1)


    def _createDocument(self):

        document = Document()
        document.setPreferences(self._preferences)
        document.aboutToClose.connect(self._onDocumentAboutToClose)

        subWindow = self._documentArea.addSubWindow(document)
        subWindow.setWindowIcon(QIcon())
        subWindow.showMaximized()

        return document


    def _createDocumentIndex(self, canonicalName):

        fileName = QFileInfo(canonicalName).fileName()
        canonicalIndex = 0

        for subWindow in self._documentArea.subWindowList():
            if QFileInfo(subWindow.widget().canonicalName()).fileName() == fileName:
                if subWindow.widget().canonicalIndex() > canonicalIndex:
                    canonicalIndex = subWindow.widget().canonicalIndex()

        return canonicalIndex + 1


    def _findDocumentWindow(self, canonicalName):

        for subWindow in self._documentArea.subWindowList():
            if subWindow.widget().canonicalName() == canonicalName:
                return subWindow

        return None


    def _activeDocument(self):

        subWindow = self._documentArea.activeSubWindow()

        return subWindow.widget() if subWindow else None


    def _openDocument(self, fileName):

        canonicalName = QFileInfo(fileName).canonicalFilePath()

        subWindow = self._findDocumentWindow(canonicalName)
        if subWindow:
            # Given document is already loaded; activate the subwindow
            self._documentArea.setActiveSubWindow(subWindow)

            # Update list of recent documents
            self._updateRecentDocuments(canonicalName)
            self._updateMenuOpenRecent()
            return True

        return self._loadDocument(canonicalName);


    def _loadDocument(self, canonicalName):

        document = self._createDocument()

        succeeded = document.load(canonicalName)
        if succeeded:
            document.setCanonicalIndex(self._createDocumentIndex(canonicalName))
            document.updateDocumentTitle()
            document.show()

            # Update list of recent documents
            self._updateRecentDocuments(canonicalName)
            self._updateMenuOpenRecent()

            # Update the application window
            self._updateActions(len(self._documentArea.subWindowList()))
            self._updateTitleBar()
        else:
            document.close()

        return succeeded


    def _updateRecentDocuments(self, canonicalName):

        if canonicalName:
            while canonicalName in self._recentDocuments:
                self._recentDocuments.remove(canonicalName)
            self._recentDocuments.insert(0, canonicalName)

        # Remove items from the list, if necessary
        while len(self._recentDocuments) > self._preferences.maximumRecentDocuments():
            self._recentDocuments.pop()

        self._updateActionRecentDocuments()