def restore_from_defaults(): """ Clears all settings (except "defaults" group) and restores all settings from the defaults group. """ settings = QSettings() for g in settings.childGroups(): if g != "defaults": settings.remove(g) for k in settings.childKeys(): settings.remove(k) defaults = QSettings() defaults.beginGroup("defaults") for k in defaults.allKeys(): settings.setValue(k, defaults.value(k))
class Config(QObject): "Configuration provider for the whole program, wrapper for QSettings" row_height_changed = Signal(int) def __init__(self, log=None): super().__init__() if log: self.log = log.getChild('Conf') self.log.setLevel(30) else: self.log = logging.getLogger() self.log.setLevel(99) self.log.debug('Initializing') self.qsettings = QSettings() self.qsettings.setIniCodec('UTF-8') self.options = None self.option_spec = self.load_option_spec() self.options = self.load_options() self.full_name = "{} {}".format(QCoreApplication.applicationName(), QCoreApplication.applicationVersion()) # options that need fast access are also defined as attributes, which # are updated by calling update_attributes() # (on paper it's 4 times faster, but I don't think it matters in my case) self.logger_table_font = None self.logger_table_font_size = None self.logger_row_height = None self.benchmark_interval = None self.update_attributes() def post_init(self): running_version = StrictVersion(QCoreApplication.applicationVersion()) config_version = self.options['cutelog_version'] if config_version == "" or config_version != running_version: self.save_running_version() def __getitem__(self, name): # self.log.debug('Getting "{}"'.format(name)) value = self.options.get(name) if value is None: raise Exception('No option with name "{}"'.format(name)) # self.log.debug('Returning "{}"'.format(value)) return value def __setitem__(self, name, value): # self.log.debug('Setting "{}"'.format(name)) if name not in self.options: raise Exception('No option with name "{}"'.format(name)) self.options[name] = value def set_option(self, name, value): self[name] = value self.qsettings.beginGroup('Configuration') self.qsettings.setValue(name, value) self.qsettings.endGroup() @staticmethod def get_resource_path(name, directory='ui'): data_dir = resource_filename('cutelog', directory) path = os.path.join(data_dir, name) if not os.path.exists(path): raise FileNotFoundError('Resource file not found in this path: "{}"'.format(path)) return path def get_ui_qfile(self, name): file = QFile(':/ui/{}'.format(name)) if not file.exists(): raise FileNotFoundError('ui file not found: ":/ui/{}"'.format(name)) file.open(QFile.ReadOnly) return file @property def listen_address(self): host = self.options.get('listen_host') port = self.options.get('listen_port') if host is None or port is None: raise Exception('Listen host or port not in options: "{}:{}"'.format(host, port)) return (host, port) def load_option_spec(self): option_spec = [] for spec in OPTION_SPEC: option = Option(*spec) option_spec.append(option) return option_spec def load_options(self): self.log.debug('Loading options') options = {} self.qsettings.beginGroup('Configuration') for option in self.option_spec: value = self.qsettings.value(option.name, option.default) if option.type == bool: value = str(value).lower() # needed because QSettings stores bools as strings value = True if value == "true" or value is True else False elif option.type == int and value is None: value = 0 # workaround for bug PYSIDE-820 else: try: value = option.type(value) except Exception: self.log.warn('Could not parse value "{}" for option "{}", falling back to the ' 'default value "{}"'.format(value, option.name, option.default)) value = option.default options[option.name] = value self.qsettings.endGroup() return options def update_options(self, new_options, save=True): self.emit_needed_changes(new_options) self.options.update(new_options) if save: self.save_options() self.update_attributes(new_options) def update_attributes(self, options=None): "Updates fast attributes and everything else outside of self.options" if options is None: options = self.options self.benchmark_interval = options.get('benchmark_interval', self.benchmark_interval) self.logger_table_font = options.get('logger_table_font', self.logger_table_font) self.logger_table_font_size = options.get('logger_table_font_size', self.logger_table_font_size) self.logger_row_height = options.get('logger_row_height', self.logger_row_height) self.set_logging_level(options.get('console_logging_level', ROOT_LOG.level)) def emit_needed_changes(self, new_options): new_row_height = new_options.get('logger_row_height') old_row_height = self.options.get('logger_row_height') if new_row_height != old_row_height: self.logger_row_height = new_row_height self.row_height_changed.emit(new_row_height) def save_options(self, sync=False): self.log.debug('Saving options') self.qsettings.beginGroup('Configuration') for option in self.option_spec: self.qsettings.setValue(option.name, self.options[option.name]) self.qsettings.endGroup() if sync: # syncing is probably not necessary here, so the default is False self.sync() def sync(self): self.log.debug('Syncing QSettings') self.qsettings.sync() def set_settings_value(self, name, value): self.qsettings.beginGroup('Configuration') self.qsettings.setValue(name, value) self.qsettings.endGroup() def set_logging_level(self, level): global ROOT_LOG ROOT_LOG.setLevel(level) self.log.setLevel(level) def get_levels_presets(self): self.qsettings.beginGroup('Levels_Presets') result = self.qsettings.childGroups() self.qsettings.endGroup() return result def save_levels_preset(self, name, levels): self.log.debug('Saving levels preset "{}"'.format(name)) s = self.qsettings s.beginGroup('Levels_Presets') s.beginWriteArray(name, len(levels)) for i, levelname in enumerate(levels): level = levels[levelname] s.setArrayIndex(i) dump = level.dumps() s.setValue('level', dump) s.endArray() s.endGroup() def load_levels_preset(self, name): from .log_levels import LogLevel self.log.debug('Loading levels preset "{}"'.format(name)) s = self.qsettings if name not in self.get_levels_presets(): return None s.beginGroup('Levels_Presets') size = s.beginReadArray(name) result = {} for i in range(size): s.setArrayIndex(i) new_level = LogLevel(None).loads(s.value('level')) result[new_level.levelname] = new_level s.endArray() s.endGroup() return result def delete_levels_preset(self, name): s = self.qsettings s.beginGroup('Levels_Presets') s.remove(name) s.endGroup() def get_header_presets(self): self.qsettings.beginGroup('Header_Presets') result = self.qsettings.childGroups() self.qsettings.endGroup() return result def save_header_preset(self, name, columns): self.log.debug('Saving header preset "{}"'.format(name)) s = self.qsettings s.beginGroup('Header_Presets') s.beginWriteArray(name, len(columns)) for i, col in enumerate(columns): s.setArrayIndex(i) # read the comment in Column.dumps() for reasoning if i == len(columns) - 1: col.width = 10 # dump = col.dumps(width=10) dump = col.dumps() s.setValue('column', dump) s.endArray() s.endGroup() def load_header_preset(self, name): from .logger_table_header import Column self.log.debug('Loading header preset "{}"'.format(name)) s = self.qsettings if name not in self.get_header_presets(): return None s.beginGroup('Header_Presets') size = s.beginReadArray(name) result = [] for i in range(size): s.setArrayIndex(i) new_column = Column().loads(s.value('column')) result.append(new_column) s.endArray() s.endGroup() return result def delete_header_preset(self, name): s = self.qsettings s.beginGroup('Header_Presets') s.remove(name) s.endGroup() def save_geometry(self, geometry): s = self.qsettings s.beginGroup('Geometry') s.setValue('Main_Window_Geometry', geometry) s.endGroup() self.sync() def load_geometry(self): s = self.qsettings s.beginGroup('Geometry') geometry = s.value('Main_Window_Geometry') s.endGroup() return geometry def save_running_version(self): version = QCoreApplication.applicationVersion() self.log.debug("Updating the config version to {}".format(version)) s = self.qsettings s.beginGroup('Configuration') s.setValue('cutelog_version', version) self.options['cutelog_version'] = version s.endGroup() self.sync() def restore_defaults(self): self.qsettings.clear() self.sync()
class MainWindow(QMainWindow): """Main window interface""" def __init__(self): super().__init__(parent=None) # Initialize private properties self._log = logging.getLogger(__name__) self._disable_window_save = False self._last_path = None # Initialize app settings self._app_settings = AppSettings() # Initialize window self._log.debug("Initializing main window...") self.setWindowTitle("Friendly Pics") self.statusBar().showMessage('Ready') self._settings = QSettings() self._load_ui() self._load_window_state() self._log.debug("Main window initialized") def _find_default_screen(self): """Screen: loads the screen ID for the screen where the application window should appear by default NOTE: this helper method assumes that the caller has already changed the active context of the self.settings object to point to the window we need to process """ groups = self._settings.childGroups() default_group_id = self._settings.value("LastScreen") all_screens = dict() for cur_screen in QApplication.screens(): all_screens[generate_screen_id(cur_screen)] = cur_screen # Favor the last used screen if it is still available if default_group_id in all_screens.keys(): self._log.debug( f"Loading layout for previously used screen {default_group_id}" ) return all_screens[default_group_id] # if last used screen is not found, see if we have any cached screen details # that map to one of our available screen cached_screen_ids = set(all_screens.keys()).intersection(set(groups)) # If so, return the first match if cached_screen_ids: return all_screens[cached_screen_ids[0]] # If all else fails and there are no defaults to be found, return the default screen default_screen_id = generate_screen_id(QApplication.screens()[0]) self._log.debug("Loading a default screen layout") return all_screens[default_screen_id] def _load_ui(self): """Internal helper method that configures the UI for the main window""" load_ui("main_window.ui", self) self.file_open_menu.triggered.connect(self.file_open_click) self.file_open_menu.setShortcut(QKeySequence.Open) self.file_settings_menu.triggered.connect(self.file_settings_click) self.window_debug_menu.triggered.connect(self.window_debug_click) self.help_about_menu.triggered.connect(self.help_about_click) # Hack: for testing on MacOS we convert menu bar to non native # works around the bug where native menu bar on Mac is read only on app launch # problem is non existent when running app from a .app package # (ie: as generated by pyinstaller) if not is_mac_app_bundle(): self.menuBar().setNativeMenuBar(False) def _load_window_state(self): """Restores window layout to it's previous state. Must be called after _load_ui""" # Load all settings for this specific window with settings_group_context(self._settings, self.objectName()): target_screen = self._find_default_screen() # by default, scale our window to half the target screen's size default_width = int(target_screen.geometry().width() / 2) default_height = int(target_screen.geometry().height() / 2) default_size = QSize(default_width, default_height) # by default, center the window within the target screen geom = QRect(QPoint(0, 0), default_size) geom.moveCenter(target_screen.geometry().center()) default_pos = geom.topLeft() with settings_group_context(self._settings, generate_screen_id(target_screen)): # TODO: do some additional sanity checking to make sure the target position and size # lie within the screen boundaries and if not, fallback to defaults self.resize(self._settings.value("size", default_size)) self.move(self._settings.value("pos", default_pos)) if self._settings.value("window_debug", False): self.debug_dock.show() self.window_debug_menu.setChecked(True) else: self.debug_dock.hide() self.window_debug_menu.setChecked(False) self._last_path = self._settings.value("last_path", None) if self._last_path: model = ImageModel(self._last_path) self.thumbnail_view.setModel(model) self.statusBar().showMessage(f"Loaded {model.max_count} images") def _save_window_state(self): """Saves the current window state so it can be restored on next run""" # Save settings just for this window with settings_group_context(self._settings, self.objectName()): # Save window layout for the currently used screen cur_screen_id = generate_screen_id(self.screen()) self._settings.setValue("LastScreen", cur_screen_id) with settings_group_context(self._settings, cur_screen_id): self._settings.setValue("size", self.size()) self._settings.setValue("pos", self.pos()) self._settings.setValue("window_debug", self.window_debug_menu.isChecked()) if self._last_path: self._settings.setValue("last_path", self._last_path) self._settings.sync() @Slot() def file_open_click(self): """callback for file-open menu""" temp_path = self._last_path or Path("~").expanduser() new_path = QFileDialog.getExistingDirectory(self, "Select folder...", str(temp_path)) if not new_path: return self._last_path = Path(new_path) model = ImageModel(self._last_path) self.thumbnail_view.setModel(model) self.statusBar().showMessage(f"Loaded {model.max_count} images") @Slot() def help_about_click(self): """callback for the help-about menu""" dlg = AboutDialog(self, self._app_settings) dlg.exec_() self._disable_window_save = dlg.cleared @Slot() def window_debug_click(self): """event handler for when the window->debug menu is clicked""" if self.window_debug_menu.isChecked(): self.debug_dock.show() else: self.debug_dock.hide() @Slot() def file_settings_click(self): """event handler for when the file->settings menu is clicked""" dlg = SettingsDialog(self, self._app_settings) dlg.exec_() def closeEvent(self, event): # pylint: disable=invalid-name """event handler called when the application is about to close Args: event (QCloseEvent): reference to the event object being raised """ self._log.debug("Shutting down") if not self._disable_window_save: self._save_window_state() self._app_settings.save() event.accept()