Coverage for graphica/gui/main_window.py: 96.6%
775 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1import os
2import re
3from contextlib import contextmanager
4import types
5import sys
6import logging
7import numpy as np
9logger = logging.getLogger(__name__)
11# 入力途中の "1e" や "1e-" も通す
12_SCIENTIFIC_INPUT_RE = re.compile(r'^[+-]?(\d+\.?\d*|\.\d+)?([eE][+-]?\d*)?$')
15def _scientific_text_from_value(self, value):
16 """末尾のゼロを落とし、必要なら指数表記にする(.16g は float64 の有効桁数)。"""
17 return f'{value:.16g}'
20def _scientific_validate(self, text, pos):
21 """QDoubleSpinBox の validate() は "1e-" のような入力途中を弾き、指数表記を打ち込めないので上書きする。"""
22 if text == '' or text in ('-', '+'):
23 return (QValidator.State.Intermediate, text, pos)
24 if _SCIENTIFIC_INPUT_RE.match(text): 24 ↛ 30line 24 didn't jump to line 30 because the condition on line 24 was always true
25 try:
26 float(text)
27 return (QValidator.State.Acceptable, text, pos)
28 except ValueError:
29 return (QValidator.State.Intermediate, text, pos)
30 return (QValidator.State.Invalid, text, pos)
33def _enable_scientific_notation_input(spin_box, minimum, maximum, single_step=0.1):
34 spin_box.setDecimals(SPIN_BOX_MAX_DECIMALS)
35 spin_box.setRange(minimum, maximum)
36 spin_box.textFromValue = types.MethodType(_scientific_text_from_value, spin_box)
37 spin_box.validate = types.MethodType(_scientific_validate, spin_box)
38 spin_box.setSingleStep(single_step)
41def _strip_trailing_colon_from_labels(widget):
42 """ラベル末尾の「:」を取る。生成物の ui_main_window.py に焼き込まれていて、.ui が無く作り直せない。
44 動的に足したラベルも含むよう、画面を組み立て終えてから呼ぶ。
45 """
46 for label in widget.findChildren(QLabel):
47 text = label.text()
48 if text.endswith(':'):
49 label.setText(text[:-1])
51DEFAULT_WINDOW_WIDTH = 1280
52DEFAULT_WINDOW_HEIGHT = 800
53CONTROL_DOCK_WIDTH = 340 # 右のインスペクタの幅(見本どおり)。軸の設定の長い行は名前を上に折り返して収める
54PROPERTY_LABEL_WIDTH = 92 # プロパティの名前の列(見本どおり)。長い名前は折り返す
55SPIN_BOX_MAX_DECIMALS = 16
58DEFAULT_DETACHED_CANVAS_WIDTH = 900
59DEFAULT_DETACHED_CANVAS_HEIGHT = 700
65AUTOSAVE_FILENAME = "autosave.gra"
68from graphica.gui import notify
73from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget,
74 QAbstractSpinBox, QLabel, QTreeWidgetItem)
75from PySide6.QtGui import QFont, QIcon, QValidator, QUndoStack
76from PySide6.QtCore import Qt, QTimer, Signal
77from graphica.models.project import ProjectModel
78from graphica.core.version import APP_NAME, __version__
79from graphica.core.i18n import tr, set_language
80from graphica.core.plugin_api import load_plugins_once
81from graphica.core.plugin_types import PluginExecutionError
82from graphica.gui import app_settings, log_axis_notes
83from graphica.gui.app_settings import disabled_plugin_names
84from graphica.gui.datasets.colors import ColorController
85from graphica.gui.datasets.transfer import TransferController
86from graphica.gui.datasets.overlays import OverlayController
87from graphica.gui.datasets.plugin_runs import PluginRunController
88from graphica.gui.datasets.property_panel import DatasetPropertyPanel
89from graphica.gui.datasets.fitting import FittingController
90from graphica.gui.datasets.host import DatasetHost
91from graphica.gui.datasets.order import DatasetOrder
92from graphica.gui.panels import create_panels, expose_panel_names
93from graphica.gui.widget_translation import translate_widget_texts
94from graphica.gui.tools import MOUSE_MODES, ToolManager, create_tools, expose_tool_names
95from graphica.gui.tools.view_navigation import ViewNavigationTool
96from graphica.gui import data_import_flow
97# テストがこのモジュールの属性として引くので残す(使うコードは data_import_flow と project_files に移した)
98import pandas as pd # noqa: F401
99from PySide6.QtWidgets import QMessageBox # noqa: F401
100from graphica.core.app_paths import get_app_data_dir # noqa: F401
101from graphica.gui import project_files
102from graphica.gui import plugin_update_flow
103from graphica.gui.data_import_flow import ( # noqa: F401
104 SUPPORTED_DATA_FILE_EXTENSIONS, find_unevaluated_formula_cells)
105from graphica.gui.project_files import ( # noqa: F401
106 AUTOSAVE_GENERATIONS, MAX_RECENT_FILES, UNSAVED_CHANGES_PROMPT_ENV, _unsaved_changes_prompt_enabled)
107from graphica.gui.builders import axis_panel
108from graphica.gui.builders import canvas_area
109from graphica.gui.builders import dataset_panel
110from graphica.gui.builders import property_sections
111from graphica.gui import dock_layout
112from graphica.gui import ui_mode
113from graphica.gui.shell.side_pages import populate_side_pages
114from graphica.gui.shell.tab_shell import build_tab_shell
115from graphica.gui.builders.common import ( # noqa: F401
116 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,
117 disable_wheel_tab_switching)
118from graphica.gui.resources import resource_path
119from graphica.gui.axis_bindings import AXIS_BINDINGS, AXIS_BUTTONS
120from graphica.gui.binding import Binder
121from graphica.gui.datasets.peaks import PeakController
122from graphica.gui.datasets.processing import ProcessingController
123from graphica.gui.plugin_context import TabPluginContext
124from graphica.core.app_paths import get_user_plugins_dir
127from graphica.ui_main_window import Ui_MainWindow
129from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE
130from graphica.core.commands import AddDatasetCommand, DeferRedrawCommand, RemoveDatasetCommand
131from graphica.gui.detached_canvas_window import DetachedCanvasWindow
132from graphica.gui import theme
133from graphica.gui.theme_color import theme_color_or_default
134from graphica.gui.theme import apply_form_spacing
135from graphica.gui.dialogs import (WelcomeDialog)
138# 文字装飾パネルの記号。(表示, mathtext のマクロ名)。引数の要るマクロ(\sqrt{...})は
139# 「$\macro$」で挿入できないので入れない(平方根は \surd)。
140LABEL_SYMBOL_PALETTE = [
141 ("α", "alpha"), ("β", "beta"), ("γ", "gamma"), ("δ", "delta"),
142 ("ε", "epsilon"), ("μ", "mu"), ("π", "pi"), ("ρ", "rho"),
143 ("Σ", "Sigma"), ("σ", "sigma"), ("τ", "tau"), ("Ω", "Omega"),
144 ("ω", "omega"), ("Δ", "Delta"), ("θ", "theta"), ("φ", "phi"),
145 ("×", "times"), ("÷", "div"), ("±", "pm"), ("∓", "mp"),
146 ("≈", "approx"), ("≠", "neq"), ("≤", "leq"), ("≥", "geq"),
147 ("∞", "infty"), ("→", "rightarrow"), ("←", "leftarrow"), ("∂", "partial"),
148 ("∇", "nabla"), ("∫", "int"), ("∝", "propto"), ("°", "degree"),
149 # マイナス記号(U+2212)は mathtext で包まず、文字のまま入れる
150 ("−", None),
151]
158from graphica.gui.dataset_style_icon import (
159 make_dataset_style_icon, make_dataset_visibility_icon, apply_dataset_visibility_text_style,
160 DATASET_TREE_VISIBILITY_COLUMN,
161)
162from graphica.gui.mathtext_preview import JP_CAPABLE_FONT_FAMILIES
163from graphica.gui.color_history import load_recent_colors_into_picker
165# グラフの既定フォント。UI のフォント("Yu Gothic UI" など)は matplotlib が解決できず文字化けする。
166# "Yu Gothic" は Windows だけにあるので、OS ごとの候補を並べたリストを QFont と matplotlib の両方に渡す
167# (無いフォント名は飛ばされる)。
168PLOT_DEFAULT_FONT_FAMILIES = JP_CAPABLE_FONT_FAMILIES
171def _make_default_plot_font():
172 """QFont(str) は1つの名前しか取らないので、setFamilies() で候補を全部入れる。"""
173 font = QFont()
174 font.setFamilies(PLOT_DEFAULT_FONT_FAMILIES)
175 return font
177from graphica.gui.mixins.ui_setup_mixin import UISetupMixin
178from graphica.gui.mixins.export_mixin import ExportMixin
179from graphica.gui.mixins.project_io_mixin import ProjectIOMixin
180from graphica.gui.mixins.help_mixin import HelpMixin
181from graphica.gui.mixins.quick_access_mixin import QuickAccessMixin
184def is_frozen():
185 return hasattr(sys, '_MEIPASS')
188def plugin_search_paths():
189 """プラグインの探索先(優先順)。同梱の plugins と、利用者のフォルダ(%LOCALAPPDATA%\\Graphica\\plugins)。
191 同梱のフォルダはソースから動かすときだけあるので、あるときだけ加える
192 (探索先は起動時に作られるので、無いものを渡すと site-packages の中に作ろうとする)。
193 """
194 paths = []
195 bundled = resource_path("plugins")
196 if not is_frozen() and os.path.isdir(bundled):
197 paths.append(bundled)
198 paths.append(get_user_plugins_dir())
199 return paths
207# 1つのタブ。機能ごとの mixin の役割は CLAUDE.md の「PlotterApp mixin composition」
208class PlotterApp(QMainWindow, UISetupMixin,
209 ExportMixin, ProjectIOMixin, HelpMixin, QuickAccessMixin):
210 # 保存・読み込みのたびに出す。MainAppWindow がタブ名を更新する
211 project_state_changed = Signal()
213 MOUSE_MODES = MOUSE_MODES
215 def _deactivate_other_mouse_modes(self, active_name):
216 return self.mouse_tool_manager.deactivate_others(active_name)
218 def _active_mouse_mode(self):
219 return self.mouse_tool_manager.active_name()
221 def __init__(self, run_startup_checks=True, tab_id=None):
222 """
223 画面を組み立てる。組み立ての各段は後の段が前の段で作った部品を使うので、呼ぶ順番は変えない。
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
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._build_tab_shell()
258 self._rebuild_label_tab()
259 self._connect_and_initialize()
260 # 英語表示のとき、組み立てた画面の文字をまとめて訳す(Designer の部品にも効かせる)
261 translate_widget_texts(self)
262 # 単独で作ったタブ(テストなど)はアドバンスドの見た目。MainAppWindow に入れると、そのモードで当て直す
263 ui_mode.apply_ui_mode(self, ui_mode.ADVANCED)
265 def _load_designer_ui(self):
266 self.ui = Ui_MainWindow()
267 self.ui.setupUi(self)
269 # ui_main_window.py は生成物なので、Designer に無い種類はここで足す
270 self.ui.plot_type_combo.addItem("Area")
271 self.ui.plot_type_combo.addItem("Bar")
272 self.ui.plot_type_combo.addItem("Step")
273 self.ui.plot_type_combo.addItem("Density Scatter")
274 self.ui.plot_type_combo.addItem(COLOR_BY_COLUMN_PLOT_TYPE)
276 # フォルダ分けのため、Designer の QListWidget を同じ位置の QTreeWidget に差し替える
277 self._replace_dataset_list_with_tree()
279 self.project = ProjectModel()
280 # 新しい変更の経路が project.notify_changed() だけで描き直せるように(既存の箇所は _update_plot を直接呼ぶ)
281 self.project.changed.connect(self._update_plot)
282 self.settings = app_settings.open_settings()
283 self._update_autosave_path()
284 load_recent_colors_into_picker(self.settings)
286 # 以降に作るメニューやボタンの tr() に効くので、画面を組み立てる前に
287 set_language(app_settings.LANGUAGE.read(self.settings))
289 # 起動時に False にし、正常に閉じたときだけ closeEvent で True に戻す。次の起動で False なら異常終了とみなし、
290 # オートセーブからの復元を勧める。アプリ全体で1つの値なので、2つ目以降のタブは触らない
291 if self._run_startup_checks:
292 self._had_clean_exit = app_settings.CLEAN_EXIT.read(self.settings)
293 app_settings.CLEAN_EXIT.write(self.settings, False)
294 else:
295 self._had_clean_exit = True
297 self.setWindowTitle(f"{APP_NAME} {__version__}")
298 icon_path = resource_path("Graphica.ico")
299 self.setWindowIcon(QIcon(icon_path))
300 self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.ui.control_dock_widget)
302 def _init_state(self):
303 # 軸の設定とデータセットの一覧の部品(状態はタブが持つ。gui/panels/)
304 self.panels = create_panels(self)
305 # 非モーダルのダイアログとバックグラウンドの処理は、回収されないよう参照を持っておく
306 self.data_editor_dialog = None
307 self.help_dialog = None
308 self.calc_help_dialog = None
309 self._data_load_task_runner = None
310 self._batch_export_task_runner = None
311 self._update_check_task_runner = None
312 self._update_download_runner = None
313 self._plugin_update_runner = None
314 # 複数ファイルをまとめて読み込むときの待ち行列と進捗
315 self._data_load_queue = []
316 self._data_load_queue_total = 0
317 self._data_load_queue_done = 0
318 # フォルダからの一括取り込みで、ファイル名から列を取り出す正規表現。待ち行列を使い切ったら None に戻す
319 self._batch_import_filename_regex = None
320 # 上書き保存先。None なら manual_save() は「名前を付けて保存」になる。
321 # タブ名もこの値から作る(ProjectModel.current_filepath はオートセーブでも変わる)。
322 self._current_project_path = None
323 self._restored_unsaved = False # オートセーブから復元し、まだ保存していない
324 # 直前に保存/読み込みした時点の内容のハッシュ(未保存の変更の判定)。None はファイルと対応していない
325 self._saved_content_fingerprint = None
327 # データエディタのセル編集の Undo とは別
328 self.undo_stack = QUndoStack(self)
330 # データセットに対する操作を機能ごとに分けたクラス。窓口(DatasetHost)は本体に使うときに触れるので、
331 # 画面を組み立てる前に作っておける(組み立ての途中からも使われる)。
332 self.dataset_order = DatasetOrder(
333 self.project, lambda: self.ui.dataset_list_widget, self._add_dataset_list_item)
334 self._dataset_host = DatasetHost(self)
335 self.peaks = PeakController(self._dataset_host)
336 self.fitting = FittingController(self._dataset_host)
337 self.processing = ProcessingController(self._dataset_host)
338 self.colors = ColorController(self._dataset_host)
339 self.transfer = TransferController(self._dataset_host)
340 self.overlays = OverlayController(self._dataset_host)
341 self.plugin_runs = PluginRunController(self._dataset_host)
342 self.property_panel = DatasetPropertyPanel(self)
343 # 軸の設定の欄と値の対応(集める・戻す・止める・つなぐ)
344 self._axis_binder = Binder(self, AXIS_BINDINGS, also_blocked=AXIS_BUTTONS)
346 # メニューを作るときに参照するので、ここで用意する(0分なら止めたまま)
347 self.autosave_timer = QTimer(self)
348 self.autosave_timer.timeout.connect(self.auto_save)
349 saved_interval_min = app_settings.AUTOSAVE_INTERVAL_MIN.read(self.settings)
350 if saved_interval_min > 0: 350 ↛ 353line 350 didn't jump to line 353 because the condition on line 350 was always true
351 self.autosave_timer.start(saved_interval_min * 60 * 1000)
353 self.all_axes = []
354 self.all_secondary_axes = []
355 self.project.all_plot_settings = []
356 self.project.active_axis_index = 0
358 # deferred_redraw() の入れ子の深さと、その間に頼まれた描き直し((light, full_resolution) か None)
359 self._redraw_defer_depth = 0
360 self._deferred_redraw_request = None
362 # マウス操作の各モード(状態は各ツールが持つ。gui/tools/)
363 self.mouse_tools = create_tools(self)
364 self.mouse_tool_manager = ToolManager(self)
365 self.view_navigation = ViewNavigationTool(self)
368 # 最初の軸の設定の既定値になる。フォントは QFont()(UI のフォント)にしない:
369 # matplotlib は "Yu Gothic UI" のような UI 用のフォントを解決できず、文字化けする
370 self._tick_font = _make_default_plot_font()
371 self._tick_color = '#000000'
372 self._tick_width = 0.8
373 self._axis_label_font = _make_default_plot_font()
374 self._axis_label_color = '#000000'
375 self._spine_width = 1.0
376 self._spine_color = '#000000'
377 self._legend_font = _make_default_plot_font()
378 self._legend_color = '#000000'
380 def _setup_window(self):
381 self.resize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
382 self.ui.control_dock_widget.setFixedWidth(CONTROL_DOCK_WIDTH)
384 # 余った縦の空きはキャンバスに回す(指定しないとデータセット一覧と均等に分けられ、一覧の下が空く)
385 self.ui.gridLayout_2.setRowStretch(1, 1) # キャンバス
386 self.ui.gridLayout_2.setRowStretch(2, 0) # データセット一覧
387 self.ui.gridLayout_2.setRowStretch(3, 0) # ボタンの行
388 disable_wheel_tab_switching(self.ui.axis_tab_widget)
390 def _build_canvas_and_toolbar(self):
391 return canvas_area.build_canvas_and_toolbar(self)
393 def _setup_status_bar(self):
394 return canvas_area.setup_status_bar(self)
396 def _build_dataset_color_picker(self):
397 return dataset_panel.build_dataset_color_picker(self)
399 def _build_dataset_list_buttons(self):
400 return dataset_panel.build_dataset_list_buttons(self)
402 def _build_dataset_style_controls(self):
403 return dataset_panel.build_dataset_style_controls(self)
405 def _build_legend_location_control(self):
406 return axis_panel.build_legend_location_control(self)
408 def _build_fit_info_and_stats(self):
409 return dataset_panel.build_fit_info_and_stats(self)
411 def _build_secondary_y_controls(self):
412 return dataset_panel.build_secondary_y_controls(self)
414 def _build_tick_grid_and_colorbar_controls(self):
415 return axis_panel.build_tick_grid_and_colorbar_controls(self)
417 def _build_tick_format_controls(self):
418 return axis_panel.build_tick_format_controls(self)
420 def _build_label_editors(self):
421 return axis_panel.build_label_editors(self)
423 def _add_subplot_target_row(self):
424 return dataset_panel.add_subplot_target_row(self)
426 def _arrange_property_docks(self):
427 return dock_layout.arrange_property_docks(self)
429 def _build_tab_shell(self):
430 return build_tab_shell(self)
432 def _rebuild_label_tab(self):
433 return axis_panel.rebuild_label_tab(self)
435 def _connect_and_initialize(self):
436 self.canvas.mpl_connect('motion_notify_event', self._on_mouse_move)
438 # グラフの要素のクリックでの選択は、どのモードでも常に有効
439 self.canvas.mpl_connect('pick_event', self._on_element_pick)
441 # 表示範囲の操作(軸の上のホイール・ドラッグ、矩形ズーム、ダブルクリックでのリセットなど)は常に有効
442 self.view_navigation.connect()
443 # 対数軸で表示されない値の説明は、どの経路で描き直しても最新にする
444 self.canvas.mpl_connect('draw_event', lambda event: log_axis_notes.update_log_axis_notes(self, event))
445 # 凡例をドラッグした位置を設定へ保存する
446 self.canvas.mpl_connect('button_release_event', self._on_legend_drag_release)
448 # プラグインの登録はメニューを作る(_create_menu_bar)より前に。読み込みはアプリ全体で1回
449 # (load_plugins_once)、メニューへの追加はタブごとに _create_menu_bar が行う
450 self.plugin_api = load_plugins_once(
451 plugin_search_paths(), disabled_names=disabled_plugin_names(self.settings)
452 )
454 # プラグインへの窓口は (このタブ, プラグイン) ごとに1つ。
455 self._plugin_contexts = {}
457 # プラグインのパネル。左端の帯の「プラグイン」のページに入れる(GUI 刷新。以前はドック)。メニューに
458 # 開く項目を足すのは _create_menu_bar() なので、それより前に作る。1つ失敗しても他のパネルとタブの起動は続ける。
459 self._plugin_panels = {}
460 for panel in self.plugin_api.get_panels():
461 try:
462 widget = panel.widget_factory(self.plugin_context(panel.plugin_name))
463 if not isinstance(widget, QWidget):
464 raise TypeError(f"QWidgetを返しませんでした(型: {type(widget).__name__})。")
465 except Exception as e:
466 logger.warning(
467 "[plugin:%s] パネル '%s' の構築に失敗しました: %s",
468 panel.plugin_name, panel.name, e,
469 )
470 continue
471 widget.setObjectName(f"PluginPanel_{panel.name}")
472 self.plugin_page.add_panel(panel.name, widget)
473 self._plugin_panels[panel.name] = widget
475 # フォーカスのあるドックの枠を強調する(プラグインのドックも含め、全部に効く)
476 theme.install_dock_focus_highlight(self)
478 # プラグインの plot_type を種類の欄の最後に足す(描き方は canvas._draw_plot_type が探す)
479 for plot_type in self.plugin_api.get_plot_types():
480 self.ui.plot_type_combo.addItem(plot_type.type_name)
482 self._connect_signals()
484 self._create_menu_bar()
485 populate_side_pages(self)
486 canvas_area.finish_card(self)
488 # クイックアクセスのピン留めを戻す。プラグインのものを含めて全メニューができた後であること
489 self._restore_quick_access_actions()
490 self._install_quick_access_context_menus()
492 # 画面の既定の状態を最初の軸の設定にする
493 default_settings = self._gather_settings_from_ui()
494 self.project.all_plot_settings.append(default_settings)
495 self._set_initial_ui_state()
496 # 生成物の ui_main_window.py のラベルには末尾に「:」が付いている。.ui が無く作り直せないので、ここで取る
497 _strip_trailing_colon_from_labels(self)
499 spacing_value = 6
500 if self.ui.formLayout_3: self.ui.formLayout_3.setSpacing(spacing_value)
502 # 入力欄の左端を揃える(プロパティ欄の7節どうし、X/Y 軸のタブどうし)。ラベルが全部できた後で
503 property_sections.align_form_label_columns(
504 self, [self._prop_form(key) for key, _title in DATASET_PROPERTY_SECTIONS],
505 width=PROPERTY_LABEL_WIDTH)
506 self._align_form_label_columns([self.ui.formLayout, self.ui.formLayout_2])
509 # 軸の範囲と目盛りの間隔は、指数表記でも入力できるようにする
510 for spin_box in [self.ui.x_min_spinbox, self.ui.x_max_spinbox,
511 self.ui.y_min_spinbox, self.ui.y_max_spinbox]:
512 _enable_scientific_notation_input(spin_box, minimum=-np.inf, maximum=np.inf)
514 for spin_box in [self.ui.x_major_tick_interval_spinbox, self.ui.y_major_tick_interval_spinbox,
515 self.ui.x_minor_tick_interval_spinbox, self.ui.y_minor_tick_interval_spinbox]:
516 _enable_scientific_notation_input(spin_box, minimum=0, maximum=np.inf)
518 # ウォーターフォールのオフセットもデータの単位なので、大きい値やマイナス・指数表記を入れられるように
519 for spin_box in [self.waterfall_offset_x_spinbox, self.waterfall_offset_y_spinbox]:
520 _enable_scientific_notation_input(spin_box, minimum=-np.inf, maximum=np.inf)
522 # 打ち込んだ数値は Enter か欄を離れたときに反映する(▲▼・矢印キー・ホイールはすぐ)。1文字ごとに反映すると、
523 # 反映で欄が書き直されて("0" → "0.00")小数を打てず、大きなデータでは打つたびに描き直しで止まる
524 for spin_box in self.ui.control_dock_widget.findChildren(QAbstractSpinBox):
525 spin_box.setKeyboardTracking(False)
527 self._update_plot()
528 # 環境設定の既定の書式テンプレート(新しいタブはどれも空のプロジェクトから始まる)
529 self._apply_default_style_template()
531 # ドックの配置は前回の状態を戻す(最初のタブだけ。窓の大きさと位置は MainAppWindow が扱う)。
532 # 既定の配置の版(DOCK_LAYOUT_VERSION)が保存時と違えば戻さない(戻すと新しい既定が既存の利用者に届かない)。
533 # 戻すのはイベントループが一巡してタブが最終の大きさになってから。ここで戻すと仮の大きさで
534 # スプリッターの位置が決まり、見た目とクリックの位置がずれる。
536 # タブに結び付けておく(先にタブが閉じられたら呼ばない。閉じたタブのドックを触ると落ちる)
537 QTimer.singleShot(0, self, self._restore_dock_layout)
539 # キャンバスを切り離したまま閉じていれば同じ状態に戻す。ダークモードなどと同じ軽い設定なので、どのタブでも戻す
540 if app_settings.CANVAS_WAS_DETACHED.read(self.settings):
541 QTimer.singleShot(0, self, lambda: self._detach_canvas(restore_geometry=True))
543 self.setAcceptDrops(True)
545 # 起動時の確認は最初のタブだけ。窓が表示される前だとダイアログが変な位置に出るので、一巡してから
546 if self._run_startup_checks:
547 QTimer.singleShot(0, self, self._check_autosave_recovery)
548 # 初回の案内は復元の確認の後(データに関わる確認を先に)
549 QTimer.singleShot(0, self, self._check_first_launch)
550 # 新しい版の確認は上の2つの邪魔をしないよう少し遅らせる。公開 API への匿名の GET 1回だけで、
551 # 新しい版があるときだけ知らせ、失敗しても何も出さない
552 QTimer.singleShot(1500, self, self._start_startup_update_check)
553 # プラグインの新しい版の確認は1日1回(入れたプラグインが無ければ通信しない)
554 QTimer.singleShot(3000, self, lambda: plugin_update_flow.start_startup_check(self))
556 # 項目の間の余白を広げる。フォームが全部できた最後に
557 apply_form_spacing(self)
559 def _restore_dock_layout(self):
560 return dock_layout.restore_dock_layout(self)
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
576 self.fitting.shutdown()
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
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
602 # 同じ理由(ダウンロードは止めると途中のファイルを消して戻る)
603 if self._update_download_runner is not None: 603 ↛ 604line 603 didn't jump to line 604 because the condition on line 603 was never true
604 try:
605 self._update_download_runner.succeeded.disconnect()
606 self._update_download_runner.failed.disconnect()
607 except (RuntimeError, TypeError):
608 pass
609 self._update_download_runner.requestInterruption()
610 self._update_download_runner.wait()
611 self._update_download_runner.deleteLater()
612 self._update_download_runner = None
614 plugin_update_flow.stop(self)
616 if self._run_startup_checks:
617 app_settings.CLEAN_EXIT.write(self.settings, True)
618 app_settings.WINDOW_STATE.write(self.settings, self.saveState())
619 app_settings.DOCK_LAYOUT_VERSION.write(self.settings, DOCK_LAYOUT_VERSION)
621 # 切り離したまま閉じたら次の起動で同じ状態に戻す。軽い設定なので最初のタブに限らない
622 if self.canvas_detached and self._canvas_detach_window is not None:
623 app_settings.CANVAS_DETACHED_GEOMETRY.write(self.settings, self._canvas_detach_window.saveGeometry())
624 app_settings.CANVAS_WAS_DETACHED.write(self.settings, self.canvas_detached)
626 # 切り離した窓が残らないよう片付ける。_reattach_canvas を通すと、上で保存した「切り離していた」を
627 # 消してしまうので通さない。窓と一緒に破棄されないよう、先にキャンバスの親を外す
628 if self._canvas_detach_window is not None:
629 self._canvas_detach_window.closed.disconnect(self._on_detach_window_closed)
630 self._canvas_detach_window.takeCentralWidget()
631 self.canvas.setParent(None)
632 self._canvas_detach_window.close()
633 self._canvas_detach_window.deleteLater()
634 self._canvas_detach_window = None
636 super().closeEvent(event)
638 def _check_autosave_recovery(self):
639 return project_files.check_autosave_recovery(self)
641 def _check_first_launch(self):
642 if app_settings.HAS_SHOWN_WELCOME.read(self.settings):
643 return
644 app_settings.HAS_SHOWN_WELCOME.write(self.settings, True)
645 self._show_welcome_dialog()
647 def _on_show_startup_screen(self):
648 self._show_welcome_dialog()
650 def _show_welcome_dialog(self, last_tab_closed=False):
651 dialog = WelcomeDialog(self, recent_files=self._get_recent_files(), last_tab_closed=last_tab_closed)
652 dialog.exec()
653 if dialog.load_sample_requested:
654 self._load_sample_data()
655 elif dialog.selected_recent_file:
656 self._on_open_recent_file(dialog.selected_recent_file)
657 elif dialog.load_template_requested:
658 self._on_load_plot_template()
659 elif dialog.open_project_requested:
660 self._on_load_project()
662 def _on_show_autosave_history(self):
663 return project_files.on_show_autosave_history(self)
665 def _load_sample_data(self):
666 sample_path = resource_path(os.path.join("sample_data", "cooling_curve_sample.csv"))
667 if not os.path.exists(sample_path):
668 notify.warning(self, "サンプルデータ", "サンプルデータファイルが見つかりませんでした。")
669 return
670 self.load_data(sample_path)
672 def _update_autosave_path(self):
673 return project_files.update_autosave_path(self)
675 def _rotate_autosave_generations(self):
676 return project_files.rotate_autosave_generations(self)
678 def _sync_project_from_ui(self):
679 return project_files.sync_project_from_ui(self)
681 def _remember_saved_content(self):
682 return project_files.remember_saved_content(self)
684 # DatasetHost と TabPluginContext が使う窓口。中は非公開の名前を呼ぶだけ(テストの差し替えが届くよう、呼ぶときに引く)
685 def current_dataset(self):
686 return self._get_current_dataset()
688 def selected_datasets(self):
689 return self._get_selected_datasets()
691 def dataset_tree_item(self, dataset):
692 return self._get_dataset_tree_item(dataset)
694 def target_folder_for_new_dataset(self):
695 return self._get_target_folder_for_new_dataset()
697 def add_dataset(self, dataset, parent_folder=None, select=True):
698 return self._add_dataset(dataset, parent_folder, select=select)
700 def add_dataset_with_undo(self, dataset, parent_folder=None, description="データセットの追加"):
701 return self._add_dataset_with_undo(dataset, parent_folder=parent_folder, description=description)
703 def push_dataset_additions(self, add, datasets, description):
704 return self._push_dataset_additions(add, datasets, description)
706 def remove_dataset_items_with_undo(self, items, description=None):
707 return self._remove_dataset_items_with_undo(items, description=description)
709 def push_dataset_property_command(self, dataset, old_values, new_values, description, skip_if_unchanged=True):
710 return self._push_dataset_property_command(dataset, old_values, new_values, description,
711 skip_if_unchanged=skip_if_unchanged)
713 def refresh_after_dataset_property_change(self, dataset, changed_keys=(), old_values=None, new_values=None):
714 return self._refresh_after_dataset_property_change(dataset, changed_keys=changed_keys, old_values=old_values,
715 new_values=new_values)
717 def add_annotation(self, axis_index, annotation, description):
718 return self._add_annotation(axis_index, annotation, description=description)
720 def redraw(self):
721 return self._update_plot()
723 @contextmanager
724 def deferred_redraw(self):
725 """中で頼まれたグラフ全体の描き直しを、抜けるときの1回にまとめる(データセットを1件ずつ足すと件数分描き直すので)。"""
726 self._begin_deferring_redraw()
727 try:
728 yield
729 finally:
730 self._end_deferring_redraw()
732 @contextmanager
733 def batched_undo_group(self, description):
734 """Undo 1回分にまとめ、描き直しも最後の1回にする。Undo・Redo のときも1回で済む。"""
735 self.begin_batched_undo_group(description)
736 try:
737 yield
738 finally:
739 self.end_batched_undo_group()
741 def begin_batched_undo_group(self, description):
742 self.undo_stack.beginMacro(description)
743 self.undo_stack.push(DeferRedrawCommand(self._begin_deferring_redraw, self._end_deferring_redraw, opening=True))
745 def end_batched_undo_group(self):
746 self.undo_stack.push(DeferRedrawCommand(self._begin_deferring_redraw, self._end_deferring_redraw, opening=False))
747 self.undo_stack.endMacro()
749 def redraw_appearance(self):
750 return self._update_plot_appearance()
752 def pending_peak_guesses(self):
753 return self._pending_peak_guesses
755 def clear_pending_peak_guesses(self):
756 return self._clear_pending_peak_guesses()
758 def toggle_peak_placement_mode(self, checked):
759 return self._toggle_peak_placement_mode(checked)
761 @property
762 def dataset_host(self):
763 return self._dataset_host
765 def show_export_preview(self):
766 """右のインスペクタの「エクスポート」のタブを開く(表示メニュー)。"""
767 self.properties_dock_widget.show()
768 self.inspector.show_page("export")
770 def show_bottom_page(self, key):
771 """グラフの下のパネルのタブを開く(表示メニュー)。シンプルでは出ないので、アドバンスドに切り替える。"""
772 if getattr(self, 'ui_mode', None) != ui_mode.ADVANCED:
773 self.show_advanced_mode()
774 self.bottom_panel.show_page(key)
776 def set_theme_color(self, color):
777 """テーマカラーを替えて保存する(⚙ のポップアップ・環境設定から)。ウィンドウに入っていれば全部のタブに当てる。"""
778 main_window = self.window()
779 if main_window is not self and hasattr(main_window, 'set_theme_color'):
780 main_window.set_theme_color(color)
781 return
782 color = theme_color_or_default(color)
783 app_settings.THEME_COLOR.write(self.settings, color)
784 theme.apply_theme(QApplication.instance(), self.canvas.dark_mode, color)
785 self.refresh_theme_colors()
787 def show_advanced_mode(self):
788 """アドバンスドに切り替える(シンプルのプロパティの案内から)。ウィンドウに入っていれば全部のタブに当てて保存する。"""
789 main_window = self.window()
790 if main_window is not self and hasattr(main_window, 'set_ui_mode'): 790 ↛ 793line 790 didn't jump to line 793 because the condition on line 790 was always true
791 main_window.set_ui_mode(ui_mode.ADVANCED)
792 else:
793 ui_mode.apply_ui_mode(self, ui_mode.ADVANCED)
795 def show_plugin_panel(self, name):
796 """左端の帯の「プラグイン」のページを開き、そのパネルを出す。"""
797 self.activity_rail.select_page("plugins")
798 self.plugin_page.show_panel(name)
800 def plugin_context(self, plugin_name):
801 context = self._plugin_contexts.get(plugin_name)
802 if context is None:
803 context = TabPluginContext(self, plugin_name)
804 self._plugin_contexts[plugin_name] = context
805 return context
807 def _notify_plugins_datasets_changed(self):
808 for context in self._plugin_contexts.values():
809 context._notify_datasets_changed()
811 def _notify_plugins_selection_changed(self):
812 current = self._get_current_dataset()
813 for context in self._plugin_contexts.values():
814 context._notify_selection_changed(current)
816 def _run_plugin_menu_action(self, menu_action):
817 try:
818 menu_action.callback(self.plugin_context(menu_action.plugin_name))
819 except Exception as e:
820 logger.exception("[plugin:%s] メニュー「%s」の実行に失敗しました", menu_action.plugin_name, menu_action.text)
821 notify.critical(self, "プラグインのエラー", str(PluginExecutionError(
822 menu_action.plugin_name, f"「{menu_action.text}」の実行に失敗しました: {e}"
823 )))
825 def document_title(self):
826 return project_files.document_title(self)
828 def has_unsaved_changes(self):
829 return project_files.has_unsaved_changes(self)
831 def confirm_unsaved_changes(self, action_text):
832 return project_files.confirm_unsaved_changes(self, action_text)
834 def auto_save(self):
835 return project_files.auto_save(self)
837 def manual_save(self):
838 return project_files.manual_save(self)
840 def manual_save_as(self):
841 return project_files.manual_save_as(self)
843 def _save_project_to_path(self, filepath):
844 return project_files.save_project_to_path(self, filepath)
846 def manual_load(self):
847 return project_files.manual_load(self)
849 def _load_project_from_path(self, filepath, add_to_recent=True):
850 return project_files.load_project_from_path(self, filepath, add_to_recent)
852 def _reset_zoom(self):
853 """設定どおりの表示範囲に戻す。
855 matplotlib の Home ボタンは作り直す前の Axes を覚えているので、描き直しを挟むと効かない。
856 """
857 self.view_navigation.forget_all_views()
858 self._update_plot()
860 def _begin_deferring_redraw(self):
861 self._redraw_defer_depth += 1
863 def _end_deferring_redraw(self):
864 self._redraw_defer_depth -= 1
865 if self._redraw_defer_depth == 0 and self._deferred_redraw_request is not None:
866 light, full_resolution = self._deferred_redraw_request
867 self._deferred_redraw_request = None
868 self._update_plot(light=light, full_resolution=full_resolution)
870 def _update_plot(self, light=False, full_resolution=False):
871 """グラフ全体を描き直す。
873 light=True は Axes の数・配置・所属が変わらないときだけ(既存の Axes の上で描き直す)。
874 full_resolution=True は表示用の間引きをしない(フル解像度でのエクスポート用)。
875 """
876 if self._redraw_defer_depth:
877 # まとめた描き直しは、一度でも重い方を頼まれたら重い方にする
878 previous = self._deferred_redraw_request
879 self._deferred_redraw_request = (
880 light and (previous is None or previous[0]),
881 full_resolution or (previous is not None and previous[1]),
882 )
883 return
884 layout_mode = getattr(self.project, 'layout_mode', 'grid')
885 if layout_mode == 'free':
886 # 自由配置では行数×列数ではなく、軸の設定の数がサブプロットの数になる
887 rows, cols = 0, 0
888 if len(self.project.all_plot_settings) == 0:
889 return
890 else:
891 rows = self.subplot_rows_spinbox.value()
892 cols = self.subplot_cols_spinbox.value()
893 if rows * cols == 0:
894 return
896 canvas_update_method = (
897 self.canvas.update_all_axes_appearance_and_data if light else self.canvas.redraw_all
898 )
899 is_secondary_visible_global = canvas_update_method(
900 self.project.datasets, rows, cols, self.project.all_plot_settings, layout_mode=layout_mode,
901 panel_labels_enabled=self.project.panel_labels_enabled,
902 share_x_axis=getattr(self.project, 'share_x_axis', False),
903 share_y_axis=getattr(self.project, 'share_y_axis', False),
904 full_resolution=full_resolution,
905 )
907 self.tick_direction_y2_label.setVisible(is_secondary_visible_global)
908 self.major_tick_direction_y2_combo.setVisible(is_secondary_visible_global)
909 self.minor_tick_direction_y2_combo.setVisible(is_secondary_visible_global)
910 self.y2_label_text_label.setVisible(is_secondary_visible_global)
911 self.y2_label_text_edit.setVisible(is_secondary_visible_global)
913 self.all_axes = self.canvas.all_axes
914 self.all_secondary_axes = self.canvas.all_secondary_axes
916 if hasattr(self, 'export_preview_panel'): 916 ↛ 920line 916 didn't jump to line 920 because the condition on line 916 was always true
917 self.export_preview_panel.refresh_preview()
919 # 全体の描き直しでハイライトも消えるので、データエディタの選択を戻す
920 self._reapply_editor_row_highlight()
922 # ミニマップは別の Figure なので、redraw_all() では描き直されない。
923 self._refresh_minimap()
924 self._notify_plugins_datasets_changed()
926 def _refresh_minimap(self):
927 """隠れていても描く(次に出したときに最新になっているように)。"""
928 if not hasattr(self, 'minimap'):
929 return
930 self.minimap.refresh(self.project.datasets, self.canvas.dark_mode)
932 def _on_minimap_range_selected(self, xmin, xmax):
933 """ミニマップで選んだ範囲を全部の軸の X の範囲にする。Axes は作り直さない(set_xlim と draw_idle で足りる)。"""
934 for ax in self.canvas.all_axes:
935 ax.set_xlim(xmin, xmax)
936 self.view_navigation.remember_x_of_every_subplot()
937 self.canvas.draw_idle()
939 def _on_toggle_minimap(self, checked):
940 self.minimap_visible = checked
941 self.minimap.setVisible(checked)
942 self.minimap_separator.setVisible(checked)
943 app_settings.MINIMAP_VISIBLE.write(self.settings, checked)
945 def _on_toggle_panel_labels(self, checked):
946 """QSettings ではなくプロジェクトに保存する。"""
947 self.project.panel_labels_enabled = checked
948 self._update_plot(light=True)
950 # キャンバスの切り離し。self.canvas はあちこちから直接参照されているので、破棄も作り直しもせず親を替えるだけ。
952 def _on_toggle_canvas_detached(self, checked):
953 if checked:
954 self._detach_canvas()
955 else:
956 self._reattach_canvas()
958 def _sync_canvas_detach_action(self):
959 """メニューのチェックと文言(「切り離す」/「元に戻す」)を状態に合わせる。"""
960 if not hasattr(self, 'canvas_detach_action'):
961 return
962 self.canvas_detach_action.blockSignals(True)
963 self.canvas_detach_action.setChecked(self.canvas_detached)
964 self.canvas_detach_action.setText(
965 tr("キャンバスを元に戻す") if self.canvas_detached else tr("キャンバスを別ウィンドウに切り離す")
966 )
967 self.canvas_detach_action.blockSignals(False)
969 def _detach_canvas(self, restore_geometry=False):
970 if self.canvas_detached:
971 return
973 self._plot_layout.removeWidget(self.canvas)
975 title = f"{APP_NAME} - {tr('グラフキャンバス')}"
976 self._canvas_detach_window = DetachedCanvasWindow(title)
977 self._canvas_detach_window.setCentralWidget(self.canvas)
978 self._canvas_detach_window.closed.connect(self._on_detach_window_closed)
980 saved_geometry = app_settings.CANVAS_DETACHED_GEOMETRY.read(self.settings) if restore_geometry else None
981 if saved_geometry is not None:
982 # 先にネイティブのハンドルを作る。窓が実体化する前に restoreGeometry() すると位置がずれる
983 self._canvas_detach_window.winId()
984 self._canvas_detach_window.restoreGeometry(saved_geometry)
985 else:
986 self._canvas_detach_window.resize(DEFAULT_DETACHED_CANVAS_WIDTH, DEFAULT_DETACHED_CANVAS_HEIGHT)
988 self._canvas_detach_window.show()
989 self.canvas.show()
991 self.canvas_detached = True
992 self._sync_canvas_detach_action()
993 app_settings.CANVAS_WAS_DETACHED.write(self.settings, True)
995 def _reattach_canvas(self):
996 """「元に戻す」と、切り離した窓を閉じたときの両方から呼ばれる。"""
997 if not self.canvas_detached:
998 return
1000 if self._canvas_detach_window is not None: 1000 ↛ 1008line 1000 didn't jump to line 1008 because the condition on line 1000 was always true
1001 app_settings.CANVAS_DETACHED_GEOMETRY.write(self.settings, self._canvas_detach_window.saveGeometry())
1002 self._canvas_detach_window.closed.disconnect(self._on_detach_window_closed)
1003 self._canvas_detach_window.takeCentralWidget()
1004 self._canvas_detach_window.close()
1005 self._canvas_detach_window.deleteLater()
1006 self._canvas_detach_window = None
1008 self.canvas.setParent(self.ui.plot_container)
1009 self._plot_layout.insertWidget(self._canvas_layout_index, self.canvas)
1010 self.canvas.show()
1012 self.canvas_detached = False
1013 self._sync_canvas_detach_action()
1014 app_settings.CANVAS_WAS_DETACHED.write(self.settings, False)
1016 def _on_detach_window_closed(self):
1017 self._reattach_canvas()
1019 def _update_plot_appearance(self):
1020 layout_mode = getattr(self.project, 'layout_mode', 'grid')
1021 if layout_mode == 'free':
1022 rows, cols = 0, 0
1023 else:
1024 rows = self.subplot_rows_spinbox.value()
1025 cols = self.subplot_cols_spinbox.value()
1026 self.canvas.update_appearance_only(
1027 self.project.all_plot_settings, datasets=self.project.datasets, rows=rows, cols=cols,
1028 layout_mode=layout_mode,
1029 share_x_axis=getattr(self.project, 'share_x_axis', False),
1030 share_y_axis=getattr(self.project, 'share_y_axis', False),
1031 )
1033 if hasattr(self, 'export_preview_panel'): 1033 ↛ exitline 1033 didn't return from function '_update_plot_appearance' because the condition on line 1033 was always true
1034 self.export_preview_panel.refresh_preview()
1036 def _on_editor_rows_highlighted(self, master_indices):
1037 """データエディタで選んだ行の点をグラフ上で強調する(逆向きは cursor_mixin の _on_pick)。"""
1038 if self.data_editor_dialog is None:
1039 return
1040 self.canvas.set_highlighted_points(self.data_editor_dialog.dataset, master_indices)
1042 def _reapply_editor_row_highlight(self):
1043 if self.data_editor_dialog is None:
1044 return
1045 self.canvas.set_highlighted_points(
1046 self.data_editor_dialog.dataset, self.data_editor_dialog.get_selected_master_indices()
1047 )
1051 def _build_dataset_property_sections(self):
1052 return property_sections.build_dataset_property_sections(self)
1054 def _prop_form(self, section_key):
1055 return property_sections.prop_form(self, section_key)
1057 def _align_form_label_columns(self, forms):
1058 return property_sections.align_form_label_columns(self, forms)
1060 def _set_dataset_property_fields_enabled(self, enabled):
1061 return property_sections.set_dataset_property_fields_enabled(self, enabled)
1063 def _load_collapsed_property_sections(self):
1064 return property_sections.load_collapsed_property_sections(self)
1066 def _on_property_section_toggled(self, section_key, checked):
1067 return property_sections.on_property_section_toggled(self, section_key, checked)
1069 def _update_property_section_visibility(self):
1070 return property_sections.update_property_section_visibility(self)
1072 # データセット一覧(QTreeWidget)。データセットの葉は UserRole に Dataset を、フォルダは None を持つ。
1074 def _replace_dataset_list_with_tree(self):
1075 return dataset_panel.replace_dataset_list_with_tree(self)
1077 def _add_dataset_list_item(self, dataset, parent_item=None):
1078 """UserRole に Dataset そのものを持たせる(並べ替えても、同名があっても対応が崩れない)。"""
1079 item = QTreeWidgetItem([dataset.name])
1080 item.setData(0, Qt.ItemDataRole.UserRole, dataset)
1081 item.setIcon(0, make_dataset_style_icon(dataset))
1082 # 目のアイコンのクリックは dataset_mixin の _on_dataset_tree_item_clicked が列で見分ける
1083 item.setIcon(DATASET_TREE_VISIBILITY_COLUMN, make_dataset_visibility_icon(dataset))
1084 apply_dataset_visibility_text_style(item, dataset)
1085 # データセット自身はフォルダではないので、ドロップ先にはしない
1086 item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsDropEnabled)
1087 if parent_item is not None:
1088 parent_item.addChild(item)
1089 else:
1090 self.ui.dataset_list_widget.addTopLevelItem(item)
1091 return item
1093 def _get_target_folder_for_new_dataset(self):
1094 """選択中の項目がフォルダならそれを返す(新しいデータセットをその中に入れる)。"""
1095 return self.dataset_order.target_folder()
1097 def _add_dataset(self, dataset, parent_folder=None, select=True):
1098 new_item = self.dataset_order.append(dataset, parent_folder)
1099 if select:
1100 self.ui.dataset_list_widget.setCurrentItem(new_item)
1101 self._update_plot()
1102 return new_item
1104 def _add_dataset_with_undo(self, dataset, parent_folder=None, description="データセットの追加"):
1105 """_add_dataset() の Undo できる版(プラグインの処理と解析の結果用)。"""
1106 def do_add():
1107 self._add_dataset(dataset, parent_folder, select=True)
1109 def do_remove():
1110 self.dataset_order.remove_added(dataset)
1111 self.property_panel.update_ui_state()
1112 self._update_plot()
1114 command = AddDatasetCommand(do_add, do_remove, description=description)
1115 self.undo_stack.push(command)
1117 def _push_dataset_additions(self, add, datasets, description):
1118 """add() で足す datasets を、Undo 1回で取り除けるように積む(積むとすぐ add() が走る)。"""
1119 def remove():
1120 for dataset in reversed(datasets):
1121 self.dataset_order.remove_added(dataset)
1122 self.property_panel.update_ui_state()
1123 self._update_plot()
1125 self.undo_stack.push(AddDatasetCommand(add, remove, description=description))
1127 def _remove_dataset_items_with_undo(self, top_level_items, description=None):
1128 """ツリーの項目(データセットかフォルダ)を Undo できるように削除する。
1130 top_level_items には最上位の対象だけを渡す(フォルダを渡すと中身も一緒に消える)。
1131 """
1132 steps = self.dataset_order.removal(top_level_items)
1133 if steps is None: 1133 ↛ 1134line 1133 didn't jump to line 1134 because the condition on line 1133 was never true
1134 return
1135 remove, restore, removed_datasets = steps
1137 if description is None:
1138 if len(removed_datasets) == 1:
1139 description = f"「{removed_datasets[0].name}」の削除"
1140 else:
1141 description = f"{len(removed_datasets)}件のデータセットの削除"
1143 def do_remove():
1144 remove()
1145 self.property_panel.update_ui_state()
1146 self._update_plot()
1148 def do_restore():
1149 restore()
1150 self.property_panel.update_ui_state()
1151 self._update_plot()
1153 self.undo_stack.push(
1154 RemoveDatasetCommand(do_remove, do_restore, description=description)
1155 )
1157 def _add_dataset_folder_item(self, name, parent_item=None):
1158 return self.dataset_order.add_folder(name, parent_item)
1160 def _flatten_dataset_tree(self, parent_item=None):
1161 """データセットの葉を表示順(深さ優先)に返す。"""
1162 return self.dataset_order.dataset_items(parent_item)
1164 def _get_current_dataset(self):
1165 return self.dataset_order.current_dataset()
1167 def _get_selected_datasets(self):
1168 return self.dataset_order.selected_datasets()
1170 def _get_dataset_tree_item(self, dataset):
1171 return self.dataset_order.item_for(dataset)
1173 def _capture_dataset_group_tree(self):
1174 return self.dataset_order.group_tree()
1176 def _rebuild_dataset_tree_widget(self):
1177 self.dataset_order.rebuild_tree()
1179 def _sync_dataset_list_widget_order(self):
1180 """フォルダの中の並びを project.datasets の順に合わせる(並べ替えの Undo/Redo 用)。"""
1181 self.dataset_order.sort_tree_to_draw_order()
1183 def dragEnterEvent(self, event):
1184 return data_import_flow.dragEnterEvent(self, event)
1186 def dropEvent(self, event):
1187 return data_import_flow.dropEvent(self, event)
1189 def _all_supported_data_file_extensions(self):
1190 return data_import_flow.all_supported_data_file_extensions(self)
1192 def _queue_data_files(self, file_paths):
1193 return data_import_flow.queue_data_files(self, file_paths)
1195 def _process_next_queued_file(self):
1196 return data_import_flow.process_next_queued_file(self)
1198 def load_data(self, file_path, queue_progress=None):
1199 return data_import_flow.load_data(self, file_path, queue_progress)
1201 def _on_data_load_succeeded(self, df, file_path):
1202 return data_import_flow.on_data_load_succeeded(self, df, file_path)
1204 def _import_loaded_dataframe(self, df, file_path):
1205 return data_import_flow.import_loaded_dataframe(self, df, file_path)
1207 @staticmethod
1208 def _apply_filename_regex_columns(df, file_path, pattern):
1209 return data_import_flow.apply_filename_regex_columns(df, file_path, pattern)
1211 def _on_import_folder(self):
1212 return data_import_flow.on_import_folder(self)
1214 def _on_paste_data_from_clipboard(self):
1215 return data_import_flow.on_paste_data_from_clipboard(self)
1217 def _on_data_load_failed(self, error_message, file_path):
1218 return data_import_flow.on_data_load_failed(self, error_message, file_path)
1220 def _cleanup_data_load_task_runner(self):
1221 return data_import_flow.cleanup_data_load_task_runner(self)
1224 def _get_recent_files(self):
1225 return project_files.get_recent_files(self)
1227 def _add_recent_file(self, file_path):
1228 return project_files.add_recent_file(self, file_path)
1230 def _update_recent_files_menu(self):
1231 return project_files.update_recent_files_menu(self)
1233 def _on_open_recent_file(self, file_path):
1234 return project_files.on_open_recent_file(self, file_path)
1236 def _on_clear_recent_files(self):
1237 return project_files.on_clear_recent_files(self)
1240# 各ツールの名前を PlotterApp の属性として出す(gui/tools/__init__.py)
1241expose_tool_names(PlotterApp)
1242# 各部品の名前を PlotterApp の属性として出す(gui/panels/__init__.py)
1243expose_panel_names(PlotterApp)