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,1015 @@
1
+ """アプリ全体(設定・ヘルプ・操作)のダイアログ。呼び出し側は `from graphica.gui.dialogs import X` で参照する。"""
2
+
3
+ from PySide6.QtWidgets import (
4
+ QCheckBox,
5
+ QComboBox,
6
+ QDialog,
7
+ QDialogButtonBox,
8
+ QDoubleSpinBox,
9
+ QFormLayout,
10
+ QGroupBox,
11
+ QHBoxLayout,
12
+ QLabel,
13
+ QLineEdit,
14
+ QListWidget,
15
+ QListWidgetItem,
16
+ QPushButton,
17
+ QSpinBox,
18
+ QTabWidget,
19
+ QTableWidget,
20
+ QTableWidgetItem,
21
+ QTextBrowser,
22
+ QVBoxLayout,
23
+ QWidget,
24
+ )
25
+ from PySide6.QtCore import QEvent, QUrl, Qt
26
+ from PySide6.QtGui import QDesktopServices, QKeySequence
27
+ from graphica.gui import notify
28
+ from graphica.gui import icon_utils
29
+ from graphica.gui.theme import apply_form_spacing
30
+
31
+
32
+ class PreferencesDialog(QDialog):
33
+ """環境設定。値の入力と表示だけで、保存は呼び出し側が get_settings() の値で行う。"""
34
+
35
+ def __init__(self, dark_mode, autosave_minutes, autosave_bounds=(0, 180), parent=None,
36
+ current_language=None, autosave_dir="", point_label_max_points=1000,
37
+ snap_to_grid_enabled=False, snap_grid_interval_px=10,
38
+ plugin_records=None, plugin_registration_errors=None, disabled_plugin_names=None):
39
+ """plugin_records=None は一度も読み込んでいない(セーフモードなど)。空のリスト(1つも無い)とは区別して表示する。"""
40
+ super().__init__(parent)
41
+ from graphica.core.i18n import tr, SUPPORTED_LANGUAGES, get_language
42
+ self.setWindowTitle(tr("環境設定"))
43
+ # 見出しのチップで各グループの上の余白が広いので、小さいと見切れる
44
+ self.resize(480, 520)
45
+
46
+ outer_layout = QVBoxLayout(self)
47
+ tabs = QTabWidget()
48
+ outer_layout.addWidget(tabs)
49
+
50
+ general_tab = QWidget()
51
+ layout = QVBoxLayout(general_tab)
52
+ tabs.addTab(general_tab, tr("一般"))
53
+
54
+ appearance_group = QGroupBox(tr("外観"))
55
+ appearance_layout = QVBoxLayout(appearance_group)
56
+ self.dark_mode_checkbox = QCheckBox(tr("ダークモードを有効にする"))
57
+ self.dark_mode_checkbox.setChecked(bool(dark_mode))
58
+ appearance_layout.addWidget(self.dark_mode_checkbox)
59
+ layout.addWidget(appearance_group)
60
+
61
+ # 切り替えは次の起動から(作った画面をその場で訳し直す仕組みは無い)
62
+ language_group = QGroupBox(tr("言語"))
63
+ language_form = QFormLayout(language_group)
64
+ self.language_combo = QComboBox()
65
+ self._language_codes = list(SUPPORTED_LANGUAGES.keys())
66
+ self.language_combo.addItems([SUPPORTED_LANGUAGES[code] for code in self._language_codes])
67
+ active_language = current_language or get_language()
68
+ if active_language in self._language_codes:
69
+ self.language_combo.setCurrentIndex(self._language_codes.index(active_language))
70
+ language_form.addRow(tr("表示言語"), self.language_combo)
71
+ language_note = QLabel(tr("※ 言語の変更は次回起動時に反映されます。"))
72
+ language_note.setStyleSheet("font-size: 9pt; color: gray;")
73
+ language_note.setWordWrap(True)
74
+ language_form.addRow(language_note)
75
+ layout.addWidget(language_group)
76
+
77
+ save_group = QGroupBox(tr("保存"))
78
+ save_form = QFormLayout(save_group)
79
+ self.autosave_spinbox = QSpinBox()
80
+ min_minutes, max_minutes = autosave_bounds
81
+ self.autosave_spinbox.setRange(min_minutes, max_minutes)
82
+ self.autosave_spinbox.setSuffix(tr(" 分"))
83
+ self.autosave_spinbox.setSpecialValueText(tr("無効"))
84
+ self.autosave_spinbox.setValue(int(autosave_minutes))
85
+ save_form.addRow(tr("オートセーブ間隔"), self.autosave_spinbox)
86
+
87
+ # 空なら core.app_paths.get_app_data_dir()
88
+ self._autosave_dir = autosave_dir or ""
89
+ autosave_dir_row = QHBoxLayout()
90
+ self.autosave_dir_edit = QLineEdit(self._autosave_dir)
91
+ self.autosave_dir_edit.setReadOnly(True)
92
+ from graphica.core.app_paths import get_app_data_dir
93
+ self.autosave_dir_edit.setPlaceholderText(tr("(既定: {path})").format(path=get_app_data_dir()))
94
+ self.autosave_dir_browse_button = QPushButton(tr("参照..."))
95
+ self.autosave_dir_browse_button.setIcon(icon_utils.icon("folder"))
96
+ self.autosave_dir_browse_button.clicked.connect(self._on_browse_autosave_dir)
97
+ self.autosave_dir_clear_button = QPushButton(tr("既定に戻す"))
98
+ self.autosave_dir_clear_button.setIcon(icon_utils.icon("refresh"))
99
+ self.autosave_dir_clear_button.clicked.connect(self._on_clear_autosave_dir)
100
+ autosave_dir_row.addWidget(self.autosave_dir_edit, 1)
101
+ autosave_dir_row.addWidget(self.autosave_dir_browse_button)
102
+ autosave_dir_row.addWidget(self.autosave_dir_clear_button)
103
+ save_form.addRow(tr("オートセーブ保存先"), autosave_dir_row)
104
+
105
+ layout.addWidget(save_group)
106
+
107
+ # 点のラベルは点の数だけ annotate するので、多いと固まる。この数を超えたら描かない
108
+ performance_group = QGroupBox(tr("パフォーマンス"))
109
+ performance_form = QFormLayout(performance_group)
110
+ self.point_label_max_spinbox = QSpinBox()
111
+ self.point_label_max_spinbox.setRange(10, 1_000_000)
112
+ self.point_label_max_spinbox.setSingleStep(100)
113
+ self.point_label_max_spinbox.setSuffix(tr(" 点"))
114
+ self.point_label_max_spinbox.setValue(int(point_label_max_points))
115
+ self.point_label_max_spinbox.setToolTip(tr(
116
+ "データ点にラベルを表示する機能は、データ点数が多いと描画が重くなり、"
117
+ "アプリがフリーズする場合があります。この件数を超えるデータセットには、"
118
+ "ラベルを有効にしていても自動的に表示しません。"
119
+ ))
120
+ performance_form.addRow(tr("データ点ラベルの表示上限"), self.point_label_max_spinbox)
121
+ layout.addWidget(performance_group)
122
+
123
+ # 注釈をドラッグで置くとき、ピクセルのグリッドに吸着させる
124
+ annotation_group = QGroupBox(tr("注釈"))
125
+ annotation_layout = QVBoxLayout(annotation_group)
126
+ self.snap_to_grid_checkbox = QCheckBox(tr("スナップ・トゥ・グリッドを有効にする"))
127
+ self.snap_to_grid_checkbox.setChecked(bool(snap_to_grid_enabled))
128
+ annotation_layout.addWidget(self.snap_to_grid_checkbox)
129
+ annotation_form = QFormLayout()
130
+ self.snap_grid_interval_spinbox = QSpinBox()
131
+ self.snap_grid_interval_spinbox.setRange(1, 200)
132
+ self.snap_grid_interval_spinbox.setSuffix(tr(" px"))
133
+ self.snap_grid_interval_spinbox.setValue(int(snap_grid_interval_px))
134
+ annotation_form.addRow(tr("グリッド間隔"), self.snap_grid_interval_spinbox)
135
+ annotation_layout.addLayout(annotation_form)
136
+ layout.addWidget(annotation_group)
137
+
138
+ # exe だけを置いた人向け。インストーラーで入れた人はインストーラーが登録・解除する
139
+ association_group = QGroupBox(tr("ファイルの関連付け"))
140
+ association_layout = QVBoxLayout(association_group)
141
+ self.file_association_status_label = QLabel()
142
+ self.file_association_status_label.setWordWrap(True)
143
+ association_layout.addWidget(self.file_association_status_label)
144
+ association_buttons = QHBoxLayout()
145
+ self.register_file_association_button = QPushButton(tr(".gra と .graphica を Graphica で開く"))
146
+ self.register_file_association_button.clicked.connect(self._on_register_file_association)
147
+ self.register_file_association_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
148
+ association_buttons.addWidget(self.register_file_association_button)
149
+ self.unregister_file_association_button = QPushButton(tr("関連付けを解除"))
150
+ self.unregister_file_association_button.clicked.connect(self._on_unregister_file_association)
151
+ self.unregister_file_association_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
152
+ association_buttons.addWidget(self.unregister_file_association_button)
153
+ association_buttons.addStretch()
154
+ association_layout.addLayout(association_buttons)
155
+ layout.addWidget(association_group)
156
+ self._refresh_file_association_state()
157
+
158
+ layout.addStretch()
159
+
160
+ plugin_tab = QWidget()
161
+ plugin_tab_layout = QVBoxLayout(plugin_tab)
162
+ tabs.addTab(plugin_tab, tr("プラグイン"))
163
+
164
+ plugin_actions_row = QHBoxLayout()
165
+ self.install_plugin_button = QPushButton(tr("プラグインをインストール..."))
166
+ self.install_plugin_button.setIcon(icon_utils.icon("download"))
167
+ self.install_plugin_button.clicked.connect(self._on_install_plugin)
168
+ # OK/キャンセルとは別のすぐ実行するボタンなので、フォーカスの枠を残さない
169
+ self.install_plugin_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
170
+ plugin_actions_row.addWidget(self.install_plugin_button)
171
+ self.open_plugins_folder_button = QPushButton(tr("プラグインフォルダを開く"))
172
+ self.open_plugins_folder_button.setIcon(icon_utils.icon("folder"))
173
+ self.open_plugins_folder_button.clicked.connect(self._on_open_plugins_folder)
174
+ self.open_plugins_folder_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
175
+ plugin_actions_row.addWidget(self.open_plugins_folder_button)
176
+ plugin_actions_row.addStretch()
177
+ plugin_tab_layout.addLayout(plugin_actions_row)
178
+
179
+ # チェックの状態は閉じるときに get_disabled_plugin_names() で読まれ、次の起動から効く
180
+ loaded_group = QGroupBox(tr("読み込み済みプラグイン"))
181
+ loaded_layout = QVBoxLayout(loaded_group)
182
+ self.plugin_list = QListWidget()
183
+ self._disabled_plugin_names = set(disabled_plugin_names or [])
184
+ self._populate_plugin_list(plugin_records, self._disabled_plugin_names)
185
+ loaded_layout.addWidget(self.plugin_list)
186
+ plugin_tab_layout.addWidget(loaded_group, 1)
187
+
188
+ # プラグイン自体は読めても、個々の register_xxx が(名前の重複などで)失敗したもの
189
+ hook_errors_group = QGroupBox(tr("フック単位の登録エラー"))
190
+ hook_errors_layout = QVBoxLayout(hook_errors_group)
191
+ self.plugin_hook_errors_list = QListWidget()
192
+ self._populate_hook_errors_list(plugin_registration_errors)
193
+ hook_errors_layout.addWidget(self.plugin_hook_errors_list)
194
+ plugin_tab_layout.addWidget(hook_errors_group)
195
+
196
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
197
+ QDialogButtonBox.StandardButton.Cancel)
198
+ button_box.accepted.connect(self.accept)
199
+ button_box.rejected.connect(self.reject)
200
+ outer_layout.addWidget(button_box)
201
+
202
+ apply_form_spacing(self)
203
+
204
+ def _populate_plugin_list(self, plugin_records, disabled_names):
205
+ """plugin_records=None なら「読み込んでいない」の1行だけ出す。エラーの行にはチェックを付けない。"""
206
+ from graphica.core.i18n import tr
207
+ self.plugin_list.clear()
208
+ if plugin_records is None:
209
+ item = QListWidgetItem(tr("(プラグインは読み込まれていません)"))
210
+ item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
211
+ self.plugin_list.addItem(item)
212
+ return
213
+ if not plugin_records:
214
+ item = QListWidgetItem(tr("(プラグインはありません)"))
215
+ item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
216
+ self.plugin_list.addItem(item)
217
+ return
218
+
219
+ for record in plugin_records:
220
+ name = record["name"]
221
+ info = record.get("info")
222
+ error = record.get("error")
223
+ disabled = record.get("disabled", False)
224
+
225
+ if disabled:
226
+ label = tr("{name} (無効化中)").format(name=name)
227
+ elif error:
228
+ label = tr("{name} — エラー: {error}").format(name=name, error=error)
229
+ elif info:
230
+ label = f"{info.get('name', name)} v{info.get('version', '?')} — {info.get('author', '?')}"
231
+ else:
232
+ label = name
233
+
234
+ item = QListWidgetItem(label)
235
+ item.setData(Qt.ItemDataRole.UserRole, name)
236
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
237
+ item.setCheckState(
238
+ Qt.CheckState.Unchecked if name in disabled_names else Qt.CheckState.Checked
239
+ )
240
+ self.plugin_list.addItem(item)
241
+
242
+ def _populate_hook_errors_list(self, plugin_registration_errors):
243
+ from graphica.core.i18n import tr
244
+ self.plugin_hook_errors_list.clear()
245
+ if not plugin_registration_errors:
246
+ item = QListWidgetItem(tr("(フック単位の登録エラーはありません)"))
247
+ item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
248
+ self.plugin_hook_errors_list.addItem(item)
249
+ return
250
+ for error in plugin_registration_errors:
251
+ text = f"[{error.plugin_name}] {error.hook_kind.value}: {error.message}"
252
+ self.plugin_hook_errors_list.addItem(QListWidgetItem(text))
253
+
254
+ def get_disabled_plugin_names(self):
255
+ """チェックを外したプラグイン名の集合(呼び出し側が保存し、次の起動から効く)。"""
256
+ disabled = set()
257
+ for i in range(self.plugin_list.count()):
258
+ item = self.plugin_list.item(i)
259
+ name = item.data(Qt.ItemDataRole.UserRole)
260
+ if name is not None and item.checkState() == Qt.CheckState.Unchecked:
261
+ disabled.add(name)
262
+ return disabled
263
+
264
+ def _refresh_file_association_state(self):
265
+ from graphica.core.i18n import tr
266
+ from graphica.gui import file_association
267
+
268
+ supported = file_association.is_supported()
269
+ registered = supported and file_association.is_registered()
270
+ if not supported:
271
+ text = tr("Windows の Graphica.exe から起動したときだけ登録できます。")
272
+ elif registered:
273
+ text = tr(".gra と .graphica のファイルは、ダブルクリックで Graphica が開きます。")
274
+ else:
275
+ text = tr("登録すると、.gra と .graphica のファイルをダブルクリックで開けます(この利用者だけ)。")
276
+ self.file_association_status_label.setText(text)
277
+ self.register_file_association_button.setEnabled(supported and not registered)
278
+ self.unregister_file_association_button.setEnabled(registered)
279
+
280
+ def _on_register_file_association(self):
281
+ from graphica.core.i18n import tr
282
+ from graphica.gui import file_association
283
+ try:
284
+ file_association.register()
285
+ except OSError as e:
286
+ notify.critical(self, tr("ファイルの関連付け"), tr("登録できませんでした:\n{error}").format(error=e))
287
+ self._refresh_file_association_state()
288
+
289
+ def _on_unregister_file_association(self):
290
+ from graphica.core.i18n import tr
291
+ from graphica.gui import file_association
292
+ try:
293
+ file_association.unregister()
294
+ except OSError as e:
295
+ notify.critical(self, tr("ファイルの関連付け"), tr("解除できませんでした:\n{error}").format(error=e))
296
+ self._refresh_file_association_state()
297
+
298
+ def _on_open_plugins_folder(self):
299
+ from graphica.core.app_paths import get_user_plugins_dir
300
+ QDesktopServices.openUrl(QUrl.fromLocalFile(get_user_plugins_dir()))
301
+
302
+ def _on_browse_autosave_dir(self):
303
+ from graphica.core.i18n import tr
304
+ directory = notify.get_existing_directory(
305
+ self, tr("オートセーブの保存先を選択"), self._autosave_dir or ""
306
+ )
307
+ if directory:
308
+ self._autosave_dir = directory
309
+ self.autosave_dir_edit.setText(directory)
310
+
311
+ def _on_clear_autosave_dir(self):
312
+ self._autosave_dir = ""
313
+ self.autosave_dir_edit.setText("")
314
+
315
+ def _on_install_plugin(self):
316
+ from graphica.core.i18n import tr
317
+ zip_path, _ = notify.get_open_file_name(
318
+ self, tr("プラグインをインストール"), "", tr("Zip files (*.zip)")
319
+ )
320
+ if not zip_path:
321
+ return
322
+
323
+ from graphica.core.plugin_install import install_plugin_zip, PluginInstallError
324
+ try:
325
+ installed_name = install_plugin_zip(zip_path)
326
+ except PluginInstallError as e:
327
+ notify.critical(self, tr("インストール失敗"), str(e))
328
+ return
329
+
330
+ notify.information(
331
+ self, tr("インストール完了"),
332
+ tr("プラグイン '{name}' をインストールしました。次回起動時に有効になります。").format(
333
+ name=installed_name
334
+ ),
335
+ )
336
+
337
+ def get_settings(self):
338
+ """(ダークモード, オートセーブの間隔(分、0 で無効), 言語, オートセーブ先(空なら既定),
339
+ 点のラベルの上限, グリッドへの吸着, グリッドの間隔(px))"""
340
+ language_code = self._language_codes[self.language_combo.currentIndex()]
341
+ return (self.dark_mode_checkbox.isChecked(), self.autosave_spinbox.value(),
342
+ language_code, self._autosave_dir, self.point_label_max_spinbox.value(),
343
+ self.snap_to_grid_checkbox.isChecked(), self.snap_grid_interval_spinbox.value())
344
+
345
+
346
+ class HelpDialog(QDialog):
347
+ """mathtext の簡単なリファレンス。"""
348
+
349
+ def __init__(self, parent=None):
350
+ super().__init__(parent)
351
+ self.setWindowTitle("mathtext クイックリファレンス")
352
+ self.resize(600, 700)
353
+
354
+ layout = QVBoxLayout(self)
355
+
356
+ text_browser = QTextBrowser()
357
+ text_browser.setReadOnly(True)
358
+ text_browser.setOpenExternalLinks(True)
359
+
360
+ help_html = r"""
361
+ <h1>Matplotlib Mathtext クイックリファレンス</h1>
362
+ <p>
363
+ <code>mathtext</code> は、プロットラベル(タイトル、軸ラベル、凡例など)に、LaTeXのような数式や特殊文字を簡単に入力するための機能です。
364
+ </p>
365
+ <p>
366
+ テキストボックスに入力する文字列を <b>$</b>(ドルマーク)で囲むと、その中身が数式として解釈されます。
367
+ </p>
368
+
369
+ <hr>
370
+
371
+ <h2>1. 基本的な書式 (太字・イタリック) 🎨</h2>
372
+ <p>ラベルの一部分、または全体を特定の書式に変更できます。</p>
373
+ <ul>
374
+ <li><b>太字 (Bold)</b>: <code>$\mathbf{...}$</code></li>
375
+ <li><b>イタリック (Italic)</b>: <code>$\mathit{...}$</code></li>
376
+ <li><b>立体 (Roman/標準)</b>: <code>$\mathrm{...}$</code></li>
377
+ </ul>
378
+
379
+ <h4>✅ 組み合わせの例</h4>
380
+ <p>「<b>Speed</b> (<i>v</i>)」と表示したい場合:</p>
381
+ <p><b>入力</b>: <code>$\mathbf{Speed}\ \mathit{(v)}$</code></p>
382
+ <p>
383
+ <ul>
384
+ <li><code>\ </code> (バックスラッシュ + スペース) は、数式内で強制的にスペースを入れたい場合に使います。</li>
385
+ </ul>
386
+ </p>
387
+ <p>「Velocity (m/s)」のように、イタリックにしたくない単位(立体にしたい)場合:</p>
388
+ <p><b>入力</b>: <code>$\mathrm{Velocity\ (m/s)}$</code></p>
389
+
390
+ <hr>
391
+
392
+ <h2>2. 上付き文字 と 下付き文字</h2>
393
+ <ul>
394
+ <li><b>上付き文字</b>: <code>^</code> (ハット)</li>
395
+ <li><b>下付き文字</b>: <code>_</code> (アンダースコア)</li>
396
+ </ul>
397
+ <p>文字が1文字以上の場合は、<code>{}</code>(中括弧)で囲みます。</p>
398
+
399
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
400
+ <tr class="header-row">
401
+ <th>表示したい文字</th>
402
+ <th>入力するテキスト</th>
403
+ </tr>
404
+ <tr>
405
+ <td>10<sup>3</sup></td>
406
+ <td><code>$10^3$</code></td>
407
+ </tr>
408
+ <tr>
409
+ <td>10<sup>-3</sup></td>
410
+ <td><code>$10^{-3}$</code></td>
411
+ </tr>
412
+ <tr>
413
+ <td>V<sub>max</sub></td>
414
+ <td><code>$V_{\mathrm{max}}$</code></td>
415
+ </tr>
416
+ <tr>
417
+ <td>k<sub>B</sub></td>
418
+ <td><code>$k_{\mathrm{B}}$</code></td>
419
+ </tr>
420
+ </table>
421
+
422
+ <hr>
423
+
424
+ <h2>3. よく使う特殊文字・記号 🇬🇷</h2>
425
+
426
+ <h4>ギリシャ文字</h4>
427
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
428
+ <tr class="header-row">
429
+ <th>表示したい文字</th>
430
+ <th>入力するテキスト</th>
431
+ </tr>
432
+ <tr><td>&alpha; (アルファ)</td><td><code>$\alpha$</code></td></tr>
433
+ <tr><td>&beta; (ベータ)</td><td><code>$\beta$</code></td></tr>
434
+ <tr><td>&gamma; (ガンマ)</td><td><code>$\gamma$</code></td></tr>
435
+ <tr><td>&Delta; (デルタ大文字)</td><td><code>$\Delta$</code></td></tr>
436
+ <tr><td>&delta; (デルタ小文字)</td><td><code>$\delta$</code></td></tr>
437
+ <tr><td>&epsilon; (イプシロン)</td><td><code>$\epsilon$</code></td></tr>
438
+ <tr><td>&mu; (ミュー)</td><td><code>$\mu$</code></td></tr>
439
+ <tr><td>&pi; (パイ)</td><td><code>$\pi$</code></td></tr>
440
+ <tr><td>&rho; (ロー)</td><td><code>$\rho$</code></td></tr>
441
+ <tr><td>&Sigma; (シグマ大文字)</td><td><code>$\Sigma$</code></td></tr>
442
+ <tr><td>&sigma; (シグマ小文字)</td><td><code>$\sigma$</code></td></tr>
443
+ <tr><td>&tau; (タウ)</td><td><code>$\tau$</code></td></tr>
444
+ <tr><td>&Omega; (オメガ大文字)</td><td><code>$\Omega$</code></td></tr>
445
+ <tr><td>&omega; (オメガ小文字)</td><td><code>$\omega$</code></td></tr>
446
+ </table>
447
+
448
+ <h4>単位・記号</h4>
449
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
450
+ <tr class="header-row">
451
+ <th>表示したい文字</th>
452
+ <th>入力するテキスト</th>
453
+ </tr>
454
+ <tr><td>°C (温度)</td><td><code>$^{\circ}$C</code></td></tr>
455
+ <tr><td>&pm; (プラスマイナス)</td><td><code>$\pm$</code></td></tr>
456
+ <tr><td>&middot; (中点ドット)</td><td><code>$\cdot$</code></td></tr>
457
+ <tr><td>&times; (掛ける)</td><td><code>$\times$</code></td></tr>
458
+ <tr><td>&approx; (ほぼイコール)</td><td><code>$\approx$</code></td></tr>
459
+ </table>
460
+
461
+ <hr>
462
+
463
+ <h2>4. 分数 と 根号</h2>
464
+ <ul>
465
+ <li><b>分数</b>: <code>$\frac{分子}{分母}$</code> (例: <code>$\frac{1}{2}$</code>)</li>
466
+ <li><b>平方根 (ルート)</b>: <code>$\sqrt{...}$</code> (例: <code>$\sqrt{2}$</code>)</li>
467
+ </ul>
468
+
469
+ <hr>
470
+
471
+ <h2>5. 総和・積分・極限などの演算子</h2>
472
+ <p>
473
+ <code>^</code>・<code>_</code>と組み合わせて、上下に添え字を付けられます。
474
+ </p>
475
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
476
+ <tr class="header-row">
477
+ <th>表示したい記号</th>
478
+ <th>入力するテキスト</th>
479
+ </tr>
480
+ <tr><td>&sum; (総和)</td><td><code>$\sum_{i=0}^{n}$</code></td></tr>
481
+ <tr><td>&prod; (総乗)</td><td><code>$\prod_{i=1}^{n}$</code></td></tr>
482
+ <tr><td>&int; (積分)</td><td><code>$\int_{0}^{\infty}$</code></td></tr>
483
+ <tr><td>&part; (偏微分)</td><td><code>$\partial$</code></td></tr>
484
+ <tr><td>&nabla; (ナブラ)</td><td><code>$\nabla$</code></td></tr>
485
+ <tr><td>&infin; (無限大)</td><td><code>$\infty$</code></td></tr>
486
+ </table>
487
+
488
+ <h4>矢印・比較演算子</h4>
489
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
490
+ <tr class="header-row">
491
+ <th>表示したい記号</th>
492
+ <th>入力するテキスト</th>
493
+ </tr>
494
+ <tr><td>&rarr; (右矢印)</td><td><code>$\rightarrow$</code></td></tr>
495
+ <tr><td>&harr; (両矢印)</td><td><code>$\leftrightarrow$</code></td></tr>
496
+ <tr><td>&le; (以下)</td><td><code>$\leq$</code></td></tr>
497
+ <tr><td>&ge; (以上)</td><td><code>$\geq$</code></td></tr>
498
+ <tr><td>&ne; (等しくない)</td><td><code>$\neq$</code></td></tr>
499
+ </table>
500
+
501
+ <h4>文字の上に記号を付ける</h4>
502
+ <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
503
+ <tr class="header-row">
504
+ <th>表示したい記号</th>
505
+ <th>入力するテキスト</th>
506
+ </tr>
507
+ <tr><td>x&#772; (上線・平均値など)</td><td><code>$\overline{x}$</code></td></tr>
508
+ <tr><td>x&#8407; (ベクトル)</td><td><code>$\vec{x}$</code></td></tr>
509
+ <tr><td>x&#770; (ハット)</td><td><code>$\hat{x}$</code></td></tr>
510
+ </table>
511
+
512
+ <p style="color:#888; margin-top: 12px;">
513
+ ※ ここに載っている記法は、いずれもタイトル/軸ラベル欄にそのまま
514
+ 半角文字で入力すれば動作します(<code>Aa</code>ボタンの装飾メニューに
515
+ あるΩボタンからも、よく使うギリシャ文字を選択部分の挿入なしでカーソル
516
+ 位置に差し込めます)。<br>
517
+ なお、これは matplotlib 内蔵の軽量な数式パーサー(mathtext)であり、
518
+ <code>\begin{matrix}</code> のような複雑なLaTeX環境や外部LaTeXパッケージ
519
+ には対応していません。
520
+ </p>
521
+ """
522
+ # 見出し行の色は HTML に書かず、テーマの色から注入する(固定の薄いグレーはダークモードで読めない)
523
+ self._text_browser = text_browser
524
+ self.refresh_theme()
525
+ text_browser.setHtml(help_html)
526
+
527
+ layout.addWidget(text_browser)
528
+
529
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
530
+
531
+ button_box.rejected.connect(self.reject)
532
+
533
+ layout.addWidget(button_box)
534
+
535
+ def refresh_theme(self):
536
+ """開いたままダークモードを切り替えられるので、見出し行の色を今のテーマで当て直す。"""
537
+ from graphica.gui import theme
538
+ _tokens = theme.current_tokens()
539
+ self._text_browser.document().setDefaultStyleSheet(
540
+ f"tr.header-row {{ background-color: {_tokens['surface_2']}; "
541
+ f"color: {_tokens['text_primary']}; }}"
542
+ )
543
+
544
+
545
+ class AboutDialog(QDialog):
546
+ """バージョンと、使っている OSS のライセンス表記。"""
547
+
548
+ def __init__(self, parent=None):
549
+ super().__init__(parent)
550
+ from graphica.core.version import APP_NAME, __version__
551
+ from graphica.core.i18n import tr
552
+
553
+ self.setWindowTitle(tr("{app} について").format(app=APP_NAME))
554
+ self.resize(420, 380)
555
+
556
+ layout = QVBoxLayout(self)
557
+
558
+ if parent is not None:
559
+ icon_label = QLabel()
560
+ pixmap = parent.windowIcon().pixmap(64, 64)
561
+ if not pixmap.isNull():
562
+ icon_label.setPixmap(pixmap)
563
+ icon_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
564
+ layout.addWidget(icon_label)
565
+
566
+ title_label = QLabel(f"<h2>{APP_NAME}</h2>")
567
+ title_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
568
+ layout.addWidget(title_label)
569
+
570
+ version_label = QLabel(f"{tr('バージョン')} {__version__}")
571
+ version_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
572
+ layout.addWidget(version_label)
573
+
574
+ credits_browser = QTextBrowser()
575
+ credits_browser.setOpenExternalLinks(True)
576
+ credits_browser.setHtml(r"""
577
+ <p>Graphica は、CSV/Excelファイルからデータを読み込み、グラフの作成・編集・
578
+ エクスポートを行うためのデータ可視化ソフトウェアです。</p>
579
+ <h3>ライセンス</h3>
580
+ <p>Graphica 本体は <b>MIT License</b> で提供しています
581
+ (Copyright (c) 2026 STsuruga)。ソースコードは
582
+ <a href="https://github.com/STsuruga/Graphica">GitHub</a> で公開しています。</p>
583
+ <h3>使用ライブラリ</h3>
584
+ <ul>
585
+ <li>PySide6 / Qt (LGPL v3)</li>
586
+ <li>Matplotlib (PSFベースのライセンス)</li>
587
+ <li>NumPy (BSD 3-Clause)</li>
588
+ <li>pandas (BSD 3-Clause)</li>
589
+ <li>SciPy (BSD 3-Clause)</li>
590
+ <li>openpyxl (MIT)</li>
591
+ <li>xlrd (BSD)</li>
592
+ <li>Tabler Icons (MIT) — アプリ内のアイコン</li>
593
+ </ul>
594
+ <p>このアプリに同梱している Qt / PySide6 は LGPL v3 です。同梱物の一覧と
595
+ 再配布時の注意は、配布物に含まれる <code>THIRD_PARTY_LICENSES.md</code> を
596
+ 参照してください。各ライブラリの詳細な条文は、それぞれの配布元をご確認ください。</p>
597
+ """)
598
+ layout.addWidget(credits_browser)
599
+
600
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
601
+ button_box.accepted.connect(self.accept)
602
+ layout.addWidget(button_box)
603
+
604
+
605
+ class WelcomeDialog(QDialog):
606
+ """初回の案内とスタートアップ画面。操作ガイド、サンプル、最近使ったファイル、書式テンプレートへの入口。
607
+
608
+ ファイルは扱わず、閉じた後に呼び出し側が load_sample_requested / selected_recent_file /
609
+ load_template_requested を見て処理する。
610
+ """
611
+
612
+ def __init__(self, parent=None, recent_files=None):
613
+ super().__init__(parent)
614
+ import os
615
+ from graphica.core.version import APP_NAME
616
+ from graphica.core.i18n import tr
617
+
618
+ self.setWindowTitle(tr("{app} へようこそ").format(app=APP_NAME))
619
+ self.resize(480, 560)
620
+ self.load_sample_requested = False
621
+ self.selected_recent_file = None
622
+ self.load_template_requested = False
623
+ self._recent_file_paths = list(recent_files) if recent_files else []
624
+
625
+ layout = QVBoxLayout(self)
626
+
627
+ if parent is not None:
628
+ icon_label = QLabel()
629
+ pixmap = parent.windowIcon().pixmap(64, 64)
630
+ if not pixmap.isNull():
631
+ icon_label.setPixmap(pixmap)
632
+ icon_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
633
+ layout.addWidget(icon_label)
634
+
635
+ title_label = QLabel(f"<h2>{tr('{app} へようこそ').format(app=APP_NAME)}</h2>")
636
+ title_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
637
+ layout.addWidget(title_label)
638
+
639
+ guide_browser = QTextBrowser()
640
+ guide_browser.setHtml(r"""
641
+ <p>{app}は、CSV/Excelファイルからデータを読み込み、グラフの作成・編集・
642
+ エクスポートを行うためのデータ可視化ソフトウェアです。</p>
643
+ <h3>はじめの一歩</h3>
644
+ <ol>
645
+ <li>画面下部の<b>「データ追加」</b>ボタン、または「ファイル」メニューの「データファイルを開く」でCSV/Excelファイルを読み込む
646
+ (下の「サンプルデータを開く」からすぐに試すこともできます)</li>
647
+ <li>X軸・Y軸に使う列を選択する</li>
648
+ <li>右側の「データセットのプロパティ」パネルで色・線種・マーカーなどを調整する</li>
649
+ <li>「曲線フィット」「ピーク検出」などの解析機能を試す</li>
650
+ <li>「ファイル」メニューの「エクスポート」▸「名前を付けてエクスポート」で画像として保存する</li>
651
+ </ol>
652
+ <p>この画面は初回起動時に表示されます。「ファイル」メニューの
653
+ 「スタートアップ画面...」からいつでも開けます。</p>
654
+ """.format(app=APP_NAME))
655
+ guide_browser.setMaximumHeight(180)
656
+ layout.addWidget(guide_browser)
657
+
658
+ # 履歴が無ければ(初回など)節ごと出さない
659
+ if self._recent_file_paths:
660
+ recent_group = QGroupBox(tr("最近使ったファイル"))
661
+ recent_layout = QVBoxLayout(recent_group)
662
+ self.recent_list = QListWidget()
663
+ for path in self._recent_file_paths:
664
+ item = QListWidgetItem(os.path.basename(path))
665
+ item.setToolTip(path)
666
+ self.recent_list.addItem(item)
667
+ self.recent_list.itemDoubleClicked.connect(self._on_recent_item_double_clicked)
668
+ recent_layout.addWidget(self.recent_list)
669
+ open_recent_button = QPushButton(tr("選択したファイルを開く"))
670
+ open_recent_button.clicked.connect(self._on_open_recent_clicked)
671
+ recent_layout.addWidget(open_recent_button)
672
+ layout.addWidget(recent_group)
673
+ else:
674
+ self.recent_list = None
675
+
676
+ button_row = QHBoxLayout()
677
+ self.load_sample_button = QPushButton(tr("サンプルデータを開く"))
678
+ self.load_sample_button.clicked.connect(self._on_load_sample_clicked)
679
+ button_row.addWidget(self.load_sample_button)
680
+
681
+ self.load_template_button = QPushButton(tr("書式テンプレートを開く..."))
682
+ self.load_template_button.clicked.connect(self._on_load_template_clicked)
683
+ button_row.addWidget(self.load_template_button)
684
+ button_row.addStretch()
685
+
686
+ close_button = QPushButton(tr("閉じる"))
687
+ close_button.clicked.connect(self.accept)
688
+ button_row.addWidget(close_button)
689
+ layout.addLayout(button_row)
690
+
691
+ def _on_load_sample_clicked(self):
692
+ self.load_sample_requested = True
693
+ self.accept()
694
+
695
+ def _on_open_recent_clicked(self):
696
+ row = self.recent_list.currentRow() if self.recent_list is not None else -1
697
+ if row < 0:
698
+ return
699
+ self.selected_recent_file = self._recent_file_paths[row]
700
+ self.accept()
701
+
702
+ def _on_recent_item_double_clicked(self, item):
703
+ row = self.recent_list.row(item)
704
+ self.selected_recent_file = self._recent_file_paths[row]
705
+ self.accept()
706
+
707
+ def _on_load_template_clicked(self):
708
+ self.load_template_requested = True
709
+ self.accept()
710
+
711
+
712
+ class ShortcutsDialog(QDialog):
713
+ """キーボードショートカットの一覧。メニューから集めるので、追加や変更で直し忘れない。"""
714
+
715
+ def __init__(self, collect_fn, parent=None):
716
+ """collect_fn は呼ぶたびにメニューの [(パス, QAction), ...] を集め直す関数。"""
717
+ super().__init__(parent)
718
+ self.setWindowTitle("キーボードショートカット一覧")
719
+ self.resize(420, 380)
720
+
721
+ layout = QVBoxLayout(self)
722
+
723
+ table = QTableWidget()
724
+ table.setColumnCount(2)
725
+ table.setHorizontalHeaderLabels(["操作", "ショートカット"])
726
+ table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
727
+ table.setSelectionMode(QTableWidget.SelectionMode.NoSelection)
728
+ table.verticalHeader().setVisible(False)
729
+
730
+ rows = []
731
+ for path, action in collect_fn():
732
+ # 属性で持っていない QAction は、集めた直後でも PySide6 が無効にしていることがある
733
+ try:
734
+ shortcut = action.shortcut()
735
+ except RuntimeError:
736
+ continue
737
+ if shortcut.isEmpty():
738
+ continue
739
+ rows.append((" > ".join(path), shortcut.toString(QKeySequence.SequenceFormat.NativeText)))
740
+
741
+ table.setRowCount(len(rows))
742
+ for row_idx, (label, shortcut_text) in enumerate(rows):
743
+ table.setItem(row_idx, 0, QTableWidgetItem(label))
744
+ table.setItem(row_idx, 1, QTableWidgetItem(shortcut_text))
745
+ table.resizeColumnsToContents()
746
+ table.horizontalHeader().setStretchLastSection(True)
747
+ layout.addWidget(table)
748
+
749
+ close_button = QPushButton("閉じる")
750
+ close_button.clicked.connect(self.reject)
751
+ layout.addWidget(close_button, alignment=Qt.AlignmentFlag.AlignRight)
752
+
753
+
754
+ class CommandPaletteDialog(QDialog):
755
+ """メニューの項目を検索して実行する(Ctrl+Shift+P)。
756
+
757
+ 属性で持っていない QAction は PySide6 が無効にすることがあるので、一覧にはパスだけを持ち、
758
+ 実行する直前に collect_fn() で取り直してすぐ trigger() する。
759
+ """
760
+
761
+ def __init__(self, collect_fn, parent=None):
762
+ """collect_fn は ShortcutsDialog と同じ。"""
763
+ super().__init__(parent)
764
+ self.setWindowTitle("コマンドパレット")
765
+ self.resize(480, 420)
766
+
767
+ self._collect_fn = collect_fn
768
+
769
+ layout = QVBoxLayout(self)
770
+ self.search_edit = QLineEdit()
771
+ self.search_edit.setPlaceholderText("コマンドを検索...")
772
+ layout.addWidget(self.search_edit)
773
+
774
+ self.list_widget = QListWidget()
775
+ layout.addWidget(self.list_widget)
776
+
777
+ self.search_edit.textChanged.connect(self._update_list)
778
+ self.list_widget.itemActivated.connect(self._on_item_activated)
779
+ self.search_edit.installEventFilter(self)
780
+
781
+ self._update_list("")
782
+
783
+ def _update_list(self, text):
784
+ query = text.strip().lower()
785
+ self.list_widget.clear()
786
+ for path, action in self._collect_fn():
787
+ if not action.isEnabled():
788
+ continue
789
+ label = " > ".join(path)
790
+ if query and query not in label.lower():
791
+ continue
792
+ item = QListWidgetItem(label)
793
+ if action.isCheckable():
794
+ item.setText(f"{'✓' if action.isChecked() else ' '} {label}")
795
+ item.setData(Qt.ItemDataRole.UserRole, path)
796
+ self.list_widget.addItem(item)
797
+ if self.list_widget.count() > 0:
798
+ self.list_widget.setCurrentRow(0)
799
+
800
+ def _on_item_activated(self, item):
801
+ target_path = item.data(Qt.ItemDataRole.UserRole)
802
+ self.accept()
803
+ if target_path is None:
804
+ return
805
+ for path, action in self._collect_fn():
806
+ if path == target_path:
807
+ action.trigger()
808
+ return
809
+
810
+ def eventFilter(self, obj, event):
811
+ """検索欄にフォーカスがあるまま、上下キーで選択を動かし Enter で実行する。"""
812
+ if obj is self.search_edit and event.type() == QEvent.Type.KeyPress:
813
+ key = event.key()
814
+ if key == Qt.Key.Key_Down:
815
+ row = min(self.list_widget.currentRow() + 1, self.list_widget.count() - 1)
816
+ self.list_widget.setCurrentRow(row)
817
+ return True
818
+ elif key == Qt.Key.Key_Up:
819
+ row = max(self.list_widget.currentRow() - 1, 0)
820
+ self.list_widget.setCurrentRow(row)
821
+ return True
822
+ elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
823
+ current = self.list_widget.currentItem()
824
+ if current is not None:
825
+ self._on_item_activated(current)
826
+ return True
827
+ return super().eventFilter(obj, event)
828
+
829
+
830
+ class QuickAccessManagerDialog(QDialog):
831
+ """クイックアクセスのピン留めを一覧から選ぶ。チェックの変更はその場で反映する(閉じるボタンだけ)。"""
832
+
833
+ def __init__(self, collect_fn, is_pinned_fn, toggle_fn, parent=None):
834
+ """collect_fn は CommandPaletteDialog と同じ。is_pinned_fn(id) -> bool、toggle_fn(id, path, checked)。"""
835
+ super().__init__(parent)
836
+ from graphica.core.i18n import tr
837
+
838
+ self.setWindowTitle(tr("クイックアクセスの管理"))
839
+ self.resize(480, 420)
840
+
841
+ self._collect_fn = collect_fn
842
+ self._is_pinned_fn = is_pinned_fn
843
+ self._toggle_fn = toggle_fn
844
+ self._updating = False # _update_list() がチェックを入れている間は itemChanged を無視する
845
+
846
+ layout = QVBoxLayout(self)
847
+
848
+ info_label = QLabel(tr("チェックした項目がクイックアクセスツールバーに表示されます。"))
849
+ info_label.setWordWrap(True)
850
+ layout.addWidget(info_label)
851
+
852
+ self.search_edit = QLineEdit()
853
+ self.search_edit.setPlaceholderText(tr("コマンドを検索..."))
854
+ layout.addWidget(self.search_edit)
855
+
856
+ self.list_widget = QListWidget()
857
+ layout.addWidget(self.list_widget)
858
+
859
+ self.search_edit.textChanged.connect(self._update_list)
860
+ self.list_widget.itemChanged.connect(self._on_item_changed)
861
+
862
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
863
+ button_box.rejected.connect(self.accept) # Close は RejectRole なので rejected が出る
864
+ layout.addWidget(button_box)
865
+
866
+ self._update_list("")
867
+
868
+ def _update_list(self, text):
869
+ query = text.strip().lower()
870
+ self._updating = True
871
+ try:
872
+ self.list_widget.clear()
873
+ for path, action in self._collect_fn():
874
+ if action.isSeparator():
875
+ continue
876
+ label = " > ".join(path)
877
+ if query and query not in label.lower():
878
+ continue
879
+ ident = " > ".join(path)
880
+ item = QListWidgetItem(label)
881
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
882
+ item.setData(Qt.ItemDataRole.UserRole, (ident, path))
883
+ item.setCheckState(
884
+ Qt.CheckState.Checked if self._is_pinned_fn(ident) else Qt.CheckState.Unchecked
885
+ )
886
+ self.list_widget.addItem(item)
887
+ finally:
888
+ self._updating = False
889
+
890
+ def _on_item_changed(self, item):
891
+ if self._updating:
892
+ return
893
+ ident, path = item.data(Qt.ItemDataRole.UserRole)
894
+ checked = item.checkState() == Qt.CheckState.Checked
895
+ self._toggle_fn(ident, path, checked)
896
+
897
+
898
+ class AutosaveHistoryDialog(QDialog):
899
+ """オートセーブの世代を一覧にして、選んだファイルのパスを返す(復元は呼び出し側)。"""
900
+
901
+ def __init__(self, generations, parent=None):
902
+ """generations は (パス, 世代のラベル, 更新日時) の新しい順のリスト。"""
903
+ super().__init__(parent)
904
+ self.setWindowTitle("自動バックアップ履歴")
905
+ self.resize(480, 320)
906
+
907
+ layout = QVBoxLayout(self)
908
+ layout.addWidget(QLabel("復元する世代を選んでください(現在の未保存の変更は失われます):"))
909
+
910
+ self.history_list = QListWidget()
911
+ for path, label, mtime_text in generations:
912
+ item = QListWidgetItem(f"{label} — {mtime_text}")
913
+ item.setData(Qt.ItemDataRole.UserRole, path)
914
+ item.setToolTip(path)
915
+ self.history_list.addItem(item)
916
+ self.history_list.itemDoubleClicked.connect(self._on_double_clicked)
917
+ layout.addWidget(self.history_list)
918
+
919
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
920
+ QDialogButtonBox.StandardButton.Cancel)
921
+ button_box.accepted.connect(self.accept)
922
+ button_box.rejected.connect(self.reject)
923
+ self.ok_button = button_box.button(QDialogButtonBox.StandardButton.Ok)
924
+ self.ok_button.setEnabled(False)
925
+ self.history_list.currentItemChanged.connect(
926
+ lambda current, _previous: self.ok_button.setEnabled(current is not None)
927
+ )
928
+ layout.addWidget(button_box)
929
+
930
+ apply_form_spacing(self)
931
+
932
+ def _on_double_clicked(self, item):
933
+ self.history_list.setCurrentItem(item)
934
+ self.accept()
935
+
936
+ def get_selected_path(self):
937
+ item = self.history_list.currentItem()
938
+ return item.data(Qt.ItemDataRole.UserRole) if item is not None else None
939
+
940
+
941
+ class PluginParamDialog(QDialog):
942
+ """プラグインの param_schema から入力欄を作る。
943
+
944
+ 各要素は {"name", "type": "int"|"float"|"str"|"bool"|"choice"} と、任意で "label" / "default" /
945
+ "min" / "max" / "choices" / "decimals"(float、既定 4)。
946
+ """
947
+
948
+ _INT_RANGE = (-2_147_483_647, 2_147_483_647) # QSpinBox の範囲
949
+
950
+ def __init__(self, title, param_schema, parent=None):
951
+ super().__init__(parent)
952
+ self.setWindowTitle(title)
953
+ self._widgets = {} # name -> (type, widget)
954
+
955
+ layout = QVBoxLayout(self)
956
+ form = QFormLayout()
957
+ for spec in param_schema:
958
+ name = spec["name"]
959
+ label = spec.get("label", name)
960
+ ptype = spec.get("type", "str")
961
+ default = spec.get("default")
962
+ widget = self._build_widget(ptype, spec, default)
963
+ self._widgets[name] = (ptype, widget)
964
+ form.addRow(label, widget)
965
+ layout.addLayout(form)
966
+
967
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
968
+ QDialogButtonBox.StandardButton.Cancel)
969
+ button_box.accepted.connect(self.accept)
970
+ button_box.rejected.connect(self.reject)
971
+ layout.addWidget(button_box)
972
+
973
+ apply_form_spacing(self)
974
+
975
+ def _build_widget(self, ptype, spec, default):
976
+ if ptype == "int":
977
+ widget = QSpinBox()
978
+ widget.setRange(spec.get("min", self._INT_RANGE[0]), spec.get("max", self._INT_RANGE[1]))
979
+ widget.setValue(int(default) if default is not None else 0)
980
+ return widget
981
+ if ptype == "float":
982
+ widget = QDoubleSpinBox()
983
+ widget.setDecimals(spec.get("decimals", 4))
984
+ widget.setRange(spec.get("min", -1e12), spec.get("max", 1e12))
985
+ widget.setValue(float(default) if default is not None else 0.0)
986
+ return widget
987
+ if ptype == "bool":
988
+ widget = QCheckBox()
989
+ widget.setChecked(bool(default))
990
+ return widget
991
+ if ptype == "choice":
992
+ widget = QComboBox()
993
+ widget.addItems([str(c) for c in spec.get("choices", [])])
994
+ if default is not None:
995
+ widget.setCurrentText(str(default))
996
+ return widget
997
+ # "str" と未知の型は文字の欄
998
+ widget = QLineEdit()
999
+ if default is not None:
1000
+ widget.setText(str(default))
1001
+ return widget
1002
+
1003
+ def get_values(self):
1004
+ """{パラメータ名: 値}。型は param_schema の type に従う。"""
1005
+ values = {}
1006
+ for name, (ptype, widget) in self._widgets.items():
1007
+ if ptype in ("int", "float"):
1008
+ values[name] = widget.value()
1009
+ elif ptype == "bool":
1010
+ values[name] = widget.isChecked()
1011
+ elif ptype == "choice":
1012
+ values[name] = widget.currentText()
1013
+ else:
1014
+ values[name] = widget.text()
1015
+ return values