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()
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)
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)
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])
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)
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()