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

1"""Pen-pressure settings kept per user (Qt-widget independent). 

2 

3Pressure depends on the person and the pen, not on the drawing, so it lives in 

4the user config instead of the project file: 

5 

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. 

10 

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 

16 

17from . import config 

18from .i18n import tr 

19 

20PRESETS_KEY = "pressure_presets" 

21ACTIVE_KEY = "pressure_active_preset" 

22BRUSH_KEY = "brush_pressure" 

23BRUSH_USES_GLOBAL_KEY = "brush_pressure_uses_global" 

24 

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} 

33 

34 

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

41 

42 

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 } 

62 

63 

64def default_preset_name(): 

65 return tr("標準") 

66 

67 

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 

83 

84 

85def save_presets(entries): 

86 config.set_value( 

87 PRESETS_KEY, 

88 [{"name": str(e["name"]), **normalize(e)} for e in entries], 

89 ) 

90 

91 

92def preset_names(): 

93 return [entry["name"] for entry in presets()] 

94 

95 

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] 

100 

101 

102def set_active_preset(name): 

103 config.set_value(ACTIVE_KEY, str(name)) 

104 

105 

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 

111 

112 

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) 

119 

120 

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 

132 

133 

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 

147 

148 

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 

156 

157 

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 

163 

164 

165def brush_settings(): 

166 return normalize(config.get_value(BRUSH_KEY, DEFAULT_SETTINGS)) 

167 

168 

169def set_brush_settings(settings): 

170 config.set_value(BRUSH_KEY, normalize(settings)) 

171 

172 

173def brush_uses_global(): 

174 return bool(config.get_value(BRUSH_USES_GLOBAL_KEY, True)) 

175 

176 

177def set_brush_uses_global(uses_global): 

178 config.set_value(BRUSH_USES_GLOBAL_KEY, bool(uses_global)) 

179 

180 

181def effective_settings(): 

182 """The setting the brush draws with right now.""" 

183 return global_settings() if brush_uses_global() else brush_settings()