Coverage for paintmaskanimator/main_window_shortcuts.py: 91%
97 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1"""ショートカットの保存と復元。``MainWindow`` が ``window.shortcut_store`` として持つ。
3ショートカット設定の変更は、これまでその場のアクションに反映するだけで
4どこにも保存しておらず、起動し直す(アップデートする)たびに既定へ戻っていた。
6- 設定ファイル(アップデートや再インストールでも残る場所)に保存する
7- 保存するのは既定から変えたものだけ。変えていない操作は、新しいバージョンで
8 既定のキーが変わればそれに付いていく
9- 保存のキーは翻訳される表示名ではなく、アクションの属性名から作る固定の ID
10- 新しいバージョンで足された既定のキーが自分で決めたキーとぶつかったら、
11 自分で決めた方を優先し、既定の側からそのキーを外す(両方に残すとどちらも
12 効かなくなるため)
13- アクションパネルのボタン(Python アクション)は ``panel:<ID>`` で保存する。
14 スクリプトを読み込み直すたびに付け直し、読み込めなかったスクリプトの
15 設定も消さずに残す
17A collaborator rather than a mixin -- see main_window_export.py for why.
18"""
19from __future__ import annotations
21from typing import TYPE_CHECKING
23from PySide6.QtGui import QAction, QKeySequence
25from . import config
26from .logging_setup import get_logger
28if TYPE_CHECKING:
29 from .main_window import MainWindow
31log = get_logger(__name__)
33#: 設定ファイルのキー。
34SHORTCUTS_KEY = "shortcuts"
35_FORMAT_VERSION = 1
36#: アクションパネルのボタンの ID に付ける接頭辞。
37PANEL_PREFIX = "panel:"
40def _key_texts(action):
41 return [
42 sequence.toString(QKeySequence.SequenceFormat.PortableText)
43 for sequence in action.shortcuts()
44 if not sequence.isEmpty()
45 ]
48def _is_hold(action):
49 return bool(action.property("holdOperation"))
52def _state(action):
53 """保存・比較に使う、アクションの今のショートカット。"""
54 if _is_hold(action):
55 stored = action.property("holdShortcutText")
56 return {"hold": "" if stored is None else str(stored)}
57 return {"keys": _key_texts(action)}
60class ShortcutStore:
61 def __init__(self, window: "MainWindow"):
62 self.window = window
63 self._actions: dict[str, QAction] = {}
64 self._defaults: dict[str, dict] = {}
66 def register_actions(self):
67 """ショートカットを持てるアクションに固定の ID を付け、既定を覚える。"""
68 actions = {}
69 for name, value in vars(self.window).items():
70 if name.startswith("a_") and isinstance(value, QAction):
71 actions[name] = value
72 for tool_id, action in getattr(self.window, "tool_actions", {}).items():
73 actions[f"tool:{tool_id}"] = action
74 actions.update(self._panel_actions())
75 for action_id, action in actions.items():
76 if not action.objectName(): 76 ↛ 75line 76 didn't jump to line 75 because the condition on line 76 was always true
77 action.setObjectName(action_id)
78 self._actions = actions
79 self._defaults = {
80 action_id: _state(action) for action_id, action in actions.items()
81 }
83 def _panel_actions(self):
84 panel = getattr(self.window, "action_panel", None)
85 if panel is None: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 return {}
87 return {
88 PANEL_PREFIX + action_id: action
89 for action_id, action in panel.own_shortcut_actions().items()
90 }
92 def register_panel_actions(self):
93 """アクションパネルを読み込み直したあと、そのボタンの分を登録し直す。"""
94 if not self._actions: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 return # 起動中の最初の読み込みは register_actions がまとめて扱う
96 for action_id in [key for key in self._actions if key.startswith(PANEL_PREFIX)]:
97 del self._actions[action_id]
98 self._defaults.pop(action_id, None)
99 panel_actions = self._panel_actions()
100 for action_id, action in panel_actions.items():
101 action.setObjectName(action_id)
102 self._actions[action_id] = action
103 self._defaults[action_id] = _state(action)
104 self.load(only=set(panel_actions))
106 # ------------------------------------------------------------------
107 @staticmethod
108 def _stored_overrides():
109 data = config.get_value(SHORTCUTS_KEY, {})
110 overrides = data.get("actions", {}) if isinstance(data, dict) else {}
111 return overrides if isinstance(overrides, dict) else {}
113 def load(self, only=None):
114 """保存したショートカットを反映する(無ければ既定のまま)。
116 ``only`` を渡すと、その ID のアクションだけに反映する。
117 """
118 overrides = self._stored_overrides()
119 claimed = set()
120 applied = set()
121 for action_id, state in overrides.items():
122 action = self._actions.get(action_id)
123 if action is None or not isinstance(state, dict):
124 continue # 消えた操作の設定は無視する
125 if only is not None and action_id not in only: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true
126 if not _is_hold(action):
127 claimed.update(_key_texts(action))
128 continue
129 if _is_hold(action):
130 text = str(state.get("hold", ""))
131 action.setProperty("holdShortcutText", text)
132 action.setShortcut(QKeySequence(text))
133 if text: 133 ↛ 139line 133 didn't jump to line 139 because the condition on line 133 was always true
134 claimed.add(text)
135 else:
136 keys = [str(key) for key in state.get("keys", []) if str(key)]
137 action.setShortcuts([QKeySequence(key) for key in keys])
138 claimed.update(_key_texts(action))
139 applied.add(action_id)
140 # 自分で決めたキーと既定のキーがぶつかったら、既定の側から外す。
141 for action_id, action in self._actions.items():
142 if action_id in applied or _is_hold(action):
143 continue
144 if only is not None and action_id not in only:
145 continue
146 keys = _key_texts(action)
147 kept = [key for key in keys if key not in claimed]
148 if kept != keys:
149 log.info("shortcut %s dropped keys taken by user settings", action_id)
150 action.setShortcuts([QKeySequence(key) for key in kept])
152 def save(self):
153 """既定から変えたショートカットだけを保存する。"""
154 # 読み込めなかったパネルのアクションの設定は、直したときのために残す。
155 overrides = {
156 action_id: state
157 for action_id, state in self._stored_overrides().items()
158 if action_id.startswith(PANEL_PREFIX) and action_id not in self._actions
159 }
160 for action_id, action in self._actions.items():
161 state = _state(action)
162 if state != self._defaults.get(action_id):
163 overrides[action_id] = state
164 config.set_value(
165 SHORTCUTS_KEY,
166 {"version": _FORMAT_VERSION, "actions": overrides} if overrides else None,
167 )