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

1"""ショートカットの保存と復元。``MainWindow`` が ``window.shortcut_store`` として持つ。 

2 

3ショートカット設定の変更は、これまでその場のアクションに反映するだけで 

4どこにも保存しておらず、起動し直す(アップデートする)たびに既定へ戻っていた。 

5 

6- 設定ファイル(アップデートや再インストールでも残る場所)に保存する 

7- 保存するのは既定から変えたものだけ。変えていない操作は、新しいバージョンで 

8 既定のキーが変わればそれに付いていく 

9- 保存のキーは翻訳される表示名ではなく、アクションの属性名から作る固定の ID 

10- 新しいバージョンで足された既定のキーが自分で決めたキーとぶつかったら、 

11 自分で決めた方を優先し、既定の側からそのキーを外す(両方に残すとどちらも 

12 効かなくなるため) 

13- アクションパネルのボタン(Python アクション)は ``panel:<ID>`` で保存する。 

14 スクリプトを読み込み直すたびに付け直し、読み込めなかったスクリプトの 

15 設定も消さずに残す 

16 

17A collaborator rather than a mixin -- see main_window_export.py for why. 

18""" 

19from __future__ import annotations 

20 

21from typing import TYPE_CHECKING 

22 

23from PySide6.QtGui import QAction, QKeySequence 

24 

25from . import config 

26from .logging_setup import get_logger 

27 

28if TYPE_CHECKING: 

29 from .main_window import MainWindow 

30 

31log = get_logger(__name__) 

32 

33#: 設定ファイルのキー。 

34SHORTCUTS_KEY = "shortcuts" 

35_FORMAT_VERSION = 1 

36#: アクションパネルのボタンの ID に付ける接頭辞。 

37PANEL_PREFIX = "panel:" 

38 

39 

40def _key_texts(action): 

41 return [ 

42 sequence.toString(QKeySequence.SequenceFormat.PortableText) 

43 for sequence in action.shortcuts() 

44 if not sequence.isEmpty() 

45 ] 

46 

47 

48def _is_hold(action): 

49 return bool(action.property("holdOperation")) 

50 

51 

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)} 

58 

59 

60class ShortcutStore: 

61 def __init__(self, window: "MainWindow"): 

62 self.window = window 

63 self._actions: dict[str, QAction] = {} 

64 self._defaults: dict[str, dict] = {} 

65 

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 } 

82 

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 } 

91 

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)) 

105 

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 {} 

112 

113 def load(self, only=None): 

114 """保存したショートカットを反映する(無ければ既定のまま)。 

115 

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]) 

151 

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 )