graphica-plot 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. graphica/Graphica.ico +0 -0
  2. graphica/__init__.py +1 -0
  3. graphica/__main__.py +111 -0
  4. graphica/assets/__init__.py +0 -0
  5. graphica/assets/icons/__init__.py +0 -0
  6. graphica/assets/icons/arrow-right.svg +21 -0
  7. graphica/assets/icons/bold.svg +20 -0
  8. graphica/assets/icons/calculator.svg +26 -0
  9. graphica/assets/icons/chart-histogram.svg +24 -0
  10. graphica/assets/icons/chart-line.svg +20 -0
  11. graphica/assets/icons/chevron-down.svg +19 -0
  12. graphica/assets/icons/chevron-right.svg +19 -0
  13. graphica/assets/icons/color-swatch.svg +22 -0
  14. graphica/assets/icons/column-insert-right.svg +21 -0
  15. graphica/assets/icons/column-remove.svg +21 -0
  16. graphica/assets/icons/copy.svg +20 -0
  17. graphica/assets/icons/download.svg +21 -0
  18. graphica/assets/icons/edit.svg +21 -0
  19. graphica/assets/icons/eye-off.svg +21 -0
  20. graphica/assets/icons/eye.svg +20 -0
  21. graphica/assets/icons/file-plus.svg +22 -0
  22. graphica/assets/icons/folder-plus.svg +21 -0
  23. graphica/assets/icons/folder.svg +19 -0
  24. graphica/assets/icons/highlight.svg +22 -0
  25. graphica/assets/icons/history.svg +20 -0
  26. graphica/assets/icons/italic.svg +21 -0
  27. graphica/assets/icons/layout-grid.svg +22 -0
  28. graphica/assets/icons/math-function.svg +22 -0
  29. graphica/assets/icons/message-2.svg +21 -0
  30. graphica/assets/icons/mountain.svg +20 -0
  31. graphica/assets/icons/palette.svg +22 -0
  32. graphica/assets/icons/pointer.svg +19 -0
  33. graphica/assets/icons/refresh.svg +20 -0
  34. graphica/assets/icons/row-insert-bottom.svg +21 -0
  35. graphica/assets/icons/row-remove.svg +21 -0
  36. graphica/assets/icons/search.svg +20 -0
  37. graphica/assets/icons/select-all.svg +35 -0
  38. graphica/assets/icons/subscript.svg +20 -0
  39. graphica/assets/icons/superscript.svg +20 -0
  40. graphica/assets/icons/table.svg +21 -0
  41. graphica/assets/icons/trash.svg +23 -0
  42. graphica/assets/icons/typography.svg +23 -0
  43. graphica/assets/icons/x.svg +20 -0
  44. graphica/core/__init__.py +0 -0
  45. graphica/core/analysis.py +1278 -0
  46. graphica/core/app_paths.py +35 -0
  47. graphica/core/axis_settings.py +128 -0
  48. graphica/core/caption_export.py +47 -0
  49. graphica/core/color_palettes.py +56 -0
  50. graphica/core/commands.py +226 -0
  51. graphica/core/cvd_simulation.py +49 -0
  52. graphica/core/dataset.py +443 -0
  53. graphica/core/diagnostics.py +105 -0
  54. graphica/core/excel_utils.py +58 -0
  55. graphica/core/fit_models.py +379 -0
  56. graphica/core/grid_data.py +128 -0
  57. graphica/core/i18n.py +44 -0
  58. graphica/core/json_utils.py +27 -0
  59. graphica/core/label_utils.py +21 -0
  60. graphica/core/methods_text.py +115 -0
  61. graphica/core/named_colors.py +126 -0
  62. graphica/core/plugin_api.py +515 -0
  63. graphica/core/plugin_context.py +167 -0
  64. graphica/core/plugin_install.py +82 -0
  65. graphica/core/plugin_manifest.py +67 -0
  66. graphica/core/plugin_testing.py +213 -0
  67. graphica/core/plugin_types.py +118 -0
  68. graphica/core/provenance.py +19 -0
  69. graphica/core/report_export.py +51 -0
  70. graphica/core/safe_eval.py +194 -0
  71. graphica/core/script_export.py +272 -0
  72. graphica/core/translations_en.py +530 -0
  73. graphica/core/unit_conversion.py +51 -0
  74. graphica/core/update_check.py +43 -0
  75. graphica/core/version.py +14 -0
  76. graphica/gui/__init__.py +0 -0
  77. graphica/gui/app_settings.py +119 -0
  78. graphica/gui/axis_bindings.py +173 -0
  79. graphica/gui/binding.py +135 -0
  80. graphica/gui/builders/__init__.py +1 -0
  81. graphica/gui/builders/axis_panel.py +431 -0
  82. graphica/gui/builders/canvas_area.py +176 -0
  83. graphica/gui/builders/common.py +120 -0
  84. graphica/gui/builders/dataset_panel.py +433 -0
  85. graphica/gui/builders/property_sections.py +208 -0
  86. graphica/gui/canvas.py +519 -0
  87. graphica/gui/color_history.py +47 -0
  88. graphica/gui/color_picker_widget.py +198 -0
  89. graphica/gui/crash_handler.py +77 -0
  90. graphica/gui/cvd_preview.py +23 -0
  91. graphica/gui/data_editor.py +761 -0
  92. graphica/gui/data_import_flow.py +355 -0
  93. graphica/gui/dataset_bindings.py +107 -0
  94. graphica/gui/dataset_style_icon.py +80 -0
  95. graphica/gui/datasets/__init__.py +1 -0
  96. graphica/gui/datasets/actions_menu.py +114 -0
  97. graphica/gui/datasets/colors.py +218 -0
  98. graphica/gui/datasets/fitting.py +70 -0
  99. graphica/gui/datasets/host.py +162 -0
  100. graphica/gui/datasets/operations/__init__.py +1 -0
  101. graphica/gui/datasets/operations/fitting.py +468 -0
  102. graphica/gui/datasets/operations/peaks.py +109 -0
  103. graphica/gui/datasets/operations/processing.py +659 -0
  104. graphica/gui/datasets/operations/runner.py +148 -0
  105. graphica/gui/datasets/operations/transfer.py +227 -0
  106. graphica/gui/datasets/order.py +310 -0
  107. graphica/gui/datasets/overlays.py +74 -0
  108. graphica/gui/datasets/peaks.py +21 -0
  109. graphica/gui/datasets/plugin_runs.py +99 -0
  110. graphica/gui/datasets/processing.py +68 -0
  111. graphica/gui/datasets/property_panel.py +538 -0
  112. graphica/gui/datasets/transfer.py +36 -0
  113. graphica/gui/detached_canvas_window.py +22 -0
  114. graphica/gui/dialogs/__init__.py +137 -0
  115. graphica/gui/dialogs/analysis.py +1322 -0
  116. graphica/gui/dialogs/app.py +1015 -0
  117. graphica/gui/dialogs/appearance.py +851 -0
  118. graphica/gui/dialogs/data_edit.py +577 -0
  119. graphica/gui/dialogs/data_import.py +679 -0
  120. graphica/gui/dialogs/export.py +433 -0
  121. graphica/gui/dock_layout.py +176 -0
  122. graphica/gui/export_preview_panel.py +321 -0
  123. graphica/gui/export_settings.py +16 -0
  124. graphica/gui/file_association.py +97 -0
  125. graphica/gui/icon_utils.py +44 -0
  126. graphica/gui/main_app_window.py +210 -0
  127. graphica/gui/main_window.py +1151 -0
  128. graphica/gui/mathtext_preview.py +168 -0
  129. graphica/gui/menu_bar.py +320 -0
  130. graphica/gui/minimap_widget.py +151 -0
  131. graphica/gui/mixins/__init__.py +0 -0
  132. graphica/gui/mixins/export_mixin.py +520 -0
  133. graphica/gui/mixins/help_mixin.py +127 -0
  134. graphica/gui/mixins/project_io_mixin.py +289 -0
  135. graphica/gui/mixins/quick_access_mixin.py +194 -0
  136. graphica/gui/mixins/ui_setup_mixin.py +206 -0
  137. graphica/gui/notify.py +61 -0
  138. graphica/gui/panels/__init__.py +43 -0
  139. graphica/gui/panels/axis_settings.py +605 -0
  140. graphica/gui/panels/dataset_tree.py +424 -0
  141. graphica/gui/plot_type_drawers.py +131 -0
  142. graphica/gui/plugin_context.py +119 -0
  143. graphica/gui/project_files.py +361 -0
  144. graphica/gui/provenance_panel.py +65 -0
  145. graphica/gui/rendering/__init__.py +1 -0
  146. graphica/gui/rendering/annotations.py +177 -0
  147. graphica/gui/rendering/appearance.py +368 -0
  148. graphica/gui/rendering/common.py +316 -0
  149. graphica/gui/rendering/data_1d.py +376 -0
  150. graphica/gui/rendering/data_2d.py +66 -0
  151. graphica/gui/residual_panel.py +62 -0
  152. graphica/gui/resources.py +16 -0
  153. graphica/gui/single_instance.py +141 -0
  154. graphica/gui/splash.py +83 -0
  155. graphica/gui/task_runner.py +41 -0
  156. graphica/gui/theme.py +864 -0
  157. graphica/gui/tools/__init__.py +56 -0
  158. graphica/gui/tools/annotation.py +275 -0
  159. graphica/gui/tools/cursor.py +259 -0
  160. graphica/gui/tools/layout_edit.py +285 -0
  161. graphica/gui/tools/manager.py +78 -0
  162. graphica/gui/tools/peak_placement.py +132 -0
  163. graphica/gui/tools/range_select.py +191 -0
  164. graphica/gui/tools/region_highlight.py +222 -0
  165. graphica/gui/tools/slice_extraction.py +200 -0
  166. graphica/gui/widget_translation.py +46 -0
  167. graphica/gui/workers.py +280 -0
  168. graphica/models/__init__.py +0 -0
  169. graphica/models/project.py +197 -0
  170. graphica/plugin/__init__.py +20 -0
  171. graphica/plugin/testing.py +57 -0
  172. graphica/sample_data/__init__.py +0 -0
  173. graphica/sample_data/cooling_curve_sample.csv +42 -0
  174. graphica/ui_main_window.py +581 -0
  175. graphica_plot-2.0.0.dist-info/METADATA +317 -0
  176. graphica_plot-2.0.0.dist-info/RECORD +181 -0
  177. graphica_plot-2.0.0.dist-info/WHEEL +5 -0
  178. graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
  179. graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
  180. graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
  181. graphica_plot-2.0.0.dist-info/top_level.txt +1 -0
graphica/gui/theme.py ADDED
@@ -0,0 +1,864 @@
1
+ """アプリ全体のテーマ。Fusion スタイルの上でパレットを切り替え、QSS でフラットな見た目にする。"""
2
+ import os
3
+ import re
4
+ import tempfile
5
+
6
+ from PySide6.QtCore import QRectF, Qt
7
+ from PySide6.QtGui import QColor, QPainter, QPainterPath, QPalette, QPen, QPixmap
8
+ from PySide6.QtWidgets import (QAbstractSpinBox, QApplication, QComboBox,
9
+ QProxyStyle, QStyle, QStyleFactory)
10
+
11
+ from graphica.gui.icon_utils import icon as _svg_icon
12
+
13
+ _original_palette = None
14
+ _original_style_name = None
15
+ _wheel_value_change_disabled = False
16
+ _current_proxy_style = None
17
+ _current_tokens = None
18
+ _last_applied_dark = None # 同じ値での apply_theme() は重い処理を省く
19
+
20
+ # 選択の角丸を描くデリゲートが使う。QSS の QTreeWidget の border-radius と同じ値にしておくこと
21
+ DATASET_LIST_ITEM_RADIUS = 8
22
+
23
+ # スピンボックスの矢印は PNG にして QSS の ::up-arrow / ::down-arrow の image で指定する。
24
+ # 枠を QSS で描いていると、QProxyStyle で矢印を描いても反映されたりされなかったりする。
25
+ _ARROW_ICON_CACHE_DIR = os.path.join(tempfile.gettempdir(), "graphica_theme_icons")
26
+
27
+
28
+ def _spinbox_arrow_icon_url(direction: str, color: str) -> str:
29
+ """三角の矢印の PNG を(無ければ)作り、QSS の url() に入れられるパスを返す。"""
30
+ os.makedirs(_ARROW_ICON_CACHE_DIR, exist_ok=True)
31
+ safe_color = color.lstrip("#")
32
+ # 大きさをファイル名に入れ、寸法を変えたとき古い PNG を使い回さない
33
+ size = 14
34
+ filename = f"spin_arrow_{direction}_{safe_color}_{size}.png"
35
+ path = os.path.join(_ARROW_ICON_CACHE_DIR, filename)
36
+
37
+ if not os.path.exists(path):
38
+ pixmap = QPixmap(size, size)
39
+ pixmap.fill(Qt.GlobalColor.transparent)
40
+ painter = QPainter(pixmap)
41
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
42
+ painter.setPen(Qt.PenStyle.NoPen)
43
+ painter.setBrush(QColor(color))
44
+
45
+ cx, cy = size / 2, size / 2
46
+ aw, ah = 4.4, 3.3
47
+ arrow_path = QPainterPath()
48
+ if direction == "up":
49
+ arrow_path.moveTo(cx - aw, cy + ah * 0.5)
50
+ arrow_path.lineTo(cx + aw, cy + ah * 0.5)
51
+ arrow_path.lineTo(cx, cy - ah * 0.5)
52
+ else:
53
+ arrow_path.moveTo(cx - aw, cy - ah * 0.5)
54
+ arrow_path.lineTo(cx + aw, cy - ah * 0.5)
55
+ arrow_path.lineTo(cx, cy + ah * 0.5)
56
+ arrow_path.closeSubpath()
57
+ painter.drawPath(arrow_path)
58
+ painter.end()
59
+ pixmap.save(path)
60
+
61
+ # QSS の url() は Windows の区切りの \ を読めない
62
+ return path.replace(os.sep, "/")
63
+
64
+ # 配色の唯一の定義。これを上書きする利用者の設定は無い(配色パレットはデータの線の色で、UI とは別)。
65
+ LIGHT_TOKENS = {
66
+ "bg": "#F6F7F9", "surface": "#FFFFFF", "surface_2": "#EEF0F3",
67
+ "border": "#DFE2E1", "border_strong": "#C9CDCB",
68
+ "text_primary": "#1B1F1E", "text_secondary": "#5B6462", "text_muted": "#8B938F",
69
+ "accent": "#1F6F78", "accent_soft": "#E4F0EF", "accent_text": "#FFFFFF",
70
+ # データセット一覧の選択だけに使う半透明の青(アクセントのティールとは別)
71
+ "selection_highlight": "rgba(37, 99, 235, 0.12)",
72
+ # selection_highlight と同じ色の不透明版(枠やチェックボックスの塗りには透明だと困る)
73
+ "selection_accent": "#2563EB",
74
+ # 欠損値のセル。選択の青とも、アクセントとも被らない
75
+ "warning_soft": "#FDF1D8",
76
+ }
77
+ DARK_TOKENS = {
78
+ "bg": "#14171A", "surface": "#1B1F22", "surface_2": "#21262A",
79
+ "border": "#2C3236", "border_strong": "#3A4147",
80
+ "text_primary": "#EDEFEF", "text_secondary": "#A6AEB2", "text_muted": "#6E777B",
81
+ "accent": "#5FB6BE", "accent_soft": "rgba(95, 182, 190, 0.16)", "accent_text": "#0E1113",
82
+ "selection_highlight": "rgba(59, 130, 246, 0.22)",
83
+ "selection_accent": "#3B82F6",
84
+ "warning_soft": "rgba(250, 204, 21, 0.16)",
85
+ }
86
+
87
+ _FLAT_QSS_TEMPLATE = """
88
+ QMainWindow, QDialog {{
89
+ background: {bg};
90
+ }}
91
+ QWidget {{
92
+ color: {text_primary};
93
+ /* 操作の強調(選択・ホバー・押下・フォーカス)は青の selection_highlight / selection_accent にそろえる。
94
+ ティールの accent は操作の強調ではないもの(QProgressBar::chunk など)だけ。 */
95
+ selection-background-color: {selection_highlight};
96
+ selection-color: {text_primary};
97
+ }}
98
+ QToolTip {{
99
+ background: {surface};
100
+ color: {text_primary};
101
+ border: 1px solid {border_strong};
102
+ padding: 4px 6px;
103
+ border-radius: 4px;
104
+ }}
105
+
106
+ /* --- メニューバー / メニュー --- */
107
+ QMenuBar {{
108
+ background: {surface};
109
+ border-bottom: 1px solid {border};
110
+ padding: 2px;
111
+ }}
112
+ QMenuBar::item {{
113
+ background: transparent;
114
+ padding: 4px 10px;
115
+ border-radius: 4px;
116
+ }}
117
+ QMenuBar::item:selected {{
118
+ background: {selection_highlight};
119
+ color: {text_primary};
120
+ }}
121
+ QMenu {{
122
+ background: {surface};
123
+ border: 1px solid {border};
124
+ border-radius: 8px;
125
+ padding: 4px;
126
+ }}
127
+ QMenu::item {{
128
+ padding: 6px 24px 6px 12px;
129
+ border-radius: 6px;
130
+ }}
131
+ QMenu::item:selected {{
132
+ background: {selection_highlight};
133
+ color: {text_primary};
134
+ }}
135
+ QMenu::separator {{
136
+ height: 1px;
137
+ background: {border};
138
+ margin: 4px 8px;
139
+ }}
140
+
141
+ /* --- 操作ボタン行のグループ区切り --- */
142
+ QFrame#button_row_separator {{
143
+ background: {border};
144
+ max-width: 1px;
145
+ border: none;
146
+ margin: 2px 4px;
147
+ }}
148
+
149
+ /* --- データセット一覧の下の短い統計値 --- */
150
+ QLabel#dataset_mini_stats_label {{
151
+ color: {text_muted};
152
+ font-size: 11.5px;
153
+ padding: 2px 4px;
154
+ }}
155
+
156
+ /* --- キャンバスの周り: 角丸の背景のカード。枠線は付けない --- */
157
+ QWidget#plot_container {{
158
+ background: {surface};
159
+ border-radius: 8px;
160
+ }}
161
+ QFrame#canvas_separator {{
162
+ background: {border};
163
+ max-height: 1px;
164
+ border: none;
165
+ }}
166
+
167
+ /* --- ツールバー / ステータスバー / ドック --- */
168
+ QToolBar {{
169
+ background: {surface};
170
+ border: none;
171
+ border-bottom: 1px solid {border};
172
+ spacing: 4px;
173
+ padding: 4px;
174
+ }}
175
+ QToolButton {{
176
+ background: transparent;
177
+ border: 1px solid transparent;
178
+ border-radius: 6px;
179
+ padding: 4px;
180
+ }}
181
+ QToolButton:hover {{
182
+ background: {surface_2};
183
+ }}
184
+ /* ツールバーのボタンは QToolButton なので、QPushButton とは別にホバーと押下を青にする。 */
185
+ QToolButton:pressed {{
186
+ background: {selection_highlight};
187
+ }}
188
+ QToolButton:checked {{
189
+ background: {selection_highlight};
190
+ border: 1px solid {selection_accent};
191
+ }}
192
+ QStatusBar {{
193
+ background: {surface};
194
+ border-top: 1px solid {border};
195
+ }}
196
+ /* QScrollArea は既定の枠を消す。背景は QScrollArea・ビューポート・中身のウィジェットの3段すべてに指定する
197
+ (アプリ全体に QSS を当てると素の QWidget もパレットの色で塗られ、ビューポートだけでは中身に覆われて効かない)。 */
198
+ QScrollArea {{
199
+ border: none;
200
+ background: {bg};
201
+ }}
202
+ QScrollArea > QWidget {{
203
+ background: {bg};
204
+ }}
205
+ QScrollArea > QWidget > QWidget {{
206
+ background: {bg};
207
+ }}
208
+ /* --- ドック: 枠と角丸で1枚のカードに見せる(積んだドックの間はスプリッターの隙間があるので二重線にならない) --- */
209
+ QDockWidget {{
210
+ /* 指定しないと OS のパレットの色が透け、背景のトークンが効かない */
211
+ background: {bg};
212
+ border: 1px solid {border};
213
+ border-radius: 8px;
214
+ }}
215
+ QDockWidget::title {{
216
+ background: {surface_2};
217
+ padding: 6px 8px;
218
+ border-bottom: 1px solid {border};
219
+ border-top-left-radius: 8px;
220
+ border-top-right-radius: 8px;
221
+ font-weight: 600;
222
+ letter-spacing: .01em;
223
+ }}
224
+ /* フォーカスのあるドックの枠。dockActive は install_dock_focus_highlight() が付け外しする。 */
225
+ QDockWidget[dockActive="true"] {{
226
+ border: 1px solid {selection_accent};
227
+ }}
228
+ QDockWidget[dockActive="true"]::title {{
229
+ border-bottom: 1px solid {selection_accent};
230
+ }}
231
+
232
+ /* --- スプリッター: 細く控えめにし、ホバーでだけ色を付ける --- */
233
+ QSplitter::handle {{
234
+ background: {border};
235
+ }}
236
+ QSplitter::handle:horizontal {{
237
+ width: 3px;
238
+ margin: 2px 0;
239
+ }}
240
+ QSplitter::handle:vertical {{
241
+ height: 3px;
242
+ margin: 0 2px;
243
+ }}
244
+ QSplitter::handle:hover {{
245
+ background: {selection_accent};
246
+ }}
247
+
248
+ /* --- グループボックス / ボタン --- */
249
+ QGroupBox {{
250
+ background: transparent;
251
+ border: 1px solid {border};
252
+ border-radius: 8px;
253
+ margin-top: 20px;
254
+ padding-top: 14px;
255
+ font-weight: 500;
256
+ }}
257
+ QGroupBox::title {{
258
+ subcontrol-origin: margin;
259
+ left: 10px;
260
+ /* 枠の外にはみ出さない位置にする。はみ出すと、QFontDialog などの Qt 標準のダイアログでは上の余白が無く見切れる。 */
261
+ top: 0px;
262
+ padding: 2px 8px;
263
+ color: {selection_accent};
264
+ font-size: 12.5px;
265
+ font-weight: 600;
266
+ letter-spacing: .02em;
267
+ background: {selection_highlight};
268
+ border-radius: 5px;
269
+ }}
270
+
271
+ /* --- ドックの中のグループボックスは枠とチップを付けない(カードの中に箱が入れ子になって窮屈に見える) --- */
272
+ QDockWidget QGroupBox {{
273
+ border: none;
274
+ border-radius: 0;
275
+ margin-top: 18px;
276
+ padding-top: 6px;
277
+ }}
278
+ /* 折りたたみの中身のグループボックス(collapsibleBody)はタイトルを外のボタンに移したので、
279
+ タイトル用の上の余白(margin-top と padding-top で 24px)を 0 にする。 */
280
+ QDockWidget QGroupBox[collapsibleBody="true"] {{
281
+ margin-top: 0px;
282
+ padding-top: 0px;
283
+ }}
284
+ QDockWidget QGroupBox::title {{
285
+ left: 0;
286
+ /* 上のモーダル側と同じく、枠の外にはみ出さない位置にする */
287
+ top: 0px;
288
+ padding: 0;
289
+ color: {text_secondary};
290
+ background: transparent;
291
+ border-radius: 0;
292
+ }}
293
+
294
+ /* --- 開閉できる見出し(データセットのプロパティ / プロットのプロパティ): 横いっぱい・左寄せ・太字 --- */
295
+ QToolButton#collapsible_section_toggle {{
296
+ background: transparent;
297
+ border: none;
298
+ border-radius: 6px;
299
+ padding: 6px 4px;
300
+ text-align: left;
301
+ font-weight: 600;
302
+ /* フォームのラベルは既定の 9pt(約 12px)で描かれるので、見出しはそれより大きくする(見出し > 節の見出し > 項目) */
303
+ font-size: {heading_pt}pt;
304
+ color: {text_secondary};
305
+ }}
306
+ QToolButton#collapsible_section_toggle:hover {{
307
+ background: {surface_2};
308
+ color: {text_primary};
309
+ }}
310
+ QToolButton#collapsible_section_toggle:pressed {{
311
+ background: {selection_highlight};
312
+ }}
313
+
314
+ /* --- プロパティ欄の節の見出し: 本文より少し大きく・太く・濃くし、中身を字下げする。
315
+ 区切りは細い罫線1本。1本目は親の見出しのすぐ下で二重線に見えるので引かない。 --- */
316
+ QToolButton#property_subsection_toggle {{
317
+ background: transparent;
318
+ border: none;
319
+ border-top: 1px solid {border};
320
+ border-radius: 0;
321
+ /* 親の見出し(padding-left: 4px)より内側に入れて、親 → 子 → 中身の階段にする。
322
+ 罫線はパディングを含む全幅に引かれるので、字下げしても横いっぱいのまま。 */
323
+ padding: 6px 2px 4px 12px;
324
+ text-align: left;
325
+ font-weight: 600;
326
+ font-size: {subheading_pt}pt;
327
+ color: {text_primary};
328
+ }}
329
+ QToolButton#property_subsection_toggle[firstSection="true"] {{
330
+ border-top: none;
331
+ padding-top: 2px;
332
+ }}
333
+ QToolButton#property_subsection_toggle:hover {{
334
+ background: {surface_2};
335
+ /* accent_text はライトでは白なので、薄い背景のホバーでは消える。青の文字で強調する */
336
+ color: {selection_accent};
337
+ }}
338
+ QToolButton#property_subsection_toggle:pressed {{
339
+ background: {selection_highlight};
340
+ }}
341
+ QPushButton {{
342
+ background: {surface};
343
+ border: 1px solid {border_strong};
344
+ border-radius: 6px;
345
+ padding: 5px 14px;
346
+ }}
347
+ QPushButton:hover {{
348
+ background: {surface_2};
349
+ border-color: {selection_accent};
350
+ }}
351
+ QPushButton:pressed {{
352
+ background: {selection_highlight};
353
+ }}
354
+ QPushButton:disabled {{
355
+ color: {text_muted};
356
+ border-color: {border};
357
+ }}
358
+ QPushButton:default {{
359
+ /* 既定のボタン(Enter で実行)もほかの強調と同じ青にする。文字色は accent_text(どちらのモードでも読める)。 */
360
+ background: {selection_accent};
361
+ border-color: {selection_accent};
362
+ color: {accent_text};
363
+ }}
364
+ /* フォーカスの枠はほかの選択・フォーカスと同じ青。ボタンの通常・ホバー・押下の色はそれぞれの規則で決める。 */
365
+ QPushButton:focus, QToolButton:focus {{
366
+ border: 1px solid {selection_accent};
367
+ }}
368
+
369
+ /* --- アイコンだけの正方形のボタン(大きさは Python 側の setFixedSize) --- */
370
+ QPushButton[iconOnly="true"] {{
371
+ padding: 4px;
372
+ }}
373
+
374
+ /* --- 入力欄 --- */
375
+ QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QTextEdit, QPlainTextEdit {{
376
+ background: {surface};
377
+ border: 1px solid {border};
378
+ border-radius: 6px;
379
+ padding: 4px 8px;
380
+ selection-background-color: {selection_highlight};
381
+ }}
382
+ QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus,
383
+ QComboBox:focus, QTextEdit:focus, QPlainTextEdit:focus {{
384
+ border-color: {selection_accent};
385
+ }}
386
+ QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QComboBox:disabled {{
387
+ color: {text_muted};
388
+ background: {surface_2};
389
+ }}
390
+
391
+ /* --- タイトルと軸ラベルのプレビュー: 入力欄と同じ見た目にし、ホバーで枠を青くしてクリックできることを示す --- */
392
+ QLabel#mathtext_preview_label {{
393
+ background: {surface};
394
+ border: 1px solid {border};
395
+ border-radius: 6px;
396
+ padding: 4px 8px;
397
+ }}
398
+ QLabel#mathtext_preview_label:hover {{
399
+ border-color: {selection_accent};
400
+ }}
401
+
402
+ /* --- スピンボックスの上下ボタン: 矢印は width / height を指定するだけで描かれなくなるので、
403
+ 画像(image: url(...))で与える。 --- */
404
+ QSpinBox, QDoubleSpinBox {{
405
+ padding-right: 20px;
406
+ }}
407
+ /* 上下のボタンはそれぞれ独立した小さな角丸にして枠から少し離し、ふだんは透明にして矢印だけを見せる
408
+ (ホバーと押下のときだけ背景を出す)。 */
409
+ QSpinBox::up-button, QDoubleSpinBox::up-button {{
410
+ subcontrol-origin: border;
411
+ subcontrol-position: top right;
412
+ width: 16px;
413
+ height: 13px;
414
+ margin: 1px 2px 1px 1px;
415
+ border: 1px solid transparent;
416
+ border-radius: 4px;
417
+ background: transparent;
418
+ }}
419
+ QSpinBox::down-button, QDoubleSpinBox::down-button {{
420
+ subcontrol-origin: border;
421
+ subcontrol-position: bottom right;
422
+ width: 16px;
423
+ height: 13px;
424
+ margin: 1px 2px 1px 1px;
425
+ border: 1px solid transparent;
426
+ border-radius: 4px;
427
+ background: transparent;
428
+ }}
429
+ QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover,
430
+ QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover {{
431
+ background: {border_strong};
432
+ }}
433
+ QSpinBox::up-button:pressed, QDoubleSpinBox::up-button:pressed,
434
+ QSpinBox::down-button:pressed, QDoubleSpinBox::down-button:pressed {{
435
+ background: {selection_highlight};
436
+ }}
437
+ QSpinBox::up-arrow, QDoubleSpinBox::up-arrow {{
438
+ image: url({spin_up_arrow_url});
439
+ width: 12px;
440
+ height: 12px;
441
+ }}
442
+ QSpinBox::down-arrow, QDoubleSpinBox::down-arrow {{
443
+ image: url({spin_down_arrow_url});
444
+ width: 12px;
445
+ height: 12px;
446
+ }}
447
+ QComboBox::drop-down {{
448
+ border: none;
449
+ width: 20px;
450
+ }}
451
+ QComboBox::down-arrow {{
452
+ /* スピンボックスと同じ理由で画像にし、大きさもそろえる */
453
+ image: url({spin_down_arrow_url});
454
+ width: 12px;
455
+ height: 12px;
456
+ }}
457
+ QComboBox QAbstractItemView {{
458
+ background: {surface};
459
+ border: 1px solid {border};
460
+ selection-background-color: {selection_highlight};
461
+ selection-color: {text_primary};
462
+ outline: none;
463
+ }}
464
+
465
+ /* --- リスト / ツリー / テーブル --- */
466
+ QTreeWidget, QListWidget, QTableWidget {{
467
+ background: {surface};
468
+ border: 1px solid {border};
469
+ border-radius: 8px;
470
+ alternate-background-color: {surface_2};
471
+ }}
472
+ QHeaderView::section {{
473
+ background: {surface_2};
474
+ color: {text_secondary};
475
+ border: none;
476
+ border-bottom: 1px solid {border};
477
+ padding: 4px 6px;
478
+ }}
479
+ QTreeWidget::item, QListWidget::item, QTableWidget::item {{
480
+ padding: 3px;
481
+ }}
482
+ QTreeWidget::item:selected, QListWidget::item:selected, QTableWidget::item:selected {{
483
+ background: {selection_highlight};
484
+ color: {text_primary};
485
+ }}
486
+ QTreeWidget::item, QListWidget::item {{
487
+ padding: 5px 4px;
488
+ border-radius: 5px;
489
+ }}
490
+
491
+ /* --- データセット一覧の選択: 背景は _DatasetTreeSelectionDelegate が1つの角丸で描く
492
+ (QSS の ::item:selected はアイコンの列と文字の列に別々の角丸を描き、隙間ができる)。
493
+ 展開矢印の字下げの列は delegate を通らない drawBranches() で描かれ、汎用の ::item:selected が
494
+ にじみ出るので、この一覧では透明にして打ち消す。 --- */
495
+ QTreeWidget#dataset_list_widget {{
496
+ selection-background-color: transparent;
497
+ selection-color: {text_primary};
498
+ /* 無いと、選択とフォーカスが重なったとき角丸の上に点線の四角が描かれる */
499
+ outline: none;
500
+ /* 枠線だけ消し、背景と角丸は残す */
501
+ border: none;
502
+ }}
503
+ /* 汎用の ::item:selected が字下げの列ににじむのを、この一覧だけ打ち消す(上の説明を参照)。 */
504
+ QTreeWidget#dataset_list_widget::item:selected {{
505
+ background: transparent;
506
+ }}
507
+
508
+ /* --- データセットの検索欄の枠線を消す --- */
509
+ QLineEdit#dataset_search_edit {{
510
+ border: none;
511
+ }}
512
+
513
+ /* --- タブ --- */
514
+ QTabWidget::pane {{
515
+ border: 1px solid {border};
516
+ border-radius: 8px;
517
+ }}
518
+ QTabBar::tab {{
519
+ background: transparent;
520
+ color: {text_secondary};
521
+ padding: 6px 14px;
522
+ margin-right: 2px;
523
+ border-top-left-radius: 6px;
524
+ border-top-right-radius: 6px;
525
+ border-bottom: 2px solid transparent;
526
+ }}
527
+ QTabBar::tab:hover {{
528
+ background: {surface_2};
529
+ color: {text_primary};
530
+ }}
531
+ QTabBar::tab:selected {{
532
+ background: {surface};
533
+ border-bottom: 2px solid {selection_accent};
534
+ color: {selection_accent};
535
+ font-weight: 600;
536
+ }}
537
+ /* QTabBar::close-button には何も指定しない。何か1つでも指定するとアイコンが描かれなくなる
538
+ (QProxyStyle で差し替えても効かない)。見た目は _FlatThemeProxyStyle で決める。 */
539
+
540
+ QToolButton#add_tab_button {{
541
+ margin: 3px 6px 3px 2px;
542
+ border-radius: 13px;
543
+ padding: 5px;
544
+ }}
545
+ QToolButton#add_tab_button:hover {{
546
+ background: {selection_highlight};
547
+ }}
548
+
549
+ /* --- チェックボックスとラジオボタン: QCheckBox::indicator には何も指定しない。何か1つでも指定すると
550
+ チェックマークが描かれず塗りつぶしの四角になる。見た目は _FlatThemeProxyStyle が描く。 --- */
551
+ QRadioButton::indicator {{
552
+ width: 14px;
553
+ height: 14px;
554
+ border: 1px solid {border_strong};
555
+ border-radius: 7px;
556
+ background: {surface};
557
+ }}
558
+ QRadioButton::indicator:checked {{
559
+ /* チェックボックスの塗りと同じ青 */
560
+ background: {selection_accent};
561
+ border-color: {selection_accent};
562
+ }}
563
+
564
+ /* --- プログレスバー --- */
565
+ QProgressBar {{
566
+ border: 1px solid {border};
567
+ border-radius: 6px;
568
+ text-align: center;
569
+ background: {surface_2};
570
+ }}
571
+ QProgressBar::chunk {{
572
+ background: {accent};
573
+ border-radius: 6px;
574
+ }}
575
+
576
+ /* --- スクロールバー --- */
577
+ QScrollBar:vertical {{
578
+ background: transparent;
579
+ width: 11px;
580
+ margin: 0;
581
+ }}
582
+ QScrollBar::handle:vertical {{
583
+ background: {border_strong};
584
+ border-radius: 5px;
585
+ min-height: 24px;
586
+ }}
587
+ QScrollBar::handle:vertical:hover, QScrollBar::handle:vertical:pressed {{
588
+ /* つかめる場所を示すだけの部品なので、青ではなく濃いグレー(text_muted)で強調する。
589
+ ドラッグ中も :hover が当たり続けるので、:pressed もそろえる。 */
590
+ background: {text_muted};
591
+ }}
592
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
593
+ height: 0;
594
+ }}
595
+ QScrollBar:horizontal {{
596
+ background: transparent;
597
+ height: 11px;
598
+ margin: 0;
599
+ }}
600
+ QScrollBar::handle:horizontal {{
601
+ background: {border_strong};
602
+ border-radius: 5px;
603
+ min-width: 24px;
604
+ }}
605
+ QScrollBar::handle:horizontal:hover, QScrollBar::handle:horizontal:pressed {{
606
+ background: {text_muted};
607
+ }}
608
+ QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
609
+ width: 0;
610
+ }}
611
+ """
612
+
613
+
614
+ # 見出しの文字サイズはアプリ既定のフォントからの相対にする。px で固定すると、OS 既定の大きさ
615
+ # (Windows 9pt、macOS 13pt 前後)によって見出しが本文より小さくなる。
616
+ HEADING_POINT_OFFSET = 2.0 # 上の2つの開閉見出し
617
+ SUBHEADING_POINT_OFFSET = 1.0 # プロパティ欄の節の見出し
618
+ _FALLBACK_BASE_POINT_SIZE = 9.0
619
+
620
+
621
+ def heading_point_sizes():
622
+ """(上の見出し, 節の見出し) の pt。"""
623
+ app = QApplication.instance()
624
+ base = app.font().pointSizeF() if app is not None else _FALLBACK_BASE_POINT_SIZE
625
+ if base <= 0:
626
+ # ピクセル指定のフォントだと pointSizeF() は -1
627
+ base = _FALLBACK_BASE_POINT_SIZE
628
+ return base + HEADING_POINT_OFFSET, base + SUBHEADING_POINT_OFFSET
629
+
630
+
631
+ def build_qss(tokens: dict) -> str:
632
+ """_FLAT_QSS_TEMPLATE の {token} を埋めた QSS。矢印の画像は text_primary の色で作る。"""
633
+ format_args = dict(tokens)
634
+ format_args["spin_up_arrow_url"] = _spinbox_arrow_icon_url("up", tokens["text_primary"])
635
+ format_args["spin_down_arrow_url"] = _spinbox_arrow_icon_url("down", tokens["text_primary"])
636
+ heading_pt, subheading_pt = heading_point_sizes()
637
+ format_args["heading_pt"] = f"{heading_pt:.1f}"
638
+ format_args["subheading_pt"] = f"{subheading_pt:.1f}"
639
+ return _FLAT_QSS_TEMPLATE.format(**format_args)
640
+
641
+
642
+ def _build_dark_palette() -> QPalette:
643
+ palette = QPalette()
644
+ palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
645
+ palette.setColor(QPalette.ColorRole.WindowText, QColor(220, 220, 220))
646
+ palette.setColor(QPalette.ColorRole.Base, QColor(35, 35, 35))
647
+ palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
648
+ palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(220, 220, 220))
649
+ palette.setColor(QPalette.ColorRole.ToolTipText, QColor(220, 220, 220))
650
+ palette.setColor(QPalette.ColorRole.Text, QColor(220, 220, 220))
651
+ palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
652
+ palette.setColor(QPalette.ColorRole.ButtonText, QColor(220, 220, 220))
653
+ palette.setColor(QPalette.ColorRole.BrightText, QColor(255, 80, 80))
654
+ palette.setColor(QPalette.ColorRole.Link, QColor(90, 160, 230))
655
+ palette.setColor(QPalette.ColorRole.Highlight, QColor(60, 120, 200))
656
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor(255, 255, 255))
657
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(127, 127, 127))
658
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(127, 127, 127))
659
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor(127, 127, 127))
660
+ return palette
661
+
662
+
663
+ class _FlatThemeProxyStyle(QProxyStyle):
664
+ """QSS では描けないもの(QSS を当てると中身が消えるもの)をここで描く。
665
+
666
+ QTabBar::close-button に QSS を当てると × が消え、QCheckBox::indicator に当てるとチェックマークが
667
+ 描かれない。どちらも QSS は当てず、ここでアイコンを差し替え、チェックボックスを自前で描く。
668
+ """
669
+ def __init__(self, base_style, tokens):
670
+ super().__init__(base_style)
671
+ self.update_tokens(tokens)
672
+
673
+ def update_tokens(self, tokens):
674
+ """色だけ替える(作り直して setStyle() し直すとクラッシュする。apply_theme() を参照)。"""
675
+ self._tokens = tokens
676
+ self._close_icon = _svg_icon("x", color=tokens["text_secondary"], size=14)
677
+ self._close_pixmap = self._close_icon.pixmap(14, 14)
678
+
679
+ def standardIcon(self, standard_icon, option=None, widget=None):
680
+ if standard_icon == QStyle.StandardPixmap.SP_TabCloseButton:
681
+ return self._close_icon
682
+ return super().standardIcon(standard_icon, option, widget)
683
+
684
+ def standardPixmap(self, standard_pixmap, option=None, widget=None):
685
+ # タブの閉じるボタンは standardIcon() ではなく standardPixmap() でアイコンを取る
686
+ if standard_pixmap == QStyle.StandardPixmap.SP_TabCloseButton:
687
+ return self._close_pixmap
688
+ return super().standardPixmap(standard_pixmap, option, widget)
689
+
690
+ def drawPrimitive(self, element, option, painter, widget=None):
691
+ if element == QStyle.PrimitiveElement.PE_IndicatorCheckBox:
692
+ self._draw_checkbox_indicator(option, painter)
693
+ return
694
+ if element == QStyle.PrimitiveElement.PE_FrameTabBarBase:
695
+ # タブにしたドックの上に出る灰色の線(Fusion が描く、QSS では消せない)を描かない
696
+ return
697
+ super().drawPrimitive(element, option, painter, widget)
698
+
699
+ def _draw_checkbox_indicator(self, option, painter):
700
+ tokens = self._tokens
701
+ checked = bool(option.state & QStyle.StateFlag.State_On)
702
+ tristate = bool(option.state & QStyle.StateFlag.State_NoChange)
703
+ enabled = bool(option.state & QStyle.StateFlag.State_Enabled)
704
+
705
+ if not enabled:
706
+ fill = QColor(tokens["surface_2"])
707
+ border = QColor(tokens["border"])
708
+ elif checked or tristate:
709
+ fill = QColor(tokens["selection_accent"])
710
+ border = fill
711
+ else:
712
+ fill = QColor(tokens["surface"])
713
+ border = QColor(tokens["border_strong"])
714
+
715
+ painter.save()
716
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
717
+
718
+ pen = QPen(border)
719
+ pen.setWidthF(1.2)
720
+ painter.setPen(pen)
721
+ painter.setBrush(fill)
722
+ rect = QRectF(option.rect).adjusted(0.75, 0.75, -0.75, -0.75)
723
+ painter.drawRoundedRect(rect, 3, 3)
724
+
725
+ if checked or tristate:
726
+ check_color = QColor(tokens["accent_text"]) if enabled else QColor(tokens["text_muted"])
727
+ check_pen = QPen(check_color)
728
+ check_pen.setWidthF(1.6)
729
+ check_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
730
+ check_pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
731
+ painter.setPen(check_pen)
732
+ painter.setBrush(Qt.BrushStyle.NoBrush)
733
+
734
+ r = option.rect
735
+ path = QPainterPath()
736
+ if tristate:
737
+ # 一部だけ選ばれている: 横棒
738
+ path.moveTo(r.x() + r.width() * 0.22, r.y() + r.height() * 0.5)
739
+ path.lineTo(r.x() + r.width() * 0.78, r.y() + r.height() * 0.5)
740
+ else:
741
+ path.moveTo(r.x() + r.width() * 0.20, r.y() + r.height() * 0.52)
742
+ path.lineTo(r.x() + r.width() * 0.42, r.y() + r.height() * 0.74)
743
+ path.lineTo(r.x() + r.width() * 0.82, r.y() + r.height() * 0.26)
744
+ painter.drawPath(path)
745
+
746
+ painter.restore()
747
+
748
+
749
+ def apply_theme(app, dark: bool):
750
+ """ライトでもダークでも Fusion を使い、パレットだけ替える(スタイルを替えるとボタンの大きさや余白まで変わる)。"""
751
+ global _original_palette, _original_style_name, _current_proxy_style, _current_tokens
752
+ global _last_applied_dark
753
+ if _original_palette is None:
754
+ _original_palette = QPalette(app.palette())
755
+ _original_style_name = app.style().objectName()
756
+
757
+ tokens = DARK_TOKENS if dark else LIGHT_TOKENS
758
+ _current_tokens = tokens
759
+
760
+ # 同じ値で何度も呼ばれる(タブごとなど)。setPalette() と setStyleSheet() は1回約130ms かかるので省く
761
+ if _current_proxy_style is not None and _last_applied_dark == dark:
762
+ return
763
+ _last_applied_dark = dark
764
+
765
+ # スタイルは QApplication に1つだけ作り、setStyle() も1回だけ。setStyle() は古いスタイルを削除するので、
766
+ # 差し替えるとほかのタブのウィジェットが削除済みのスタイルを触ってプロセスごと落ちる
767
+ if _current_proxy_style is None:
768
+ base_style = QStyleFactory.create('Fusion')
769
+ _current_proxy_style = _FlatThemeProxyStyle(base_style, tokens)
770
+ app.setStyle(_current_proxy_style)
771
+ else:
772
+ _current_proxy_style.update_tokens(tokens)
773
+ if dark:
774
+ app.setPalette(_build_dark_palette())
775
+ else:
776
+ app.setPalette(_original_palette)
777
+
778
+ app.setStyleSheet(build_qss(tokens))
779
+
780
+
781
+ def current_tokens() -> dict:
782
+ """今のテーマの配色トークン。apply_theme() の前なら LIGHT_TOKENS。"""
783
+ return _current_tokens or LIGHT_TOKENS
784
+
785
+
786
+ def current_selection_highlight_qcolor() -> QColor:
787
+ """selection_highlight を QColor で返す。
788
+
789
+ トークンは QSS 用の "rgba(r, g, b, a)" で、QColor(str) はこれを読めず不透明の黒になるので自前で解釈する。
790
+ """
791
+ tokens = _current_tokens or LIGHT_TOKENS
792
+ raw = tokens["selection_highlight"]
793
+ match = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)", raw)
794
+ if not match:
795
+ return QColor(raw)
796
+ r, g, b, a = match.groups()
797
+ color = QColor(int(r), int(g), int(b))
798
+ color.setAlphaF(float(a))
799
+ return color
800
+
801
+
802
+ def install_dock_focus_highlight(window):
803
+ """フォーカスのあるドックの枠を強調する(dockActive プロパティを QSS で描く)。
804
+
805
+ focusChanged はプロセス全体で1つなので、ほかのタブのドックにフォーカスが移ったら、このウィンドウの強調を消すだけにする。
806
+ 後から足したドック(プラグインのパネル)も、祖先を辿るので登録しなくてよい。
807
+ """
808
+ from PySide6.QtWidgets import QApplication, QDockWidget
809
+
810
+ def _dock_ancestor(widget):
811
+ while widget is not None and not isinstance(widget, QDockWidget):
812
+ widget = widget.parentWidget()
813
+ return widget
814
+
815
+ def _set_active(dock, active):
816
+ if dock is None:
817
+ return
818
+ dock.setProperty("dockActive", active)
819
+ style = dock.style()
820
+ style.unpolish(dock)
821
+ style.polish(dock)
822
+
823
+ state = {"active": None}
824
+
825
+ def _on_focus_changed(old, new):
826
+ dock = _dock_ancestor(new) if new is not None else None
827
+ if dock is not None and dock.window() is not window:
828
+ dock = None
829
+ if dock is state["active"]:
830
+ return
831
+ _set_active(state["active"], False)
832
+ _set_active(dock, True)
833
+ state["active"] = dock
834
+
835
+ app = QApplication.instance()
836
+ app.focusChanged.connect(_on_focus_changed)
837
+ # 窓が破棄された後に接続が残らないように
838
+ window.destroyed.connect(lambda: app.focusChanged.disconnect(_on_focus_changed))
839
+
840
+
841
+ def apply_form_spacing(widget, spacing=12):
842
+ """フォームの行の間隔を spacing まで広げる(既に広いものは縮めない)。"""
843
+ from PySide6.QtWidgets import QFormLayout
844
+ for form_layout in widget.findChildren(QFormLayout):
845
+ if form_layout.verticalSpacing() < spacing:
846
+ form_layout.setVerticalSpacing(spacing)
847
+
848
+
849
+ def disable_scroll_value_change():
850
+ """スピンボックスとコンボでホイールによる値の変更を止める(スクロールしただけで値が変わる)。
851
+
852
+ フォーカスの有無では判定できない(フォーカスはカーソルが離れても残る)ので常に止める。無視したイベントは
853
+ 親に渡るので、スクロールはそのままできる。クラスのメソッドを書き換えるので、起動時に1回呼ぶ。
854
+ """
855
+ global _wheel_value_change_disabled
856
+ if _wheel_value_change_disabled:
857
+ return
858
+ _wheel_value_change_disabled = True
859
+
860
+ def _ignore_wheel_event(self, event):
861
+ event.ignore()
862
+
863
+ QAbstractSpinBox.wheelEvent = _ignore_wheel_event
864
+ QComboBox.wheelEvent = _ignore_wheel_event