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,289 @@
|
|
|
1
|
+
"""プロジェクトの保存と読み込みのメニュー、書式テンプレート、環境設定、設定の書き出しと読み込み。"""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from graphica.gui import notify
|
|
6
|
+
from graphica.gui import app_settings
|
|
7
|
+
from graphica.gui.dialogs import PreferencesDialog
|
|
8
|
+
from graphica.gui.datasets.operations.transfer import STYLE_ATTRS
|
|
9
|
+
from graphica.core.i18n import tr, get_language
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
AUTOSAVE_INTERVAL_MIN_BOUNDS = (0, 180)
|
|
14
|
+
|
|
15
|
+
TEMPLATE_FORMAT_VERSION = 1
|
|
16
|
+
|
|
17
|
+
# 書き出すキーは app_settings.EXPORTED_SETTINGS。ウィンドウの状態や最近使ったファイルなど、
|
|
18
|
+
# その環境だけの項目は入れない(別の PC に持ち込んでも意味が無いか害になる)
|
|
19
|
+
SETTINGS_EXPORT_FORMAT_VERSION = 1
|
|
20
|
+
|
|
21
|
+
# サブプロットの中身に近いもの。テンプレートでは保存も適用もしない(適用先の値を残す)
|
|
22
|
+
TEMPLATE_EXCLUDED_AXIS_SETTING_KEYS = ('annotations', 'legend_order', 'free_rect')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProjectIOMixin:
|
|
26
|
+
def _on_save_project(self):
|
|
27
|
+
self.manual_save()
|
|
28
|
+
|
|
29
|
+
def _on_save_project_as(self):
|
|
30
|
+
self.manual_save_as()
|
|
31
|
+
|
|
32
|
+
def _on_load_project(self):
|
|
33
|
+
self.manual_load()
|
|
34
|
+
|
|
35
|
+
def _on_configure_autosave_interval(self):
|
|
36
|
+
"""0 分で無効。"""
|
|
37
|
+
current_minutes = (self.autosave_timer.interval() // 60000) if self.autosave_timer.isActive() else 0
|
|
38
|
+
min_minutes, max_minutes = AUTOSAVE_INTERVAL_MIN_BOUNDS
|
|
39
|
+
minutes, ok = notify.get_int(
|
|
40
|
+
self, "オートセーブ間隔の設定",
|
|
41
|
+
"オートセーブの間隔を分単位で入力してください (0で無効化):",
|
|
42
|
+
current_minutes, min_minutes, max_minutes
|
|
43
|
+
)
|
|
44
|
+
if not ok:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
self._apply_autosave_interval(minutes)
|
|
48
|
+
|
|
49
|
+
def _apply_autosave_interval(self, minutes):
|
|
50
|
+
"""メニューと環境設定の両方から呼ばれる。"""
|
|
51
|
+
app_settings.AUTOSAVE_INTERVAL_MIN.write(self.settings, minutes)
|
|
52
|
+
if minutes <= 0:
|
|
53
|
+
self.autosave_timer.stop()
|
|
54
|
+
self.statusBar().showMessage("オートセーブを無効化しました", 3000)
|
|
55
|
+
else:
|
|
56
|
+
self.autosave_timer.start(minutes * 60 * 1000)
|
|
57
|
+
self.statusBar().showMessage(f"オートセーブ間隔を{minutes}分に設定しました", 3000)
|
|
58
|
+
|
|
59
|
+
self._update_autosave_menu_text()
|
|
60
|
+
|
|
61
|
+
def _on_show_preferences(self):
|
|
62
|
+
current_minutes = (self.autosave_timer.interval() // 60000) if self.autosave_timer.isActive() else 0
|
|
63
|
+
# 言語だけは、未設定なら今の表示言語を既定にする
|
|
64
|
+
current_language = app_settings.LANGUAGE.read(self.settings, default=get_language())
|
|
65
|
+
current_autosave_dir = app_settings.AUTOSAVE_DIR.read(self.settings)
|
|
66
|
+
current_point_label_max = app_settings.POINT_LABEL_MAX_POINTS.read(self.settings)
|
|
67
|
+
current_snap_to_grid = app_settings.SNAP_TO_GRID_ENABLED.read(self.settings)
|
|
68
|
+
current_snap_grid_interval = app_settings.SNAP_GRID_INTERVAL_PX.read(self.settings)
|
|
69
|
+
|
|
70
|
+
# main_window がこの mixin を import しているので、関数の中で import する
|
|
71
|
+
from graphica.core.plugin_api import get_loaded_plugin_records, get_plugin_registration_errors
|
|
72
|
+
current_disabled_plugin_names = app_settings.disabled_plugin_names(self.settings)
|
|
73
|
+
|
|
74
|
+
dlg = PreferencesDialog(
|
|
75
|
+
self.canvas.dark_mode, current_minutes,
|
|
76
|
+
autosave_bounds=AUTOSAVE_INTERVAL_MIN_BOUNDS, parent=self,
|
|
77
|
+
current_language=current_language, autosave_dir=current_autosave_dir,
|
|
78
|
+
point_label_max_points=current_point_label_max,
|
|
79
|
+
snap_to_grid_enabled=current_snap_to_grid,
|
|
80
|
+
snap_grid_interval_px=current_snap_grid_interval,
|
|
81
|
+
plugin_records=get_loaded_plugin_records(),
|
|
82
|
+
plugin_registration_errors=get_plugin_registration_errors(),
|
|
83
|
+
disabled_plugin_names=current_disabled_plugin_names,
|
|
84
|
+
)
|
|
85
|
+
if dlg.exec() != PreferencesDialog.DialogCode.Accepted:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
(new_dark_mode, new_autosave_minutes, new_language,
|
|
89
|
+
new_autosave_dir, new_point_label_max,
|
|
90
|
+
new_snap_to_grid, new_snap_grid_interval) = dlg.get_settings()
|
|
91
|
+
|
|
92
|
+
# 次の起動から効く(読み込んだプラグインをその場で入れ替える仕組みは無い)
|
|
93
|
+
new_disabled_plugin_names = dlg.get_disabled_plugin_names()
|
|
94
|
+
if new_disabled_plugin_names != current_disabled_plugin_names:
|
|
95
|
+
app_settings.DISABLED_PLUGINS.write(self.settings, list(new_disabled_plugin_names))
|
|
96
|
+
|
|
97
|
+
if new_autosave_dir != current_autosave_dir:
|
|
98
|
+
app_settings.AUTOSAVE_DIR.write(self.settings, new_autosave_dir)
|
|
99
|
+
self._update_autosave_path()
|
|
100
|
+
|
|
101
|
+
# 表示メニューのチェック経由で切り替える(toggled から _on_toggle_dark_mode が適用し、チェックの状態も揃う)
|
|
102
|
+
if new_dark_mode != self.canvas.dark_mode:
|
|
103
|
+
self.dark_mode_action.setChecked(new_dark_mode)
|
|
104
|
+
|
|
105
|
+
if new_autosave_minutes != current_minutes:
|
|
106
|
+
self._apply_autosave_interval(new_autosave_minutes)
|
|
107
|
+
|
|
108
|
+
if new_point_label_max != current_point_label_max:
|
|
109
|
+
app_settings.POINT_LABEL_MAX_POINTS.write(self.settings, new_point_label_max)
|
|
110
|
+
self.canvas.point_label_max_points = new_point_label_max
|
|
111
|
+
self._update_plot()
|
|
112
|
+
self.property_panel.update_point_labels_limit_note()
|
|
113
|
+
|
|
114
|
+
if new_snap_to_grid != current_snap_to_grid:
|
|
115
|
+
app_settings.SNAP_TO_GRID_ENABLED.write(self.settings, new_snap_to_grid)
|
|
116
|
+
self.snap_to_grid_enabled = new_snap_to_grid
|
|
117
|
+
if new_snap_grid_interval != current_snap_grid_interval:
|
|
118
|
+
app_settings.SNAP_GRID_INTERVAL_PX.write(self.settings, new_snap_grid_interval)
|
|
119
|
+
self.snap_grid_interval_px = new_snap_grid_interval
|
|
120
|
+
|
|
121
|
+
# 作った画面をその場で訳し直す仕組みは無いので、次の起動からと知らせる
|
|
122
|
+
if new_language != current_language:
|
|
123
|
+
app_settings.LANGUAGE.write(self.settings, new_language)
|
|
124
|
+
notify.information(
|
|
125
|
+
self, tr("表示言語の変更"),
|
|
126
|
+
tr("表示言語の変更は、次回起動時に反映されます。")
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _update_autosave_menu_text(self):
|
|
130
|
+
if not self.autosave_timer.isActive():
|
|
131
|
+
self.autosave_interval_action.setText(tr("オートセーブ: 無効(&I)..."))
|
|
132
|
+
else:
|
|
133
|
+
minutes = self.autosave_timer.interval() // 60000
|
|
134
|
+
self.autosave_interval_action.setText(tr("オートセーブ: {minutes}分間隔(&I)...").format(minutes=minutes))
|
|
135
|
+
|
|
136
|
+
def _on_save_plot_template(self):
|
|
137
|
+
"""全サブプロットの見た目と全データセットのスタイルを *.graphica-style に保存する(データは含めない)。"""
|
|
138
|
+
file_path, _ = notify.get_save_file_name(
|
|
139
|
+
self, "書式テンプレートを保存", "", "Graphica Style Template (*.graphica-style)"
|
|
140
|
+
)
|
|
141
|
+
if not file_path:
|
|
142
|
+
return
|
|
143
|
+
if not (file_path.endswith('.graphica-style') or file_path.endswith('.json')):
|
|
144
|
+
file_path += '.graphica-style'
|
|
145
|
+
|
|
146
|
+
# 今の軸の欄の値はまだ all_plot_settings に入っていない
|
|
147
|
+
if self.project.active_axis_index < len(self.project.all_plot_settings):
|
|
148
|
+
self.project.all_plot_settings[self.project.active_axis_index] = self._gather_settings_from_ui()
|
|
149
|
+
|
|
150
|
+
subplot_styles = [
|
|
151
|
+
{k: v for k, v in settings.items() if k not in TEMPLATE_EXCLUDED_AXIS_SETTING_KEYS}
|
|
152
|
+
for settings in self.project.all_plot_settings
|
|
153
|
+
]
|
|
154
|
+
dataset_styles = [
|
|
155
|
+
{attr: getattr(ds, attr) for attr in STYLE_ATTRS}
|
|
156
|
+
for ds in self.project.datasets
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
template_data = {
|
|
160
|
+
'format_version': TEMPLATE_FORMAT_VERSION,
|
|
161
|
+
'subplot_styles': subplot_styles,
|
|
162
|
+
'dataset_styles': dataset_styles,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
167
|
+
json.dump(template_data, f, indent=4, ensure_ascii=False)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
notify.warning(self, "保存エラー", f"テンプレートの保存中にエラーが発生しました:\n{e}")
|
|
170
|
+
logger.exception("テンプレートの保存中にエラー")
|
|
171
|
+
|
|
172
|
+
def _on_load_plot_template(self):
|
|
173
|
+
"""書式テンプレートを今のプロジェクトに当てる。Undo はできない。
|
|
174
|
+
|
|
175
|
+
新しい形式(format_version あり)は、保存した順にサブプロットとデータセットへ繰り返し当てる(数が違ってもよい)。
|
|
176
|
+
古い .json(plot_settings だけ)は今の軸だけに当てる。
|
|
177
|
+
"""
|
|
178
|
+
file_path, _ = notify.get_open_file_name(
|
|
179
|
+
self, "書式テンプレートを適用", "", "Graphica Style Template (*.graphica-style *.json)"
|
|
180
|
+
)
|
|
181
|
+
if not file_path:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
186
|
+
template_data = json.load(f)
|
|
187
|
+
|
|
188
|
+
if 'format_version' in template_data:
|
|
189
|
+
subplot_styles = template_data.get('subplot_styles') or []
|
|
190
|
+
if not subplot_styles:
|
|
191
|
+
notify.warning(self, "読込エラー", "有効な書式設定がファイルに含まれていません。")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
for i, settings in enumerate(self.project.all_plot_settings):
|
|
195
|
+
style = subplot_styles[i % len(subplot_styles)]
|
|
196
|
+
merged = dict(settings)
|
|
197
|
+
for k, v in style.items():
|
|
198
|
+
if k not in TEMPLATE_EXCLUDED_AXIS_SETTING_KEYS:
|
|
199
|
+
merged[k] = v
|
|
200
|
+
self.project.all_plot_settings[i] = merged
|
|
201
|
+
if i == self.project.active_axis_index:
|
|
202
|
+
self._apply_settings_to_ui_controls(merged)
|
|
203
|
+
|
|
204
|
+
dataset_styles = template_data.get('dataset_styles') or []
|
|
205
|
+
if dataset_styles:
|
|
206
|
+
for i, dataset in enumerate(self.project.datasets):
|
|
207
|
+
style = dataset_styles[i % len(dataset_styles)]
|
|
208
|
+
for attr, value in style.items():
|
|
209
|
+
setattr(dataset, attr, value)
|
|
210
|
+
|
|
211
|
+
self._update_plot()
|
|
212
|
+
else:
|
|
213
|
+
# 古い形式: 今の軸だけ
|
|
214
|
+
settings = template_data.get('plot_settings', {})
|
|
215
|
+
if not settings:
|
|
216
|
+
notify.warning(self, "読込エラー", "有効な書式設定がファイルに含まれていません。")
|
|
217
|
+
return
|
|
218
|
+
self._apply_settings_to_ui_controls(settings)
|
|
219
|
+
self._on_axis_setting_changed()
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
notify.warning(self, "読込エラー", f"テンプレートの読み込み中にエラーが発生しました:\n{e}")
|
|
223
|
+
logger.exception("テンプレートの読み込み中にエラー")
|
|
224
|
+
|
|
225
|
+
def _on_export_settings(self):
|
|
226
|
+
"""app_settings.EXPORTED_SETTINGS のキーだけを JSON に書き出す(別の PC や研究室での共有用)。"""
|
|
227
|
+
file_path, _ = notify.get_save_file_name(
|
|
228
|
+
self, "設定・スタイルをエクスポート", "graphica_settings.json", "JSON Files (*.json)"
|
|
229
|
+
)
|
|
230
|
+
if not file_path:
|
|
231
|
+
return
|
|
232
|
+
if not file_path.endswith('.json'):
|
|
233
|
+
file_path += '.json'
|
|
234
|
+
|
|
235
|
+
exported = {setting.key: setting.read_for_export(self.settings) for setting in app_settings.EXPORTED_SETTINGS}
|
|
236
|
+
|
|
237
|
+
payload = {'format_version': SETTINGS_EXPORT_FORMAT_VERSION, 'settings': exported}
|
|
238
|
+
try:
|
|
239
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
240
|
+
json.dump(payload, f, indent=4, ensure_ascii=False)
|
|
241
|
+
notify.information(self, "設定・スタイルをエクスポート", f"エクスポートしました:\n{file_path}")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
notify.warning(self, "保存エラー", f"設定のエクスポート中にエラーが発生しました:\n{e}")
|
|
244
|
+
logger.exception("設定のエクスポート中にエラー")
|
|
245
|
+
|
|
246
|
+
def _on_import_settings(self):
|
|
247
|
+
"""書き出した JSON から app_settings.EXPORTED_SETTINGS のキーだけを QSettings に入れる(未知のキーは無視)。
|
|
248
|
+
|
|
249
|
+
言語・ダークモード・パレットなどをその場で当て直すと影響が広いので、次の起動から効くと知らせる。
|
|
250
|
+
"""
|
|
251
|
+
file_path, _ = notify.get_open_file_name(
|
|
252
|
+
self, "設定・スタイルをインポート", "", "JSON Files (*.json)"
|
|
253
|
+
)
|
|
254
|
+
if not file_path:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
259
|
+
payload = json.load(f)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
notify.warning(self, "設定・スタイルをインポート", f"ファイルの読み込みに失敗しました:\n{e}")
|
|
262
|
+
logger.exception("設定のインポート中にエラー")
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
data = payload.get('settings') if isinstance(payload, dict) else None
|
|
266
|
+
if not isinstance(data, dict):
|
|
267
|
+
# 手で書いた設定ファイルも読めるよう、settings キーが無くてもよい
|
|
268
|
+
data = payload if isinstance(payload, dict) else None
|
|
269
|
+
if not isinstance(data, dict):
|
|
270
|
+
notify.warning(self, "設定・スタイルをインポート", "対応していないファイル形式です。")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
valid_keys = {setting.key for setting in app_settings.EXPORTED_SETTINGS}
|
|
274
|
+
imported_count = 0
|
|
275
|
+
for key, value in data.items():
|
|
276
|
+
if key not in valid_keys:
|
|
277
|
+
continue
|
|
278
|
+
self.settings.setValue(key, value)
|
|
279
|
+
imported_count += 1
|
|
280
|
+
|
|
281
|
+
if imported_count == 0:
|
|
282
|
+
notify.information(self, "設定・スタイルをインポート", "インポート対象の設定が見つかりませんでした。")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
notify.information(
|
|
286
|
+
self, "設定・スタイルをインポート",
|
|
287
|
+
f"{imported_count}件の設定を反映しました。\n"
|
|
288
|
+
"表示言語など一部の設定は、次回起動時に反映されます。"
|
|
289
|
+
)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""クイックアクセス: よく使うメニュー項目をツールバーにピン留めする。
|
|
2
|
+
|
|
3
|
+
ツールバーにはメニューの QAction そのものを addAction する(写しを作ると状態がずれる)。
|
|
4
|
+
ピン留めは QSettings の "quick_access_pinned_actions" に、メニューのパスを " > " でつないだ文字列で保存する
|
|
5
|
+
(QAction は起動のたびに作り直されるので、表示文字のパスを識別子にする)。見つからないものは黙って飛ばす。
|
|
6
|
+
ツールバーはタブごとに作り、共有するのは保存した識別子だけ。
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import Qt
|
|
11
|
+
from PySide6.QtGui import QAction
|
|
12
|
+
from PySide6.QtWidgets import QToolBar, QMenu
|
|
13
|
+
|
|
14
|
+
from graphica.core.i18n import tr
|
|
15
|
+
from graphica.gui import app_settings
|
|
16
|
+
from graphica.gui.dialogs import QuickAccessManagerDialog
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# _migrate_pinned_quick_access_ids() が末尾の項目名を取り出すのにも使う
|
|
23
|
+
SEPARATOR = " > "
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def quick_access_action_identifier(path):
|
|
27
|
+
return SEPARATOR.join(path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class QuickAccessMixin:
|
|
31
|
+
def _create_quick_access_toolbar(self):
|
|
32
|
+
"""空のツールバーと、表示メニューの切り替えを作る。
|
|
33
|
+
|
|
34
|
+
ピン留めを戻すのは _restore_quick_access_actions()(プラグインのメニューを含めて全部できた後でないと見つからない)。
|
|
35
|
+
"""
|
|
36
|
+
self._quick_access_actions = {} # 識別子 -> QAction(回収されないよう持つ)
|
|
37
|
+
|
|
38
|
+
toolbar = QToolBar(tr("クイックアクセス"), self)
|
|
39
|
+
# saveState() が見分けられるように
|
|
40
|
+
toolbar.setObjectName("quick_access_toolbar")
|
|
41
|
+
# 上に固定するので、移動用のつまみは出さない(QSS では隠せない)
|
|
42
|
+
toolbar.setMovable(False)
|
|
43
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)
|
|
44
|
+
self.quick_access_toolbar = toolbar
|
|
45
|
+
|
|
46
|
+
# ピン留めが0件でも管理ダイアログに行けるよう、先頭に置く
|
|
47
|
+
self.quick_access_manage_action = QAction(tr("クイックアクセスの管理..."), self)
|
|
48
|
+
self.quick_access_manage_action.triggered.connect(self._on_manage_quick_access)
|
|
49
|
+
toolbar.addAction(self.quick_access_manage_action)
|
|
50
|
+
toolbar.addSeparator()
|
|
51
|
+
|
|
52
|
+
toggle_action = toolbar.toggleViewAction()
|
|
53
|
+
toggle_action.setText(tr("クイックアクセスツールバー"))
|
|
54
|
+
self._view_menu.addAction(toggle_action)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _quick_access_pinnable_menus(self):
|
|
58
|
+
menus = [self._file_menu, self._edit_menu, self._view_menu, self._help_menu]
|
|
59
|
+
plugin_menu = getattr(self, '_plugin_menu', None)
|
|
60
|
+
if plugin_menu is not None:
|
|
61
|
+
menus.append(plugin_menu)
|
|
62
|
+
return menus
|
|
63
|
+
|
|
64
|
+
def _install_quick_access_context_menus(self):
|
|
65
|
+
"""各メニューの項目の右クリックでピン留めと解除をできるようにする(全部のメニューができた後に1回)。"""
|
|
66
|
+
for menu in self._quick_access_pinnable_menus():
|
|
67
|
+
menu.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
68
|
+
menu.customContextMenuRequested.connect(
|
|
69
|
+
lambda pos, m=menu: self._on_quick_access_menu_context_menu(m, pos)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _resolve_quick_access_menu_target(self, menu, pos):
|
|
73
|
+
"""pos の項目の (識別子, パス, QAction)。区切り線やサブメニューなら None。UI を開かないのでテストできる。"""
|
|
74
|
+
action = menu.actionAt(pos)
|
|
75
|
+
if action is None or action.isSeparator() or action.menu() is not None:
|
|
76
|
+
return None
|
|
77
|
+
text = action.text().replace('&', '').strip()
|
|
78
|
+
if not text:
|
|
79
|
+
return None
|
|
80
|
+
path = [menu.title().replace('&', ''), text]
|
|
81
|
+
ident = quick_access_action_identifier(path)
|
|
82
|
+
return ident, path, action
|
|
83
|
+
|
|
84
|
+
def _on_quick_access_menu_context_menu(self, menu, pos):
|
|
85
|
+
target = self._resolve_quick_access_menu_target(menu, pos)
|
|
86
|
+
if target is None:
|
|
87
|
+
return
|
|
88
|
+
ident, path, action = target
|
|
89
|
+
|
|
90
|
+
popup = QMenu(self)
|
|
91
|
+
if self.is_quick_access_pinned(ident):
|
|
92
|
+
remove_action = popup.addAction(tr("クイックアクセスから削除"))
|
|
93
|
+
remove_action.triggered.connect(lambda: self.unpin_quick_access_action(ident))
|
|
94
|
+
else:
|
|
95
|
+
add_action = popup.addAction(tr("クイックアクセスに追加"))
|
|
96
|
+
add_action.triggered.connect(lambda: self.pin_quick_access_action(ident, action))
|
|
97
|
+
popup.exec(menu.mapToGlobal(pos))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_pinned_quick_access_ids(self):
|
|
101
|
+
"""ピン留めした順の識別子。"""
|
|
102
|
+
return app_settings.as_list_keeping_empty_string(app_settings.QUICK_ACCESS_PINNED_ACTIONS.read(self.settings))
|
|
103
|
+
|
|
104
|
+
def _set_pinned_quick_access_ids(self, ids):
|
|
105
|
+
app_settings.QUICK_ACCESS_PINNED_ACTIONS.write(self.settings, ids)
|
|
106
|
+
|
|
107
|
+
def _migrate_pinned_quick_access_ids(self, ids, available):
|
|
108
|
+
"""メニューの組み替えでパスが変わったピン留めを、新しい識別子に読み替える。((識別子のリスト, 変えたか))
|
|
109
|
+
|
|
110
|
+
一致しない識別子は黙って飛ばされ、ピンが知らないうちに消えるので読み替える。末尾の項目名が一致する候補が
|
|
111
|
+
今のメニューにちょうど1つあるときだけ読み替え、複数あれば触らない。項目名自体が変わったものは読み替えない。
|
|
112
|
+
"""
|
|
113
|
+
by_leaf = {}
|
|
114
|
+
for ident in available:
|
|
115
|
+
leaf = ident.rsplit(SEPARATOR, 1)[-1]
|
|
116
|
+
by_leaf.setdefault(leaf, []).append(ident)
|
|
117
|
+
|
|
118
|
+
migrated, changed = [], False
|
|
119
|
+
for ident in ids:
|
|
120
|
+
if ident in available:
|
|
121
|
+
migrated.append(ident)
|
|
122
|
+
continue
|
|
123
|
+
candidates = by_leaf.get(ident.rsplit(SEPARATOR, 1)[-1], [])
|
|
124
|
+
if len(candidates) == 1:
|
|
125
|
+
logger.info(
|
|
126
|
+
"クイックアクセスのピン留めを移動先へ読み替えました: %r -> %r",
|
|
127
|
+
ident, candidates[0],
|
|
128
|
+
)
|
|
129
|
+
migrated.append(candidates[0])
|
|
130
|
+
changed = True
|
|
131
|
+
else:
|
|
132
|
+
migrated.append(ident)
|
|
133
|
+
return migrated, changed
|
|
134
|
+
|
|
135
|
+
def _restore_quick_access_actions(self):
|
|
136
|
+
"""保存した識別子のうち、今のメニューにあるものをツールバーに戻す(起動時に1回)。"""
|
|
137
|
+
ids = self._get_pinned_quick_access_ids()
|
|
138
|
+
if not ids:
|
|
139
|
+
return
|
|
140
|
+
available = {
|
|
141
|
+
quick_access_action_identifier(path): action
|
|
142
|
+
for path, action in self._collect_menu_actions()
|
|
143
|
+
}
|
|
144
|
+
ids, changed = self._migrate_pinned_quick_access_ids(ids, available)
|
|
145
|
+
if changed:
|
|
146
|
+
# 書き戻して、次からは読み替えずに済むように
|
|
147
|
+
self._set_pinned_quick_access_ids(ids)
|
|
148
|
+
for ident in ids:
|
|
149
|
+
action = available.get(ident)
|
|
150
|
+
if action is not None:
|
|
151
|
+
self._add_action_to_quick_access_toolbar(ident, action)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _add_action_to_quick_access_toolbar(self, ident, action):
|
|
155
|
+
if ident in self._quick_access_actions:
|
|
156
|
+
return
|
|
157
|
+
self.quick_access_toolbar.addAction(action)
|
|
158
|
+
self._quick_access_actions[ident] = action
|
|
159
|
+
|
|
160
|
+
def pin_quick_access_action(self, ident, action):
|
|
161
|
+
self._add_action_to_quick_access_toolbar(ident, action)
|
|
162
|
+
ids = self._get_pinned_quick_access_ids()
|
|
163
|
+
if ident not in ids:
|
|
164
|
+
ids.append(ident)
|
|
165
|
+
self._set_pinned_quick_access_ids(ids)
|
|
166
|
+
|
|
167
|
+
def unpin_quick_access_action(self, ident):
|
|
168
|
+
action = self._quick_access_actions.pop(ident, None)
|
|
169
|
+
if action is not None:
|
|
170
|
+
self.quick_access_toolbar.removeAction(action)
|
|
171
|
+
ids = self._get_pinned_quick_access_ids()
|
|
172
|
+
if ident in ids:
|
|
173
|
+
ids.remove(ident)
|
|
174
|
+
self._set_pinned_quick_access_ids(ids)
|
|
175
|
+
|
|
176
|
+
def is_quick_access_pinned(self, ident):
|
|
177
|
+
return ident in self._quick_access_actions
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _on_manage_quick_access(self):
|
|
181
|
+
def toggle(ident, path, checked):
|
|
182
|
+
if checked:
|
|
183
|
+
# 一覧を作ったときの QAction は使わず、実行する直前にパスで取り直す
|
|
184
|
+
for p, action in self._collect_menu_actions():
|
|
185
|
+
if p == path:
|
|
186
|
+
self.pin_quick_access_action(ident, action)
|
|
187
|
+
break
|
|
188
|
+
else:
|
|
189
|
+
self.unpin_quick_access_action(ident)
|
|
190
|
+
|
|
191
|
+
dialog = QuickAccessManagerDialog(
|
|
192
|
+
self._collect_menu_actions, self.is_quick_access_pinned, toggle, self
|
|
193
|
+
)
|
|
194
|
+
dialog.exec()
|