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