From 54b7d516b32d49aa40788c34aad41e9668d9dbb6 Mon Sep 17 00:00:00 2001 From: Leonardo Covarrubias Date: Fri, 6 Sep 2024 12:30:34 -0400 Subject: [PATCH] chore: wip --- examples/toolbar_app/main_window.py | 121 ++++++------ src/pyside_app_core/mixin/settings_mixin.py | 10 +- .../resources/core/iconoir/settings.svg | 1 + .../services/preferences_service.py | 182 ++++++++++++++++-- src/pyside_app_core/types/preferences.py | 157 +-------------- src/pyside_app_core/ui/__init__.py | 10 +- .../ui/standard/main_window.py | 4 +- src/pyside_app_core/ui/widgets/base_app.py | 5 + src/pyside_app_core/ui/widgets/file_picker.py | 27 ++- .../ui/widgets/preferences_manager.py | 132 +++---------- .../ui/widgets/tool_bar_ctx.py | 14 +- src/pyside_app_core/utils/property.py | 6 + 12 files changed, 312 insertions(+), 357 deletions(-) create mode 100644 src/pyside_app_core/resources/core/iconoir/settings.svg create mode 100644 src/pyside_app_core/utils/property.py diff --git a/examples/toolbar_app/main_window.py b/examples/toolbar_app/main_window.py index 807e03c..af69fe2 100644 --- a/examples/toolbar_app/main_window.py +++ b/examples/toolbar_app/main_window.py @@ -4,13 +4,13 @@ from PySide6.QtGui import QAction from PySide6.QtWidgets import QLabel, QVBoxLayout -from pyside_app_core.services.preferences_service import PreferencesService -from pyside_app_core.types.preferences import Pref, PrefsConfig, PrefsGroup +from pyside_app_core.services.preferences_service import PreferencesService, PrefGroup, PrefValue from pyside_app_core.ui.standard import MainWindow from pyside_app_core.ui.widgets.connection_manager import ConnectionManager from pyside_app_core.ui.widgets.core_icon import CoreIcon from pyside_app_core.ui.widgets.multi_combo_box import MultiComboBox from pyside_app_core.ui.widgets.preferences_manager import PreferencesManager +from pyside_app_core.ui.widgets.tool_bar_ctx import ToolBarContext class SimpleMainWindow(MainWindow): @@ -21,23 +21,23 @@ def __init__(self) -> None: self.setMinimumSize(QSize(480, 240)) self._prefs_mgr: PreferencesManager | None = None - self._preferences = PrefsConfig( - PrefsGroup( - "Application", - [ - Pref("Remember Position", True), - Pref("Remember Size", True), - Pref("Default Path", Path.home() / "one" / "two" / "three" / "four"), - ], + PreferencesService.add_groups( + PrefGroup( + "app", "Application", + PrefGroup( + "remember", "Keep Between Sessions", + PrefValue("pos", "Remember Position", True), + PrefValue("size", "Remember Size", True), + ), + PrefValue("path", "Default Path", Path.home() / "one" / "two" / "three" / "four"), ), - PrefsGroup( - "Developer", - [ - Pref("Debug Mode", False), - ], + PrefGroup( + "dev", "Developer", + PrefValue("debug", "Debug Mode", False), ), ) - PreferencesService.load_config(self._preferences) + + print(PreferencesService.instance()) self._menus() self._content() @@ -48,46 +48,59 @@ def _menus(self) -> None: file_menu.action("Preferences...") as prefs_action, ): prefs_action.setMenuRole(QAction.MenuRole.PreferencesRole) - prefs_action.triggered.connect(self._open_preferences) + prefs_action.triggered.connect(PreferencesManager.open) def _content(self) -> None: - _tool_bar = self.addToolBar("main") + _tool_bar = ToolBarContext("top", self) _tool_bar.setObjectName("main-tool-bar") - plug_action = _tool_bar.addAction( - CoreIcon( - ":/core/iconoir/ev-plug-charging.svg", - ":/core/iconoir/ev-plug-xmark.svg", - ), - "Connect", - ) - plug_action.setCheckable(True) - plug_action2 = _tool_bar.addAction( - CoreIcon( - ":/core/iconoir/ev-plug-charging.svg", - ":/core/iconoir/ev-plug-xmark.svg", - ), - "Connect", - ) - plug_action2.setCheckable(True) - plug_action2.setChecked(True) - reload_action = _tool_bar.addAction( - CoreIcon( - ":/core/iconoir/refresh-circle.svg", - ), - "Reload", - ) - reload_action.setDisabled(True) - _raise_action = _tool_bar.addAction( - CoreIcon( - ":/core/iconoir/floppy-disk.svg", - ), - "Save", - ) - - def _raise() -> None: - raise Exception("This is a test error") # noqa - _raise_action.triggered.connect(_raise) + with _tool_bar.add_action( + "Connect", + CoreIcon( + ":/core/iconoir/ev-plug-charging.svg", + ":/core/iconoir/ev-plug-xmark.svg", + ), + ) as plug_action: + plug_action.setCheckable(True) + + with _tool_bar.add_action( + "Connect", + CoreIcon( + ":/core/iconoir/ev-plug-charging.svg", + ":/core/iconoir/ev-plug-xmark.svg", + ), + ) as plug_action: + plug_action.setCheckable(True) + plug_action.setChecked(True) + + with _tool_bar.add_action( + "Reload", + CoreIcon( + ":/core/iconoir/refresh-circle.svg", + ), + ) as reload_action: + reload_action.setDisabled(True) + + with _tool_bar.add_action( + "Save", + CoreIcon( + ":/core/iconoir/floppy-disk.svg", + ), + ) as raise_action: + def _raise() -> None: + raise Exception("This is a test error") # noqa + + raise_action.triggered.connect(_raise) + + _tool_bar.add_stretch() + + with _tool_bar.add_action( + "Preferences", + CoreIcon( + ":/core/iconoir/settings.svg", + ), + ) as prefs_action: + prefs_action.triggered.connect(PreferencesManager.open) # ----- _central_layout = QVBoxLayout() @@ -107,9 +120,9 @@ def _raise() -> None: self.statusBar().showMessage("Hi There") - def _open_preferences(self) -> None: + def open_preferences(self) -> None: if self._prefs_mgr: self._prefs_mgr.close() - self._prefs_mgr = PreferencesManager(self._preferences) + self._prefs_mgr = PreferencesManager() self._prefs_mgr.show() diff --git a/src/pyside_app_core/mixin/settings_mixin.py b/src/pyside_app_core/mixin/settings_mixin.py index 0687af4..7a1b56f 100644 --- a/src/pyside_app_core/mixin/settings_mixin.py +++ b/src/pyside_app_core/mixin/settings_mixin.py @@ -1,11 +1,9 @@ -from typing import Any, TypeVar, cast +from typing import Any, cast, TypeVar from PySide6.QtCore import QCoreApplication, QObject, QSettings from PySide6.QtGui import QShowEvent from PySide6.QtWidgets import QWidget -from pyside_app_core.app.application_service import AppMetadata - _SV = TypeVar("_SV") @@ -13,11 +11,7 @@ class SettingsMixin: def __init__(self, parent: QObject | None = None, *args: object, **kwargs: object): super().__init__(*args, **kwargs) - self._settings = QSettings( - AppMetadata.id, - AppMetadata.name, - parent, - ) + self._settings = QSettings(parent) self._restored = False diff --git a/src/pyside_app_core/resources/core/iconoir/settings.svg b/src/pyside_app_core/resources/core/iconoir/settings.svg new file mode 100644 index 0000000..2b26a53 --- /dev/null +++ b/src/pyside_app_core/resources/core/iconoir/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pyside_app_core/services/preferences_service.py b/src/pyside_app_core/services/preferences_service.py index 7b1d8be..f133f63 100644 --- a/src/pyside_app_core/services/preferences_service.py +++ b/src/pyside_app_core/services/preferences_service.py @@ -1,13 +1,141 @@ -from typing import Any +from __future__ import annotations -from PySide6.QtCore import QCoreApplication, QObject, Signal +from enum import IntEnum +from typing import Any, Iterator, cast + +from PySide6.QtCore import QCoreApplication, QObject, Qt, Signal, QAbstractProxyModel +from PySide6.QtGui import QStandardItem, QStandardItemModel from pyside_app_core.mixin.settings_mixin import SettingsMixin -from pyside_app_core.types.preferences import Pref, PrefsConfig, PrefsGroup, PrefsValue +from pyside_app_core.utils.property import ro_classproperty + + +class PrefRole(IntEnum): + DISPLAY_NAME = Qt.ItemDataRole.DisplayRole + NAME = Qt.ItemDataRole.UserRole + 1 + DTYPE = Qt.ItemDataRole.UserRole + 2 + EDITOR_WIDGET = Qt.ItemDataRole.UserRole + 3 + VALUE = Qt.ItemDataRole.UserRole + 4 + + +class Node(SettingsMixin, QStandardItem): + + def __init__( + self, + name: str, + display_name: str, + *children: Node + ): + super().__init__() + self.setData(name, PrefRole.NAME) + self.setData(display_name, PrefRole.DISPLAY_NAME) + + self.appendRows(children) + + @property + def name(self) -> str: + return self.data(PrefRole.NAME) + + @property + def display_name(self) -> str: + return self.data(PrefRole.DISPLAY_NAME) + + @property + def fqdn(self) -> str: + return f"{self.parent().fqdn}.{self.name}" if self.parent() is not None else self.name + + @property + def level(self) -> int: + def _lvl(node, lvl): + if node and node.parent(): + return _lvl(node.parent(), lvl + 1) + return lvl + + return _lvl(self, 0) + + def __str__(self): + children = "\n".join( + str(self.child(r, 0)) for r in range(self.rowCount()) + ) + return f"[{self.level}] {' '*self.level*2}{self.name}:\n{children}" + + +class PrefGroup(Node): + def __init__( + self, + name: str, + display_name: str, + *children: Node + ): + super().__init__( + name, + display_name, + *children, + ) + + +class PrefValue(Node): + def __init__( + self, + name: str, + display_name: str, + default_value: PrefsValue, + widget: type[PrefWidget[PrefsValue]] | None = None, + *children: Node + ): + super().__init__( + name, + display_name, + *children, + ) + self.setData(type(default_value), PrefRole.DTYPE) + self.setData(widget, PrefRole.EDITOR_WIDGET) + + # update from local storage ---- + self.setData(self.data_type(self.get_setting(self.fqdn, default_value)), PrefRole.VALUE) + + def setData(self, value: Any, role=Qt.ItemDataRole): + if role == PrefRole.VALUE: + self.store_setting(self.fqdn, value) + super().setData(value, role) + + @property + def data_type(self) -> type[PrefsValue]: + return self.data(PrefRole.DTYPE) + @property + def widget(self) -> type[PrefWidget[PrefsValue]] | None: + return self.data(PrefRole.EDITOR_WIDGET) -class PreferencesService(SettingsMixin, QObject): - pref_changed = Signal(str, PrefsValue) # type: ignore[arg-type] + @property + def value(self) -> PrefsValue: + return self.data(PrefRole.VALUE) + + def __str__(self): + children = "\n".join( + str(self.child(r, 0)) for r in range(self.rowCount()) + ) + return f"[{self.level}] {' '*self.level*2}{self.name}: \"{self.fqdn}\" {self.data_type.__name__}<{self.value}>{children}" + + +class PrefGroupsProxy(QAbstractProxyModel): + + def __init__(self, parent: QObject | None = None): + super().__init__(parent) + self._root = None + + def setRootIndex(self, index: QModelIndex): + + + def mapToSource(self, proxyIndex: QModelIndex) -> QModelIndex: + pass + + def mapFromSource(self, source: QModelIndex) -> QModelIndex: + pass + + +class PreferencesService(QObject): + _pref_changed = Signal(str, object) # type: ignore[arg-type] def __new__(cls) -> "PreferencesService": if hasattr(cls, "_instance"): @@ -16,6 +144,10 @@ def __new__(cls) -> "PreferencesService": cls._instance = super().__new__(cls) return cls._instance + @ro_classproperty + def pref_changed(cls) -> Signal: + return cls.instance()._pref_changed + @classmethod def instance(cls) -> "PreferencesService": if not hasattr(cls, "_instance"): @@ -24,22 +156,34 @@ def instance(cls) -> "PreferencesService": return cls._instance @classmethod - def save_pref(cls, pref: Pref[Any]) -> None: - cls.instance().store_setting(pref.fqdn, pref.value) - - @classmethod - def load_pref(cls, pref: Pref[Any]) -> None: - pref.value = pref.data_type(cls.instance().get_setting(pref.fqdn, pref.value)) - - @classmethod - def load_group(cls, group: PrefsGroup) -> None: - for item in group.items: - cls.load_pref(item) + def add_groups(cls, *groups: PrefGroup) -> None: + cls.instance()._model.invisibleRootItem().appendRows(list(groups)) @classmethod - def load_config(cls, config: PrefsConfig) -> None: - for group in config: - cls.load_group(group) + def model(cls) -> QStandardItemModel: + return cls.instance()._model def __init__(self) -> None: super().__init__(parent=QCoreApplication.instance()) + + self._model = QStandardItemModel(parent=self) + + def __getitem__( + self, + key: str, + ) -> PrefGroup: + return self._model[key] + + def __len__(self) -> int: + return self._model.rowCount() + + def __iter__(self) -> Iterator[PrefGroup]: + return iter(cast(PrefGroup, self._model.item(r, 0)) for r in range(self._model.rowCount())) + + def __str__(self) -> str: + return "\n".join( + [ + "Preferences:", + *[str(g) for g in self], + ] + ) diff --git a/src/pyside_app_core/types/preferences.py b/src/pyside_app_core/types/preferences.py index 1c81f69..f226333 100644 --- a/src/pyside_app_core/types/preferences.py +++ b/src/pyside_app_core/types/preferences.py @@ -1,13 +1,9 @@ -from abc import abstractmethod -from collections.abc import Sequence from pathlib import Path -from typing import Any, Generic, Protocol, TypeVar, cast, overload +from typing import Any, Protocol, TypeVar from PySide6.QtCore import SignalInstance from PySide6.QtWidgets import QWidget -from pyside_app_core.types.file_picker import DEFAULT_DIR_CONFIG, DEFAULT_FILE_CONFIG, DirConfig, FileConfig - PrefsValue = str | int | float | bool | Path _PV_contra = TypeVar("_PV_contra", contravariant=True, bound=PrefsValue) @@ -19,154 +15,3 @@ def setValue(self, value: _PV_contra) -> None: ... @property def valueChanged(self) -> SignalInstance: ... - - -class _Parent(Protocol): - @property - def fqdn(self) -> str: ... - - -class Pref(Generic[_PV_contra]): - def __init__( - self, - name: str, - default: _PV_contra, - widget: type[PrefWidget[_PV_contra]] | None = None, - ) -> None: - self._name = name - self._type = type(default) - self._value = default - self._widget = widget - - # assigned when building PrefsGroup - self._parent: _Parent | None = None - - @property - def name(self) -> str: - return self._name - - @property - def data_type(self) -> type[_PV_contra]: - return self._type - - @property - def widget(self) -> type[PrefWidget[_PV_contra]] | None: - return self._widget - - @property - def parent(self) -> _Parent | None: - return self._parent - - @property - def value(self) -> PrefsValue: - return self._value - - @value.setter - def value(self, value: _PV_contra) -> None: - self._value = value - - @property - def fqdn(self) -> str: - return f"{self.parent.fqdn}.{self._name}" if self.parent is not None else self._name - - def copy(self, new_default: _PV_contra) -> "Pref[_PV_contra]": - pref = Pref( - name=self.name, - default=new_default, - widget=self.widget, - ) - pref._parent = self.parent # noqa: SLF001 - return pref - - def __str__(self) -> str: - return f"<{self.fqdn}[{self.data_type.__name__}]> {self.value}" - - -class FilePref(Pref[_PV_contra]): - def __init__( - self, - name: str, - default: _PV_contra, - widget: type[PrefWidget[_PV_contra]] | None = None, - config: FileConfig = DEFAULT_FILE_CONFIG, - ) -> None: - self._name = name - self._value = default - self._widget = widget - self._config = config - - @property - def config(self) -> FileConfig: - return self._config - - -class DirPref(Pref[_PV_contra]): - def __init__( - self, - name: str, - default: _PV_contra, - widget: type[PrefWidget[_PV_contra]] | None = None, - config: DirConfig = DEFAULT_DIR_CONFIG, - ) -> None: - self._name = name - self._value = default - self._widget = widget - self._config = config - - -class PrefsGroup: - def __init__( - self, - name: str, - items: list[Pref[Any]], - mode: str = "", - ) -> None: - self._name = name - self._items = [] - for item in items: - item._parent = self # noqa: SLF001 - self._items.append(item) - - self._mode = mode - - @property - def name(self) -> str: - return self._name - - @property - def items(self) -> list[Pref[Any]]: - return self._items - - @property - def fqdn(self) -> str: - return self._name - - def __str__(self) -> str: - return "\n".join([str(i) for i in self.items]) - - -class PrefsConfig(Sequence[PrefsGroup]): - @overload - @abstractmethod - def __getitem__(self, index: int) -> PrefsGroup: ... - - @overload - @abstractmethod - def __getitem__(self, index: slice) -> tuple[PrefsGroup]: ... - - def __getitem__(self, index: int | slice) -> PrefsGroup | tuple[PrefsGroup]: - return self._prefs[index] - - def __len__(self) -> int: - return len(self._prefs) - - def __init__(self, *prefs: PrefsGroup) -> None: - self._prefs: tuple[PrefsGroup] = cast(tuple[PrefsGroup], prefs) - - def __str__(self) -> str: - return "\n".join( - [ - "Preferences", - *[str(i) for g in self for i in g.items], - ] - ) diff --git a/src/pyside_app_core/ui/__init__.py b/src/pyside_app_core/ui/__init__.py index 0ecd01b..f54b6b7 100644 --- a/src/pyside_app_core/ui/__init__.py +++ b/src/pyside_app_core/ui/__init__.py @@ -5,6 +5,7 @@ from PySide6.QtCore import QResource from pyside_app_core import log +from pyside_app_core.errors.basic_errors import ApplicationError def assert_resources_file(rcc: Path | None) -> Path: @@ -22,7 +23,7 @@ def assert_resources_file(rcc: Path | None) -> Path: rcc = caller.parent / "resources.rcc" tried.append(str(rcc)) if rcc.exists(): - return rcc + break except IndexError: # ignore caller frame depth index errors pass @@ -30,12 +31,15 @@ def assert_resources_file(rcc: Path | None) -> Path: # raised in standalone Nuitka build break - log.error( + log.debug(f"searched for resource.rcc in: [{', '.join(tried)}]") + if rcc and rcc.exists(): + return rcc + + raise ApplicationError( f"No resource.rcc file given or found, attempted:\n" f'{pprint.pformat([t for t in sorted(set(tried)) if "/" in t], compact=False)}\n' f"Will now exit.", ) - sys.exit(1) def register_resource_file(rcc: Path | None) -> None: diff --git a/src/pyside_app_core/ui/standard/main_window.py b/src/pyside_app_core/ui/standard/main_window.py index 1e4d39e..eb76be0 100644 --- a/src/pyside_app_core/ui/standard/main_window.py +++ b/src/pyside_app_core/ui/standard/main_window.py @@ -72,8 +72,8 @@ def closeEvent(self, event: QCloseEvent) -> None: class MainToolbarWindow(MainWindow): - def __init__(self) -> None: - super().__init__() + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) self._tool_bar = ToolBarContext(area="top", parent=self, movable=False) self._tool_bar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) diff --git a/src/pyside_app_core/ui/widgets/base_app.py b/src/pyside_app_core/ui/widgets/base_app.py index 4a171f6..3d2c212 100644 --- a/src/pyside_app_core/ui/widgets/base_app.py +++ b/src/pyside_app_core/ui/widgets/base_app.py @@ -27,6 +27,11 @@ def __init__( register_resource_file(resources_rcc) + self.setApplicationName(AppMetadata.name.replace(" ", "-")) + self.setApplicationDisplayName(AppMetadata.name) + self.setOrganizationName(AppMetadata.id) + self.setApplicationVersion(AppMetadata.version) + self.setWindowIcon( QIcon(AppMetadata.icon) if AppMetadata.icon diff --git a/src/pyside_app_core/ui/widgets/file_picker.py b/src/pyside_app_core/ui/widgets/file_picker.py index b0b43ce..8e68859 100644 --- a/src/pyside_app_core/ui/widgets/file_picker.py +++ b/src/pyside_app_core/ui/widgets/file_picker.py @@ -4,7 +4,6 @@ from PySide6.QtCore import Signal, SignalInstance from PySide6.QtWidgets import QFileDialog, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget - from pyside_app_core.types.file_picker import DEFAULT_FILE_CONFIG, DirConfig, FileConfig @@ -63,7 +62,7 @@ def set_file_path(self, file_path: Path | str | None) -> None: if self._truncate_path > 0 and self._file_path is not None: parts = self._file_path.parts - shortened = parts[-min(len(parts), self._truncate_path) :] + shortened = parts[-min(len(parts), self._truncate_path):] if len(shortened) < len(parts): shortened = ("...", *shortened) self._path_edit.setText(os.sep.join(shortened)) @@ -88,14 +87,28 @@ def _on_browse_btn_clicked(self) -> None: "caption": self._browse_config.caption, } if self._browse_config.starting_directory: - kwargs["starting_directory"] = self._browse_config.starting_directory + kwargs["dir"] = self._browse_config.starting_directory if self._browse_config.options: kwargs["options"] = self._browse_config.options if isinstance(self._browse_config, FileConfig): if self._browse_config.selection_filter: - kwargs["selection_filter"] = self._browse_config.selection_filter - path, _ = QFileDialog.getOpenFileName(**kwargs) + kwargs["filter"] = self._browse_config.selection_filter + + path, _ = QFileDialog.getOpenFileName( + self, + kwargs.get("caption"), + str(kwargs.get("dir", "")), + kwargs.get("filter", ""), + "", + kwargs.get("options"), + ) else: - path = QFileDialog.getExistingDirectory(**kwargs) - self._path_edit.setText(path) + path = QFileDialog.getExistingDirectory( + self, + kwargs.get("caption"), + kwargs.get("dir"), + kwargs.get("options"), + ) + + self.set_file_path(path) diff --git a/src/pyside_app_core/ui/widgets/preferences_manager.py b/src/pyside_app_core/ui/widgets/preferences_manager.py index c859832..e90fa69 100644 --- a/src/pyside_app_core/ui/widgets/preferences_manager.py +++ b/src/pyside_app_core/ui/widgets/preferences_manager.py @@ -1,139 +1,63 @@ -from pathlib import Path -from typing import Any, cast - -from PySide6.QtCore import Qt +from PySide6.QtCore import QModelIndex, Qt from PySide6.QtGui import QCloseEvent from PySide6.QtWidgets import ( - QDoubleSpinBox, - QFormLayout, - QHBoxLayout, - QLabel, - QSpinBox, QStackedWidget, - QTreeWidget, - QTreeWidgetItem, - QVBoxLayout, - QWidget, + QTreeView, QVBoxLayout, QWidget, QSplitter, ) -from pyside_app_core.errors.basic_errors import PreferencesError from pyside_app_core.services.preferences_service import PreferencesService -from pyside_app_core.types.preferences import Pref, PrefsConfig, PrefsValue, PrefWidget -from pyside_app_core.ui.widgets.file_picker import FilePicker -from pyside_app_core.ui.widgets.layout import HLine -from pyside_app_core.ui.widgets.pref_check_box import PrefCheckBox from pyside_app_core.ui.widgets.window_settings_mixin import WindowSettingsMixin +_mgr = None -class PreferenceContent(QWidget): - def __init__(self, group: str, prefs: list[Pref[Any]], parent: QWidget): - super().__init__(parent=parent) - self._group = group - _ly = QVBoxLayout() - self.setLayout(_ly) +class PreferencesManager(WindowSettingsMixin, QWidget): - _heading = QLabel(group) - _h_font = _heading.font() - _h_font.setBold(True) - _heading.setFont(_h_font) - _ly.addWidget(_heading) - - _ly.addWidget(HLine(self)) - - _form = QFormLayout() - _ly.addLayout(_form) - - for pref in prefs: - if pref.value is None and pref.widget is None: - continue - _form.addRow(pref.name, cast(QWidget, self._build_widget(pref))) - - _ly.addStretch() - - @staticmethod - def _update_value(value: PrefsValue, pref: Pref[PrefsValue]) -> None: - pref.value = value - PreferencesService.save_pref(pref) - - def _build_widget(self, pref: Pref[PrefsValue]) -> PrefWidget[Any]: - if pref.widget is not None: - w: PrefWidget[Any] = pref.widget(parent=self) - w.setValue(pref.value) - w.valueChanged.connect(lambda v: self._update_value(v, pref)) - return w - if pref.data_type is bool: - w: PrefCheckBox = PrefCheckBox(parent=self) # type: ignore[no-redef] - w.setValue(pref.value) - w.valueChanged.connect(lambda v: self._update_value(v, pref)) - return cast(PrefWidget[bool], w) - if pref.data_type is int: - w: QSpinBox = QSpinBox(parent=self) # type: ignore[no-redef] - w.setValue(pref.value) - w.valueChanged.connect(lambda v: self._update_value(v, pref)) - return cast(PrefWidget[int], w) - if pref.data_type is float: - w: QDoubleSpinBox = QDoubleSpinBox(parent=self) # type: ignore[no-redef] - w.setValue(pref.value) - w.valueChanged.connect(lambda v: self._update_value(v, pref)) - return cast(PrefWidget[float], w) - if pref.data_type is str: - w: PrefCheckBox = PrefCheckBox(parent=self) # type: ignore[no-redef] - w.setValue(pref.value) - w.valueChanged.connect(lambda v: self._update_value(v, pref)) - return cast(PrefWidget[str], w) - if issubclass(pref.data_type, Path): - w: FilePicker = FilePicker(parent=self) # type: ignore[no-redef] - w.setValue(pref.value) - w.valueChanged.connect(lambda v: self._update_value(v, pref)) - return cast(PrefWidget[Path], w) - - raise PreferencesError(f'"{pref}" is not understood') + @classmethod + def open(cls): + global _mgr + if _mgr is not None: + _mgr.close() + _mgr.deleteLater() + _mgr = PreferencesManager() + _mgr.show() -class PreferencesManager(WindowSettingsMixin, QWidget): - def __init__(self, config: PrefsConfig, parent: QWidget | None = None): + def __init__(self, parent: QWidget | None = None): super().__init__(parent=parent) self.setWindowTitle("Preferences") # --- - self._config = config - - # --- - _ly = QHBoxLayout() + _ly = QVBoxLayout() # _ly.setContentsMargins(0, 0, 0, 0) self.setLayout(_ly) - self._group_list = QTreeWidget(self) + _split = QSplitter(Qt.Orientation.Horizontal, parent=self) + _ly.addWidget(_split) + + self._group_list = QTreeView(self) self._group_list.setHeaderHidden(True) - _ly.addWidget(self._group_list, stretch=1) + _split.addWidget(self._group_list) self._pref_stack = QStackedWidget(self) - _ly.addWidget(self._pref_stack, stretch=4) + _split.addWidget(self._pref_stack) # --- - for group in config: - self._add_group(group.name, group.items) + _split.setStretchFactor(0, 1) + _split.setStretchFactor(1, 3) + self._group_list.setModel(PreferencesService.model()) # --- - self._group_list.currentItemChanged.connect(self._on_pick) + self._group_list.clicked.connect(self._on_pick) self._group_list.setCurrentIndex( - self._group_list.model().index(self.get_setting("selected-group-idx", 0, int), 0) + PreferencesService.model().index(self.get_setting("selected-group-idx", 0, int), 0) ) - def _add_group(self, group: str, prefs: list[Pref[Any]]) -> None: - stack_idx = self._pref_stack.addWidget(PreferenceContent(group, prefs, self)) - - grp = QTreeWidgetItem() - grp.setText(0, group) - grp.setData(0, Qt.ItemDataRole.UserRole, stack_idx) - self._group_list.addTopLevelItem(grp) - - def _on_pick(self, current: QTreeWidgetItem, _: QTreeWidgetItem) -> None: - idx: int = current.data(0, Qt.ItemDataRole.UserRole) - self._pref_stack.setCurrentIndex(idx) + def _on_pick(self, index: QModelIndex): + item = PreferencesService.model().itemFromIndex(index) + print(item) def closeEvent(self, event: QCloseEvent) -> None: self.store_setting("selected-group-idx", self._pref_stack.currentIndex()) diff --git a/src/pyside_app_core/ui/widgets/tool_bar_ctx.py b/src/pyside_app_core/ui/widgets/tool_bar_ctx.py index bc024b5..b51a585 100644 --- a/src/pyside_app_core/ui/widgets/tool_bar_ctx.py +++ b/src/pyside_app_core/ui/widgets/tool_bar_ctx.py @@ -2,19 +2,20 @@ from collections.abc import Iterator from typing import Literal +from PySide6 import QtWidgets from PySide6.QtCore import QSize, Qt from PySide6.QtGui import QAction, QIcon -from PySide6.QtWidgets import QMainWindow, QToolBar, QToolButton +from PySide6.QtWidgets import QMainWindow, QToolBar, QToolButton, QWidget from pyside_app_core.mixin.object_name_mixin import ObjectNameMixin ToolBarArea = Literal["top", "bottom", "left", "right"] _TOOL_BAR_AREA_MAP = { - "top": Qt.ToolBarArea.TopToolBarArea, + "top": Qt.ToolBarArea.TopToolBarArea, "bottom": Qt.ToolBarArea.BottomToolBarArea, - "right": Qt.ToolBarArea.RightToolBarArea, - "left": Qt.ToolBarArea.LeftToolBarArea, + "right": Qt.ToolBarArea.RightToolBarArea, + "left": Qt.ToolBarArea.LeftToolBarArea, } @@ -42,6 +43,11 @@ def __init__(self, area: ToolBarArea, parent: QMainWindow, *, movable: bool = Fa parent.addToolBar(_TOOL_BAR_AREA_MAP[self._area], self) + def add_stretch(self): + stretch = QWidget(self) + stretch.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.addWidget(stretch) + @contextlib.contextmanager def add_action(self, name: str, icon: QIcon | None = None) -> Iterator[QAction]: action = QAction(text=name, parent=self) diff --git a/src/pyside_app_core/utils/property.py b/src/pyside_app_core/utils/property.py new file mode 100644 index 0000000..5c4f0f0 --- /dev/null +++ b/src/pyside_app_core/utils/property.py @@ -0,0 +1,6 @@ +class ro_classproperty: + def __init__(self, f): + self.f = f + + def __get__(self, obj, owner): + return self.f(owner)