Coverage for paintmaskanimator/main_window_tool_hold.py: 90%

66 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1"""ツールの一時切り替え; owned by ``MainWindow`` as ``window.tool_hold``. 

2 

3CLIP STUDIO PAINT の「一時切り替え」と同じく、ツールのキーを押すとすぐその 

4ツールに切り替え、環境設定の長さ以上押し続けてから離したら元のツールへ戻す。 

5長さを 0 ms にすると、押している間だけ切り替わる(完全な一時切り替え)。 

6環境設定で OFF のとき(既定)は、これまでどおり押したツールに切り替えたままにする。 

7 

8キーを押したことは、ショートカットの直前に届く ``ShortcutOverride`` で知る 

9(ショートカットになったキーには ``KeyPress`` が届かないため)。メニューから 

10選んだときは長押しにならないよう、キーの押下と結び付いたときだけ扱う。 

11 

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

13""" 

14from __future__ import annotations 

15 

16import time 

17from dataclasses import dataclass 

18from typing import TYPE_CHECKING, FrozenSet, Optional 

19 

20from PySide6.QtCore import QTimer 

21 

22from . import preferences 

23 

24if TYPE_CHECKING: 

25 from .main_window import MainWindow 

26 

27_MODIFIERS = frozenset({"Ctrl", "Shift", "Alt", "Meta"}) 

28#: キーの押下(ShortcutOverride)からショートカットの実行までに許す時間(秒)。 

29_PRESS_MATCH_SECONDS = 0.5 

30#: 描いている途中で戻すのを待つとき、描き終わりを確かめる間隔(ミリ秒)。 

31_WAIT_FOR_STROKE_MS = 30 

32 

33 

34@dataclass 

35class _Hold: 

36 tool: str 

37 previous: str 

38 keys: FrozenSet[str] 

39 started: float 

40 

41 

42class ToolHoldController: 

43 """Owned by ``MainWindow`` as ``window.tool_hold``.""" 

44 

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

46 self.window = window 

47 self._hold: Optional[_Hold] = None 

48 self._last_key: Optional[tuple] = None 

49 self._revert_to: Optional[tuple] = None 

50 

51 def key_pressed(self, token): 

52 """キーが押された(ショートカットになる前の ShortcutOverride で呼ぶ)。""" 

53 if token: 53 ↛ exitline 53 didn't return from function 'key_pressed' because the condition on line 53 was always true

54 self._last_key = (token, time.monotonic()) 

55 

56 def select(self, tool_id, action): 

57 """ツールのショートカット・メニューが選ばれた。""" 

58 tools = self.window.tools 

59 keys = frozenset( 

60 token for token in self.window._shortcut_tokens(action, "") 

61 if token not in _MODIFIERS 

62 ) 

63 hold = self._hold 

64 if hold is not None and hold.tool == tool_id: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 return # キーの自動リピート 

66 from_key = ( 

67 self._last_key is not None 

68 and self._last_key[0] in keys 

69 and time.monotonic() - self._last_key[1] < _PRESS_MATCH_SECONDS 

70 ) 

71 self._last_key = None 

72 if not preferences.tool_hold_enabled() or not from_key: 

73 self._hold = None 

74 tools.select_tool(tool_id) 

75 return 

76 # 一時切り替え中に別のツールのキーを押したら、戻り先は最初のツールのまま。 

77 previous = hold.previous if hold is not None else tools.active_tool 

78 self._hold = _Hold(tool_id, previous, keys, time.monotonic()) 

79 tools.select_tool(tool_id) 

80 

81 def key_released(self, token): 

82 """キーが離された。長押しだったら元のツールへ戻す。""" 

83 hold = self._hold 

84 if hold is None or token not in hold.keys: 

85 return 

86 self._hold = None 

87 held_ms = (time.monotonic() - hold.started) * 1000.0 

88 if held_ms >= preferences.tool_hold_ms(): 

89 self._revert(hold.tool, hold.previous) 

90 

91 def release_all(self): 

92 """ウィンドウからフォーカスが外れた。押していたキーは離したものとみなす。""" 

93 hold = self._hold 

94 if hold is not None: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true

95 self.key_released(next(iter(hold.keys), None)) 

96 

97 def _revert(self, tool, previous): 

98 self._revert_to = (tool, previous) 

99 self._try_revert() 

100 

101 def _try_revert(self): 

102 pending = self._revert_to 

103 if pending is None: 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true

104 return 

105 if self.window.canvas.drawing: 

106 # 描き終わってから戻す(描いている途中でツールを変えない)。 

107 QTimer.singleShot(_WAIT_FOR_STROKE_MS, self._try_revert) 

108 return 

109 self._revert_to = None 

110 tool, previous = pending 

111 # その間に別のツールを選んでいたら、そちらを優先する。 

112 if self.window.tools.active_tool == tool and previous != tool: 112 ↛ exitline 112 didn't return from function '_try_revert' because the condition on line 112 was always true

113 self.window.tools.select_tool(previous) 

114 

115 

116__all__ = ["ToolHoldController"]