graphica-plot 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. graphica/Graphica.ico +0 -0
  2. graphica/__init__.py +1 -0
  3. graphica/__main__.py +111 -0
  4. graphica/assets/__init__.py +0 -0
  5. graphica/assets/icons/__init__.py +0 -0
  6. graphica/assets/icons/arrow-right.svg +21 -0
  7. graphica/assets/icons/bold.svg +20 -0
  8. graphica/assets/icons/calculator.svg +26 -0
  9. graphica/assets/icons/chart-histogram.svg +24 -0
  10. graphica/assets/icons/chart-line.svg +20 -0
  11. graphica/assets/icons/chevron-down.svg +19 -0
  12. graphica/assets/icons/chevron-right.svg +19 -0
  13. graphica/assets/icons/color-swatch.svg +22 -0
  14. graphica/assets/icons/column-insert-right.svg +21 -0
  15. graphica/assets/icons/column-remove.svg +21 -0
  16. graphica/assets/icons/copy.svg +20 -0
  17. graphica/assets/icons/download.svg +21 -0
  18. graphica/assets/icons/edit.svg +21 -0
  19. graphica/assets/icons/eye-off.svg +21 -0
  20. graphica/assets/icons/eye.svg +20 -0
  21. graphica/assets/icons/file-plus.svg +22 -0
  22. graphica/assets/icons/folder-plus.svg +21 -0
  23. graphica/assets/icons/folder.svg +19 -0
  24. graphica/assets/icons/highlight.svg +22 -0
  25. graphica/assets/icons/history.svg +20 -0
  26. graphica/assets/icons/italic.svg +21 -0
  27. graphica/assets/icons/layout-grid.svg +22 -0
  28. graphica/assets/icons/math-function.svg +22 -0
  29. graphica/assets/icons/message-2.svg +21 -0
  30. graphica/assets/icons/mountain.svg +20 -0
  31. graphica/assets/icons/palette.svg +22 -0
  32. graphica/assets/icons/pointer.svg +19 -0
  33. graphica/assets/icons/refresh.svg +20 -0
  34. graphica/assets/icons/row-insert-bottom.svg +21 -0
  35. graphica/assets/icons/row-remove.svg +21 -0
  36. graphica/assets/icons/search.svg +20 -0
  37. graphica/assets/icons/select-all.svg +35 -0
  38. graphica/assets/icons/subscript.svg +20 -0
  39. graphica/assets/icons/superscript.svg +20 -0
  40. graphica/assets/icons/table.svg +21 -0
  41. graphica/assets/icons/trash.svg +23 -0
  42. graphica/assets/icons/typography.svg +23 -0
  43. graphica/assets/icons/x.svg +20 -0
  44. graphica/core/__init__.py +0 -0
  45. graphica/core/analysis.py +1278 -0
  46. graphica/core/app_paths.py +35 -0
  47. graphica/core/axis_settings.py +128 -0
  48. graphica/core/caption_export.py +47 -0
  49. graphica/core/color_palettes.py +56 -0
  50. graphica/core/commands.py +226 -0
  51. graphica/core/cvd_simulation.py +49 -0
  52. graphica/core/dataset.py +443 -0
  53. graphica/core/diagnostics.py +105 -0
  54. graphica/core/excel_utils.py +58 -0
  55. graphica/core/fit_models.py +379 -0
  56. graphica/core/grid_data.py +128 -0
  57. graphica/core/i18n.py +44 -0
  58. graphica/core/json_utils.py +27 -0
  59. graphica/core/label_utils.py +21 -0
  60. graphica/core/methods_text.py +115 -0
  61. graphica/core/named_colors.py +126 -0
  62. graphica/core/plugin_api.py +515 -0
  63. graphica/core/plugin_context.py +167 -0
  64. graphica/core/plugin_install.py +82 -0
  65. graphica/core/plugin_manifest.py +67 -0
  66. graphica/core/plugin_testing.py +213 -0
  67. graphica/core/plugin_types.py +118 -0
  68. graphica/core/provenance.py +19 -0
  69. graphica/core/report_export.py +51 -0
  70. graphica/core/safe_eval.py +194 -0
  71. graphica/core/script_export.py +272 -0
  72. graphica/core/translations_en.py +530 -0
  73. graphica/core/unit_conversion.py +51 -0
  74. graphica/core/update_check.py +43 -0
  75. graphica/core/version.py +14 -0
  76. graphica/gui/__init__.py +0 -0
  77. graphica/gui/app_settings.py +119 -0
  78. graphica/gui/axis_bindings.py +173 -0
  79. graphica/gui/binding.py +135 -0
  80. graphica/gui/builders/__init__.py +1 -0
  81. graphica/gui/builders/axis_panel.py +431 -0
  82. graphica/gui/builders/canvas_area.py +176 -0
  83. graphica/gui/builders/common.py +120 -0
  84. graphica/gui/builders/dataset_panel.py +433 -0
  85. graphica/gui/builders/property_sections.py +208 -0
  86. graphica/gui/canvas.py +519 -0
  87. graphica/gui/color_history.py +47 -0
  88. graphica/gui/color_picker_widget.py +198 -0
  89. graphica/gui/crash_handler.py +77 -0
  90. graphica/gui/cvd_preview.py +23 -0
  91. graphica/gui/data_editor.py +761 -0
  92. graphica/gui/data_import_flow.py +355 -0
  93. graphica/gui/dataset_bindings.py +107 -0
  94. graphica/gui/dataset_style_icon.py +80 -0
  95. graphica/gui/datasets/__init__.py +1 -0
  96. graphica/gui/datasets/actions_menu.py +114 -0
  97. graphica/gui/datasets/colors.py +218 -0
  98. graphica/gui/datasets/fitting.py +70 -0
  99. graphica/gui/datasets/host.py +162 -0
  100. graphica/gui/datasets/operations/__init__.py +1 -0
  101. graphica/gui/datasets/operations/fitting.py +468 -0
  102. graphica/gui/datasets/operations/peaks.py +109 -0
  103. graphica/gui/datasets/operations/processing.py +659 -0
  104. graphica/gui/datasets/operations/runner.py +148 -0
  105. graphica/gui/datasets/operations/transfer.py +227 -0
  106. graphica/gui/datasets/order.py +310 -0
  107. graphica/gui/datasets/overlays.py +74 -0
  108. graphica/gui/datasets/peaks.py +21 -0
  109. graphica/gui/datasets/plugin_runs.py +99 -0
  110. graphica/gui/datasets/processing.py +68 -0
  111. graphica/gui/datasets/property_panel.py +538 -0
  112. graphica/gui/datasets/transfer.py +36 -0
  113. graphica/gui/detached_canvas_window.py +22 -0
  114. graphica/gui/dialogs/__init__.py +137 -0
  115. graphica/gui/dialogs/analysis.py +1322 -0
  116. graphica/gui/dialogs/app.py +1015 -0
  117. graphica/gui/dialogs/appearance.py +851 -0
  118. graphica/gui/dialogs/data_edit.py +577 -0
  119. graphica/gui/dialogs/data_import.py +679 -0
  120. graphica/gui/dialogs/export.py +433 -0
  121. graphica/gui/dock_layout.py +176 -0
  122. graphica/gui/export_preview_panel.py +321 -0
  123. graphica/gui/export_settings.py +16 -0
  124. graphica/gui/file_association.py +97 -0
  125. graphica/gui/icon_utils.py +44 -0
  126. graphica/gui/main_app_window.py +210 -0
  127. graphica/gui/main_window.py +1151 -0
  128. graphica/gui/mathtext_preview.py +168 -0
  129. graphica/gui/menu_bar.py +320 -0
  130. graphica/gui/minimap_widget.py +151 -0
  131. graphica/gui/mixins/__init__.py +0 -0
  132. graphica/gui/mixins/export_mixin.py +520 -0
  133. graphica/gui/mixins/help_mixin.py +127 -0
  134. graphica/gui/mixins/project_io_mixin.py +289 -0
  135. graphica/gui/mixins/quick_access_mixin.py +194 -0
  136. graphica/gui/mixins/ui_setup_mixin.py +206 -0
  137. graphica/gui/notify.py +61 -0
  138. graphica/gui/panels/__init__.py +43 -0
  139. graphica/gui/panels/axis_settings.py +605 -0
  140. graphica/gui/panels/dataset_tree.py +424 -0
  141. graphica/gui/plot_type_drawers.py +131 -0
  142. graphica/gui/plugin_context.py +119 -0
  143. graphica/gui/project_files.py +361 -0
  144. graphica/gui/provenance_panel.py +65 -0
  145. graphica/gui/rendering/__init__.py +1 -0
  146. graphica/gui/rendering/annotations.py +177 -0
  147. graphica/gui/rendering/appearance.py +368 -0
  148. graphica/gui/rendering/common.py +316 -0
  149. graphica/gui/rendering/data_1d.py +376 -0
  150. graphica/gui/rendering/data_2d.py +66 -0
  151. graphica/gui/residual_panel.py +62 -0
  152. graphica/gui/resources.py +16 -0
  153. graphica/gui/single_instance.py +141 -0
  154. graphica/gui/splash.py +83 -0
  155. graphica/gui/task_runner.py +41 -0
  156. graphica/gui/theme.py +864 -0
  157. graphica/gui/tools/__init__.py +56 -0
  158. graphica/gui/tools/annotation.py +275 -0
  159. graphica/gui/tools/cursor.py +259 -0
  160. graphica/gui/tools/layout_edit.py +285 -0
  161. graphica/gui/tools/manager.py +78 -0
  162. graphica/gui/tools/peak_placement.py +132 -0
  163. graphica/gui/tools/range_select.py +191 -0
  164. graphica/gui/tools/region_highlight.py +222 -0
  165. graphica/gui/tools/slice_extraction.py +200 -0
  166. graphica/gui/widget_translation.py +46 -0
  167. graphica/gui/workers.py +280 -0
  168. graphica/models/__init__.py +0 -0
  169. graphica/models/project.py +197 -0
  170. graphica/plugin/__init__.py +20 -0
  171. graphica/plugin/testing.py +57 -0
  172. graphica/sample_data/__init__.py +0 -0
  173. graphica/sample_data/cooling_curve_sample.csv +42 -0
  174. graphica/ui_main_window.py +581 -0
  175. graphica_plot-2.0.0.dist-info/METADATA +317 -0
  176. graphica_plot-2.0.0.dist-info/RECORD +181 -0
  177. graphica_plot-2.0.0.dist-info/WHEEL +5 -0
  178. graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
  179. graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
  180. graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
  181. graphica_plot-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,433 @@
1
+ """書き出しと出力のダイアログ。呼び出し側は `from graphica.gui.dialogs import X` で参照する。"""
2
+
3
+ import os
4
+ from PySide6.QtWidgets import (
5
+ QApplication,
6
+ QCheckBox,
7
+ QComboBox,
8
+ QDialog,
9
+ QDialogButtonBox,
10
+ QDoubleSpinBox,
11
+ QFormLayout,
12
+ QHBoxLayout,
13
+ QLabel,
14
+ QLineEdit,
15
+ QListWidget,
16
+ QListWidgetItem,
17
+ QPlainTextEdit,
18
+ QPushButton,
19
+ QSpinBox,
20
+ QStackedWidget,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+ from PySide6.QtCore import Qt
25
+ from PySide6.QtGui import QFont, QPixmap
26
+ from graphica.core.i18n import tr
27
+ from graphica.gui import notify
28
+ from graphica.gui.theme import apply_form_spacing
29
+ from graphica.core.cvd_simulation import CVD_TYPE_LABELS
30
+ from graphica.gui.cvd_preview import simulate_qimage
31
+
32
+
33
+ class ExportDialog(QDialog):
34
+ """書き出しの大きさ・単位・解像度。プレビューを作るのは呼び出し側(PlotterApp._generate_preview)。"""
35
+ def __init__(self, parent=None):
36
+ super().__init__(parent)
37
+ self.setWindowTitle("プロットのエクスポート")
38
+
39
+
40
+ # 学術誌の段組の幅(単段 85mm / 1.5段 114mm / 2段 170mm)。高さは図ごとに違うので決めない
41
+ self.JOURNAL_PRESET_CUSTOM = "カスタム"
42
+ self.JOURNAL_PRESETS = {
43
+ "学術誌 単段 (85mm)": 85.0,
44
+ "学術誌 1.5段 (114mm)": 114.0,
45
+ "学術誌 2段 (170mm)": 170.0,
46
+ }
47
+ self.journal_preset_combo = QComboBox()
48
+ self.journal_preset_combo.addItem(self.JOURNAL_PRESET_CUSTOM)
49
+ self.journal_preset_combo.addItems(self.JOURNAL_PRESETS.keys())
50
+ self.journal_preset_combo.currentTextChanged.connect(self._on_journal_preset_changed)
51
+
52
+ self.width_spinbox = QDoubleSpinBox()
53
+ self.width_spinbox.setRange(1, 10000)
54
+ self.width_spinbox.setValue(800)
55
+ self.width_spinbox.setDecimals(1) # インチ指定のため小数を許す
56
+
57
+ self.height_spinbox = QDoubleSpinBox()
58
+ self.height_spinbox.setRange(1, 10000)
59
+ self.height_spinbox.setValue(600)
60
+ self.height_spinbox.setDecimals(1)
61
+
62
+ # 学術誌のプリセットを選ぶとミリメートルに切り替わる
63
+ self.unit_combo = QComboBox()
64
+ # 値は日本語の元の文字(書き出しの処理がそれで単位を見分ける)。表示だけを訳す
65
+ for unit in ("ピクセル (px)", "インチ (in)", "センチメートル (cm)", "ミリメートル (mm)"):
66
+ self.unit_combo.addItem(tr(unit), unit)
67
+
68
+ self.dpi_spinbox = QSpinBox()
69
+ self.dpi_spinbox.setRange(50, 1200)
70
+ self.dpi_spinbox.setValue(300)
71
+ self.dpi_spinbox.setSuffix(" dpi")
72
+
73
+ self.transparent_checkbox = QCheckBox("背景を透過")
74
+ self.transparent_checkbox.setChecked(True)
75
+
76
+ # 既定は文字のまま(検索や編集ができる)。フォントの無い環境で化けるのを避けたいときだけパスにする。SVG だけに効く
77
+ self.svg_text_as_path_checkbox = QCheckBox("文字をアウトライン化する(SVG)")
78
+ self.svg_text_as_path_checkbox.setToolTip(
79
+ "SVG出力時、目盛りの数字やラベルの文字をテキスト要素ではなく"
80
+ "パス(輪郭線)として出力します。フォントが無い環境でも見た目が"
81
+ "崩れませんが、テキストとしての検索・編集はできなくなります。"
82
+ )
83
+
84
+ # 表示用の間引き(線は 20,000 点超、2D マップは1軸 500 点超)をしない
85
+ self.full_resolution_checkbox = QCheckBox("フル解像度でエクスポート(間引きなし)")
86
+ self.full_resolution_checkbox.setToolTip(
87
+ "通常、点数の多いLine(折れ線)データセットや2Dマップ(ヒートマップ/"
88
+ "等高線)は画面表示同様に間引いて描画されます。このチェックを入れると"
89
+ "間引きを無効化し、常に全データ点/全解像度でエクスポートします"
90
+ "(処理が遅くなる場合があります)。Scatter/Line+Scatterのマーカーは"
91
+ "この設定に関わらず常に全点描画されます。"
92
+ )
93
+
94
+ self.preview_button = QPushButton("プレビュー更新")
95
+ self.preview_button.setToolTip("現在の設定でプレビュー画像を生成します。")
96
+
97
+ self.preview_label = QLabel("プレビューがここに表示されます")
98
+ self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
99
+ self.preview_label.setFixedSize(400, 300)
100
+ self.preview_label.setFrameShape(QLabel.Shape.StyledPanel)
101
+
102
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save |
103
+ QDialogButtonBox.StandardButton.Cancel)
104
+ button_box.accepted.connect(self.accept)
105
+ button_box.rejected.connect(self.reject)
106
+
107
+
108
+ form_layout = QFormLayout()
109
+ form_layout.addRow("サイズプリセット", self.journal_preset_combo)
110
+ form_layout.addRow("幅", self.width_spinbox)
111
+ form_layout.addRow("高さ", self.height_spinbox)
112
+ form_layout.addRow("単位", self.unit_combo)
113
+ form_layout.addRow("解像度", self.dpi_spinbox)
114
+ form_layout.addRow(self.transparent_checkbox)
115
+ form_layout.addRow(self.svg_text_as_path_checkbox)
116
+ form_layout.addRow(self.full_resolution_checkbox)
117
+
118
+ main_layout = QVBoxLayout()
119
+ main_layout.addLayout(form_layout)
120
+ main_layout.addWidget(self.preview_button)
121
+ main_layout.addWidget(self.preview_label)
122
+ main_layout.addWidget(button_box)
123
+
124
+ self.setLayout(main_layout)
125
+
126
+ apply_form_spacing(self)
127
+
128
+ def _on_journal_preset_changed(self, preset_name):
129
+ """「カスタム」なら入力した値をそのまま残す。"""
130
+ if preset_name == self.JOURNAL_PRESET_CUSTOM:
131
+ return
132
+ self.unit_combo.setCurrentIndex(self.unit_combo.findData("ミリメートル (mm)"))
133
+ self.width_spinbox.setValue(self.JOURNAL_PRESETS[preset_name])
134
+
135
+ def get_options(self):
136
+ return {
137
+ "width": self.width_spinbox.value(),
138
+ "height": self.height_spinbox.value(),
139
+ "unit": self.unit_combo.currentData(),
140
+ "dpi": self.dpi_spinbox.value(),
141
+ "transparent": self.transparent_checkbox.isChecked(),
142
+ "svg_text_as_path": self.svg_text_as_path_checkbox.isChecked(),
143
+ "full_resolution": self.full_resolution_checkbox.isChecked(),
144
+ }
145
+
146
+
147
+ class BatchExportDialog(QDialog):
148
+ """サブプロットごと、または複数のプロジェクトファイルをまとめて書き出す設定。書き出しは呼び出し側。"""
149
+
150
+ def __init__(self, subplot_count, parent=None, extra_formats=None):
151
+ """extra_formats はプラグインの書き出し形式の名前(PNG / PDF / SVG に足す)。"""
152
+ super().__init__(parent)
153
+ self.setWindowTitle("バッチエクスポート")
154
+ self.resize(480, 480)
155
+
156
+ layout = QVBoxLayout(self)
157
+
158
+ self.mode_combo = QComboBox()
159
+ self.mode_combo.addItems([
160
+ "現在のプロジェクトの各サブプロットを個別に書き出す",
161
+ "複数のプロジェクトファイルをまとめて書き出す",
162
+ ])
163
+ layout.addWidget(self.mode_combo)
164
+
165
+ self.stack = QStackedWidget()
166
+ layout.addWidget(self.stack, 1)
167
+
168
+ subplot_page = QWidget()
169
+ subplot_page_layout = QVBoxLayout(subplot_page)
170
+ subplot_page_layout.addWidget(QLabel("書き出すサブプロットを選択してください:"))
171
+ self.subplot_list = QListWidget()
172
+ self.subplot_list.setSelectionMode(QListWidget.SelectionMode.NoSelection)
173
+ for i in range(max(subplot_count, 1)):
174
+ item = QListWidgetItem(f"プロット {i + 1}")
175
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
176
+ item.setCheckState(Qt.CheckState.Checked)
177
+ self.subplot_list.addItem(item)
178
+ subplot_page_layout.addWidget(self.subplot_list)
179
+ self.stack.addWidget(subplot_page)
180
+
181
+ files_page = QWidget()
182
+ files_page_layout = QVBoxLayout(files_page)
183
+ files_page_layout.addWidget(QLabel("書き出すプロジェクトファイル(.gra/.graphica)を追加してください:"))
184
+ self.project_files_list = QListWidget()
185
+ files_page_layout.addWidget(self.project_files_list)
186
+ files_button_row = QHBoxLayout()
187
+ add_files_button = QPushButton("追加...")
188
+ add_files_button.clicked.connect(self._on_add_project_files)
189
+ remove_files_button = QPushButton("削除")
190
+ remove_files_button.clicked.connect(self._on_remove_selected_project_files)
191
+ files_button_row.addWidget(add_files_button)
192
+ files_button_row.addWidget(remove_files_button)
193
+ files_button_row.addStretch()
194
+ files_page_layout.addLayout(files_button_row)
195
+ self.stack.addWidget(files_page)
196
+
197
+ self.mode_combo.currentIndexChanged.connect(self.stack.setCurrentIndex)
198
+
199
+ form = QFormLayout()
200
+
201
+ output_dir_row = QHBoxLayout()
202
+ self.output_dir_edit = QLineEdit()
203
+ self.output_dir_edit.setReadOnly(True)
204
+ browse_button = QPushButton("参照...")
205
+ browse_button.clicked.connect(self._on_browse_output_dir)
206
+ output_dir_row.addWidget(self.output_dir_edit)
207
+ output_dir_row.addWidget(browse_button)
208
+ form.addRow("出力先フォルダ", output_dir_row)
209
+
210
+ self.prefix_edit = QLineEdit("export")
211
+ form.addRow("ファイル名の接頭辞", self.prefix_edit)
212
+
213
+ self.format_combo = QComboBox()
214
+ self.format_combo.addItems(["PNG", "PDF", "SVG"])
215
+ if extra_formats:
216
+ self.format_combo.addItems(extra_formats)
217
+ form.addRow("形式", self.format_combo)
218
+
219
+ self.dpi_spinbox = QSpinBox()
220
+ self.dpi_spinbox.setRange(50, 1200)
221
+ self.dpi_spinbox.setValue(150)
222
+ self.dpi_spinbox.setSuffix(" dpi")
223
+ form.addRow("解像度(ラスター形式時)", self.dpi_spinbox)
224
+
225
+ self.transparent_checkbox = QCheckBox("背景を透過")
226
+ self.transparent_checkbox.setChecked(True)
227
+ form.addRow(self.transparent_checkbox)
228
+
229
+ self.svg_text_as_path_checkbox = QCheckBox("文字をアウトライン化する(SVG)")
230
+ self.svg_text_as_path_checkbox.setToolTip(
231
+ "SVG出力時、目盛りの数字やラベルの文字をパス(輪郭線)として出力します。"
232
+ "PNG/PDF形式には影響しません。"
233
+ )
234
+ form.addRow(self.svg_text_as_path_checkbox)
235
+
236
+ self.full_resolution_checkbox = QCheckBox("フル解像度でエクスポート(間引きなし)")
237
+ self.full_resolution_checkbox.setToolTip(
238
+ "通常、点数の多いLine(折れ線)データセットや2Dマップ(ヒートマップ/"
239
+ "等高線)は画面表示同様に間引いて描画されます。このチェックを入れると"
240
+ "間引きを無効化し、常に全データ点/全解像度でエクスポートします"
241
+ "(処理が遅くなる場合があります)。Scatter/Line+Scatterのマーカーは"
242
+ "この設定に関わらず常に全点描画されます。"
243
+ )
244
+ form.addRow(self.full_resolution_checkbox)
245
+
246
+ layout.addLayout(form)
247
+
248
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
249
+ QDialogButtonBox.StandardButton.Cancel)
250
+ button_box.button(QDialogButtonBox.StandardButton.Ok).setText("実行")
251
+ button_box.accepted.connect(self.accept)
252
+ button_box.rejected.connect(self.reject)
253
+ layout.addWidget(button_box)
254
+
255
+ apply_form_spacing(self)
256
+
257
+ def _on_add_project_files(self):
258
+ paths, _ = notify.get_open_file_names(
259
+ self, "プロジェクトファイルを選択", "", "Graphica Project (*.gra *.graphica)"
260
+ )
261
+ for path in paths:
262
+ self.project_files_list.addItem(path)
263
+
264
+ def _on_remove_selected_project_files(self):
265
+ for item in self.project_files_list.selectedItems():
266
+ self.project_files_list.takeItem(self.project_files_list.row(item))
267
+
268
+ def _on_browse_output_dir(self):
269
+ directory = notify.get_existing_directory(self, "出力先フォルダを選択")
270
+ if directory:
271
+ self.output_dir_edit.setText(directory)
272
+
273
+ def get_mode(self):
274
+ """'subplots' か 'project_files'。"""
275
+ return "subplots" if self.mode_combo.currentIndex() == 0 else "project_files"
276
+
277
+ def get_selected_subplot_indices(self):
278
+ return [
279
+ i for i in range(self.subplot_list.count())
280
+ if self.subplot_list.item(i).checkState() == Qt.CheckState.Checked
281
+ ]
282
+
283
+ def get_project_file_paths(self):
284
+ return [self.project_files_list.item(i).text() for i in range(self.project_files_list.count())]
285
+
286
+ def get_common_options(self):
287
+ return {
288
+ 'output_dir': self.output_dir_edit.text(),
289
+ 'prefix': self.prefix_edit.text().strip() or "export",
290
+ 'format': self.format_combo.currentText().lower(),
291
+ 'dpi': self.dpi_spinbox.value(),
292
+ 'transparent': self.transparent_checkbox.isChecked(),
293
+ 'svg_text_as_path': self.svg_text_as_path_checkbox.isChecked(),
294
+ 'full_resolution': self.full_resolution_checkbox.isChecked(),
295
+ }
296
+
297
+
298
+ class CaptionGeneratorDialog(QDialog):
299
+ """論文用の LaTeX の \\includegraphics 一式と Word 用のキャプションを作ってコピーする。画像ファイル名は自由入力。"""
300
+
301
+ WIDTH_OPTIONS = [r'\linewidth', r'0.8\linewidth', r'0.5\linewidth', r'\textwidth']
302
+
303
+ def __init__(self, default_caption="", default_label="", parent=None):
304
+ super().__init__(parent)
305
+ self.setWindowTitle("LaTeX/Word用キャプションを生成")
306
+ self.resize(480, 420)
307
+
308
+ layout = QVBoxLayout(self)
309
+
310
+ form = QFormLayout()
311
+ self.caption_edit = QLineEdit(default_caption)
312
+ form.addRow("キャプション", self.caption_edit)
313
+
314
+ image_layout = QHBoxLayout()
315
+ self.image_name_edit = QLineEdit()
316
+ self.image_name_edit.setPlaceholderText("例: figure1.pdf(空欄可、後から書き換えてください)")
317
+ browse_button = QPushButton("参照...")
318
+ browse_button.clicked.connect(self._on_browse_image)
319
+ image_layout.addWidget(self.image_name_edit, 1)
320
+ image_layout.addWidget(browse_button)
321
+ form.addRow("画像ファイル名", image_layout)
322
+
323
+ self.label_edit = QLineEdit(default_label)
324
+ form.addRow("LaTeXラベル", self.label_edit)
325
+
326
+ self.width_combo = QComboBox()
327
+ self.width_combo.addItems(self.WIDTH_OPTIONS)
328
+ form.addRow("幅(\\includegraphics)", self.width_combo)
329
+ layout.addLayout(form)
330
+
331
+ layout.addWidget(QLabel("LaTeXプレビュー"))
332
+ self.latex_preview = QPlainTextEdit()
333
+ self.latex_preview.setReadOnly(True)
334
+ self.latex_preview.setFont(QFont("Consolas", 9))
335
+ layout.addWidget(self.latex_preview, 1)
336
+
337
+ button_layout = QHBoxLayout()
338
+ copy_latex_button = QPushButton("LaTeXをコピー")
339
+ copy_latex_button.clicked.connect(self._on_copy_latex)
340
+ copy_caption_button = QPushButton("キャプション文をコピー(Word用)")
341
+ copy_caption_button.clicked.connect(self._on_copy_caption)
342
+ button_layout.addWidget(copy_latex_button)
343
+ button_layout.addWidget(copy_caption_button)
344
+ button_layout.addStretch()
345
+ layout.addLayout(button_layout)
346
+
347
+ close_button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
348
+ close_button_box.rejected.connect(self.reject)
349
+ close_button_box.button(QDialogButtonBox.StandardButton.Close).clicked.connect(self.reject)
350
+ layout.addWidget(close_button_box)
351
+
352
+ for widget in (self.caption_edit, self.image_name_edit, self.label_edit):
353
+ widget.textChanged.connect(self._update_preview)
354
+ self.width_combo.currentTextChanged.connect(self._update_preview)
355
+ self._update_preview()
356
+
357
+ apply_form_spacing(self)
358
+
359
+ def _on_browse_image(self):
360
+ file_path, _ = notify.get_open_file_name(
361
+ self, "画像ファイルを選択", "", "画像ファイル (*.pdf *.svg *.png *.eps)"
362
+ )
363
+ if file_path:
364
+ self.image_name_edit.setText(os.path.basename(file_path))
365
+
366
+ def _update_preview(self):
367
+ from graphica.core.caption_export import generate_latex_figure
368
+ latex_code = generate_latex_figure(
369
+ self.image_name_edit.text().strip(), self.caption_edit.text(),
370
+ self.label_edit.text().strip(), self.width_combo.currentText(),
371
+ )
372
+ self.latex_preview.setPlainText(latex_code)
373
+
374
+ def _on_copy_latex(self):
375
+ QApplication.clipboard().setText(self.latex_preview.toPlainText())
376
+
377
+ def _on_copy_caption(self):
378
+ QApplication.clipboard().setText(self.caption_edit.text())
379
+
380
+
381
+ class CVDSimulationDialog(QDialog):
382
+ """色覚シミュレーションのプレビュー。呼び出し側が1回撮った画像を、モードごとに変換して見せる(グラフは描き直さない)。"""
383
+
384
+ MODE_NORMAL = "通常(変換なし)"
385
+
386
+ def __init__(self, source_image, parent=None):
387
+ super().__init__(parent)
388
+ self.setWindowTitle("色覚シミュレーションプレビュー")
389
+ self.resize(700, 560)
390
+ self._source_image = source_image
391
+
392
+ layout = QVBoxLayout(self)
393
+
394
+ form = QFormLayout()
395
+ self.mode_combo = QComboBox()
396
+ self.mode_combo.addItem(self.MODE_NORMAL, None)
397
+ for key, label in CVD_TYPE_LABELS.items():
398
+ self.mode_combo.addItem(label, key)
399
+ self.mode_combo.currentIndexChanged.connect(self._update_preview)
400
+ form.addRow("表示モード", self.mode_combo)
401
+ layout.addLayout(form)
402
+
403
+ self.preview_label = QLabel()
404
+ self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
405
+ self.preview_label.setMinimumSize(400, 300)
406
+ self.preview_label.setFrameShape(QLabel.Shape.StyledPanel)
407
+ layout.addWidget(self.preview_label, 1)
408
+
409
+ info_label = QLabel(
410
+ "sRGB値への直接変換による簡易的な近似シミュレーションです。"
411
+ "実際の見え方を完全に再現するものではありません。"
412
+ )
413
+ info_label.setWordWrap(True)
414
+ layout.addWidget(info_label)
415
+
416
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
417
+ button_box.rejected.connect(self.reject)
418
+ button_box.button(QDialogButtonBox.StandardButton.Close).clicked.connect(self.reject)
419
+ layout.addWidget(button_box)
420
+
421
+ self._update_preview()
422
+
423
+ def _update_preview(self):
424
+ cvd_type = self.mode_combo.currentData()
425
+ image = self._source_image if cvd_type is None else simulate_qimage(self._source_image, cvd_type)
426
+ pixmap = QPixmap.fromImage(image)
427
+ target_width = max(self.preview_label.width(), 400)
428
+ target_height = max(self.preview_label.height(), 300)
429
+ scaled = pixmap.scaled(
430
+ target_width, target_height,
431
+ Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation,
432
+ )
433
+ self.preview_label.setPixmap(scaled)
@@ -0,0 +1,176 @@
1
+ """ドックの配置: 組み立て、起動時の復元、名前を付けた配置の保存・読み込み・リセット。"""
2
+ import base64
3
+ import json
4
+ import logging
5
+ from PySide6.QtCore import QByteArray, Qt
6
+ from PySide6.QtWidgets import QDockWidget, QGroupBox, QScrollArea, QVBoxLayout, QWidget
7
+ from graphica.core.i18n import tr
8
+ from graphica.gui import app_settings, notify
9
+ from graphica.gui.builders.common import DOCK_LAYOUT_VERSION, EXPORT_PREVIEW_DOCK_INITIAL_HEIGHT
10
+ from graphica.gui.export_preview_panel import ExportPreviewPanel
11
+ from graphica.gui.provenance_panel import ProvenancePanel
12
+ from graphica.gui.residual_panel import ResidualPanel
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def arrange_property_docks(app):
18
+ # 「プロットのプロパティ」と「データセットのプロパティ」を1つのドックに縦に並べ、1枚のパネルに見せる。
19
+ # properties_dock_widget は control_dock_widget の別名(表示メニューなどがこの名前で使う)
20
+ app.properties_dock_widget = app.ui.control_dock_widget
21
+ app.ui.control_dock_widget.setWindowTitle(tr("プロパティ"))
22
+
23
+ original_control_widget = app.ui.control_dock_widget.widget()
24
+ plot_properties_group = QGroupBox(tr("プロットのプロパティ"))
25
+ plot_properties_layout = QVBoxLayout(plot_properties_group)
26
+ plot_properties_layout.setContentsMargins(0, 4, 0, 0)
27
+ if original_control_widget:
28
+ plot_properties_layout.addWidget(original_control_widget)
29
+ else:
30
+ logger.warning("control_dock_widget の中身が見つかりません。")
31
+
32
+ app.ui.properties_groupbox.setTitle(tr("データセットのプロパティ"))
33
+
34
+ # どちらも長いので開閉できるようにする。中身を1つずつ隠すと入れ子のレイアウトを取りこぼすので、
35
+ # グループボックスごと出し入れする開閉ボタンを外に付ける(見出しはボタン側だけに出す)
36
+ dataset_section = app._wrap_in_collapsible_section(
37
+ app.ui.properties_groupbox, tr("データセットのプロパティ"))
38
+ plot_section = app._wrap_in_collapsible_section(
39
+ plot_properties_group, tr("プロットのプロパティ"))
40
+
41
+ # 既定の余白だと縦スクロールバーの分だけ中身がはみ出し、横スクロールバーが出るので左右を詰める
42
+ merged_properties_container = QWidget()
43
+ merged_properties_layout = QVBoxLayout(merged_properties_container)
44
+ merged_properties_layout.setContentsMargins(2, 4, 2, 4)
45
+ merged_properties_layout.addWidget(dataset_section)
46
+ merged_properties_layout.addWidget(plot_section)
47
+ merged_properties_layout.addStretch()
48
+
49
+ merged_scroll_area = QScrollArea()
50
+ merged_scroll_area.setWidgetResizable(True)
51
+ merged_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
52
+ merged_scroll_area.setWidget(merged_properties_container)
53
+ app.ui.control_dock_widget.setWidget(merged_scroll_area)
54
+
55
+ # エクスポートのプレビュー。描き続けると重いので既定では隠し、表示メニューから開く
56
+ app.export_preview_panel = ExportPreviewPanel(app)
57
+ app.export_preview_dock_widget = QDockWidget(tr("エクスポートプレビュー"), app)
58
+ app.export_preview_dock_widget.setObjectName("ExportPreviewDockWidget")
59
+ app.export_preview_dock_widget.setWidget(app.export_preview_panel)
60
+ # 必要なときだけ見るものなので、キャンバスを狭めないよう独立した窓で開く(ドッキングもできる)
61
+ app.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, app.export_preview_dock_widget)
62
+ app.export_preview_dock_widget.setFloating(True)
63
+ app.export_preview_dock_widget.hide()
64
+ app._export_preview_first_show = True
65
+
66
+ def _on_export_preview_visibility_changed(visible):
67
+ if not visible:
68
+ return
69
+ # 浮いたドックは表示されるまで大きさを持たないことがあるので、最初に表示したときに整える
70
+ if app._export_preview_first_show and app.export_preview_dock_widget.isFloating():
71
+ app._export_preview_first_show = False
72
+ preview_width, preview_height = 820, 680
73
+ app.export_preview_dock_widget.resize(preview_width, preview_height)
74
+ center = app.geometry().center()
75
+ app.export_preview_dock_widget.move(
76
+ center.x() - preview_width // 2, center.y() - preview_height // 2
77
+ )
78
+ app.export_preview_panel.refresh_preview()
79
+
80
+ # 選んだデータセットのフィットの残差。既定では隠し、開くときはキャンバスの下に付ける
81
+ app.residual_panel = ResidualPanel(app)
82
+ app.residual_dock_widget = QDockWidget(tr("残差プロット"), app)
83
+ app.residual_dock_widget.setObjectName("ResidualDockWidget")
84
+ app.residual_dock_widget.setWidget(app.residual_panel)
85
+ app.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, app.residual_dock_widget)
86
+ app.residual_dock_widget.hide()
87
+
88
+ # 選んだデータセットの処理の履歴。既定では隠す
89
+ app.provenance_panel = ProvenancePanel(app)
90
+ app.provenance_dock_widget = QDockWidget(tr("処理履歴"), app)
91
+ app.provenance_dock_widget.setObjectName("ProvenanceDockWidget")
92
+ app.provenance_dock_widget.setWidget(app.provenance_panel)
93
+ app.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, app.provenance_dock_widget)
94
+ app.provenance_dock_widget.hide()
95
+
96
+ app.export_preview_dock_widget.visibilityChanged.connect(_on_export_preview_visibility_changed)
97
+
98
+
99
+ def restore_dock_layout(app):
100
+ """前回のドック配置を戻す。タブが最終の大きさになってから呼ぶ(でないとスプリッターの位置がずれる)。"""
101
+ saved_layout_version = (app_settings.DOCK_LAYOUT_VERSION.read(app.settings) if app._run_startup_checks
102
+ else DOCK_LAYOUT_VERSION)
103
+ saved_state = app_settings.WINDOW_STATE.read(app.settings) if app._run_startup_checks else None
104
+ state_restored = False
105
+ if saved_state is not None and saved_layout_version == DOCK_LAYOUT_VERSION:
106
+ state_restored = bool(app.restoreState(saved_state))
107
+
108
+ if not state_restored:
109
+ try:
110
+ app.resizeDocks(
111
+ [app.export_preview_dock_widget],
112
+ [EXPORT_PREVIEW_DOCK_INITIAL_HEIGHT],
113
+ Qt.Orientation.Vertical
114
+ )
115
+ except Exception:
116
+ logger.exception("resizeDocks に失敗しました")
117
+
118
+
119
+ def load_dock_layout_presets(app):
120
+ """{名前: base64 の saveState}"""
121
+ raw = app_settings.DOCK_LAYOUT_PRESETS.read(app.settings)
122
+ if not isinstance(raw, str):
123
+ raw = "{}"
124
+ try:
125
+ presets = json.loads(raw)
126
+ except (TypeError, ValueError):
127
+ logger.warning("ドックレイアウトプリセットの読み込みに失敗しました。空として扱います。")
128
+ return {}
129
+ return presets if isinstance(presets, dict) else {}
130
+
131
+
132
+ def save_dock_layout_presets(app, presets: dict):
133
+ app_settings.DOCK_LAYOUT_PRESETS.write(app.settings, json.dumps(presets))
134
+
135
+
136
+ def on_save_dock_layout_preset(app):
137
+ name, ok = notify.get_text(app, "レイアウトを保存", "プリセット名:")
138
+ name = name.strip()
139
+ if not ok or not name:
140
+ return
141
+ presets = app._load_dock_layout_presets()
142
+ presets[name] = base64.b64encode(bytes(app.saveState())).decode('ascii')
143
+ app._save_dock_layout_presets(presets)
144
+ app.statusBar().showMessage(f"レイアウト「{name}」を保存しました", 3000)
145
+
146
+
147
+ def populate_load_layout_menu(app):
148
+ """保存や削除のたびに更新しなくて済むよう、開く直前に作り直す。"""
149
+ app.load_layout_menu.clear()
150
+ presets = app._load_dock_layout_presets()
151
+ if not presets:
152
+ empty_action = app.load_layout_menu.addAction("(保存済みレイアウトはありません)")
153
+ empty_action.setEnabled(False)
154
+ return
155
+ for name in sorted(presets.keys()):
156
+ action = app.load_layout_menu.addAction(name)
157
+ action.triggered.connect(lambda checked=False, n=name: app._on_load_dock_layout_preset(n))
158
+
159
+
160
+ def on_load_dock_layout_preset(app, name):
161
+ presets = app._load_dock_layout_presets()
162
+ state_b64 = presets.get(name)
163
+ if state_b64 is None:
164
+ return
165
+ try:
166
+ state_bytes = base64.b64decode(state_b64)
167
+ except (ValueError, TypeError):
168
+ notify.warning(app, "レイアウトの復元", "保存されたレイアウトデータが壊れています。")
169
+ return
170
+ if not app.restoreState(QByteArray(state_bytes)):
171
+ notify.warning(app, "レイアウトの復元", "レイアウトの復元に失敗しました。")
172
+
173
+
174
+ def on_reset_dock_layout(app):
175
+ """組み立てた直後、配置を戻す前に控えた状態(_pristine_dock_state)に戻す。"""
176
+ app.restoreState(app._pristine_dock_state)