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,1151 @@
1
+ import os
2
+ import re
3
+ import types
4
+ import sys
5
+ import logging
6
+ import numpy as np
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # 入力途中の "1e" や "1e-" も通す
11
+ _SCIENTIFIC_INPUT_RE = re.compile(r'^[+-]?(\d+\.?\d*|\.\d+)?([eE][+-]?\d*)?$')
12
+
13
+
14
+ def _scientific_text_from_value(self, value):
15
+ """末尾のゼロを落とし、必要なら指数表記にする(.16g は float64 の有効桁数)。"""
16
+ return f'{value:.16g}'
17
+
18
+
19
+ def _scientific_validate(self, text, pos):
20
+ """QDoubleSpinBox の validate() は "1e-" のような入力途中を弾き、指数表記を打ち込めないので上書きする。"""
21
+ if text == '' or text in ('-', '+'):
22
+ return (QValidator.State.Intermediate, text, pos)
23
+ if _SCIENTIFIC_INPUT_RE.match(text):
24
+ try:
25
+ float(text)
26
+ return (QValidator.State.Acceptable, text, pos)
27
+ except ValueError:
28
+ return (QValidator.State.Intermediate, text, pos)
29
+ return (QValidator.State.Invalid, text, pos)
30
+
31
+
32
+ def _enable_scientific_notation_input(spin_box, minimum, maximum, single_step=0.1):
33
+ spin_box.setDecimals(SPIN_BOX_MAX_DECIMALS)
34
+ spin_box.setRange(minimum, maximum)
35
+ spin_box.textFromValue = types.MethodType(_scientific_text_from_value, spin_box)
36
+ spin_box.validate = types.MethodType(_scientific_validate, spin_box)
37
+ spin_box.setSingleStep(single_step)
38
+
39
+
40
+ def _strip_trailing_colon_from_labels(widget):
41
+ """ラベル末尾の「:」を取る。生成物の ui_main_window.py に焼き込まれていて、.ui が無く作り直せない。
42
+
43
+ 動的に足したラベルも含むよう、画面を組み立て終えてから呼ぶ。
44
+ """
45
+ for label in widget.findChildren(QLabel):
46
+ text = label.text()
47
+ if text.endswith(':'):
48
+ label.setText(text[:-1])
49
+
50
+ DEFAULT_WINDOW_WIDTH = 1280
51
+ DEFAULT_WINDOW_HEIGHT = 800
52
+ CONTROL_DOCK_WIDTH = 472 # 縦スクロールバーの分まで含めた幅。狭めると横スクロールバーが出る
53
+ SPIN_BOX_MAX_DECIMALS = 16
54
+
55
+
56
+ DEFAULT_DETACHED_CANVAS_WIDTH = 900
57
+ DEFAULT_DETACHED_CANVAS_HEIGHT = 700
58
+
59
+
60
+
61
+
62
+
63
+ AUTOSAVE_FILENAME = "autosave.gra"
64
+
65
+
66
+ from graphica.gui import notify
67
+
68
+
69
+
70
+
71
+ from PySide6.QtWidgets import (QMainWindow, QWidget,
72
+ QDockWidget, QLabel, QTreeWidgetItem)
73
+ from PySide6.QtGui import QFont, QIcon, QValidator, QUndoStack
74
+ from PySide6.QtCore import Qt, QTimer, Signal
75
+ from graphica.models.project import ProjectModel
76
+ from graphica.core.version import APP_NAME, __version__
77
+ from graphica.core.i18n import tr, set_language
78
+ from graphica.core.plugin_api import load_plugins_once
79
+ from graphica.core.plugin_types import PluginExecutionError
80
+ from graphica.gui import app_settings
81
+ from graphica.gui.app_settings import disabled_plugin_names
82
+ from graphica.gui.datasets.colors import ColorController
83
+ from graphica.gui.datasets.transfer import TransferController
84
+ from graphica.gui.datasets.overlays import OverlayController
85
+ from graphica.gui.datasets.plugin_runs import PluginRunController
86
+ from graphica.gui.datasets.property_panel import DatasetPropertyPanel
87
+ from graphica.gui.datasets.fitting import FittingController
88
+ from graphica.gui.datasets.host import DatasetHost
89
+ from graphica.gui.datasets.order import DatasetOrder
90
+ from graphica.gui.panels import create_panels, expose_panel_names
91
+ from graphica.gui.widget_translation import translate_widget_texts
92
+ from graphica.gui.tools import MOUSE_MODES, ToolManager, create_tools, expose_tool_names
93
+ from graphica.gui import data_import_flow
94
+ # テストがこのモジュールの属性として引くので残す(使うコードは data_import_flow と project_files に移した)
95
+ import pandas as pd # noqa: F401
96
+ from PySide6.QtWidgets import QMessageBox # noqa: F401
97
+ from graphica.core.app_paths import get_app_data_dir # noqa: F401
98
+ from graphica.gui import project_files
99
+ from graphica.gui.data_import_flow import ( # noqa: F401
100
+ SUPPORTED_DATA_FILE_EXTENSIONS, find_unevaluated_formula_cells)
101
+ from graphica.gui.project_files import ( # noqa: F401
102
+ AUTOSAVE_GENERATIONS, MAX_RECENT_FILES, UNSAVED_CHANGES_PROMPT_ENV, _unsaved_changes_prompt_enabled)
103
+ from graphica.gui.builders import axis_panel
104
+ from graphica.gui.builders import canvas_area
105
+ from graphica.gui.builders import dataset_panel
106
+ from graphica.gui.builders import property_sections
107
+ from graphica.gui import dock_layout
108
+ from graphica.gui.builders.common import ( # noqa: F401
109
+ EXPORT_PREVIEW_DOCK_INITIAL_HEIGHT, DATASET_TREE_VISIBILITY_COLUMN_WIDTH, DOCK_LAYOUT_VERSION, DATASET_PROPERTY_SECTIONS, PLOT_TYPE_COMBO_MIN_CHARS, COLORMAP_CHOICES, TOOLBAR_ICON_SIZE, _svg_icon, _DatasetTreeSelectionDelegate, _ClickableMathPreviewLabel, _insert_form_row_after)
110
+ from graphica.gui.resources import resource_path
111
+ from graphica.gui.axis_bindings import AXIS_BINDINGS, AXIS_BUTTONS
112
+ from graphica.gui.binding import Binder
113
+ from graphica.gui.datasets.peaks import PeakController
114
+ from graphica.gui.datasets.processing import ProcessingController
115
+ from graphica.gui.plugin_context import TabPluginContext
116
+ from graphica.core.app_paths import get_user_plugins_dir
117
+
118
+
119
+ from graphica.ui_main_window import Ui_MainWindow
120
+
121
+ from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
122
+ from graphica.core.commands import AddDatasetCommand, RemoveDatasetCommand
123
+ from graphica.gui.detached_canvas_window import DetachedCanvasWindow
124
+ from graphica.gui import theme
125
+ from graphica.gui.theme import apply_form_spacing
126
+ from graphica.gui.dialogs import (WelcomeDialog)
127
+
128
+
129
+ # 文字装飾パネルの記号。(表示, mathtext のマクロ名)。引数の要るマクロ(\sqrt{...})は
130
+ # 「$\macro$」で挿入できないので入れない(平方根は \surd)。
131
+ LABEL_SYMBOL_PALETTE = [
132
+ ("α", "alpha"), ("β", "beta"), ("γ", "gamma"), ("δ", "delta"),
133
+ ("ε", "epsilon"), ("μ", "mu"), ("π", "pi"), ("ρ", "rho"),
134
+ ("Σ", "Sigma"), ("σ", "sigma"), ("τ", "tau"), ("Ω", "Omega"),
135
+ ("ω", "omega"), ("Δ", "Delta"), ("θ", "theta"), ("φ", "phi"),
136
+ ("×", "times"), ("÷", "div"), ("±", "pm"), ("∓", "mp"),
137
+ ("≈", "approx"), ("≠", "neq"), ("≤", "leq"), ("≥", "geq"),
138
+ ("∞", "infty"), ("→", "rightarrow"), ("←", "leftarrow"), ("∂", "partial"),
139
+ ("∇", "nabla"), ("∫", "int"), ("∝", "propto"), ("°", "degree"),
140
+ # マイナス記号(U+2212)は mathtext で包まず、文字のまま入れる
141
+ ("−", None),
142
+ ]
143
+
144
+
145
+
146
+
147
+
148
+
149
+ from graphica.gui.dataset_style_icon import (
150
+ make_dataset_style_icon, make_dataset_visibility_icon, apply_dataset_visibility_text_style,
151
+ DATASET_TREE_VISIBILITY_COLUMN,
152
+ )
153
+ from graphica.gui.mathtext_preview import JP_CAPABLE_FONT_FAMILIES
154
+ from graphica.gui.color_history import load_recent_colors_into_picker
155
+
156
+ # グラフの既定フォント。UI のフォント("Yu Gothic UI" など)は matplotlib が解決できず文字化けする。
157
+ # "Yu Gothic" は Windows だけにあるので、OS ごとの候補を並べたリストを QFont と matplotlib の両方に渡す
158
+ # (無いフォント名は飛ばされる)。
159
+ PLOT_DEFAULT_FONT_FAMILIES = JP_CAPABLE_FONT_FAMILIES
160
+
161
+
162
+ def _make_default_plot_font():
163
+ """QFont(str) は1つの名前しか取らないので、setFamilies() で候補を全部入れる。"""
164
+ font = QFont()
165
+ font.setFamilies(PLOT_DEFAULT_FONT_FAMILIES)
166
+ return font
167
+
168
+ from graphica.gui.mixins.ui_setup_mixin import UISetupMixin
169
+ from graphica.gui.mixins.export_mixin import ExportMixin
170
+ from graphica.gui.mixins.project_io_mixin import ProjectIOMixin
171
+ from graphica.gui.mixins.help_mixin import HelpMixin
172
+ from graphica.gui.mixins.quick_access_mixin import QuickAccessMixin
173
+
174
+
175
+ def is_frozen():
176
+ return hasattr(sys, '_MEIPASS')
177
+
178
+
179
+ def plugin_search_paths():
180
+ """プラグインの探索先(優先順)。同梱の plugins と、利用者のフォルダ(%LOCALAPPDATA%\\Graphica\\plugins)。
181
+
182
+ 同梱のフォルダはソースから動かすときだけあるので、あるときだけ加える
183
+ (探索先は起動時に作られるので、無いものを渡すと site-packages の中に作ろうとする)。
184
+ """
185
+ paths = []
186
+ bundled = resource_path("plugins")
187
+ if not is_frozen() and os.path.isdir(bundled):
188
+ paths.append(bundled)
189
+ paths.append(get_user_plugins_dir())
190
+ return paths
191
+
192
+
193
+ # core は Qt に依存しないので、area の文字列から Qt の値への変換はここでする
194
+ _PLUGIN_PANEL_AREA_MAP = {
195
+ "right": Qt.DockWidgetArea.RightDockWidgetArea,
196
+ "left": Qt.DockWidgetArea.LeftDockWidgetArea,
197
+ "top": Qt.DockWidgetArea.TopDockWidgetArea,
198
+ "bottom": Qt.DockWidgetArea.BottomDockWidgetArea,
199
+ }
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+ # 1つのタブ。機能ごとの mixin の役割は CLAUDE.md の「PlotterApp mixin composition」
208
+ class PlotterApp(QMainWindow, UISetupMixin,
209
+ ExportMixin, ProjectIOMixin, HelpMixin, QuickAccessMixin):
210
+ # 保存・読み込みのたびに出す。MainAppWindow がタブ名を更新する
211
+ project_state_changed = Signal()
212
+
213
+ MOUSE_MODES = MOUSE_MODES
214
+
215
+ def _deactivate_other_mouse_modes(self, active_name):
216
+ return self.mouse_tool_manager.deactivate_others(active_name)
217
+
218
+ def _active_mouse_mode(self):
219
+ return self.mouse_tool_manager.active_name()
220
+
221
+ def __init__(self, run_startup_checks=True, tab_id=None):
222
+ """
223
+ 画面を組み立てる。組み立ての各段は後の段が前の段で作った部品を使うので、呼ぶ順番は変えない。
224
+
225
+ Args:
226
+ run_startup_checks (bool): オートセーブからの復元確認・初回の案内・ドック配置の復元と保存・
227
+ clean_exit の管理をするか。アプリ全体で1回だけ意味を持つので、2つ目以降のタブでは False。
228
+ tab_id (int, optional): 2つ目以降のタブの番号。オートセーブのファイル名が重ならないようにする。
229
+ None なら autosave.gra。
230
+ """
231
+ super().__init__()
232
+ self._run_startup_checks = run_startup_checks
233
+ self.tab_id = tab_id
234
+ # 保存先のフォルダは self.settings を作った後に _update_autosave_path で決める
235
+ self._autosave_base_filename = (
236
+ AUTOSAVE_FILENAME if not tab_id
237
+ else f"autosave_tab{tab_id}{os.path.splitext(AUTOSAVE_FILENAME)[1]}"
238
+ )
239
+ self._autosave_filename = self._autosave_base_filename
240
+
241
+ self._load_designer_ui()
242
+ self._init_state()
243
+ self._setup_window()
244
+ self._build_canvas_and_toolbar()
245
+ self._setup_status_bar()
246
+ self._build_dataset_color_picker()
247
+ self._build_dataset_list_buttons()
248
+ self._build_dataset_style_controls()
249
+ self._build_legend_location_control()
250
+ self._build_fit_info_and_stats()
251
+ self._build_secondary_y_controls()
252
+ self._build_tick_grid_and_colorbar_controls()
253
+ self._build_tick_format_controls()
254
+ self._build_label_editors()
255
+ self._add_subplot_target_row()
256
+ self._arrange_property_docks()
257
+ self._rebuild_label_tab()
258
+ self._connect_and_initialize()
259
+ # 英語表示のとき、組み立てた画面の文字をまとめて訳す(Designer の部品にも効かせる)
260
+ translate_widget_texts(self)
261
+
262
+ def _load_designer_ui(self):
263
+ self.ui = Ui_MainWindow()
264
+ self.ui.setupUi(self)
265
+
266
+ # ui_main_window.py は生成物なので、Designer に無い種類はここで足す
267
+ self.ui.plot_type_combo.addItem("Area")
268
+ self.ui.plot_type_combo.addItem("Bar")
269
+ self.ui.plot_type_combo.addItem("Step")
270
+ self.ui.plot_type_combo.addItem("Density Scatter")
271
+ self.ui.plot_type_combo.addItem(COLOR_BY_COLUMN_PLOT_TYPE)
272
+
273
+ # フォルダ分けのため、Designer の QListWidget を同じ位置の QTreeWidget に差し替える
274
+ self._replace_dataset_list_with_tree()
275
+
276
+ self.project = ProjectModel()
277
+ # 新しい変更の経路が project.notify_changed() だけで描き直せるように(既存の箇所は _update_plot を直接呼ぶ)
278
+ self.project.changed.connect(self._update_plot)
279
+ self.settings = app_settings.open_settings()
280
+ self._update_autosave_path()
281
+ load_recent_colors_into_picker(self.settings)
282
+
283
+ # 以降に作るメニューやボタンの tr() に効くので、画面を組み立てる前に
284
+ set_language(app_settings.LANGUAGE.read(self.settings))
285
+
286
+ # 起動時に False にし、正常に閉じたときだけ closeEvent で True に戻す。次の起動で False なら異常終了とみなし、
287
+ # オートセーブからの復元を勧める。アプリ全体で1つの値なので、2つ目以降のタブは触らない
288
+ if self._run_startup_checks:
289
+ self._had_clean_exit = app_settings.CLEAN_EXIT.read(self.settings)
290
+ app_settings.CLEAN_EXIT.write(self.settings, False)
291
+ else:
292
+ self._had_clean_exit = True
293
+
294
+ self.setWindowTitle(f"{APP_NAME} {__version__}")
295
+ icon_path = resource_path("Graphica.ico")
296
+ self.setWindowIcon(QIcon(icon_path))
297
+ self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.ui.control_dock_widget)
298
+
299
+ def _init_state(self):
300
+ # 軸の設定とデータセットの一覧の部品(状態はタブが持つ。gui/panels/)
301
+ self.panels = create_panels(self)
302
+ # 非モーダルのダイアログとバックグラウンドの処理は、回収されないよう参照を持っておく
303
+ self.data_editor_dialog = None
304
+ self.help_dialog = None
305
+ self.calc_help_dialog = None
306
+ self._data_load_task_runner = None
307
+ self._batch_export_task_runner = None
308
+ self._update_check_task_runner = None
309
+ # 複数ファイルをまとめて読み込むときの待ち行列と進捗
310
+ self._data_load_queue = []
311
+ self._data_load_queue_total = 0
312
+ self._data_load_queue_done = 0
313
+ # フォルダからの一括取り込みで、ファイル名から列を取り出す正規表現。待ち行列を使い切ったら None に戻す
314
+ self._batch_import_filename_regex = None
315
+ # 上書き保存先。None なら manual_save() は「名前を付けて保存」になる。
316
+ # タブ名もこの値から作る(ProjectModel.current_filepath はオートセーブでも変わる)。
317
+ self._current_project_path = None
318
+ self._restored_unsaved = False # オートセーブから復元し、まだ保存していない
319
+ # 直前に保存/読み込みした時点の内容のハッシュ(未保存の変更の判定)。None はファイルと対応していない
320
+ self._saved_content_fingerprint = None
321
+
322
+ # データエディタのセル編集の Undo とは別
323
+ self.undo_stack = QUndoStack(self)
324
+
325
+ # データセットに対する操作を機能ごとに分けたクラス。窓口(DatasetHost)は本体に使うときに触れるので、
326
+ # 画面を組み立てる前に作っておける(組み立ての途中からも使われる)。
327
+ self.dataset_order = DatasetOrder(
328
+ self.project, lambda: self.ui.dataset_list_widget, self._add_dataset_list_item)
329
+ self._dataset_host = DatasetHost(self)
330
+ self.peaks = PeakController(self._dataset_host)
331
+ self.fitting = FittingController(self._dataset_host)
332
+ self.processing = ProcessingController(self._dataset_host)
333
+ self.colors = ColorController(self._dataset_host)
334
+ self.transfer = TransferController(self._dataset_host)
335
+ self.overlays = OverlayController(self._dataset_host)
336
+ self.plugin_runs = PluginRunController(self._dataset_host)
337
+ self.property_panel = DatasetPropertyPanel(self)
338
+ # 軸の設定の欄と値の対応(集める・戻す・止める・つなぐ)
339
+ self._axis_binder = Binder(self, AXIS_BINDINGS, also_blocked=AXIS_BUTTONS)
340
+
341
+ # メニューを作るときに参照するので、ここで用意する(0分なら止めたまま)
342
+ self.autosave_timer = QTimer(self)
343
+ self.autosave_timer.timeout.connect(self.auto_save)
344
+ saved_interval_min = app_settings.AUTOSAVE_INTERVAL_MIN.read(self.settings)
345
+ if saved_interval_min > 0:
346
+ self.autosave_timer.start(saved_interval_min * 60 * 1000)
347
+
348
+ self.all_axes = []
349
+ self.all_secondary_axes = []
350
+ self.project.all_plot_settings = []
351
+ self.project.active_axis_index = 0
352
+
353
+ # マウス操作の各モード(状態は各ツールが持つ。gui/tools/)
354
+ self.mouse_tools = create_tools(self)
355
+ self.mouse_tool_manager = ToolManager(self)
356
+
357
+
358
+ # 最初の軸の設定の既定値になる。フォントは QFont()(UI のフォント)にしない:
359
+ # matplotlib は "Yu Gothic UI" のような UI 用のフォントを解決できず、文字化けする
360
+ self._tick_font = _make_default_plot_font()
361
+ self._tick_color = '#000000'
362
+ self._tick_width = 0.8
363
+ self._axis_label_font = _make_default_plot_font()
364
+ self._axis_label_color = '#000000'
365
+ self._spine_width = 1.0
366
+ self._spine_color = '#000000'
367
+ self._legend_font = _make_default_plot_font()
368
+ self._legend_color = '#000000'
369
+
370
+ def _setup_window(self):
371
+ self.resize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
372
+ self.ui.control_dock_widget.setFixedWidth(CONTROL_DOCK_WIDTH)
373
+
374
+ # 余った縦の空きはキャンバスに回す(指定しないとデータセット一覧と均等に分けられ、一覧の下が空く)
375
+ self.ui.gridLayout_2.setRowStretch(1, 1) # キャンバス
376
+ self.ui.gridLayout_2.setRowStretch(2, 0) # データセット一覧
377
+ self.ui.gridLayout_2.setRowStretch(3, 0) # ボタンの行
378
+
379
+ def _build_canvas_and_toolbar(self):
380
+ return canvas_area.build_canvas_and_toolbar(self)
381
+
382
+ def _setup_status_bar(self):
383
+ return canvas_area.setup_status_bar(self)
384
+
385
+ def _build_dataset_color_picker(self):
386
+ return dataset_panel.build_dataset_color_picker(self)
387
+
388
+ def _build_dataset_list_buttons(self):
389
+ return dataset_panel.build_dataset_list_buttons(self)
390
+
391
+ def _build_dataset_style_controls(self):
392
+ return dataset_panel.build_dataset_style_controls(self)
393
+
394
+ def _build_legend_location_control(self):
395
+ return axis_panel.build_legend_location_control(self)
396
+
397
+ def _build_fit_info_and_stats(self):
398
+ return dataset_panel.build_fit_info_and_stats(self)
399
+
400
+ def _build_secondary_y_controls(self):
401
+ return dataset_panel.build_secondary_y_controls(self)
402
+
403
+ def _build_tick_grid_and_colorbar_controls(self):
404
+ return axis_panel.build_tick_grid_and_colorbar_controls(self)
405
+
406
+ def _build_tick_format_controls(self):
407
+ return axis_panel.build_tick_format_controls(self)
408
+
409
+ def _build_label_editors(self):
410
+ return axis_panel.build_label_editors(self)
411
+
412
+ def _add_subplot_target_row(self):
413
+ return dataset_panel.add_subplot_target_row(self)
414
+
415
+ def _arrange_property_docks(self):
416
+ return dock_layout.arrange_property_docks(self)
417
+
418
+ def _rebuild_label_tab(self):
419
+ return axis_panel.rebuild_label_tab(self)
420
+
421
+ def _connect_and_initialize(self):
422
+ self.canvas.mpl_connect('motion_notify_event', self._on_mouse_move)
423
+
424
+ # グラフの要素のクリックでの選択は、どのモードでも常に有効
425
+ self.canvas.mpl_connect('pick_event', self._on_element_pick)
426
+
427
+ # ホイールでのズームと中ボタンでのパンは、各モードの左クリックとぶつからないので常に有効
428
+ self.canvas.mpl_connect('scroll_event', self._on_scroll_zoom)
429
+ self.canvas.mpl_connect('button_press_event', self._on_middle_button_press_pan)
430
+ self.canvas.mpl_connect('motion_notify_event', self._on_middle_button_motion_pan)
431
+ self.canvas.mpl_connect('button_release_event', self._on_middle_button_release_pan)
432
+ # 凡例をドラッグした位置を設定へ保存する
433
+ self.canvas.mpl_connect('button_release_event', self._on_legend_drag_release)
434
+
435
+ # プラグインの登録はメニューを作る(_create_menu_bar)より前に。読み込みはアプリ全体で1回
436
+ # (load_plugins_once)、メニューへの追加はタブごとに _create_menu_bar が行う
437
+ self.plugin_api = load_plugins_once(
438
+ plugin_search_paths(), disabled_names=disabled_plugin_names(self.settings)
439
+ )
440
+
441
+ # プラグインへの窓口は (このタブ, プラグイン) ごとに1つ。
442
+ self._plugin_contexts = {}
443
+
444
+ # プラグインのパネル。メニューに表示切替を足すのは _create_menu_bar() なので、
445
+ # ドックはそれより前に作る。1つ失敗しても他のパネルとタブの起動は続ける。
446
+ self._plugin_panel_docks = {}
447
+ for panel in self.plugin_api.get_panels():
448
+ try:
449
+ widget = panel.widget_factory(self.plugin_context(panel.plugin_name))
450
+ if not isinstance(widget, QWidget):
451
+ raise TypeError(f"QWidgetを返しませんでした(型: {type(widget).__name__})。")
452
+ except Exception as e:
453
+ logger.warning(
454
+ "[plugin:%s] パネル '%s' の構築に失敗しました: %s",
455
+ panel.plugin_name, panel.name, e,
456
+ )
457
+ continue
458
+ dock = QDockWidget(panel.name, self)
459
+ dock.setObjectName(f"PluginPanel_{panel.name}")
460
+ dock.setWidget(widget)
461
+ self.addDockWidget(_PLUGIN_PANEL_AREA_MAP.get(panel.area, Qt.DockWidgetArea.RightDockWidgetArea), dock)
462
+ dock.hide() # 表示するかはドック配置の復元に任せる
463
+ self._plugin_panel_docks[panel.name] = dock
464
+
465
+ # フォーカスのあるドックの枠を強調する(プラグインのドックも含め、全部に効く)
466
+ theme.install_dock_focus_highlight(self)
467
+
468
+ # プラグインの plot_type を種類の欄の最後に足す(描き方は canvas._draw_plot_type が探す)
469
+ for plot_type in self.plugin_api.get_plot_types():
470
+ self.ui.plot_type_combo.addItem(plot_type.type_name)
471
+
472
+ self._connect_signals()
473
+
474
+ self._create_menu_bar()
475
+
476
+ # クイックアクセスのピン留めを戻す。プラグインのものを含めて全メニューができた後であること
477
+ self._restore_quick_access_actions()
478
+ self._install_quick_access_context_menus()
479
+
480
+ # 画面の既定の状態を最初の軸の設定にする
481
+ default_settings = self._gather_settings_from_ui()
482
+ self.project.all_plot_settings.append(default_settings)
483
+ self._set_initial_ui_state()
484
+ # 生成物の ui_main_window.py のラベルには末尾に「:」が付いている。.ui が無く作り直せないので、ここで取る
485
+ _strip_trailing_colon_from_labels(self)
486
+ # 下の1行の統計値の分だけ、一覧の高さの上限を控えめにする
487
+ self.ui.dataset_list_widget.setMaximumHeight(175)
488
+
489
+ spacing_value = 6
490
+ if self.ui.formLayout_3: self.ui.formLayout_3.setSpacing(spacing_value)
491
+ # formLayout_4 は空なので、各節の QFormLayout に掛ける
492
+ for key, _title in DATASET_PROPERTY_SECTIONS:
493
+ self._prop_form(key).setSpacing(spacing_value)
494
+
495
+ # 入力欄の左端を揃える(プロパティ欄の7節どうし、X/Y 軸のタブどうし)。ラベルが全部できた後で
496
+ self._align_form_label_columns(
497
+ [self._prop_form(key) for key, _title in DATASET_PROPERTY_SECTIONS])
498
+ self._align_form_label_columns([self.ui.formLayout, self.ui.formLayout_2])
499
+
500
+
501
+ # 軸の範囲と目盛りの間隔は、指数表記でも入力できるようにする
502
+ for spin_box in [self.ui.x_min_spinbox, self.ui.x_max_spinbox,
503
+ self.ui.y_min_spinbox, self.ui.y_max_spinbox]:
504
+ _enable_scientific_notation_input(spin_box, minimum=-np.inf, maximum=np.inf)
505
+
506
+ for spin_box in [self.ui.x_major_tick_interval_spinbox, self.ui.y_major_tick_interval_spinbox,
507
+ self.ui.x_minor_tick_interval_spinbox, self.ui.y_minor_tick_interval_spinbox]:
508
+ _enable_scientific_notation_input(spin_box, minimum=0, maximum=np.inf)
509
+
510
+ self._update_plot()
511
+
512
+ # ドックの配置は前回の状態を戻す(最初のタブだけ。窓の大きさと位置は MainAppWindow が扱う)。
513
+ # 既定の配置の版(DOCK_LAYOUT_VERSION)が保存時と違えば戻さない(戻すと新しい既定が既存の利用者に届かない)。
514
+ # 戻すのはイベントループが一巡してタブが最終の大きさになってから。ここで戻すと仮の大きさで
515
+ # スプリッターの位置が決まり、見た目とクリックの位置がずれる。
516
+ # 「リセット」用の素の配置は、戻す前のここで控える
517
+ self._pristine_dock_state = self.saveState()
518
+
519
+ QTimer.singleShot(0, self._restore_dock_layout)
520
+
521
+ # キャンバスを切り離したまま閉じていれば同じ状態に戻す。ダークモードなどと同じ軽い設定なので、どのタブでも戻す
522
+ if app_settings.CANVAS_WAS_DETACHED.read(self.settings):
523
+ QTimer.singleShot(0, lambda: self._detach_canvas(restore_geometry=True))
524
+
525
+ self.setAcceptDrops(True)
526
+
527
+ # 起動時の確認は最初のタブだけ。窓が表示される前だとダイアログが変な位置に出るので、一巡してから
528
+ if self._run_startup_checks:
529
+ QTimer.singleShot(0, self._check_autosave_recovery)
530
+ # 初回の案内は復元の確認の後(データに関わる確認を先に)
531
+ QTimer.singleShot(0, self._check_first_launch)
532
+ # 新しい版の確認は上の2つの邪魔をしないよう少し遅らせる。公開 API への匿名の GET 1回だけで、
533
+ # 新しい版があるときだけ知らせ、失敗しても何も出さない
534
+ QTimer.singleShot(1500, self._start_startup_update_check)
535
+
536
+ # 項目の間の余白を広げる。フォームが全部できた最後に
537
+ apply_form_spacing(self)
538
+
539
+ def _restore_dock_layout(self):
540
+ return dock_layout.restore_dock_layout(self)
541
+
542
+ # 名前付きのドック配置。起動時の復元(最初のタブだけ)と違い、どのタブでもいつでも使える
543
+
544
+ def _load_dock_layout_presets(self):
545
+ return dock_layout.load_dock_layout_presets(self)
546
+
547
+ def _save_dock_layout_presets(self, presets: dict):
548
+ return dock_layout.save_dock_layout_presets(self, presets)
549
+
550
+ def _on_save_dock_layout_preset(self):
551
+ return dock_layout.on_save_dock_layout_preset(self)
552
+
553
+ def _populate_load_layout_menu(self):
554
+ return dock_layout.populate_load_layout_menu(self)
555
+
556
+ def _on_load_dock_layout_preset(self, name):
557
+ return dock_layout.on_load_dock_layout_preset(self, name)
558
+
559
+ def _on_reset_dock_layout(self):
560
+ return dock_layout.on_reset_dock_layout(self)
561
+
562
+ def closeEvent(self, event):
563
+ # 読み込み中の QThread を破棄すると Qt がプロセスごと止め、ほかのタブの未保存データも失う。
564
+ # 読み込みは中断できないので終わるまで待つ。待つ間に届く完了の通知が閉じかけの窓で動かないよう、先に切る
565
+ if self._data_load_task_runner is not None:
566
+ try:
567
+ self._data_load_task_runner.succeeded.disconnect()
568
+ self._data_load_task_runner.failed.disconnect()
569
+ except (RuntimeError, TypeError):
570
+ pass
571
+ self._data_load_task_runner.requestInterruption()
572
+ self._data_load_task_runner.wait()
573
+ self._data_load_task_runner.deleteLater()
574
+ self._data_load_task_runner = None
575
+
576
+ self.fitting.shutdown()
577
+
578
+ # 同じ理由
579
+ if self._batch_export_task_runner is not None:
580
+ try:
581
+ self._batch_export_task_runner.succeeded.disconnect()
582
+ self._batch_export_task_runner.failed.disconnect()
583
+ except (RuntimeError, TypeError):
584
+ pass
585
+ self._batch_export_task_runner.requestInterruption()
586
+ self._batch_export_task_runner.wait()
587
+ self._batch_export_task_runner.deleteLater()
588
+ self._batch_export_task_runner = None
589
+
590
+ # 同じ理由
591
+ if self._update_check_task_runner is not None:
592
+ try:
593
+ self._update_check_task_runner.succeeded.disconnect()
594
+ self._update_check_task_runner.failed.disconnect()
595
+ except (RuntimeError, TypeError):
596
+ pass
597
+ self._update_check_task_runner.requestInterruption()
598
+ self._update_check_task_runner.wait()
599
+ self._update_check_task_runner.deleteLater()
600
+ self._update_check_task_runner = None
601
+
602
+ if self._run_startup_checks:
603
+ app_settings.CLEAN_EXIT.write(self.settings, True)
604
+ app_settings.WINDOW_STATE.write(self.settings, self.saveState())
605
+ app_settings.DOCK_LAYOUT_VERSION.write(self.settings, DOCK_LAYOUT_VERSION)
606
+
607
+ # 切り離したまま閉じたら次の起動で同じ状態に戻す。軽い設定なので最初のタブに限らない
608
+ if self.canvas_detached and self._canvas_detach_window is not None:
609
+ app_settings.CANVAS_DETACHED_GEOMETRY.write(self.settings, self._canvas_detach_window.saveGeometry())
610
+ app_settings.CANVAS_WAS_DETACHED.write(self.settings, self.canvas_detached)
611
+
612
+ # 切り離した窓が残らないよう片付ける。_reattach_canvas を通すと、上で保存した「切り離していた」を
613
+ # 消してしまうので通さない。窓と一緒に破棄されないよう、先にキャンバスの親を外す
614
+ if self._canvas_detach_window is not None:
615
+ self._canvas_detach_window.closed.disconnect(self._on_detach_window_closed)
616
+ self._canvas_detach_window.takeCentralWidget()
617
+ self.canvas.setParent(None)
618
+ self._canvas_detach_window.close()
619
+ self._canvas_detach_window.deleteLater()
620
+ self._canvas_detach_window = None
621
+
622
+ super().closeEvent(event)
623
+
624
+ def _check_autosave_recovery(self):
625
+ return project_files.check_autosave_recovery(self)
626
+
627
+ def _check_first_launch(self):
628
+ if app_settings.HAS_SHOWN_WELCOME.read(self.settings):
629
+ return
630
+ app_settings.HAS_SHOWN_WELCOME.write(self.settings, True)
631
+ self._show_welcome_dialog()
632
+
633
+ def _on_show_startup_screen(self):
634
+ self._show_welcome_dialog()
635
+
636
+ def _show_welcome_dialog(self):
637
+ dialog = WelcomeDialog(self, recent_files=self._get_recent_files())
638
+ dialog.exec()
639
+ if dialog.load_sample_requested:
640
+ self._load_sample_data()
641
+ elif dialog.selected_recent_file:
642
+ self._on_open_recent_file(dialog.selected_recent_file)
643
+ elif dialog.load_template_requested:
644
+ self._on_load_plot_template()
645
+
646
+ def _on_show_autosave_history(self):
647
+ return project_files.on_show_autosave_history(self)
648
+
649
+ def _load_sample_data(self):
650
+ sample_path = resource_path(os.path.join("sample_data", "cooling_curve_sample.csv"))
651
+ if not os.path.exists(sample_path):
652
+ notify.warning(self, "サンプルデータ", "サンプルデータファイルが見つかりませんでした。")
653
+ return
654
+ self.load_data(sample_path)
655
+
656
+ def _update_autosave_path(self):
657
+ return project_files.update_autosave_path(self)
658
+
659
+ def _rotate_autosave_generations(self):
660
+ return project_files.rotate_autosave_generations(self)
661
+
662
+ def _sync_project_from_ui(self):
663
+ return project_files.sync_project_from_ui(self)
664
+
665
+ def _remember_saved_content(self):
666
+ return project_files.remember_saved_content(self)
667
+
668
+ # DatasetHost と TabPluginContext が使う窓口。中は非公開の名前を呼ぶだけ(テストの差し替えが届くよう、呼ぶときに引く)
669
+ def current_dataset(self):
670
+ return self._get_current_dataset()
671
+
672
+ def selected_datasets(self):
673
+ return self._get_selected_datasets()
674
+
675
+ def dataset_tree_item(self, dataset):
676
+ return self._get_dataset_tree_item(dataset)
677
+
678
+ def target_folder_for_new_dataset(self):
679
+ return self._get_target_folder_for_new_dataset()
680
+
681
+ def add_dataset(self, dataset, parent_folder=None, select=True):
682
+ return self._add_dataset(dataset, parent_folder, select=select)
683
+
684
+ def add_dataset_with_undo(self, dataset, parent_folder=None, description="データセットの追加"):
685
+ return self._add_dataset_with_undo(dataset, parent_folder=parent_folder, description=description)
686
+
687
+ def push_dataset_additions(self, add, datasets, description):
688
+ return self._push_dataset_additions(add, datasets, description)
689
+
690
+ def remove_dataset_items_with_undo(self, items, description=None):
691
+ return self._remove_dataset_items_with_undo(items, description=description)
692
+
693
+ def push_dataset_property_command(self, dataset, old_values, new_values, description, skip_if_unchanged=True):
694
+ return self._push_dataset_property_command(dataset, old_values, new_values, description,
695
+ skip_if_unchanged=skip_if_unchanged)
696
+
697
+ def refresh_after_dataset_property_change(self, dataset, changed_keys=(), old_values=None, new_values=None):
698
+ return self._refresh_after_dataset_property_change(dataset, changed_keys=changed_keys, old_values=old_values,
699
+ new_values=new_values)
700
+
701
+ def add_annotation(self, axis_index, annotation, description):
702
+ return self._add_annotation(axis_index, annotation, description=description)
703
+
704
+ def redraw(self):
705
+ return self._update_plot()
706
+
707
+ def redraw_appearance(self):
708
+ return self._update_plot_appearance()
709
+
710
+ def pending_peak_guesses(self):
711
+ return self._pending_peak_guesses
712
+
713
+ def clear_pending_peak_guesses(self):
714
+ return self._clear_pending_peak_guesses()
715
+
716
+ def toggle_peak_placement_mode(self, checked):
717
+ return self._toggle_peak_placement_mode(checked)
718
+
719
+ @property
720
+ def dataset_host(self):
721
+ return self._dataset_host
722
+
723
+ def plugin_context(self, plugin_name):
724
+ context = self._plugin_contexts.get(plugin_name)
725
+ if context is None:
726
+ context = TabPluginContext(self, plugin_name)
727
+ self._plugin_contexts[plugin_name] = context
728
+ return context
729
+
730
+ def _notify_plugins_datasets_changed(self):
731
+ for context in self._plugin_contexts.values():
732
+ context._notify_datasets_changed()
733
+
734
+ def _notify_plugins_selection_changed(self):
735
+ current = self._get_current_dataset()
736
+ for context in self._plugin_contexts.values():
737
+ context._notify_selection_changed(current)
738
+
739
+ def _run_plugin_menu_action(self, menu_action):
740
+ try:
741
+ menu_action.callback(self.plugin_context(menu_action.plugin_name))
742
+ except Exception as e:
743
+ logger.exception("[plugin:%s] メニュー「%s」の実行に失敗しました", menu_action.plugin_name, menu_action.text)
744
+ notify.critical(self, "プラグインのエラー", str(PluginExecutionError(
745
+ menu_action.plugin_name, f"「{menu_action.text}」の実行に失敗しました: {e}"
746
+ )))
747
+
748
+ def document_title(self):
749
+ return project_files.document_title(self)
750
+
751
+ def has_unsaved_changes(self):
752
+ return project_files.has_unsaved_changes(self)
753
+
754
+ def confirm_unsaved_changes(self, action_text):
755
+ return project_files.confirm_unsaved_changes(self, action_text)
756
+
757
+ def auto_save(self):
758
+ return project_files.auto_save(self)
759
+
760
+ def manual_save(self):
761
+ return project_files.manual_save(self)
762
+
763
+ def manual_save_as(self):
764
+ return project_files.manual_save_as(self)
765
+
766
+ def _save_project_to_path(self, filepath):
767
+ return project_files.save_project_to_path(self, filepath)
768
+
769
+ def manual_load(self):
770
+ return project_files.manual_load(self)
771
+
772
+ def _load_project_from_path(self, filepath, add_to_recent=True):
773
+ return project_files.load_project_from_path(self, filepath, add_to_recent)
774
+
775
+ def _reset_zoom(self):
776
+ """設定どおりの表示範囲に戻す。
777
+
778
+ matplotlib の Home ボタンは作り直す前の Axes を覚えているので、描き直しを挟むと効かない。
779
+ """
780
+ self._update_plot()
781
+
782
+ def _update_plot(self, light=False, full_resolution=False):
783
+ """グラフ全体を描き直す。
784
+
785
+ light=True は Axes の数・配置・所属が変わらないときだけ(既存の Axes の上で描き直す)。
786
+ full_resolution=True は表示用の間引きをしない(フル解像度でのエクスポート用)。
787
+ """
788
+ layout_mode = getattr(self.project, 'layout_mode', 'grid')
789
+ if layout_mode == 'free':
790
+ # 自由配置では行数×列数ではなく、軸の設定の数がサブプロットの数になる
791
+ rows, cols = 0, 0
792
+ if len(self.project.all_plot_settings) == 0:
793
+ return
794
+ else:
795
+ rows = self.subplot_rows_spinbox.value()
796
+ cols = self.subplot_cols_spinbox.value()
797
+ if rows * cols == 0:
798
+ return
799
+
800
+ canvas_update_method = (
801
+ self.canvas.update_all_axes_appearance_and_data if light else self.canvas.redraw_all
802
+ )
803
+ is_secondary_visible_global = canvas_update_method(
804
+ self.project.datasets, rows, cols, self.project.all_plot_settings, layout_mode=layout_mode,
805
+ panel_labels_enabled=self.project.panel_labels_enabled,
806
+ share_x_axis=getattr(self.project, 'share_x_axis', False),
807
+ share_y_axis=getattr(self.project, 'share_y_axis', False),
808
+ full_resolution=full_resolution,
809
+ )
810
+
811
+ self.tick_direction_y2_label.setVisible(is_secondary_visible_global)
812
+ self.major_tick_direction_y2_combo.setVisible(is_secondary_visible_global)
813
+ self.minor_tick_direction_y2_combo.setVisible(is_secondary_visible_global)
814
+ self.y2_label_text_label.setVisible(is_secondary_visible_global)
815
+ self.y2_label_text_edit.setVisible(is_secondary_visible_global)
816
+
817
+ self.all_axes = self.canvas.all_axes
818
+ self.all_secondary_axes = self.canvas.all_secondary_axes
819
+
820
+ if hasattr(self, 'export_preview_panel'):
821
+ self.export_preview_panel.refresh_preview()
822
+
823
+ # 全体の描き直しでハイライトも消えるので、データエディタの選択を戻す
824
+ self._reapply_editor_row_highlight()
825
+
826
+ # ミニマップは別の Figure なので、redraw_all() では描き直されない。
827
+ self._refresh_minimap()
828
+ self._notify_plugins_datasets_changed()
829
+
830
+ def _refresh_minimap(self):
831
+ """隠れていても描く(次に出したときに最新になっているように)。"""
832
+ if not hasattr(self, 'minimap'):
833
+ return
834
+ self.minimap.refresh(self.project.datasets, self.canvas.dark_mode)
835
+
836
+ def _on_minimap_range_selected(self, xmin, xmax):
837
+ """ミニマップで選んだ範囲を全部の軸の X の範囲にする。Axes は作り直さない(set_xlim と draw_idle で足りる)。"""
838
+ for ax in self.canvas.all_axes:
839
+ ax.set_xlim(xmin, xmax)
840
+ self.canvas.draw_idle()
841
+
842
+ def _on_toggle_minimap(self, checked):
843
+ self.minimap_visible = checked
844
+ self.minimap.setVisible(checked)
845
+ self.minimap_separator.setVisible(checked)
846
+ app_settings.MINIMAP_VISIBLE.write(self.settings, checked)
847
+
848
+ def _on_toggle_panel_labels(self, checked):
849
+ """QSettings ではなくプロジェクトに保存する。"""
850
+ self.project.panel_labels_enabled = checked
851
+ self._update_plot(light=True)
852
+
853
+ # キャンバスの切り離し。self.canvas はあちこちから直接参照されているので、破棄も作り直しもせず親を替えるだけ。
854
+
855
+ def _on_toggle_canvas_detached(self, checked):
856
+ if checked:
857
+ self._detach_canvas()
858
+ else:
859
+ self._reattach_canvas()
860
+
861
+ def _sync_canvas_detach_action(self):
862
+ """メニューのチェックと文言(「切り離す」/「元に戻す」)を状態に合わせる。"""
863
+ if not hasattr(self, 'canvas_detach_action'):
864
+ return
865
+ self.canvas_detach_action.blockSignals(True)
866
+ self.canvas_detach_action.setChecked(self.canvas_detached)
867
+ self.canvas_detach_action.setText(
868
+ tr("キャンバスを元に戻す") if self.canvas_detached else tr("キャンバスを別ウィンドウに切り離す")
869
+ )
870
+ self.canvas_detach_action.blockSignals(False)
871
+
872
+ def _detach_canvas(self, restore_geometry=False):
873
+ if self.canvas_detached:
874
+ return
875
+
876
+ self._plot_layout.removeWidget(self.canvas)
877
+
878
+ title = f"{APP_NAME} - {tr('グラフキャンバス')}"
879
+ self._canvas_detach_window = DetachedCanvasWindow(title)
880
+ self._canvas_detach_window.setCentralWidget(self.canvas)
881
+ self._canvas_detach_window.closed.connect(self._on_detach_window_closed)
882
+
883
+ saved_geometry = app_settings.CANVAS_DETACHED_GEOMETRY.read(self.settings) if restore_geometry else None
884
+ if saved_geometry is not None:
885
+ # 先にネイティブのハンドルを作る。窓が実体化する前に restoreGeometry() すると位置がずれる
886
+ self._canvas_detach_window.winId()
887
+ self._canvas_detach_window.restoreGeometry(saved_geometry)
888
+ else:
889
+ self._canvas_detach_window.resize(DEFAULT_DETACHED_CANVAS_WIDTH, DEFAULT_DETACHED_CANVAS_HEIGHT)
890
+
891
+ self._canvas_detach_window.show()
892
+ self.canvas.show()
893
+
894
+ self.canvas_detached = True
895
+ self._sync_canvas_detach_action()
896
+ app_settings.CANVAS_WAS_DETACHED.write(self.settings, True)
897
+
898
+ def _reattach_canvas(self):
899
+ """「元に戻す」と、切り離した窓を閉じたときの両方から呼ばれる。"""
900
+ if not self.canvas_detached:
901
+ return
902
+
903
+ if self._canvas_detach_window is not None:
904
+ app_settings.CANVAS_DETACHED_GEOMETRY.write(self.settings, self._canvas_detach_window.saveGeometry())
905
+ self._canvas_detach_window.closed.disconnect(self._on_detach_window_closed)
906
+ self._canvas_detach_window.takeCentralWidget()
907
+ self._canvas_detach_window.close()
908
+ self._canvas_detach_window.deleteLater()
909
+ self._canvas_detach_window = None
910
+
911
+ self.canvas.setParent(self.ui.plot_container)
912
+ self._plot_layout.insertWidget(self._canvas_layout_index, self.canvas)
913
+ self.canvas.show()
914
+
915
+ self.canvas_detached = False
916
+ self._sync_canvas_detach_action()
917
+ app_settings.CANVAS_WAS_DETACHED.write(self.settings, False)
918
+
919
+ def _on_detach_window_closed(self):
920
+ self._reattach_canvas()
921
+
922
+ def _update_plot_appearance(self):
923
+ layout_mode = getattr(self.project, 'layout_mode', 'grid')
924
+ if layout_mode == 'free':
925
+ rows, cols = 0, 0
926
+ else:
927
+ rows = self.subplot_rows_spinbox.value()
928
+ cols = self.subplot_cols_spinbox.value()
929
+ self.canvas.update_appearance_only(
930
+ self.project.all_plot_settings, datasets=self.project.datasets, rows=rows, cols=cols,
931
+ layout_mode=layout_mode,
932
+ share_x_axis=getattr(self.project, 'share_x_axis', False),
933
+ share_y_axis=getattr(self.project, 'share_y_axis', False),
934
+ )
935
+
936
+ if hasattr(self, 'export_preview_panel'):
937
+ self.export_preview_panel.refresh_preview()
938
+
939
+ def _on_editor_rows_highlighted(self, master_indices):
940
+ """データエディタで選んだ行の点をグラフ上で強調する(逆向きは cursor_mixin の _on_pick)。"""
941
+ if self.data_editor_dialog is None:
942
+ return
943
+ self.canvas.set_highlighted_points(self.data_editor_dialog.dataset, master_indices)
944
+
945
+ def _reapply_editor_row_highlight(self):
946
+ if self.data_editor_dialog is None:
947
+ return
948
+ self.canvas.set_highlighted_points(
949
+ self.data_editor_dialog.dataset, self.data_editor_dialog.get_selected_master_indices()
950
+ )
951
+
952
+ def _wrap_in_collapsible_section(self, group_box, title):
953
+ return property_sections.wrap_in_collapsible_section(self, group_box, title)
954
+
955
+
956
+ def _build_dataset_property_sections(self):
957
+ return property_sections.build_dataset_property_sections(self)
958
+
959
+ def _prop_form(self, section_key):
960
+ return property_sections.prop_form(self, section_key)
961
+
962
+ def _align_form_label_columns(self, forms):
963
+ return property_sections.align_form_label_columns(self, forms)
964
+
965
+ def _set_dataset_property_fields_enabled(self, enabled):
966
+ return property_sections.set_dataset_property_fields_enabled(self, enabled)
967
+
968
+ def _load_collapsed_property_sections(self):
969
+ return property_sections.load_collapsed_property_sections(self)
970
+
971
+ def _on_property_section_toggled(self, section_key, checked):
972
+ return property_sections.on_property_section_toggled(self, section_key, checked)
973
+
974
+ def _update_property_section_visibility(self):
975
+ return property_sections.update_property_section_visibility(self)
976
+
977
+ # データセット一覧(QTreeWidget)。データセットの葉は UserRole に Dataset を、フォルダは None を持つ。
978
+
979
+ def _replace_dataset_list_with_tree(self):
980
+ return dataset_panel.replace_dataset_list_with_tree(self)
981
+
982
+ def _add_dataset_list_item(self, dataset, parent_item=None):
983
+ """UserRole に Dataset そのものを持たせる(並べ替えても、同名があっても対応が崩れない)。"""
984
+ item = QTreeWidgetItem([dataset.name])
985
+ item.setData(0, Qt.ItemDataRole.UserRole, dataset)
986
+ item.setIcon(0, make_dataset_style_icon(dataset))
987
+ # 目のアイコンのクリックは dataset_mixin の _on_dataset_tree_item_clicked が列で見分ける
988
+ item.setIcon(DATASET_TREE_VISIBILITY_COLUMN, make_dataset_visibility_icon(dataset))
989
+ apply_dataset_visibility_text_style(item, dataset)
990
+ # データセット自身はフォルダではないので、ドロップ先にはしない
991
+ item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsDropEnabled)
992
+ if parent_item is not None:
993
+ parent_item.addChild(item)
994
+ else:
995
+ self.ui.dataset_list_widget.addTopLevelItem(item)
996
+ return item
997
+
998
+ def _get_target_folder_for_new_dataset(self):
999
+ """選択中の項目がフォルダならそれを返す(新しいデータセットをその中に入れる)。"""
1000
+ return self.dataset_order.target_folder()
1001
+
1002
+ def _add_dataset(self, dataset, parent_folder=None, select=True):
1003
+ new_item = self.dataset_order.append(dataset, parent_folder)
1004
+ if select:
1005
+ self.ui.dataset_list_widget.setCurrentItem(new_item)
1006
+ self._update_plot()
1007
+ return new_item
1008
+
1009
+ def _add_dataset_with_undo(self, dataset, parent_folder=None, description="データセットの追加"):
1010
+ """_add_dataset() の Undo できる版(プラグインの処理と解析の結果用)。"""
1011
+ def do_add():
1012
+ self._add_dataset(dataset, parent_folder, select=True)
1013
+
1014
+ def do_remove():
1015
+ self.dataset_order.remove_added(dataset)
1016
+ self.property_panel.update_ui_state()
1017
+ self._update_plot()
1018
+
1019
+ command = AddDatasetCommand(do_add, do_remove, description=description)
1020
+ self.undo_stack.push(command)
1021
+
1022
+ def _push_dataset_additions(self, add, datasets, description):
1023
+ """add() で足す datasets を、Undo 1回で取り除けるように積む(積むとすぐ add() が走る)。"""
1024
+ def remove():
1025
+ for dataset in reversed(datasets):
1026
+ self.dataset_order.remove_added(dataset)
1027
+ self.property_panel.update_ui_state()
1028
+ self._update_plot()
1029
+
1030
+ self.undo_stack.push(AddDatasetCommand(add, remove, description=description))
1031
+
1032
+ def _remove_dataset_items_with_undo(self, top_level_items, description=None):
1033
+ """ツリーの項目(データセットかフォルダ)を Undo できるように削除する。
1034
+
1035
+ top_level_items には最上位の対象だけを渡す(フォルダを渡すと中身も一緒に消える)。
1036
+ """
1037
+ steps = self.dataset_order.removal(top_level_items)
1038
+ if steps is None:
1039
+ return
1040
+ remove, restore, removed_datasets = steps
1041
+
1042
+ if description is None:
1043
+ if len(removed_datasets) == 1:
1044
+ description = f"「{removed_datasets[0].name}」の削除"
1045
+ else:
1046
+ description = f"{len(removed_datasets)}件のデータセットの削除"
1047
+
1048
+ def do_remove():
1049
+ remove()
1050
+ self.property_panel.update_ui_state()
1051
+ self._update_plot()
1052
+
1053
+ def do_restore():
1054
+ restore()
1055
+ self.property_panel.update_ui_state()
1056
+ self._update_plot()
1057
+
1058
+ self.undo_stack.push(
1059
+ RemoveDatasetCommand(do_remove, do_restore, description=description)
1060
+ )
1061
+
1062
+ def _add_dataset_folder_item(self, name, parent_item=None):
1063
+ return self.dataset_order.add_folder(name, parent_item)
1064
+
1065
+ def _flatten_dataset_tree(self, parent_item=None):
1066
+ """データセットの葉を表示順(深さ優先)に返す。"""
1067
+ return self.dataset_order.dataset_items(parent_item)
1068
+
1069
+ def _get_current_dataset(self):
1070
+ return self.dataset_order.current_dataset()
1071
+
1072
+ def _get_selected_datasets(self):
1073
+ return self.dataset_order.selected_datasets()
1074
+
1075
+ def _get_dataset_tree_item(self, dataset):
1076
+ return self.dataset_order.item_for(dataset)
1077
+
1078
+ def _capture_dataset_group_tree(self):
1079
+ return self.dataset_order.group_tree()
1080
+
1081
+ def _rebuild_dataset_tree_widget(self):
1082
+ self.dataset_order.rebuild_tree()
1083
+
1084
+ def _sync_dataset_list_widget_order(self):
1085
+ """フォルダの中の並びを project.datasets の順に合わせる(並べ替えの Undo/Redo 用)。"""
1086
+ self.dataset_order.sort_tree_to_draw_order()
1087
+
1088
+ def dragEnterEvent(self, event):
1089
+ return data_import_flow.dragEnterEvent(self, event)
1090
+
1091
+ def dropEvent(self, event):
1092
+ return data_import_flow.dropEvent(self, event)
1093
+
1094
+ def _all_supported_data_file_extensions(self):
1095
+ return data_import_flow.all_supported_data_file_extensions(self)
1096
+
1097
+ def _queue_data_files(self, file_paths):
1098
+ return data_import_flow.queue_data_files(self, file_paths)
1099
+
1100
+ def _process_next_queued_file(self):
1101
+ return data_import_flow.process_next_queued_file(self)
1102
+
1103
+ def load_data(self, file_path, queue_progress=None):
1104
+ return data_import_flow.load_data(self, file_path, queue_progress)
1105
+
1106
+ def _on_data_load_succeeded(self, df, file_path):
1107
+ return data_import_flow.on_data_load_succeeded(self, df, file_path)
1108
+
1109
+ def _import_loaded_dataframe(self, df, file_path):
1110
+ return data_import_flow.import_loaded_dataframe(self, df, file_path)
1111
+
1112
+ @staticmethod
1113
+ def _apply_filename_regex_columns(df, file_path, pattern):
1114
+ return data_import_flow.apply_filename_regex_columns(df, file_path, pattern)
1115
+
1116
+ def _on_import_folder(self):
1117
+ return data_import_flow.on_import_folder(self)
1118
+
1119
+ def _on_paste_data_from_clipboard(self):
1120
+ return data_import_flow.on_paste_data_from_clipboard(self)
1121
+
1122
+ def _on_data_load_failed(self, error_message, file_path):
1123
+ return data_import_flow.on_data_load_failed(self, error_message, file_path)
1124
+
1125
+ def _localize_navigation_toolbar(self, toolbar):
1126
+ return canvas_area.localize_navigation_toolbar(self, toolbar)
1127
+
1128
+ def _cleanup_data_load_task_runner(self):
1129
+ return data_import_flow.cleanup_data_load_task_runner(self)
1130
+
1131
+
1132
+ def _get_recent_files(self):
1133
+ return project_files.get_recent_files(self)
1134
+
1135
+ def _add_recent_file(self, file_path):
1136
+ return project_files.add_recent_file(self, file_path)
1137
+
1138
+ def _update_recent_files_menu(self):
1139
+ return project_files.update_recent_files_menu(self)
1140
+
1141
+ def _on_open_recent_file(self, file_path):
1142
+ return project_files.on_open_recent_file(self, file_path)
1143
+
1144
+ def _on_clear_recent_files(self):
1145
+ return project_files.on_clear_recent_files(self)
1146
+
1147
+
1148
+ # 各ツールの名前を PlotterApp の属性として出す(gui/tools/__init__.py)
1149
+ expose_tool_names(PlotterApp)
1150
+ # 各部品の名前を PlotterApp の属性として出す(gui/panels/__init__.py)
1151
+ expose_panel_names(PlotterApp)