From 270d4b454e9136f027254a5a9e757e2d8b1ecdc1 Mon Sep 17 00:00:00 2001 From: Yoonchae Lee Date: Mon, 4 Aug 2025 22:49:59 +0900 Subject: [PATCH] Squashed 'src/addon/ankiaddonconfig/' changes from 705d0f8..ad2d249 ad2d249 make ConfigWindow a Modal fixes bug on Anki where after opening 'Advanced' in ConfigWindow, close button in main Anki window disappears even after closing f4bc2bc remove WA_DeleteOnClose property 52fdc92 update README 52ed3fe format with black 5cf729e mypy ignore line 7d7faba qt enum values must be namespaced direct value access is no longer supported 254973e feat: Add `shortcut_input` to `ConfigLayout` (#2) 92ed29e Merge pull request #3 from RisingOrange/fix/dropdown-key-values 8146103 fix: dropdown - set value instead of label 954c030 Merge pull request #1 from chuanqixu/master 48422f3 update/changes when QComboBox items are changed b160071 fix conf.get() not deep-copying object 29b47c4 fix color input dialog not showing current color ff4e8c2 update ankiaddonconfig README 21c34a3 add instructions to use git subtree to use ankiaddonconfig git-subtree-dir: src/addon/ankiaddonconfig git-subtree-split: ad2d2495a3aa72e98bcb3013955b7e4558e36477 --- README.md | 20 ++++++++++++++--- manager.py | 2 +- window.py | 64 ++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d8a9a9e..4d38911 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,25 @@ Each widget is linked to a single config entry. When the user interacts with a w Each ConfigManager instance stores its own config separately. And its configs are synced with `meta.json` only when `load()` and `save()` is called. This is intended so the config value changing while the add-on is running will not cause unanticipated errors. You should only call `conf.load()` when it is safe to do so. With that in mind, it is recommended to use separate ConfigManager instances for your config window. +## Add to your project -### Compatibility +To download ankiaddonconfig to your project: +```sh +git remote add ankiaddonconfig +git subtree add --prefix ankiaddonconfig master --squash +``` + +If you want to pull new changes in ankiaddonconfig: +```sh +git subtree pull --prefix ankiaddonconfig master --squash +``` + +## Compatibility This library is compatible from Anki v2.1.0+. And atleast python v3.6. It should also remain compatible with newer Anki versions for a long time. +## Basic Documentation ### Methods in ConfigLayout When you call `ConfigWindow.add_tab(name)`, you get a ConfigLayout object. Creating the widgets is done in ConfigLayout. All the below methods are methods of the ConfigLayout. @@ -85,7 +98,7 @@ def vlayout(self) -> ConfigLayout: # Top to bottom ConfigLayout ``` -## Using ConfigManager +### Using ConfigManager ```python from .ankiaddon import ConfigManager conf = ConfigManager() @@ -115,7 +128,8 @@ conf.clone() # returns a deepcopy of the config dictionary ## Contributing -Please run mypy and black before creating a pull request. You may need to run `python -m pip install aqt PyQt5-stubs` for mypy checks to work. +Please run mypy and black before creating a pull request. You may need to run `python -m pip install aqt` for mypy checks to work. You may need to uninstall `PyQt5-stubs` and `PyQt5` packages for mypy checks to work. + ``` python -m mypy . python -m black . diff --git a/manager.py b/manager.py index bb90f5b..9da63d8 100644 --- a/manager.py +++ b/manager.py @@ -45,7 +45,7 @@ def get_from_dict(self, dict_obj: dict, key: str) -> Any: if isinstance(return_val, list): level = int(level) return_val = return_val[level] - return return_val + return copy.deepcopy(return_val) def copy(self) -> Dict: return copy.deepcopy(self._config) diff --git a/window.py b/window.py index 4bb2832..06e3909 100644 --- a/window.py +++ b/window.py @@ -17,6 +17,7 @@ class ConfigWindow(QDialog): def __init__(self, conf: "ConfigManager") -> None: QDialog.__init__(self, mw, Qt.WindowType.Window) # type: ignore + self.setModal(True) self.conf = conf self.mgr = mw.addonManager self.widget_updates: List[Callable[[], None]] = [] @@ -26,7 +27,6 @@ def __init__(self, conf: "ConfigManager") -> None: self.geom_key = f"addonconfig-{conf.addon_name}" self.setWindowTitle(f"Config for {conf.addon_name}") - self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.setup() def setup(self) -> None: @@ -44,7 +44,6 @@ def setup(self) -> None: self.setup_buttons(self.btn_layout) def setup_buttons(self, btn_box: "ConfigLayout") -> None: - self.advanced_btn = QPushButton("Advanced") self.advanced_btn.clicked.connect(self.on_advanced) btn_box.addWidget(self.advanced_btn) @@ -209,7 +208,7 @@ def update() -> None: checkbox.stateChanged.connect( lambda s: self.conf.set( key, - s == (Qt.CheckState.Checked.value if QT6 else Qt.CheckState.Checked), + s == (Qt.CheckState.Checked.value if QT6 else Qt.CheckState.Checked), # type: ignore ) ) self.addWidget(checkbox) @@ -349,6 +348,7 @@ def color_input( If opacity is true, allows changing opacity. Note that color is stored in RGBA format, not ARGB. When creating using the RGBA in Qt, you need to change it to ARGB format first. """ + color: QColor button = QPushButton() button.setFixedWidth(25) button.setFixedHeight(25) @@ -356,11 +356,8 @@ def color_input( if tooltip is not None: button.setToolTip(tooltip) - color_dialog = QColorDialog(self.config_window) - if opacity: - color_dialog.setOptions(QColorDialog.ShowAlphaChannel) - def set_color(rgb: str) -> None: + nonlocal color if len(rgb) == 9: rgb = "#" + rgb[7:] + rgb[1:7] # RGBA to ARGB @@ -372,7 +369,6 @@ def set_color(rgb: str) -> None: color.setNamedColor(rgb) # Accepts #RGB, #RRGGBB or #AARRGGBB if not color.isValid(): raise InvalidConfigValueError(key, "rgb hex color string", rgb) - color_dialog.setCurrentColor(color) def update() -> None: value = self.conf.get(key) @@ -387,9 +383,17 @@ def save(color: QColor) -> None: self.conf.set(key, rgb) set_color(rgb) + def open_color_dialog() -> None: + color_dialog = QColorDialog(self.config_window) + if opacity: + color_dialog.setOptions(QColorDialog.ColorDialogOption.ShowAlphaChannel) + color_dialog.setCurrentColor(color) + color_dialog.colorSelected.connect(lambda c: save(c)) + color_dialog.exec() + self.widget_updates.append(update) - color_dialog.colorSelected.connect(lambda color: save(color)) - button.clicked.connect(lambda _: color_dialog.exec()) + + button.clicked.connect(lambda _: open_color_dialog()) if description is not None: row = self.hlayout() @@ -451,6 +455,44 @@ def get_path() -> None: return (line_edit, button) + def shortcut_input( + self, key: str, description: Optional[str] = None, tooltip: Optional[str] = None + ) -> Tuple[QKeySequenceEdit, QPushButton]: + edit = QKeySequenceEdit() + + if description is not None: + row = self.hlayout() + row.text(description, tooltip=tooltip) + + def update() -> None: + val = self.conf.get(key) + if not isinstance(val, str): + raise InvalidConfigValueError(key, "str", val) + val = val.replace(" ", "") + edit.setKeySequence(val) + + self.widget_updates.append(update) + + edit.keySequenceChanged.connect( # type: ignore + lambda s: self.conf.set(key, edit.keySequence().toString()) + ) + + def on_shortcut_clear_btn_click() -> None: + edit.clear() + + shortcut_clear_btn = QPushButton("Clear") + shortcut_clear_btn.setSizePolicy( + QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed + ) + shortcut_clear_btn.clicked.connect(on_shortcut_clear_btn_click) # type: ignore + + layout = QHBoxLayout() + layout.addWidget(edit) + layout.addWidget(shortcut_clear_btn) + + self.addLayout(layout) + return edit, shortcut_clear_btn + # Layout widgets def text( @@ -623,7 +665,7 @@ def scroll_layout( """Legacy. Adds QScrollArea > QWidget*2 > ConfigLayout, returns the layout.""" return self._scroll_layout( QSizePolicy.Policy.Expanding if horizontal else QSizePolicy.Policy.Minimum, - QSizePolicy.Policy.Expanding if vertical else QSizePolicy.Minimum, + QSizePolicy.Policy.Expanding if vertical else QSizePolicy.Policy.Minimum, Qt.ScrollBarPolicy.ScrollBarAsNeeded, Qt.ScrollBarPolicy.ScrollBarAsNeeded, )