Coverage for paintmaskanimator/pressure_settings.py: 94%
108 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"""Pen-pressure settings kept per user (Qt-widget independent).
3Pressure depends on the person and the pen, not on the drawing, so it lives in
4the user config instead of the project file:
6* **Presets** -- named settings edited in Preferences › 筆圧. The one chosen
7 there is the *global* pressure setting.
8* **Brush** -- the brush tool's own setting, and whether the brush follows the
9 global preset (the default) or uses its own.
11A settings dict is ``{"enabled", "minimum", "maximum", "points"}`` with the
12same meaning as the canvas attributes ``pressure_enabled`` / ``pressure_min``
13/ ``pressure_max`` / ``pressure_curve_points``.
14"""
15import copy
17from . import config
18from .i18n import tr
20PRESETS_KEY = "pressure_presets"
21ACTIVE_KEY = "pressure_active_preset"
22BRUSH_KEY = "brush_pressure"
23BRUSH_USES_GLOBAL_KEY = "brush_pressure_uses_global"
25MINIMUM_RANGE = (0.01, 1.0)
26MAXIMUM_RANGE = (0.1, 3.0)
27DEFAULT_SETTINGS = {
28 "enabled": True,
29 "minimum": 0.1,
30 "maximum": 1.0,
31 "points": [[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]],
32}
35def _clamp(value, bounds, default):
36 try:
37 value = float(value)
38 except (TypeError, ValueError):
39 return default
40 return max(bounds[0], min(bounds[1], value))
43def normalize(settings):
44 """Return a complete, in-range copy of ``settings`` (missing keys -> default)."""
45 settings = settings if isinstance(settings, dict) else {}
46 points = []
47 for point in settings.get("points") or []:
48 try:
49 x, y = float(point[0]), float(point[1])
50 except (TypeError, ValueError, IndexError, KeyError):
51 continue
52 points.append([max(0.0, min(1.0, x)), max(0.0, min(1.0, y))])
53 points.sort(key=lambda p: p[0])
54 if len(points) < 2:
55 points = copy.deepcopy(DEFAULT_SETTINGS["points"])
56 return {
57 "enabled": bool(settings.get("enabled", DEFAULT_SETTINGS["enabled"])),
58 "minimum": _clamp(settings.get("minimum"), MINIMUM_RANGE, DEFAULT_SETTINGS["minimum"]),
59 "maximum": _clamp(settings.get("maximum"), MAXIMUM_RANGE, DEFAULT_SETTINGS["maximum"]),
60 "points": points,
61 }
64def default_preset_name():
65 return tr("標準")
68def presets():
69 """All presets as ``[{"name": str, **settings}]``; never empty."""
70 result = []
71 seen = set()
72 for entry in config.get_value(PRESETS_KEY, []) or []:
73 if not isinstance(entry, dict): 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true
74 continue
75 name = str(entry.get("name", "")).strip()
76 if not name or name in seen: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 continue
78 seen.add(name)
79 result.append({"name": name, **normalize(entry)})
80 if not result:
81 result.append({"name": default_preset_name(), **normalize(DEFAULT_SETTINGS)})
82 return result
85def save_presets(entries):
86 config.set_value(
87 PRESETS_KEY,
88 [{"name": str(e["name"]), **normalize(e)} for e in entries],
89 )
92def preset_names():
93 return [entry["name"] for entry in presets()]
96def active_preset_name():
97 names = preset_names()
98 name = config.get_value(ACTIVE_KEY)
99 return name if isinstance(name, str) and name in names else names[0]
102def set_active_preset(name):
103 config.set_value(ACTIVE_KEY, str(name))
106def preset(name):
107 for entry in presets(): 107 ↛ 110line 107 didn't jump to line 110 because the loop on line 107 didn't complete
108 if entry["name"] == name:
109 return normalize(entry)
110 return None
113def update_preset(name, settings):
114 entries = presets()
115 for entry in entries:
116 if entry["name"] == name:
117 entry.update(normalize(settings))
118 save_presets(entries)
121def add_preset(name, settings):
122 """Add a preset (renamed ``name 2`` etc. if taken); return the used name."""
123 entries = presets()
124 names = {e["name"] for e in entries}
125 base = str(name).strip() or default_preset_name()
126 unique, n = base, 2
127 while unique in names:
128 unique, n = f"{base} {n}", n + 1
129 entries.append({"name": unique, **normalize(settings)})
130 save_presets(entries)
131 return unique
134def rename_preset(old, new):
135 """Rename a preset; return False when ``new`` is empty or already used."""
136 new = str(new).strip()
137 entries = presets()
138 if not new or any(e["name"] == new for e in entries):
139 return False
140 for entry in entries:
141 if entry["name"] == old:
142 entry["name"] = new
143 save_presets(entries)
144 if config.get_value(ACTIVE_KEY) == old: 144 ↛ 146line 144 didn't jump to line 146 because the condition on line 144 was always true
145 set_active_preset(new)
146 return True
149def remove_preset(name):
150 """Remove a preset; the last one cannot be removed."""
151 entries = [e for e in presets() if e["name"] != name]
152 if not entries:
153 return False
154 save_presets(entries)
155 return True
158def global_settings():
159 settings = preset(active_preset_name())
160 # active_preset_name() always names one of presets(), so this is never None.
161 assert settings is not None
162 return settings
165def brush_settings():
166 return normalize(config.get_value(BRUSH_KEY, DEFAULT_SETTINGS))
169def set_brush_settings(settings):
170 config.set_value(BRUSH_KEY, normalize(settings))
173def brush_uses_global():
174 return bool(config.get_value(BRUSH_USES_GLOBAL_KEY, True))
177def set_brush_uses_global(uses_global):
178 config.set_value(BRUSH_USES_GLOBAL_KEY, bool(uses_global))
181def effective_settings():
182 """The setting the brush draws with right now."""
183 return global_settings() if brush_uses_global() else brush_settings()