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
@@ -0,0 +1,851 @@
1
+ """見た目と注釈のダイアログ。呼び出し側は `from graphica.gui.dialogs import X` で参照する。"""
2
+
3
+ import re
4
+ from PySide6.QtWidgets import (
5
+ QColorDialog,
6
+ QComboBox,
7
+ QDialog,
8
+ QDialogButtonBox,
9
+ QDoubleSpinBox,
10
+ QFormLayout,
11
+ QGridLayout,
12
+ QHBoxLayout,
13
+ QLabel,
14
+ QLineEdit,
15
+ QListWidget,
16
+ QListWidgetItem,
17
+ QMenu,
18
+ QMessageBox,
19
+ QPushButton,
20
+ QToolButton,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ QWidgetAction,
24
+ )
25
+ from PySide6.QtCore import QSize, Qt
26
+ from PySide6.QtGui import QColor, QFont, QIcon, QPixmap
27
+ from graphica.gui import notify
28
+ from graphica.gui import icon_utils
29
+ from graphica.gui.color_history import get_color_with_history
30
+ from graphica.gui.theme import apply_form_spacing
31
+ from graphica.gui.mathtext_preview import FitWidthPixmapLabel
32
+ from graphica.core.i18n import tr
33
+ from graphica.core.color_palettes import BUILTIN_PALETTES, default_color_cycle
34
+ from graphica.core.named_colors import (
35
+ NamedColorError,
36
+ add_named_color,
37
+ load_named_colors,
38
+ move_named_color,
39
+ remove_named_color,
40
+ save_named_colors,
41
+ update_named_color,
42
+ )
43
+
44
+
45
+ class LabelEditDialog(QDialog):
46
+ """タイトルと軸ラベルの編集。装飾ボタンは選択範囲を mathtext で包み、記号はカーソル位置に入れる。"""
47
+
48
+ def __init__(self, initial_text, window_title, symbol_palette, parent=None):
49
+ """symbol_palette は main_window の LABEL_SYMBOL_PALETTE(dialogs は main_window を import できないので渡してもらう)。"""
50
+ super().__init__(parent)
51
+ self.setWindowTitle(window_title)
52
+ self.setMinimumWidth(420)
53
+
54
+ layout = QVBoxLayout(self)
55
+
56
+ self.text_edit = QLineEdit(initial_text)
57
+ self.text_edit.setMinimumHeight(34)
58
+ larger_font = QFont(self.text_edit.font())
59
+ larger_font.setPointSize(larger_font.pointSize() + 1)
60
+ self.text_edit.setFont(larger_font)
61
+ layout.addWidget(self.text_edit)
62
+
63
+ # QLineEdit では装飾を見せられないので、描いた結果を下に出す
64
+ self.preview_label = FitWidthPixmapLabel()
65
+ self.preview_label.setObjectName("mathtext_preview_label")
66
+ self.preview_label.setMinimumHeight(36)
67
+ self.preview_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
68
+ layout.addWidget(self.preview_label)
69
+ self.text_edit.textChanged.connect(self._refresh_preview)
70
+
71
+ button_row = QHBoxLayout()
72
+ button_row.setSpacing(4)
73
+
74
+ def _make_icon_button(icon_name, tooltip):
75
+ button = QPushButton()
76
+ button.setIcon(icon_utils.icon(icon_name, size=16))
77
+ button.setToolTip(tooltip)
78
+ button.setProperty("iconOnly", True)
79
+ button.setFixedSize(28, 28)
80
+ # フォーカスを取るとテキスト欄の選択が消える(下の説明)
81
+ button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
82
+ button_row.addWidget(button)
83
+ return button
84
+
85
+ bold_button = _make_icon_button("bold", "太字")
86
+ italic_button = _make_icon_button("italic", "イタリック")
87
+ superscript_button = _make_icon_button("superscript", "上付き文字")
88
+ subscript_button = _make_icon_button("subscript", "下付き文字")
89
+
90
+ # ボタンがフォーカスを取ると、pressed が出るより前にテキスト欄の選択が消える。
91
+ # だから装飾ボタンはフォーカスを取らない。pressed で選択を控えるのは、それでも移る環境のための保険
92
+ self._pending_selection = None # (start, selected_text)、選択が無ければ None
93
+ self._pending_cursor = 0
94
+
95
+ for button in (bold_button, italic_button, superscript_button, subscript_button):
96
+ button.pressed.connect(self._capture_pending_selection)
97
+
98
+ bold_button.clicked.connect(lambda: self._apply_wrap("bold", lambda s: f"\\mathbf{{{s}}}"))
99
+ italic_button.clicked.connect(lambda: self._apply_wrap("italic", lambda s: f"\\mathit{{{s}}}"))
100
+ superscript_button.clicked.connect(lambda: self._apply_wrap("super", lambda s: f"{{}}^{{{s}}}"))
101
+ subscript_button.clicked.connect(lambda: self._apply_wrap("sub", lambda s: f"{{}}_{{{s}}}"))
102
+
103
+ # 記号は32個あり、常に並べると場所を取るのでドロップダウンにする
104
+ symbol_button = QToolButton()
105
+ symbol_button.setText("Ω")
106
+ symbol_button.setToolTip("ギリシャ文字・数学記号を挿入")
107
+ symbol_button.setProperty("iconOnly", True)
108
+ symbol_button.setFixedSize(28, 28)
109
+ symbol_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
110
+ # 既定でも NoFocus だが、装飾ボタンと同じ理由で明示する
111
+ symbol_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
112
+ # メニューを開く動作でも選択が消えうるので、開く前に控える
113
+ symbol_button.pressed.connect(self._capture_pending_selection)
114
+
115
+ symbol_menu = QMenu(symbol_button)
116
+ symbol_panel = QWidget()
117
+ symbol_grid = QGridLayout(symbol_panel)
118
+ symbol_grid.setContentsMargins(6, 6, 6, 6)
119
+ symbol_grid.setSpacing(2)
120
+ # 既定の大きさでは記号が読みにくい
121
+ symbol_font = QFont(self.font())
122
+ symbol_font.setPointSize(symbol_font.pointSize() + 4)
123
+ for index, (glyph, macro) in enumerate(symbol_palette):
124
+ item_button = QToolButton()
125
+ item_button.setText(glyph)
126
+ item_button.setFont(symbol_font)
127
+ # macro=None は mathtext のマクロを持たない文字そのもの(マイナス記号など)
128
+ item_button.setToolTip(f"\\{macro}" if macro else glyph)
129
+ item_button.setFixedSize(30, 30)
130
+ item_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
131
+ item_button.clicked.connect(
132
+ lambda checked=False, g=glyph, m=macro, sm=symbol_menu: (self._insert_symbol(g, m), sm.close())
133
+ )
134
+ symbol_grid.addWidget(item_button, index // 4, index % 4)
135
+ symbol_widget_action = QWidgetAction(symbol_button)
136
+ symbol_widget_action.setDefaultWidget(symbol_panel)
137
+ symbol_menu.addAction(symbol_widget_action)
138
+ symbol_button.setMenu(symbol_menu)
139
+ button_row.addWidget(symbol_button)
140
+
141
+ button_row.addStretch(1)
142
+ layout.addLayout(button_row)
143
+
144
+ button_box = QDialogButtonBox(
145
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
146
+ )
147
+ button_box.accepted.connect(self.accept)
148
+ button_box.rejected.connect(self.reject)
149
+ layout.addWidget(button_box)
150
+
151
+ self._refresh_preview()
152
+
153
+ def _refresh_preview(self):
154
+ """テキスト欄の内容を描いてプレビューに出す(タイトル欄のプレビューと同じ描き方)。"""
155
+ from graphica.gui import theme
156
+ from graphica.gui.mathtext_preview import render_mathtext_to_pixmap
157
+
158
+ tokens = theme.current_tokens()
159
+ text = self.text_edit.text()
160
+ color = tokens["text_primary"] if text else tokens["text_muted"]
161
+ pixmap = render_mathtext_to_pixmap(text if text else " ", color=color, fontsize=15)
162
+ self.preview_label.set_natural_pixmap(pixmap)
163
+
164
+ def _capture_pending_selection(self):
165
+ """ボタンが押された瞬間の選択範囲を控える(clicked まで待つと消えている環境がある)。"""
166
+ if self.text_edit.hasSelectedText():
167
+ self._pending_selection = (
168
+ self.text_edit.selectionStart(), self.text_edit.selectedText()
169
+ )
170
+ else:
171
+ self._pending_selection = None
172
+ self._pending_cursor = self.text_edit.cursorPosition()
173
+
174
+ # 選択が $...$ で囲んだ1つの断片か(直前の装飾の結果をさらに装飾するとき)
175
+ _MATH_SPAN_RE = re.compile(r'^\$(.*)\$$', re.DOTALL)
176
+ _MATHBF_RE = re.compile(r'^\\mathbf\{(.*)\}$', re.DOTALL)
177
+ _MATHIT_RE = re.compile(r'^\\mathit\{(.*)\}$', re.DOTALL)
178
+ _BOLDSYMBOL_RE = re.compile(r'^\\boldsymbol\{(.*)\}$', re.DOTALL)
179
+ _SUPER_RE = re.compile(r'^\{\}\^\{(.*)\}$', re.DOTALL)
180
+ _SUB_RE = re.compile(r'^\{\}_\{(.*)\}$', re.DOTALL)
181
+
182
+ def _apply_wrap(self, kind, wrap_fn):
183
+ """選択範囲を wrap_fn の中身で包み、$...$ で囲む。kind は "bold" / "italic" / "super" / "sub"。
184
+
185
+ 選択が既に $...$ の断片なら、中身を取り出してから包み直す($ が入れ子になると壊れた構文になる)。
186
+ \\mathbf と \\mathit は入れ子にしても合成されないので、両方なら \\boldsymbol にする。
187
+ 同じ装飾がもう外側にかかっていれば外す(連打で入れ子が積み重ならないように)。
188
+ 装飾が無くなれば $ も外す(mathtext の中の裸の文字は斜体になる)。
189
+ """
190
+ if not self._pending_selection:
191
+ # 選択が無ければ何もしない(案内は出さない)
192
+ return
193
+ start, selected = self._pending_selection
194
+ span_match = self._MATH_SPAN_RE.match(selected)
195
+ inner = span_match.group(1) if span_match else selected
196
+
197
+ combined = None
198
+ if kind == "bold":
199
+ m = self._BOLDSYMBOL_RE.match(inner)
200
+ if m:
201
+ combined = f"\\mathit{{{m.group(1)}}}" # 太字だけ外し、イタリックは残す
202
+ else:
203
+ m = self._MATHIT_RE.match(inner)
204
+ if m:
205
+ combined = f"\\boldsymbol{{{m.group(1)}}}"
206
+ else:
207
+ m = self._MATHBF_RE.match(inner)
208
+ if m:
209
+ combined = m.group(1)
210
+ elif kind == "italic":
211
+ m = self._BOLDSYMBOL_RE.match(inner)
212
+ if m:
213
+ combined = f"\\mathbf{{{m.group(1)}}}" # イタリックだけ外し、太字は残す
214
+ else:
215
+ m = self._MATHBF_RE.match(inner)
216
+ if m:
217
+ combined = f"\\boldsymbol{{{m.group(1)}}}"
218
+ else:
219
+ m = self._MATHIT_RE.match(inner)
220
+ if m:
221
+ combined = m.group(1)
222
+ elif kind == "super":
223
+ m = self._SUPER_RE.match(inner)
224
+ if m:
225
+ combined = m.group(1)
226
+ elif kind == "sub":
227
+ m = self._SUB_RE.match(inner)
228
+ if m:
229
+ combined = m.group(1)
230
+
231
+ new_inner = combined if combined is not None else wrap_fn(inner)
232
+ if re.search(r'[\\^_{}]', new_inner):
233
+ replacement = f"${new_inner}$"
234
+ else:
235
+ replacement = new_inner
236
+ text = self.text_edit.text()
237
+ self.text_edit.setText(text[:start] + replacement + text[start + len(selected):])
238
+ self.text_edit.setSelection(start, len(replacement))
239
+
240
+ def _insert_symbol(self, glyph, macro):
241
+ """記号を選択範囲に置き換えるか、カーソル位置に入れる。macro が None なら $ で包まない(包むと斜体になる)。"""
242
+ text = self.text_edit.text()
243
+ if self._pending_selection:
244
+ start, selected = self._pending_selection
245
+ end = start + len(selected)
246
+ else:
247
+ start = end = self._pending_cursor
248
+ replacement = glyph if macro is None else f"$\\{macro}$"
249
+ self.text_edit.setText(text[:start] + replacement + text[end:])
250
+ self.text_edit.setCursorPosition(start + len(replacement))
251
+
252
+ def get_text(self):
253
+ return self.text_edit.text()
254
+
255
+
256
+ class ColorPaletteDialog(QDialog):
257
+ """「自動配色」のパレットを作り、選ぶ。「Matplotlib既定」と組み込みのパレットは読み取り専用。"""
258
+
259
+ DEFAULT_PALETTE_NAME = "Matplotlib既定"
260
+
261
+ def __init__(self, palettes: dict, active_name: str, parent=None):
262
+ super().__init__(parent)
263
+ self.setWindowTitle("配色パレットの管理")
264
+ self.resize(420, 440)
265
+
266
+ # キャンセルしたとき元に戻せるよう、呼び出し側の辞書は変えない
267
+ self.palettes = {name: list(colors) for name, colors in palettes.items()}
268
+
269
+ layout = QVBoxLayout(self)
270
+
271
+ combo_layout = QHBoxLayout()
272
+ combo_layout.addWidget(QLabel("パレット"))
273
+ self.palette_combo = QComboBox()
274
+ self.palette_combo.addItem(self.DEFAULT_PALETTE_NAME)
275
+ self.palette_combo.addItems(sorted(BUILTIN_PALETTES.keys()))
276
+ self.palette_combo.addItems(sorted(self.palettes.keys()))
277
+ combo_layout.addWidget(self.palette_combo, stretch=1)
278
+ layout.addLayout(combo_layout)
279
+
280
+ palette_button_layout = QHBoxLayout()
281
+ self.new_palette_button = QPushButton("新規パレット...")
282
+ self.rename_palette_button = QPushButton("名前を変更...")
283
+ self.delete_palette_button = QPushButton("削除")
284
+ palette_button_layout.addWidget(self.new_palette_button)
285
+ palette_button_layout.addWidget(self.rename_palette_button)
286
+ palette_button_layout.addWidget(self.delete_palette_button)
287
+ layout.addLayout(palette_button_layout)
288
+
289
+ self.color_list = QListWidget()
290
+ layout.addWidget(self.color_list)
291
+
292
+ color_button_layout = QHBoxLayout()
293
+ self.add_color_button = QPushButton("色を追加...")
294
+ self.remove_color_button = QPushButton("選択した色を削除")
295
+ color_button_layout.addWidget(self.add_color_button)
296
+ color_button_layout.addWidget(self.remove_color_button)
297
+ color_button_layout.addStretch()
298
+ layout.addLayout(color_button_layout)
299
+
300
+ info_label = QLabel("「OK」を押すと、選択中のパレットが以後「自動配色」ボタンで使われます。")
301
+ info_label.setWordWrap(True)
302
+ layout.addWidget(info_label)
303
+
304
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
305
+ QDialogButtonBox.StandardButton.Cancel)
306
+ button_box.accepted.connect(self.accept)
307
+ button_box.rejected.connect(self.reject)
308
+ layout.addWidget(button_box)
309
+
310
+ self.palette_combo.currentTextChanged.connect(self._on_palette_selected)
311
+ self.new_palette_button.clicked.connect(self._on_new_palette)
312
+ self.rename_palette_button.clicked.connect(self._on_rename_palette)
313
+ self.delete_palette_button.clicked.connect(self._on_delete_palette)
314
+ self.add_color_button.clicked.connect(self._on_add_color)
315
+ self.remove_color_button.clicked.connect(self._on_remove_color)
316
+
317
+ # 組み込みのパレットも初期の選択にする(でないと既定の表示のまま OK を押して上書きしてしまう)
318
+ if active_name in self.palettes or active_name in BUILTIN_PALETTES:
319
+ self.palette_combo.setCurrentText(active_name)
320
+ else:
321
+ self.palette_combo.setCurrentText(self.DEFAULT_PALETTE_NAME)
322
+ self._refresh_color_list()
323
+ self._update_button_states()
324
+
325
+ def _is_default_selected(self):
326
+ return self._is_readonly_palette(self.palette_combo.currentText())
327
+
328
+ def _is_readonly_palette(self, name):
329
+ """「Matplotlib既定」か組み込みのパレット(どちらも編集できない)。"""
330
+ return name == self.DEFAULT_PALETTE_NAME or name in BUILTIN_PALETTES
331
+
332
+ def _update_button_states(self):
333
+ editable = not self._is_default_selected()
334
+ self.rename_palette_button.setEnabled(editable)
335
+ self.delete_palette_button.setEnabled(editable)
336
+ self.add_color_button.setEnabled(editable)
337
+ self.remove_color_button.setEnabled(editable)
338
+
339
+ def _refresh_color_list(self):
340
+ # 行は QSS に任せず色見本+テーマの文字色のラベルにする。QSS で ::item に何か当てると、
341
+ # setBackground / setForeground が無視され、明るい色や暗い色の行が読めなくなる
342
+ self.color_list.clear()
343
+ name = self.palette_combo.currentText()
344
+ if name == self.DEFAULT_PALETTE_NAME:
345
+ colors = default_color_cycle()
346
+ elif name in BUILTIN_PALETTES:
347
+ colors = BUILTIN_PALETTES[name]
348
+ else:
349
+ colors = self.palettes.get(name, [])
350
+ from graphica.gui import theme
351
+ border_color = theme.current_tokens()["border_strong"]
352
+ for color_hex in colors:
353
+ item = QListWidgetItem()
354
+ self.color_list.addItem(item)
355
+
356
+ row_widget = QWidget()
357
+ row_layout = QHBoxLayout(row_widget)
358
+ row_layout.setContentsMargins(6, 2, 6, 2)
359
+ row_layout.setSpacing(8)
360
+
361
+ swatch = QLabel()
362
+ swatch.setFixedSize(20, 20)
363
+ swatch.setStyleSheet(
364
+ f"background-color: {color_hex}; border: 1px solid {border_color}; "
365
+ f"border-radius: 4px;"
366
+ )
367
+ row_layout.addWidget(swatch)
368
+ row_layout.addWidget(QLabel(color_hex), 1)
369
+
370
+ item.setSizeHint(row_widget.sizeHint())
371
+ self.color_list.setItemWidget(item, row_widget)
372
+
373
+ def _on_palette_selected(self, _name):
374
+ self._refresh_color_list()
375
+ self._update_button_states()
376
+
377
+ def _on_new_palette(self):
378
+ name, ok = notify.get_text(self, "新規パレット", "パレット名")
379
+ if not ok or not name:
380
+ return
381
+ if self._is_readonly_palette(name) or name in self.palettes:
382
+ notify.warning(self, "エラー", f"パレット名 '{name}' は既に使われています。")
383
+ return
384
+ self.palettes[name] = []
385
+ self.palette_combo.addItem(name)
386
+ self.palette_combo.setCurrentText(name)
387
+
388
+ def _on_rename_palette(self):
389
+ old_name = self.palette_combo.currentText()
390
+ if self._is_readonly_palette(old_name):
391
+ return
392
+ new_name, ok = notify.get_text(self, "名前を変更", "新しいパレット名", text=old_name)
393
+ if not ok or not new_name or new_name == old_name:
394
+ return
395
+ if self._is_readonly_palette(new_name) or new_name in self.palettes:
396
+ notify.warning(self, "エラー", f"パレット名 '{new_name}' は既に使われています。")
397
+ return
398
+ self.palettes[new_name] = self.palettes.pop(old_name)
399
+ self.palette_combo.setItemText(self.palette_combo.currentIndex(), new_name)
400
+
401
+ def _on_delete_palette(self):
402
+ name = self.palette_combo.currentText()
403
+ if self._is_readonly_palette(name):
404
+ return
405
+ reply = notify.question(
406
+ self, "パレットを削除", f"パレット '{name}' を削除しますか?",
407
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
408
+ )
409
+ if reply != QMessageBox.StandardButton.Yes:
410
+ return
411
+ del self.palettes[name]
412
+ self.palette_combo.removeItem(self.palette_combo.currentIndex())
413
+
414
+ def _on_add_color(self):
415
+ name = self.palette_combo.currentText()
416
+ if self._is_readonly_palette(name):
417
+ return
418
+ color = QColorDialog.getColor()
419
+ if not color.isValid():
420
+ return
421
+ self.palettes.setdefault(name, []).append(color.name())
422
+ self._refresh_color_list()
423
+
424
+ def _on_remove_color(self):
425
+ name = self.palette_combo.currentText()
426
+ if self._is_readonly_palette(name):
427
+ return
428
+ row = self.color_list.currentRow()
429
+ if row < 0:
430
+ return
431
+ del self.palettes[name][row]
432
+ self._refresh_color_list()
433
+
434
+ def get_result(self):
435
+ """(パレットの辞書, 選んだパレット名)。QSettings への保存は呼び出し側。"""
436
+ return self.palettes, self.palette_combo.currentText()
437
+
438
+
439
+ def _named_color_icon(color_name, size=16):
440
+ """リスト行の先頭に出す色見本(色欄のポップアップと同じ見た目に揃える)。"""
441
+ from graphica.gui.color_picker_widget import _color_icon
442
+ return _color_icon(color_name, size=size)
443
+
444
+
445
+ class NamedColorManagerDialog(QDialog):
446
+ """名前を付けて登録した色(core/named_colors.py)の管理。配色パレット(色の並び)とは別物。
447
+
448
+ 登録簿を育てる操作で、プロットの見た目は変えないので、OK を待たずにその場で保存する。
449
+ """
450
+
451
+ def __init__(self, settings, parent=None):
452
+ super().__init__(parent)
453
+ self.setWindowTitle(tr("色名の管理"))
454
+ self.resize(420, 400)
455
+ self._settings = settings
456
+ self.entries = load_named_colors(settings)
457
+
458
+ layout = QVBoxLayout(self)
459
+
460
+ description = QLabel(tr(
461
+ "よく使う色に名前を付けて登録します。データセットの色欄で"
462
+ "スウォッチを押すと、ここに登録した色を名前から選べます。"))
463
+ description.setWordWrap(True)
464
+ layout.addWidget(description)
465
+
466
+ self.color_list = QListWidget()
467
+ self.color_list.setIconSize(QSize(16, 16))
468
+ self.color_list.itemDoubleClicked.connect(lambda _item: self._on_edit())
469
+ layout.addWidget(self.color_list, stretch=1)
470
+
471
+ button_layout = QHBoxLayout()
472
+ self.add_button = QPushButton(tr("追加..."))
473
+ self.edit_button = QPushButton(tr("編集..."))
474
+ self.delete_button = QPushButton(tr("削除"))
475
+ self.up_button = QPushButton(tr("上へ"))
476
+ self.down_button = QPushButton(tr("下へ"))
477
+ for button in (self.add_button, self.edit_button, self.delete_button):
478
+ button_layout.addWidget(button)
479
+ button_layout.addStretch()
480
+ button_layout.addWidget(self.up_button)
481
+ button_layout.addWidget(self.down_button)
482
+ layout.addLayout(button_layout)
483
+
484
+ order_note = QLabel(tr("並び順は、色欄のポップアップにそのまま反映されます。"))
485
+ order_note.setWordWrap(True)
486
+ layout.addWidget(order_note)
487
+
488
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
489
+ button_box.rejected.connect(self.reject)
490
+ button_box.accepted.connect(self.accept)
491
+ layout.addWidget(button_box)
492
+
493
+ self.add_button.clicked.connect(self._on_add)
494
+ self.edit_button.clicked.connect(self._on_edit)
495
+ self.delete_button.clicked.connect(self._on_delete)
496
+ self.up_button.clicked.connect(lambda: self._on_move(-1))
497
+ self.down_button.clicked.connect(lambda: self._on_move(1))
498
+
499
+ self._reload_list()
500
+
501
+
502
+ def _reload_list(self, select_index=None):
503
+ self.color_list.clear()
504
+ for entry in self.entries:
505
+ item = QListWidgetItem(f'{entry["name"]} {entry["color"]}')
506
+ item.setIcon(_named_color_icon(entry["color"]))
507
+ self.color_list.addItem(item)
508
+ if select_index is not None and 0 <= select_index < self.color_list.count():
509
+ self.color_list.setCurrentRow(select_index)
510
+
511
+ def _commit(self, entries, select_index=None):
512
+ self.entries = entries
513
+ save_named_colors(self._settings, self.entries)
514
+ self._reload_list(select_index)
515
+
516
+ def _current_index(self):
517
+ return self.color_list.currentRow()
518
+
519
+ def _ask_name_and_color(self, title, name="", color="#1f77b4"):
520
+ """名前と色を尋ねる。(name, color) かキャンセルなら None。"""
521
+ chosen = QColorDialog.getColor(QColor(color), self, tr("色を選択"))
522
+ if not chosen.isValid():
523
+ return None
524
+ text, ok = notify.get_text(
525
+ self, title, tr("登録名"), text=name)
526
+ if not ok:
527
+ return None
528
+ return text, chosen.name()
529
+
530
+
531
+ def _on_add(self):
532
+ result = self._ask_name_and_color(tr("色を登録"))
533
+ if result is None:
534
+ return
535
+ name, color = result
536
+ try:
537
+ entries = add_named_color(self.entries, name, color)
538
+ except NamedColorError as e:
539
+ notify.warning(self, tr("色を登録"), str(e))
540
+ return
541
+ self._commit(entries, len(entries) - 1)
542
+
543
+ def _on_edit(self):
544
+ index = self._current_index()
545
+ if index < 0:
546
+ notify.information(self, tr("色名の管理"), tr("編集する登録を選んでください。"))
547
+ return
548
+ current = self.entries[index]
549
+ result = self._ask_name_and_color(
550
+ tr("登録を編集"), current["name"], current["color"])
551
+ if result is None:
552
+ return
553
+ name, color = result
554
+ try:
555
+ entries = update_named_color(self.entries, index, name, color)
556
+ except NamedColorError as e:
557
+ notify.warning(self, tr("登録を編集"), str(e))
558
+ return
559
+ self._commit(entries, index)
560
+
561
+ def _on_delete(self):
562
+ index = self._current_index()
563
+ if index < 0:
564
+ notify.information(self, tr("色名の管理"), tr("削除する登録を選んでください。"))
565
+ return
566
+ try:
567
+ entries = remove_named_color(self.entries, index)
568
+ except NamedColorError as e:
569
+ notify.warning(self, tr("色名の管理"), str(e))
570
+ return
571
+ self._commit(entries, min(index, len(entries) - 1))
572
+
573
+ def _on_move(self, offset):
574
+ index = self._current_index()
575
+ if index < 0:
576
+ return
577
+ try:
578
+ entries = move_named_color(self.entries, index, offset)
579
+ except NamedColorError:
580
+ return
581
+ new_index = max(0, min(index + offset, len(entries) - 1))
582
+ self._commit(entries, new_index)
583
+
584
+
585
+ class NamedColorPickerDialog(QDialog):
586
+ """登録した色を名前で絞り込んで選ぶ(色欄のポップアップは先頭 POPUP_LIMIT 件しか並べない)。編集はしない。"""
587
+
588
+ def __init__(self, settings, parent=None, title=None):
589
+ super().__init__(parent)
590
+ self.setWindowTitle(title or tr("登録色を選択"))
591
+ self.resize(380, 420)
592
+ self.entries = load_named_colors(settings)
593
+ self._selected = None
594
+
595
+ layout = QVBoxLayout(self)
596
+
597
+ self.filter_edit = QLineEdit()
598
+ self.filter_edit.setPlaceholderText(tr("名前または色コードで絞り込み"))
599
+ self.filter_edit.setClearButtonEnabled(True)
600
+ layout.addWidget(self.filter_edit)
601
+
602
+ self.color_list = QListWidget()
603
+ self.color_list.setIconSize(QSize(16, 16))
604
+ layout.addWidget(self.color_list, stretch=1)
605
+
606
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
607
+ QDialogButtonBox.StandardButton.Cancel)
608
+ button_box.accepted.connect(self.accept)
609
+ button_box.rejected.connect(self.reject)
610
+ layout.addWidget(button_box)
611
+
612
+ self.filter_edit.textChanged.connect(self._reload_list)
613
+ self.color_list.itemDoubleClicked.connect(lambda _item: self.accept())
614
+
615
+ self._reload_list()
616
+
617
+ def _reload_list(self, _text=None):
618
+ """名前と色コードの両方で絞り込む。"""
619
+ needle = self.filter_edit.text().strip().lower()
620
+ self.color_list.clear()
621
+ for entry in self.entries:
622
+ if needle and needle not in entry["name"].lower() and needle not in entry["color"].lower():
623
+ continue
624
+ item = QListWidgetItem(f'{entry["name"]} {entry["color"]}')
625
+ item.setIcon(_named_color_icon(entry["color"]))
626
+ item.setData(Qt.ItemDataRole.UserRole, entry)
627
+ self.color_list.addItem(item)
628
+ if self.color_list.count():
629
+ self.color_list.setCurrentRow(0)
630
+
631
+ def accept(self):
632
+ item = self.color_list.currentItem()
633
+ self._selected = item.data(Qt.ItemDataRole.UserRole) if item else None
634
+ super().accept()
635
+
636
+ def selected_entry(self):
637
+ """選んだ登録({'name', 'color'})。キャンセルか未選択なら None。"""
638
+ return self._selected
639
+
640
+
641
+ class LegendOrderDialog(QDialog):
642
+ """凡例の並びを、描画順とは別にドラッグで決める。"""
643
+
644
+ def __init__(self, labels, parent=None):
645
+ super().__init__(parent)
646
+ self.setWindowTitle("凡例の順序")
647
+ self.resize(320, 380)
648
+
649
+ layout = QVBoxLayout(self)
650
+ layout.addWidget(QLabel("ドラッグして凡例の表示順序を変更できます:"))
651
+
652
+ self.list_widget = QListWidget()
653
+ self.list_widget.setDragDropMode(QListWidget.DragDropMode.InternalMove)
654
+ self.list_widget.addItems(labels)
655
+ layout.addWidget(self.list_widget)
656
+
657
+ reset_button = QPushButton("描画順にリセット")
658
+ reset_button.clicked.connect(self._on_reset)
659
+ layout.addWidget(reset_button)
660
+ self._original_labels = list(labels)
661
+
662
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
663
+ QDialogButtonBox.StandardButton.Cancel)
664
+ button_box.accepted.connect(self.accept)
665
+ button_box.rejected.connect(self.reject)
666
+ layout.addWidget(button_box)
667
+
668
+ def _on_reset(self):
669
+ self.list_widget.clear()
670
+
671
+ def get_order(self):
672
+ """今の並び。「描画順にリセット」なら空のリスト(描画順に従う)。"""
673
+ return [self.list_widget.item(i).text() for i in range(self.list_widget.count())]
674
+
675
+
676
+ DEFAULT_ANNOTATION_COLOR = '#000000'
677
+
678
+
679
+ class _AnnotationColorButton(QPushButton):
680
+ """注釈の色の見本。押すと色を選ぶ(最近使った色は settings に残す)。"""
681
+
682
+ def __init__(self, settings, color=DEFAULT_ANNOTATION_COLOR, parent=None):
683
+ super().__init__(parent)
684
+ self._settings = settings
685
+ self.set_color(color)
686
+ self.clicked.connect(self._choose_color)
687
+
688
+ def color(self):
689
+ return self._color
690
+
691
+ def set_color(self, color):
692
+ self._color = QColor(color).name()
693
+ swatch = QPixmap(16, 16)
694
+ swatch.fill(QColor(self._color))
695
+ self.setIcon(QIcon(swatch))
696
+ self.setText(self._color)
697
+
698
+ def _choose_color(self):
699
+ color = get_color_with_history(self._settings, self, initial=QColor(self._color))
700
+ if color.isValid():
701
+ self.set_color(color.name())
702
+
703
+
704
+ class TextAnnotationDialog(QDialog):
705
+ """文字の注釈の文字と色。"""
706
+
707
+ def __init__(self, parent=None, settings=None):
708
+ super().__init__(parent)
709
+ self.setWindowTitle("テキスト注釈の追加")
710
+ self.resize(360, 140)
711
+
712
+ layout = QVBoxLayout(self)
713
+ layout.addWidget(QLabel("表示するテキスト:"))
714
+ self.text_edit = QLineEdit()
715
+ layout.addWidget(self.text_edit)
716
+
717
+ form = QFormLayout()
718
+ self.color_button = _AnnotationColorButton(settings)
719
+ form.addRow("色", self.color_button)
720
+ layout.addLayout(form)
721
+
722
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
723
+ QDialogButtonBox.StandardButton.Cancel)
724
+ button_box.accepted.connect(self.accept)
725
+ button_box.rejected.connect(self.reject)
726
+ layout.addWidget(button_box)
727
+
728
+ apply_form_spacing(self)
729
+
730
+ def get_text(self):
731
+ return self.text_edit.text().strip()
732
+
733
+ def color(self):
734
+ return self.color_button.color()
735
+
736
+
737
+ class ArrowAnnotationDialog(QDialog):
738
+ """矢印の注釈のラベル、形('single' / 'double' / 'bracket')、曲がり具合。"""
739
+
740
+ STYLE_SINGLE = "通常(片矢印)"
741
+ STYLE_DOUBLE = "両矢印"
742
+ STYLE_BRACKET = "ブラケット(有意差表示等)"
743
+ STYLES = [STYLE_SINGLE, STYLE_DOUBLE, STYLE_BRACKET]
744
+ _STYLE_KEY_BY_LABEL = {STYLE_SINGLE: "single", STYLE_DOUBLE: "double", STYLE_BRACKET: "bracket"}
745
+
746
+ def __init__(self, parent=None, settings=None):
747
+ super().__init__(parent)
748
+ self.setWindowTitle("矢印注釈の追加")
749
+ self.resize(360, 200)
750
+
751
+ layout = QVBoxLayout(self)
752
+ layout.addWidget(QLabel("ラベル(空欄可):"))
753
+ self.text_edit = QLineEdit()
754
+ layout.addWidget(self.text_edit)
755
+
756
+ form = QFormLayout()
757
+ self.style_combo = QComboBox()
758
+ self.style_combo.addItems(self.STYLES)
759
+ form.addRow("矢印の形状", self.style_combo)
760
+
761
+ self.curvature_spinbox = QDoubleSpinBox()
762
+ self.curvature_spinbox.setRange(-1.0, 1.0)
763
+ self.curvature_spinbox.setSingleStep(0.05)
764
+ self.curvature_spinbox.setDecimals(2)
765
+ self.curvature_spinbox.setValue(0.0)
766
+ self.curvature_spinbox.setToolTip(
767
+ "0で直線。正/負で曲がる向きが変わります(matplotlibのarc3 rad相当)。"
768
+ )
769
+ form.addRow("曲率", self.curvature_spinbox)
770
+
771
+ self.color_button = _AnnotationColorButton(settings)
772
+ form.addRow("色", self.color_button)
773
+ layout.addLayout(form)
774
+
775
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
776
+ QDialogButtonBox.StandardButton.Cancel)
777
+ button_box.accepted.connect(self.accept)
778
+ button_box.rejected.connect(self.reject)
779
+ layout.addWidget(button_box)
780
+
781
+ apply_form_spacing(self)
782
+
783
+ def get_settings(self):
784
+ """(ラベル, 形, 曲がり具合)"""
785
+ style_text = self.style_combo.currentText()
786
+ style = self._STYLE_KEY_BY_LABEL[style_text]
787
+ return self.text_edit.text().strip(), style, self.curvature_spinbox.value()
788
+
789
+ def color(self):
790
+ return self.color_button.color()
791
+
792
+
793
+ class InsetDialog(QDialog):
794
+ """拡大図の位置(角と大きさ)と、拡大する X の範囲。"""
795
+
796
+ CORNERS = ["右上", "左上", "右下", "左下"]
797
+
798
+ def __init__(self, x_min, x_max, default_zoom_min, default_zoom_max, parent=None):
799
+ super().__init__(parent)
800
+ self.setWindowTitle("インセット(拡大図)を追加")
801
+ self.resize(380, 260)
802
+
803
+ layout = QVBoxLayout(self)
804
+ info_label = QLabel(
805
+ f"データのX範囲: {x_min:.4g} 〜 {x_max:.4g}\n"
806
+ "拡大表示したいX範囲と、インセットを表示する位置を選んでください。"
807
+ )
808
+ info_label.setWordWrap(True)
809
+ layout.addWidget(info_label)
810
+
811
+ form = QFormLayout()
812
+ self.zoom_min_spinbox = QDoubleSpinBox()
813
+ self.zoom_min_spinbox.setRange(-1e12, 1e12)
814
+ self.zoom_min_spinbox.setDecimals(4)
815
+ self.zoom_min_spinbox.setValue(default_zoom_min)
816
+ form.addRow("拡大範囲(最小)", self.zoom_min_spinbox)
817
+
818
+ self.zoom_max_spinbox = QDoubleSpinBox()
819
+ self.zoom_max_spinbox.setRange(-1e12, 1e12)
820
+ self.zoom_max_spinbox.setDecimals(4)
821
+ self.zoom_max_spinbox.setValue(default_zoom_max)
822
+ form.addRow("拡大範囲(最大)", self.zoom_max_spinbox)
823
+
824
+ self.corner_combo = QComboBox()
825
+ self.corner_combo.addItems(self.CORNERS)
826
+ form.addRow("表示位置", self.corner_combo)
827
+
828
+ self.size_spinbox = QDoubleSpinBox()
829
+ self.size_spinbox.setRange(0.15, 0.6)
830
+ self.size_spinbox.setSingleStep(0.05)
831
+ self.size_spinbox.setValue(0.35)
832
+ form.addRow("大きさ(軸に対する比率)", self.size_spinbox)
833
+ layout.addLayout(form)
834
+
835
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
836
+ QDialogButtonBox.StandardButton.Cancel)
837
+ button_box.accepted.connect(self.accept)
838
+ button_box.rejected.connect(self.reject)
839
+ layout.addWidget(button_box)
840
+
841
+ apply_form_spacing(self)
842
+
843
+ def get_settings(self):
844
+ """{'corner', 'size', 'zoom_x_range'(昇順)}"""
845
+ lo = self.zoom_min_spinbox.value()
846
+ hi = self.zoom_max_spinbox.value()
847
+ return {
848
+ 'corner': self.corner_combo.currentText(),
849
+ 'size': self.size_spinbox.value(),
850
+ 'zoom_x_range': (min(lo, hi), max(lo, hi)),
851
+ }