Exemple #1
0
 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))
Exemple #2
0
 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))
Exemple #3
0
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()