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