diff --git a/qt/preferences.py b/qt/preferences.py index 37538fa4..70aa83f3 100644 --- a/qt/preferences.py +++ b/qt/preferences.py @@ -4,15 +4,176 @@ # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html -from PyQt5.QtWidgets import QApplication -from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication, QDockWidget +from PyQt5.QtCore import Qt, QSettings, QRect, QObject, pyqtSignal, QStandardPaths from PyQt5.QtGui import QColor from hscommon import trans -from hscommon.plat import ISLINUX +from hscommon.plat import ISLINUX, ISWINDOWS from core.app import AppMode from core.scanner import ScanType -from qtlib.preferences import Preferences as PreferencesBase +from hscommon.util import tryint +from core.util import executable_folder + +from os import path as op + + +def get_langnames(): + tr = trans.trget("qtlib") # TODO migrate this to ui instead + return { + "cs": tr("Czech"), + "de": tr("German"), + "el": tr("Greek"), + "en": tr("English"), + "es": tr("Spanish"), + "fr": tr("French"), + "hy": tr("Armenian"), + "it": tr("Italian"), + "ja": tr("Japanese"), + "ko": tr("Korean"), + "ms": tr("Malay"), + "nl": tr("Dutch"), + "pl_PL": tr("Polish"), + "pt_BR": tr("Brazilian"), + "ru": tr("Russian"), + "tr": tr("Turkish"), + "uk": tr("Ukrainian"), + "vi": tr("Vietnamese"), + "zh_CN": tr("Chinese (Simplified)"), + } + + +def _normalize_for_serialization(v): + # QSettings doesn't consider set/tuple as "native" typs for serialization, so if we don't + # change them into a list, we get a weird serialized QVariant value which isn't a very + # "portable" value. + if isinstance(v, (set, tuple)): + v = list(v) + if isinstance(v, list): + v = [_normalize_for_serialization(item) for item in v] + return v + + +def _adjust_after_deserialization(v): + # In some cases, when reading from prefs, we end up with strings that are supposed to be + # bool or int. Convert these. + if isinstance(v, list): + return [_adjust_after_deserialization(sub) for sub in v] + if isinstance(v, str): + # might be bool or int, try them + if v == "true": + return True + elif v == "false": + return False + else: + return tryint(v, v) + return v + + +def create_qsettings(): + # Create a QSettings instance with the correct arguments. + config_location = op.join(executable_folder(), "settings.ini") + if op.isfile(config_location): + settings = QSettings(config_location, QSettings.IniFormat) + settings.setValue("Portable", True) + elif ISWINDOWS: + # On windows use an ini file in the AppDataLocation instead of registry if possible as it + # makes it easier for a user to clear it out when there are issues. + locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation) + if locations: + settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.IniFormat) + else: + settings = QSettings() + settings.setValue("Portable", False) + else: + settings = QSettings() + settings.setValue("Portable", False) + return settings + + +class PreferencesBase(QObject): + prefsChanged = pyqtSignal() + + def __init__(self): + QObject.__init__(self) + self.reset() + self._settings = create_qsettings() + + def _load_values(self, settings): + # Implemented in subclasses + pass + + def get_rect(self, name, default=None): + r = self.get_value(name, default) + if r is not None: + return QRect(*r) + else: + return None + + def get_value(self, name, default=None): + if self._settings.contains(name): + result = _adjust_after_deserialization(self._settings.value(name)) + if result is not None: + return result + else: + # If result is None, but still present in self._settings, it usually means a value + # like "@Invalid". + return default + else: + return default + + def load(self): + self.reset() + self._load_values(self._settings) + + def reset(self): + # Implemented in subclasses + pass + + def _save_values(self, settings): + # Implemented in subclasses + pass + + def save(self): + self._save_values(self._settings) + self._settings.sync() + + def set_rect(self, name, r): + # About QRect conversion: + # I think Qt supports putting basic structures like QRect directly in QSettings, but I prefer not + # to rely on it and stay with generic structures. + if isinstance(r, QRect): + rect_as_list = [r.x(), r.y(), r.width(), r.height()] + self.set_value(name, rect_as_list) + + def set_value(self, name, value): + self._settings.setValue(name, _normalize_for_serialization(value)) + + def saveGeometry(self, name, widget): + # We save geometry under a 7-sized int array: first item is a flag + # for whether the widget is maximized, second item is a flag for whether + # the widget is docked, third item is a Qt::DockWidgetArea enum value, + # and the other 4 are (x, y, w, h). + m = 1 if widget.isMaximized() else 0 + d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0 + area = widget.parent.dockWidgetArea(widget) if d else 0 + r = widget.geometry() + rect_as_list = [r.x(), r.y(), r.width(), r.height()] + self.set_value(name, [m, d, area] + rect_as_list) + + def restoreGeometry(self, name, widget): + geometry = self.get_value(name) + if geometry and len(geometry) == 7: + m, d, area, x, y, w, h = geometry + if m: + widget.setWindowState(Qt.WindowMaximized) + else: + r = QRect(x, y, w, h) + widget.setGeometry(r) + if isinstance(widget, QDockWidget): + # Inform of the previous dock state and the area used + return bool(d), area + return False, 0 class Preferences(PreferencesBase): diff --git a/qt/preferences_dialog.py b/qt/preferences_dialog.py index b70a1a1c..60760caf 100644 --- a/qt/preferences_dialog.py +++ b/qt/preferences_dialog.py @@ -34,35 +34,13 @@ from hscommon import desktop, plat from hscommon.trans import trget from hscommon.plat import ISLINUX from qtlib.util import horizontal_wrap, move_to_screen_center -from qtlib.preferences import get_langnames +from qt.preferences import get_langnames from enum import Flag, auto from .preferences import Preferences tr = trget("ui") -SUPPORTED_LANGUAGES = [ - "cs", - "de", - "el", - "en", - "es", - "fr", - "hy", - "it", - "ja", - "ko", - "ms", - "nl", - "pl_PL", - "pt_BR", - "ru", - "tr", - "uk", - "vi", - "zh_CN", -] - class Sections(Flag): """Filter blocks of preferences when reset or loaded""" @@ -78,8 +56,7 @@ class PreferencesDialogBase(QDialog): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self.app = app - all_languages = get_langnames() - self.supportedLanguages = sorted(SUPPORTED_LANGUAGES, key=lambda lang: all_languages[lang]) + self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1])) self._setupUi() self.filterHardnessSlider.valueChanged["int"].connect(self.filterHardnessLabel.setNum) @@ -148,8 +125,8 @@ class PreferencesDialogBase(QDialog): layout = QVBoxLayout() self.languageLabel = QLabel(tr("Language:"), self) self.languageComboBox = QComboBox(self) - for lang in self.supportedLanguages: - self.languageComboBox.addItem(get_langnames()[lang]) + for lang_code, lang_str in self.supportedLanguages.items(): + self.languageComboBox.addItem(lang_str, userData=lang_code) layout.addLayout(horizontal_wrap([self.languageLabel, self.languageComboBox, None])) self._setupAddCheckbox( "tabs_default_pos", @@ -337,10 +314,10 @@ use the modifier key to drag the floating window around" self.result_table_ref_background_color.setColor(prefs.result_table_ref_background_color) self.result_table_delta_foreground_color.setColor(prefs.result_table_delta_foreground_color) try: - langindex = self.supportedLanguages.index(self.app.prefs.language) - except ValueError: - langindex = 0 - self.languageComboBox.setCurrentIndex(langindex) + selected_lang = self.supportedLanguages[self.app.prefs.language] + except KeyError: + selected_lang = self.supportedLanguages["en"] + self.languageComboBox.setCurrentText(selected_lang) if section & Sections.DEBUG: setchecked(self.debugModeBox, prefs.debug_mode) setchecked(self.profile_scan_box, prefs.profile_scan) @@ -373,17 +350,17 @@ use the modifier key to drag the floating window around" prefs.use_native_dialogs = ischecked(self.use_native_dialogs) if plat.ISWINDOWS: prefs.use_dark_style = ischecked(self.use_dark_style) - lang = self.supportedLanguages[self.languageComboBox.currentIndex()] - oldlang = self.app.prefs.language - if oldlang not in self.supportedLanguages: - oldlang = "en" - if lang != oldlang: + lang_code = self.languageComboBox.currentData() + old_lang_code = self.app.prefs.language + if old_lang_code not in self.supportedLanguages.keys(): + old_lang_code = "en" + if lang_code != old_lang_code: QMessageBox.information( self, "", tr("dupeGuru has to restart for language changes to take effect."), ) - self.app.prefs.language = lang + self.app.prefs.language = lang_code self._save(prefs, ischecked) def resetToDefaults(self, section_to_update): diff --git a/qtlib/.gitignore b/qtlib/.gitignore deleted file mode 100644 index 7f1e166c..00000000 --- a/qtlib/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -__pycache__ -*.pyc -*.mo -.DS_Store \ No newline at end of file diff --git a/qtlib/app.py b/qtlib/app.py deleted file mode 100644 index 043a201d..00000000 --- a/qtlib/app.py +++ /dev/null @@ -1,20 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-10-16 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import pyqtSignal, QTimer, QObject - - -class Application(QObject): - finishedLaunching = pyqtSignal() - - def __init__(self): - QObject.__init__(self) - QTimer.singleShot(0, self.__launchTimerTimedOut) - - def __launchTimerTimedOut(self): - self.finishedLaunching.emit() diff --git a/qtlib/preferences.py b/qtlib/preferences.py deleted file mode 100644 index 0ef139a4..00000000 --- a/qtlib/preferences.py +++ /dev/null @@ -1,178 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-03 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import Qt, QSettings, QRect, QObject, pyqtSignal, QStandardPaths -from PyQt5.QtWidgets import QDockWidget - -from hscommon.trans import trget -from hscommon.util import tryint -from hscommon.plat import ISWINDOWS -from core.util import executable_folder - -from os import path as op - -tr = trget("qtlib") - - -def get_langnames(): - return { - "cs": tr("Czech"), - "de": tr("German"), - "el": tr("Greek"), - "en": tr("English"), - "es": tr("Spanish"), - "fr": tr("French"), - "hy": tr("Armenian"), - "it": tr("Italian"), - "ja": tr("Japanese"), - "ko": tr("Korean"), - "ms": tr("Malay"), - "nl": tr("Dutch"), - "pl_PL": tr("Polish"), - "pt_BR": tr("Brazilian"), - "ru": tr("Russian"), - "tr": tr("Turkish"), - "uk": tr("Ukrainian"), - "vi": tr("Vietnamese"), - "zh_CN": tr("Chinese (Simplified)"), - } - - -def normalize_for_serialization(v): - # QSettings doesn't consider set/tuple as "native" typs for serialization, so if we don't - # change them into a list, we get a weird serialized QVariant value which isn't a very - # "portable" value. - if isinstance(v, (set, tuple)): - v = list(v) - if isinstance(v, list): - v = [normalize_for_serialization(item) for item in v] - return v - - -def adjust_after_deserialization(v): - # In some cases, when reading from prefs, we end up with strings that are supposed to be - # bool or int. Convert these. - if isinstance(v, list): - return [adjust_after_deserialization(sub) for sub in v] - if isinstance(v, str): - # might be bool or int, try them - if v == "true": - return True - elif v == "false": - return False - else: - return tryint(v, v) - return v - - -def create_qsettings(): - # Create a QSettings instance with the correct arguments. - config_location = op.join(executable_folder(), "settings.ini") - if op.isfile(config_location): - settings = QSettings(config_location, QSettings.IniFormat) - settings.setValue("Portable", True) - elif ISWINDOWS: - # On windows use an ini file in the AppDataLocation instead of registry if possible as it - # makes it easier for a user to clear it out when there are issues. - locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation) - if locations: - settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.IniFormat) - else: - settings = QSettings() - settings.setValue("Portable", False) - else: - settings = QSettings() - settings.setValue("Portable", False) - return settings - - -# About QRect conversion: -# I think Qt supports putting basic structures like QRect directly in QSettings, but I prefer not -# to rely on it and stay with generic structures. - - -class Preferences(QObject): - prefsChanged = pyqtSignal() - - def __init__(self): - QObject.__init__(self) - self.reset() - self._settings = create_qsettings() - - def _load_values(self, settings): - # Implemented in subclasses - pass - - def get_rect(self, name, default=None): - r = self.get_value(name, default) - if r is not None: - return QRect(*r) - else: - return None - - def get_value(self, name, default=None): - if self._settings.contains(name): - result = adjust_after_deserialization(self._settings.value(name)) - if result is not None: - return result - else: - # If result is None, but still present in self._settings, it usually means a value - # like "@Invalid". - return default - else: - return default - - def load(self): - self.reset() - self._load_values(self._settings) - - def reset(self): - # Implemented in subclasses - pass - - def _save_values(self, settings): - # Implemented in subclasses - pass - - def save(self): - self._save_values(self._settings) - self._settings.sync() - - def set_rect(self, name, r): - if isinstance(r, QRect): - rect_as_list = [r.x(), r.y(), r.width(), r.height()] - self.set_value(name, rect_as_list) - - def set_value(self, name, value): - self._settings.setValue(name, normalize_for_serialization(value)) - - def saveGeometry(self, name, widget): - # We save geometry under a 7-sized int array: first item is a flag - # for whether the widget is maximized, second item is a flag for whether - # the widget is docked, third item is a Qt::DockWidgetArea enum value, - # and the other 4 are (x, y, w, h). - m = 1 if widget.isMaximized() else 0 - d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0 - area = widget.parent.dockWidgetArea(widget) if d else 0 - r = widget.geometry() - rect_as_list = [r.x(), r.y(), r.width(), r.height()] - self.set_value(name, [m, d, area] + rect_as_list) - - def restoreGeometry(self, name, widget): - geometry = self.get_value(name) - if geometry and len(geometry) == 7: - m, d, area, x, y, w, h = geometry - if m: - widget.setWindowState(Qt.WindowMaximized) - else: - r = QRect(x, y, w, h) - widget.setGeometry(r) - if isinstance(widget, QDockWidget): - # Inform of the previous dock state and the area used - return bool(d), area - return False, 0 diff --git a/run.py b/run.py index 8cfa7d45..0d7ff851 100644 --- a/run.py +++ b/run.py @@ -16,7 +16,7 @@ from PyQt5.QtWidgets import QApplication from hscommon.trans import install_gettext_trans_under_qt from qtlib.error_report_dialog import install_excepthook from qtlib.util import setup_qt_logging -from qtlib.preferences import create_qsettings +from qt.preferences import create_qsettings from qt import dg_rc # noqa: F401 from qt.platform import BASE_PATH from core import __version__, __appname__ @@ -74,7 +74,7 @@ def main(): app.setWindowIcon(QIcon(QPixmap(f":/{DupeGuru.LOGO_NAME}"))) global dgapp dgapp = DupeGuru() - install_excepthook("https://github.com/hsoft/dupeguru/issues") + install_excepthook("https://github.com/arsenetar/dupeguru/issues") result = app.exec() # I was getting weird crashes when quitting under Windows, and manually deleting main app # references with gc.collect() in between seems to fix the problem.