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.
- graphica/Graphica.ico +0 -0
- graphica/__init__.py +1 -0
- graphica/__main__.py +111 -0
- graphica/assets/__init__.py +0 -0
- graphica/assets/icons/__init__.py +0 -0
- graphica/assets/icons/arrow-right.svg +21 -0
- graphica/assets/icons/bold.svg +20 -0
- graphica/assets/icons/calculator.svg +26 -0
- graphica/assets/icons/chart-histogram.svg +24 -0
- graphica/assets/icons/chart-line.svg +20 -0
- graphica/assets/icons/chevron-down.svg +19 -0
- graphica/assets/icons/chevron-right.svg +19 -0
- graphica/assets/icons/color-swatch.svg +22 -0
- graphica/assets/icons/column-insert-right.svg +21 -0
- graphica/assets/icons/column-remove.svg +21 -0
- graphica/assets/icons/copy.svg +20 -0
- graphica/assets/icons/download.svg +21 -0
- graphica/assets/icons/edit.svg +21 -0
- graphica/assets/icons/eye-off.svg +21 -0
- graphica/assets/icons/eye.svg +20 -0
- graphica/assets/icons/file-plus.svg +22 -0
- graphica/assets/icons/folder-plus.svg +21 -0
- graphica/assets/icons/folder.svg +19 -0
- graphica/assets/icons/highlight.svg +22 -0
- graphica/assets/icons/history.svg +20 -0
- graphica/assets/icons/italic.svg +21 -0
- graphica/assets/icons/layout-grid.svg +22 -0
- graphica/assets/icons/math-function.svg +22 -0
- graphica/assets/icons/message-2.svg +21 -0
- graphica/assets/icons/mountain.svg +20 -0
- graphica/assets/icons/palette.svg +22 -0
- graphica/assets/icons/pointer.svg +19 -0
- graphica/assets/icons/refresh.svg +20 -0
- graphica/assets/icons/row-insert-bottom.svg +21 -0
- graphica/assets/icons/row-remove.svg +21 -0
- graphica/assets/icons/search.svg +20 -0
- graphica/assets/icons/select-all.svg +35 -0
- graphica/assets/icons/subscript.svg +20 -0
- graphica/assets/icons/superscript.svg +20 -0
- graphica/assets/icons/table.svg +21 -0
- graphica/assets/icons/trash.svg +23 -0
- graphica/assets/icons/typography.svg +23 -0
- graphica/assets/icons/x.svg +20 -0
- graphica/core/__init__.py +0 -0
- graphica/core/analysis.py +1278 -0
- graphica/core/app_paths.py +35 -0
- graphica/core/axis_settings.py +128 -0
- graphica/core/caption_export.py +47 -0
- graphica/core/color_palettes.py +56 -0
- graphica/core/commands.py +226 -0
- graphica/core/cvd_simulation.py +49 -0
- graphica/core/dataset.py +443 -0
- graphica/core/diagnostics.py +105 -0
- graphica/core/excel_utils.py +58 -0
- graphica/core/fit_models.py +379 -0
- graphica/core/grid_data.py +128 -0
- graphica/core/i18n.py +44 -0
- graphica/core/json_utils.py +27 -0
- graphica/core/label_utils.py +21 -0
- graphica/core/methods_text.py +115 -0
- graphica/core/named_colors.py +126 -0
- graphica/core/plugin_api.py +515 -0
- graphica/core/plugin_context.py +167 -0
- graphica/core/plugin_install.py +82 -0
- graphica/core/plugin_manifest.py +67 -0
- graphica/core/plugin_testing.py +213 -0
- graphica/core/plugin_types.py +118 -0
- graphica/core/provenance.py +19 -0
- graphica/core/report_export.py +51 -0
- graphica/core/safe_eval.py +194 -0
- graphica/core/script_export.py +272 -0
- graphica/core/translations_en.py +530 -0
- graphica/core/unit_conversion.py +51 -0
- graphica/core/update_check.py +43 -0
- graphica/core/version.py +14 -0
- graphica/gui/__init__.py +0 -0
- graphica/gui/app_settings.py +119 -0
- graphica/gui/axis_bindings.py +173 -0
- graphica/gui/binding.py +135 -0
- graphica/gui/builders/__init__.py +1 -0
- graphica/gui/builders/axis_panel.py +431 -0
- graphica/gui/builders/canvas_area.py +176 -0
- graphica/gui/builders/common.py +120 -0
- graphica/gui/builders/dataset_panel.py +433 -0
- graphica/gui/builders/property_sections.py +208 -0
- graphica/gui/canvas.py +519 -0
- graphica/gui/color_history.py +47 -0
- graphica/gui/color_picker_widget.py +198 -0
- graphica/gui/crash_handler.py +77 -0
- graphica/gui/cvd_preview.py +23 -0
- graphica/gui/data_editor.py +761 -0
- graphica/gui/data_import_flow.py +355 -0
- graphica/gui/dataset_bindings.py +107 -0
- graphica/gui/dataset_style_icon.py +80 -0
- graphica/gui/datasets/__init__.py +1 -0
- graphica/gui/datasets/actions_menu.py +114 -0
- graphica/gui/datasets/colors.py +218 -0
- graphica/gui/datasets/fitting.py +70 -0
- graphica/gui/datasets/host.py +162 -0
- graphica/gui/datasets/operations/__init__.py +1 -0
- graphica/gui/datasets/operations/fitting.py +468 -0
- graphica/gui/datasets/operations/peaks.py +109 -0
- graphica/gui/datasets/operations/processing.py +659 -0
- graphica/gui/datasets/operations/runner.py +148 -0
- graphica/gui/datasets/operations/transfer.py +227 -0
- graphica/gui/datasets/order.py +310 -0
- graphica/gui/datasets/overlays.py +74 -0
- graphica/gui/datasets/peaks.py +21 -0
- graphica/gui/datasets/plugin_runs.py +99 -0
- graphica/gui/datasets/processing.py +68 -0
- graphica/gui/datasets/property_panel.py +538 -0
- graphica/gui/datasets/transfer.py +36 -0
- graphica/gui/detached_canvas_window.py +22 -0
- graphica/gui/dialogs/__init__.py +137 -0
- graphica/gui/dialogs/analysis.py +1322 -0
- graphica/gui/dialogs/app.py +1015 -0
- graphica/gui/dialogs/appearance.py +851 -0
- graphica/gui/dialogs/data_edit.py +577 -0
- graphica/gui/dialogs/data_import.py +679 -0
- graphica/gui/dialogs/export.py +433 -0
- graphica/gui/dock_layout.py +176 -0
- graphica/gui/export_preview_panel.py +321 -0
- graphica/gui/export_settings.py +16 -0
- graphica/gui/file_association.py +97 -0
- graphica/gui/icon_utils.py +44 -0
- graphica/gui/main_app_window.py +210 -0
- graphica/gui/main_window.py +1151 -0
- graphica/gui/mathtext_preview.py +168 -0
- graphica/gui/menu_bar.py +320 -0
- graphica/gui/minimap_widget.py +151 -0
- graphica/gui/mixins/__init__.py +0 -0
- graphica/gui/mixins/export_mixin.py +520 -0
- graphica/gui/mixins/help_mixin.py +127 -0
- graphica/gui/mixins/project_io_mixin.py +289 -0
- graphica/gui/mixins/quick_access_mixin.py +194 -0
- graphica/gui/mixins/ui_setup_mixin.py +206 -0
- graphica/gui/notify.py +61 -0
- graphica/gui/panels/__init__.py +43 -0
- graphica/gui/panels/axis_settings.py +605 -0
- graphica/gui/panels/dataset_tree.py +424 -0
- graphica/gui/plot_type_drawers.py +131 -0
- graphica/gui/plugin_context.py +119 -0
- graphica/gui/project_files.py +361 -0
- graphica/gui/provenance_panel.py +65 -0
- graphica/gui/rendering/__init__.py +1 -0
- graphica/gui/rendering/annotations.py +177 -0
- graphica/gui/rendering/appearance.py +368 -0
- graphica/gui/rendering/common.py +316 -0
- graphica/gui/rendering/data_1d.py +376 -0
- graphica/gui/rendering/data_2d.py +66 -0
- graphica/gui/residual_panel.py +62 -0
- graphica/gui/resources.py +16 -0
- graphica/gui/single_instance.py +141 -0
- graphica/gui/splash.py +83 -0
- graphica/gui/task_runner.py +41 -0
- graphica/gui/theme.py +864 -0
- graphica/gui/tools/__init__.py +56 -0
- graphica/gui/tools/annotation.py +275 -0
- graphica/gui/tools/cursor.py +259 -0
- graphica/gui/tools/layout_edit.py +285 -0
- graphica/gui/tools/manager.py +78 -0
- graphica/gui/tools/peak_placement.py +132 -0
- graphica/gui/tools/range_select.py +191 -0
- graphica/gui/tools/region_highlight.py +222 -0
- graphica/gui/tools/slice_extraction.py +200 -0
- graphica/gui/widget_translation.py +46 -0
- graphica/gui/workers.py +280 -0
- graphica/models/__init__.py +0 -0
- graphica/models/project.py +197 -0
- graphica/plugin/__init__.py +20 -0
- graphica/plugin/testing.py +57 -0
- graphica/sample_data/__init__.py +0 -0
- graphica/sample_data/cooling_curve_sample.csv +42 -0
- graphica/ui_main_window.py +581 -0
- graphica_plot-2.0.0.dist-info/METADATA +317 -0
- graphica_plot-2.0.0.dist-info/RECORD +181 -0
- graphica_plot-2.0.0.dist-info/WHEEL +5 -0
- graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
- graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
- graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
- 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)
|