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,520 @@
|
|
|
1
|
+
"""画像・PDF・SVG への書き出し、印刷、クリップボード、書き出しのプレビュー。"""
|
|
2
|
+
import datetime
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import dataclasses
|
|
6
|
+
import logging
|
|
7
|
+
import matplotlib as mpl
|
|
8
|
+
from PySide6.QtCore import Qt
|
|
9
|
+
from PySide6.QtGui import QPixmap, QPainter, QImage
|
|
10
|
+
from PySide6.QtWidgets import QApplication, QDialog, QProgressDialog
|
|
11
|
+
from PySide6.QtPrintSupport import QPrinter, QPrintDialog
|
|
12
|
+
from matplotlib.figure import Figure
|
|
13
|
+
from matplotlib.backends.backend_pdf import PdfPages
|
|
14
|
+
|
|
15
|
+
from graphica.gui import notify
|
|
16
|
+
from graphica.core.axis_settings import axis_setting
|
|
17
|
+
from graphica.gui.dialogs import ExportDialog, BatchExportDialog, CaptionGeneratorDialog, CVDSimulationDialog
|
|
18
|
+
from graphica.gui.canvas import _HeadlessRenderCanvas, fit_tight_layout
|
|
19
|
+
from graphica.gui.export_settings import export_rc_params
|
|
20
|
+
from graphica.gui.task_runner import TaskRunner
|
|
21
|
+
from graphica.models.project import ProjectModel
|
|
22
|
+
from graphica.core.plugin_api import get_plugin_api, get_registered_exporters
|
|
23
|
+
from graphica.core.plugin_types import PluginExecutionError
|
|
24
|
+
from graphica.core.script_export import generate_python_script
|
|
25
|
+
from graphica.core.caption_export import sanitize_label
|
|
26
|
+
from graphica.core.report_export import collect_methods_sections, generate_html_report
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
def _project_has_raster_gradient_fill(project):
|
|
31
|
+
"""グラデーションの塗り(imshow で描くのでベクター形式でもラスタになる)を使う系列があるか。"""
|
|
32
|
+
return any(
|
|
33
|
+
ds.plot_type == 'Area' and ds.gradient_enabled and ds.gradient_target in ('fill', 'both')
|
|
34
|
+
for ds in project.datasets
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
CLIPBOARD_COPY_DPI = 150
|
|
39
|
+
|
|
40
|
+
PRINT_RENDER_DPI = 200
|
|
41
|
+
|
|
42
|
+
BATCH_EXPORT_FIGSIZE = (8, 6)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExportMixin:
|
|
46
|
+
def _on_copy_plot_to_clipboard(self):
|
|
47
|
+
buf = io.BytesIO()
|
|
48
|
+
try:
|
|
49
|
+
self.canvas.fig.savefig(buf, format='png', dpi=CLIPBOARD_COPY_DPI, bbox_inches='tight')
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.exception("グラフのクリップボードコピーに失敗しました。")
|
|
52
|
+
notify.warning(self, "コピーエラー", f"グラフのコピー中にエラーが発生しました:\n{e}")
|
|
53
|
+
return
|
|
54
|
+
buf.seek(0)
|
|
55
|
+
|
|
56
|
+
pixmap = QPixmap()
|
|
57
|
+
pixmap.loadFromData(buf.read())
|
|
58
|
+
buf.close()
|
|
59
|
+
|
|
60
|
+
QApplication.clipboard().setPixmap(pixmap)
|
|
61
|
+
|
|
62
|
+
def _on_show_cvd_simulation(self):
|
|
63
|
+
"""今のグラフを1回だけ PNG にし、ダイアログはその画像を変換して見せる(グラフは描き直さない)。"""
|
|
64
|
+
buf = io.BytesIO()
|
|
65
|
+
try:
|
|
66
|
+
self.canvas.fig.savefig(buf, format='png', dpi=CLIPBOARD_COPY_DPI, bbox_inches='tight')
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.exception("色覚シミュレーションプレビュー用の画像生成に失敗しました。")
|
|
69
|
+
notify.warning(self, "プレビューエラー", f"プレビュー画像の生成中にエラーが発生しました:\n{e}")
|
|
70
|
+
return
|
|
71
|
+
buf.seek(0)
|
|
72
|
+
image = QImage()
|
|
73
|
+
image.loadFromData(buf.read())
|
|
74
|
+
buf.close()
|
|
75
|
+
|
|
76
|
+
dialog = CVDSimulationDialog(image, self)
|
|
77
|
+
dialog.exec()
|
|
78
|
+
|
|
79
|
+
def _on_print_plot(self):
|
|
80
|
+
"""PNG に描いてから QPrinter に貼る(書き出しと同じ描画結果になる)。"""
|
|
81
|
+
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
|
|
82
|
+
print_dialog = QPrintDialog(printer, self)
|
|
83
|
+
if print_dialog.exec() != QDialog.DialogCode.Accepted:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
buf = io.BytesIO()
|
|
87
|
+
try:
|
|
88
|
+
self.canvas.fig.savefig(buf, format='png', dpi=PRINT_RENDER_DPI, bbox_inches='tight')
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.exception("印刷用画像の生成に失敗しました。")
|
|
91
|
+
notify.warning(self, "印刷エラー", f"印刷用の画像生成中にエラーが発生しました:\n{e}")
|
|
92
|
+
return
|
|
93
|
+
buf.seek(0)
|
|
94
|
+
|
|
95
|
+
pixmap = QPixmap()
|
|
96
|
+
pixmap.loadFromData(buf.read())
|
|
97
|
+
buf.close()
|
|
98
|
+
|
|
99
|
+
if pixmap.isNull():
|
|
100
|
+
notify.warning(self, "印刷エラー", "印刷用の画像生成に失敗しました。")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
painter = QPainter()
|
|
104
|
+
if not painter.begin(printer):
|
|
105
|
+
notify.warning(self, "印刷エラー", "プリンターへの描画を開始できませんでした。")
|
|
106
|
+
return
|
|
107
|
+
try:
|
|
108
|
+
page_rect = printer.pageRect(QPrinter.Unit.DevicePixel).toRect()
|
|
109
|
+
scaled = pixmap.scaled(
|
|
110
|
+
page_rect.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
|
|
111
|
+
)
|
|
112
|
+
x = page_rect.x() + (page_rect.width() - scaled.width()) // 2
|
|
113
|
+
y = page_rect.y() + (page_rect.height() - scaled.height()) // 2
|
|
114
|
+
painter.drawPixmap(x, y, scaled)
|
|
115
|
+
finally:
|
|
116
|
+
painter.end()
|
|
117
|
+
|
|
118
|
+
self.statusBar().showMessage("印刷を実行しました", 3000)
|
|
119
|
+
|
|
120
|
+
def _on_batch_export(self):
|
|
121
|
+
"""サブプロットごと、または複数のプロジェクトファイルをまとめて書き出す。別スレッドで1本の TaskRunner で行う。
|
|
122
|
+
|
|
123
|
+
rc_context はプロセス全体の rcParams を書き換えるので、並列にはしない。
|
|
124
|
+
"""
|
|
125
|
+
if self._batch_export_task_runner is not None:
|
|
126
|
+
notify.information(self, "実行中", "別のバッチエクスポート処理が実行中です。完了までお待ちください。")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
extra_formats = [exp.format_name for exp in get_registered_exporters()]
|
|
130
|
+
dialog = BatchExportDialog(len(self.project.all_plot_settings), self, extra_formats=extra_formats)
|
|
131
|
+
if dialog.exec() != QDialog.DialogCode.Accepted:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
options = dialog.get_common_options()
|
|
135
|
+
if not options['output_dir']:
|
|
136
|
+
notify.warning(self, "バッチエクスポート", "出力先フォルダを指定してください。")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
mode = dialog.get_mode()
|
|
140
|
+
if mode == "subplots":
|
|
141
|
+
indices = dialog.get_selected_subplot_indices()
|
|
142
|
+
if not indices:
|
|
143
|
+
notify.warning(self, "バッチエクスポート", "書き出すサブプロットを選択してください。")
|
|
144
|
+
return
|
|
145
|
+
items = indices
|
|
146
|
+
worker_fn = self._batch_export_subplots
|
|
147
|
+
else:
|
|
148
|
+
paths = dialog.get_project_file_paths()
|
|
149
|
+
if not paths:
|
|
150
|
+
notify.warning(self, "バッチエクスポート", "プロジェクトファイルを追加してください。")
|
|
151
|
+
return
|
|
152
|
+
items = paths
|
|
153
|
+
worker_fn = self._batch_export_project_files
|
|
154
|
+
|
|
155
|
+
progress_dialog = QProgressDialog("バッチエクスポートを実行中...", "キャンセル", 0, len(items), self)
|
|
156
|
+
progress_dialog.setWindowModality(Qt.WindowModality.WindowModal)
|
|
157
|
+
progress_dialog.setMinimumDuration(0)
|
|
158
|
+
progress_dialog.setValue(0)
|
|
159
|
+
|
|
160
|
+
runner = TaskRunner(worker_fn, items, options)
|
|
161
|
+
runner.progress.connect(lambda done, total, message: progress_dialog.setValue(done))
|
|
162
|
+
progress_dialog.canceled.connect(runner.requestInterruption)
|
|
163
|
+
runner.succeeded.connect(lambda results: self._on_batch_export_succeeded(results, progress_dialog))
|
|
164
|
+
runner.failed.connect(lambda msg: self._on_batch_export_failed(msg, progress_dialog))
|
|
165
|
+
self._batch_export_task_runner = runner
|
|
166
|
+
runner.start()
|
|
167
|
+
|
|
168
|
+
def _cleanup_batch_export_task_runner(self):
|
|
169
|
+
if self._batch_export_task_runner is not None:
|
|
170
|
+
self._batch_export_task_runner.wait()
|
|
171
|
+
self._batch_export_task_runner.deleteLater()
|
|
172
|
+
self._batch_export_task_runner = None
|
|
173
|
+
|
|
174
|
+
def _on_batch_export_failed(self, error_message, progress_dialog):
|
|
175
|
+
self._cleanup_batch_export_task_runner()
|
|
176
|
+
progress_dialog.close()
|
|
177
|
+
notify.warning(self, "バッチエクスポート", f"バッチエクスポート処理に失敗しました:\n{error_message}")
|
|
178
|
+
|
|
179
|
+
def _on_batch_export_succeeded(self, results, progress_dialog):
|
|
180
|
+
"""結果は [(出力ファイル名, エラーか None), ...]。キャンセルなら終わった分だけ。"""
|
|
181
|
+
self._cleanup_batch_export_task_runner()
|
|
182
|
+
progress_dialog.close()
|
|
183
|
+
|
|
184
|
+
succeeded = [name for name, error in results if error is None]
|
|
185
|
+
failed = [(name, error) for name, error in results if error is not None]
|
|
186
|
+
|
|
187
|
+
if not succeeded and not failed:
|
|
188
|
+
return # 1件も終わらないうちにキャンセルされた
|
|
189
|
+
|
|
190
|
+
message = f"{len(succeeded)}件を書き出しました。"
|
|
191
|
+
if failed:
|
|
192
|
+
message += "\n\n失敗:\n" + "\n".join(f"{name}: {error}" for name, error in failed)
|
|
193
|
+
notify.information(self, "バッチエクスポート完了", message)
|
|
194
|
+
|
|
195
|
+
def _save_figure_with_options(self, fig, out_path, options):
|
|
196
|
+
"""bbox_inches='tight' で保存する。rc の設定は export_rc_params(SVG の文字の扱い、PDF の TrueType 埋め込み)。
|
|
197
|
+
|
|
198
|
+
形式がプラグインの書き出しの名前なら、プラグインの writer を呼ぶ。
|
|
199
|
+
"""
|
|
200
|
+
api = get_plugin_api()
|
|
201
|
+
exporter = api.get_exporter(options['format']) if api is not None else None
|
|
202
|
+
if exporter is not None:
|
|
203
|
+
try:
|
|
204
|
+
exporter.writer(fig, out_path)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
raise PluginExecutionError(exporter.name, f"「{out_path}」への書き出しに失敗しました: {e}") from e
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
save_kwargs = {'transparent': options.get('transparent', True), 'bbox_inches': 'tight'}
|
|
210
|
+
if options['format'] not in ('pdf', 'svg'):
|
|
211
|
+
save_kwargs['dpi'] = options['dpi']
|
|
212
|
+
with mpl.rc_context(export_rc_params(options['format'], options.get('svg_text_as_path', False))):
|
|
213
|
+
fig.savefig(out_path, **save_kwargs)
|
|
214
|
+
|
|
215
|
+
def _batch_export_subplots(self, indices, options, report_progress=None, is_cancelled=None):
|
|
216
|
+
"""サブプロットを1枚ずつ書き出す。1x1 の一時キャンバスなので、subplot_target を 0 にした写しを渡す。
|
|
217
|
+
|
|
218
|
+
別スレッドで呼ばれるので Qt には触れない(_HeadlessRenderCanvas は QWidget ではない)。
|
|
219
|
+
キャンセルは1件ごとにしか見ない(1件の描画と保存は中断できない)。
|
|
220
|
+
"""
|
|
221
|
+
results = []
|
|
222
|
+
total = len(indices)
|
|
223
|
+
for i, idx in enumerate(indices):
|
|
224
|
+
if is_cancelled is not None and is_cancelled():
|
|
225
|
+
break
|
|
226
|
+
if report_progress is not None:
|
|
227
|
+
report_progress(i, total, f"P{idx + 1}")
|
|
228
|
+
out_name = f"{options['prefix']}_P{idx + 1}.{options['format']}"
|
|
229
|
+
out_path = os.path.join(options['output_dir'], out_name)
|
|
230
|
+
try:
|
|
231
|
+
settings = self.project.all_plot_settings[idx]
|
|
232
|
+
datasets_for_axis = [
|
|
233
|
+
dataclasses.replace(ds, subplot_target=0)
|
|
234
|
+
for ds in self.project.datasets if ds.subplot_target == idx
|
|
235
|
+
]
|
|
236
|
+
temp_canvas = _HeadlessRenderCanvas(width=BATCH_EXPORT_FIGSIZE[0], height=BATCH_EXPORT_FIGSIZE[1], dpi=options['dpi'])
|
|
237
|
+
temp_canvas.dark_mode = self.canvas.dark_mode
|
|
238
|
+
temp_canvas.redraw_all(datasets_for_axis, 1, 1, [settings], full_resolution=options.get('full_resolution', False))
|
|
239
|
+
self._save_figure_with_options(temp_canvas.fig, out_path, options)
|
|
240
|
+
results.append((out_name, None))
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.exception("バッチエクスポート(サブプロット)に失敗しました: %s", out_name)
|
|
243
|
+
results.append((out_name, str(e)))
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
def _batch_export_project_files(self, paths, options, report_progress=None, is_cancelled=None):
|
|
247
|
+
"""プロジェクトファイルを1つずつ読み、その図を書き出す。開いているプロジェクトには触れない。別スレッドで呼ばれる。"""
|
|
248
|
+
results = []
|
|
249
|
+
total = len(paths)
|
|
250
|
+
for i, path in enumerate(paths):
|
|
251
|
+
if is_cancelled is not None and is_cancelled():
|
|
252
|
+
break
|
|
253
|
+
if report_progress is not None:
|
|
254
|
+
report_progress(i, total, os.path.basename(path))
|
|
255
|
+
base_name = os.path.splitext(os.path.basename(path))[0]
|
|
256
|
+
out_name = f"{options['prefix']}_{base_name}.{options['format']}"
|
|
257
|
+
out_path = os.path.join(options['output_dir'], out_name)
|
|
258
|
+
try:
|
|
259
|
+
temp_project = ProjectModel()
|
|
260
|
+
temp_project.load_project(path)
|
|
261
|
+
layout_mode = getattr(temp_project, 'layout_mode', 'grid')
|
|
262
|
+
if layout_mode == 'free':
|
|
263
|
+
# 自由配置には行×列が無いので、サブプロットの数に応じた大きさにする
|
|
264
|
+
if not temp_project.all_plot_settings:
|
|
265
|
+
raise ValueError("有効なプロット設定が見つかりません")
|
|
266
|
+
rows, cols = 0, 0
|
|
267
|
+
fig_width, fig_height = BATCH_EXPORT_FIGSIZE[0] * 2, BATCH_EXPORT_FIGSIZE[1] * 2
|
|
268
|
+
else:
|
|
269
|
+
rows, cols = temp_project.layout_rows, temp_project.layout_cols
|
|
270
|
+
if rows * cols == 0 or not temp_project.all_plot_settings:
|
|
271
|
+
raise ValueError("有効なプロット設定が見つかりません")
|
|
272
|
+
fig_width, fig_height = BATCH_EXPORT_FIGSIZE[0] * cols, BATCH_EXPORT_FIGSIZE[1] * rows
|
|
273
|
+
|
|
274
|
+
temp_canvas = _HeadlessRenderCanvas(width=fig_width, height=fig_height, dpi=options['dpi'])
|
|
275
|
+
temp_canvas.dark_mode = self.canvas.dark_mode
|
|
276
|
+
temp_canvas.redraw_all(
|
|
277
|
+
temp_project.datasets, rows, cols, temp_project.all_plot_settings, layout_mode=layout_mode,
|
|
278
|
+
panel_labels_enabled=temp_project.panel_labels_enabled,
|
|
279
|
+
full_resolution=options.get('full_resolution', False),
|
|
280
|
+
)
|
|
281
|
+
self._save_figure_with_options(temp_canvas.fig, out_path, options)
|
|
282
|
+
results.append((out_name, None))
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.exception("バッチエクスポート(プロジェクトファイル)に失敗しました: %s", path)
|
|
285
|
+
results.append((os.path.basename(path), str(e)))
|
|
286
|
+
return results
|
|
287
|
+
|
|
288
|
+
def _on_export_python_script(self):
|
|
289
|
+
"""matplotlib だけで図を再現するスクリプトを書き出す(生成は core/script_export.py)。"""
|
|
290
|
+
file_path, _ = notify.get_save_file_name(
|
|
291
|
+
self, "Pythonスクリプトとしてエクスポート", "", "Python Files (*.py)"
|
|
292
|
+
)
|
|
293
|
+
if not file_path:
|
|
294
|
+
return
|
|
295
|
+
if not file_path.endswith('.py'):
|
|
296
|
+
file_path += '.py'
|
|
297
|
+
|
|
298
|
+
# サブプロットの行数と列数は画面にしか無いことがある(保存の前)。古い値のままだと 2 つ目以降の軸が抜ける
|
|
299
|
+
self._sync_project_from_ui()
|
|
300
|
+
try:
|
|
301
|
+
script_text = generate_python_script(self.project)
|
|
302
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
303
|
+
f.write(script_text)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
notify.warning(self, "保存エラー", f"スクリプトの書き出し中にエラーが発生しました:\n{e}")
|
|
306
|
+
logger.exception("Pythonスクリプトの書き出し中にエラー")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
self.statusBar().showMessage(f"Pythonスクリプトを書き出しました: {file_path}", 3000)
|
|
310
|
+
|
|
311
|
+
def _on_generate_caption(self):
|
|
312
|
+
"""今の軸のタイトルを初期値にキャプションのダイアログを出す(生成は core/caption_export.py)。"""
|
|
313
|
+
settings = {}
|
|
314
|
+
if 0 <= self.project.active_axis_index < len(self.project.all_plot_settings):
|
|
315
|
+
settings = self.project.all_plot_settings[self.project.active_axis_index]
|
|
316
|
+
default_caption = axis_setting(settings, 'title') or ''
|
|
317
|
+
default_label = sanitize_label(default_caption)
|
|
318
|
+
|
|
319
|
+
dialog = CaptionGeneratorDialog(default_caption, default_label, self)
|
|
320
|
+
dialog.exec()
|
|
321
|
+
|
|
322
|
+
def _on_generate_report(self):
|
|
323
|
+
"""グラフの画像と、履歴のある全データセットの方法の文を1つのレポートにする。形式は拡張子(.html / .pdf)で決まる。"""
|
|
324
|
+
settings = {}
|
|
325
|
+
if 0 <= self.project.active_axis_index < len(self.project.all_plot_settings):
|
|
326
|
+
settings = self.project.all_plot_settings[self.project.active_axis_index]
|
|
327
|
+
title = axis_setting(settings, 'title') or "実験レポート"
|
|
328
|
+
|
|
329
|
+
file_path, _ = notify.get_save_file_name(
|
|
330
|
+
self, "実験レポートを生成", "", "HTML Files (*.html);;PDF Files (*.pdf)"
|
|
331
|
+
)
|
|
332
|
+
if not file_path:
|
|
333
|
+
return
|
|
334
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
|
335
|
+
if file_ext not in ('.html', '.pdf'):
|
|
336
|
+
file_ext = '.html'
|
|
337
|
+
file_path += file_ext
|
|
338
|
+
|
|
339
|
+
methods_sections = collect_methods_sections(self.project)
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
if file_ext == '.pdf':
|
|
343
|
+
self._write_pdf_report(file_path, title, methods_sections)
|
|
344
|
+
else:
|
|
345
|
+
buf = io.BytesIO()
|
|
346
|
+
self.canvas.fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
|
347
|
+
html_text = generate_html_report(buf.getvalue(), methods_sections, title=title)
|
|
348
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
349
|
+
f.write(html_text)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.exception("実験レポートの生成中にエラーが発生しました。")
|
|
352
|
+
notify.warning(self, "保存エラー", f"実験レポートの生成中にエラーが発生しました:\n{e}")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
self.statusBar().showMessage(f"実験レポートを書き出しました: {file_path}", 3000)
|
|
356
|
+
|
|
357
|
+
def _write_pdf_report(self, file_path, title, methods_sections):
|
|
358
|
+
"""1ページ目にグラフ、2ページ目にタイトルと方法の文。"""
|
|
359
|
+
from graphica.gui.mathtext_preview import JP_CAPABLE_FONT_FAMILIES
|
|
360
|
+
|
|
361
|
+
with mpl.rc_context(export_rc_params('pdf')), PdfPages(file_path) as pdf:
|
|
362
|
+
pdf.savefig(self.canvas.fig, bbox_inches='tight')
|
|
363
|
+
|
|
364
|
+
text_fig = Figure(figsize=(8.27, 11.69)) # A4 縦
|
|
365
|
+
text_fig.text(0.08, 0.95, title, fontsize=16, fontweight='bold', va='top',
|
|
366
|
+
family=JP_CAPABLE_FONT_FAMILIES)
|
|
367
|
+
body_lines = [f"生成日時: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}", "", "方法:"]
|
|
368
|
+
if methods_sections:
|
|
369
|
+
for name, text in methods_sections:
|
|
370
|
+
body_lines.append(f"・{name}: {text}")
|
|
371
|
+
else:
|
|
372
|
+
body_lines.append("(処理履歴を持つデータセットはありません)")
|
|
373
|
+
text_fig.text(0.08, 0.88, "\n".join(body_lines), fontsize=10, va='top', wrap=True,
|
|
374
|
+
family=JP_CAPABLE_FONT_FAMILIES)
|
|
375
|
+
pdf.savefig(text_fig)
|
|
376
|
+
|
|
377
|
+
def _on_export_plot(self):
|
|
378
|
+
dialog = ExportDialog(self)
|
|
379
|
+
|
|
380
|
+
dialog.preview_button.clicked.connect(
|
|
381
|
+
lambda: self._generate_preview(dialog)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
385
|
+
options = dialog.get_options()
|
|
386
|
+
|
|
387
|
+
filter_parts = ["PNG (*.png)", "PDF (*.pdf)", "SVG (*.svg)"]
|
|
388
|
+
for exp in get_registered_exporters():
|
|
389
|
+
filter_parts.append(f"{exp.format_name} (*{exp.extension})")
|
|
390
|
+
file_path, _ = notify.get_save_file_name(
|
|
391
|
+
self, "プロットを保存", "", ";;".join(filter_parts)
|
|
392
|
+
)
|
|
393
|
+
if not file_path:
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
# グラデーションの塗りはベクター形式でもラスタで埋め込まれる。書き出しは続け、知らせるだけ
|
|
397
|
+
export_ext = os.path.splitext(file_path)[1].lower()
|
|
398
|
+
if export_ext in ('.svg', '.pdf') and _project_has_raster_gradient_fill(self.project):
|
|
399
|
+
notify.warning(
|
|
400
|
+
self, "ベクター出力時の注意",
|
|
401
|
+
"グラデーション塗りが有効なデータセットが含まれています。\n"
|
|
402
|
+
"この部分は画像(ラスタ)として埋め込まれるため、拡大すると"
|
|
403
|
+
"他の要素のようにはくっきり表示されません。\n\n"
|
|
404
|
+
"エクスポートは続行します。"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
width_in, height_in = self._calculate_size_in_inches(options)
|
|
408
|
+
|
|
409
|
+
# フル解像度なら間引かずに描き直してから保存する(finally で画面用に戻す)
|
|
410
|
+
full_resolution = options.get('full_resolution', False)
|
|
411
|
+
if full_resolution:
|
|
412
|
+
self._update_plot(full_resolution=True)
|
|
413
|
+
|
|
414
|
+
original_size = self.canvas.fig.get_size_inches()
|
|
415
|
+
|
|
416
|
+
self.canvas.fig.set_size_inches(width_in, height_in)
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
|
420
|
+
|
|
421
|
+
api = get_plugin_api()
|
|
422
|
+
exporter = api.get_exporter_for_extension(file_ext) if api is not None else None
|
|
423
|
+
if exporter is not None:
|
|
424
|
+
try:
|
|
425
|
+
exporter.writer(self.canvas.fig, file_path)
|
|
426
|
+
except Exception as e:
|
|
427
|
+
raise PluginExecutionError(
|
|
428
|
+
exporter.name, f"「{file_path}」への書き出しに失敗しました: {e}"
|
|
429
|
+
) from e
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
save_kwargs = {'transparent': options.get('transparent', True)}
|
|
433
|
+
save_kwargs['bbox_inches'] = 'tight'
|
|
434
|
+
|
|
435
|
+
if file_ext in ['.pdf', '.svg']:
|
|
436
|
+
pass
|
|
437
|
+
else:
|
|
438
|
+
save_kwargs['dpi'] = options["dpi"]
|
|
439
|
+
|
|
440
|
+
with mpl.rc_context(export_rc_params(file_ext, options.get('svg_text_as_path', False))):
|
|
441
|
+
self.canvas.fig.savefig(file_path, **save_kwargs)
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.exception("エクスポートに失敗しました")
|
|
444
|
+
notify.warning(self, "保存エラー", f"エクスポート中にエラーが発生しました:\n{e}")
|
|
445
|
+
finally:
|
|
446
|
+
# 失敗しても画面の大きさに戻す
|
|
447
|
+
self.canvas.fig.set_size_inches(original_size)
|
|
448
|
+
if full_resolution:
|
|
449
|
+
self._update_plot(full_resolution=False)
|
|
450
|
+
self.canvas.draw_idle()
|
|
451
|
+
|
|
452
|
+
def _generate_preview(self, dialog):
|
|
453
|
+
"""今の軸だけを、書き出しの大きさの一時的な Figure に描いて見せる。"""
|
|
454
|
+
|
|
455
|
+
options = dialog.get_options()
|
|
456
|
+
|
|
457
|
+
width_in, height_in = self._calculate_size_in_inches(options)
|
|
458
|
+
|
|
459
|
+
temp_fig = Figure(figsize=(width_in, height_in), dpi=100)
|
|
460
|
+
temp_ax = temp_fig.add_subplot(111)
|
|
461
|
+
|
|
462
|
+
active_index = self.project.active_axis_index
|
|
463
|
+
if active_index >= len(self.project.all_plot_settings):
|
|
464
|
+
logger.warning("プレビュー生成時、アクティブな軸設定が見つかりません。")
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
active_settings = self.project.all_plot_settings[active_index]
|
|
468
|
+
|
|
469
|
+
# _draw_data は第2Y軸をこの一時的な Figure に作って all_secondary_axes に入れる。
|
|
470
|
+
# 失敗しても必ず戻さないと、画面の第2Y軸がプレビューの軸を指したままになる。
|
|
471
|
+
original_secondary = self.canvas.all_secondary_axes.copy()
|
|
472
|
+
try:
|
|
473
|
+
while len(self.canvas.all_secondary_axes) <= active_index:
|
|
474
|
+
self.canvas.all_secondary_axes.append(None)
|
|
475
|
+
|
|
476
|
+
self.canvas._draw_data(
|
|
477
|
+
temp_ax, active_index, self.project.datasets,
|
|
478
|
+
full_resolution=options.get('full_resolution', False),
|
|
479
|
+
)
|
|
480
|
+
self.canvas._apply_appearance(temp_ax, active_index, active_settings)
|
|
481
|
+
except Exception:
|
|
482
|
+
logger.exception("エクスポートのプレビューを描画できませんでした")
|
|
483
|
+
finally:
|
|
484
|
+
self.canvas.all_secondary_axes = original_secondary
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
fit_tight_layout(temp_fig)
|
|
488
|
+
except ValueError:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
buf = io.BytesIO()
|
|
492
|
+
temp_fig.savefig(buf, format='png', dpi=100)
|
|
493
|
+
buf.seek(0)
|
|
494
|
+
|
|
495
|
+
pixmap = QPixmap()
|
|
496
|
+
pixmap.loadFromData(buf.read())
|
|
497
|
+
|
|
498
|
+
dialog.preview_label.setPixmap(
|
|
499
|
+
pixmap.scaled(dialog.preview_label.size(),
|
|
500
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
501
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
buf.close()
|
|
505
|
+
del temp_fig
|
|
506
|
+
|
|
507
|
+
def _calculate_size_in_inches(self, options):
|
|
508
|
+
"""ダイアログの幅と高さをインチにする。"""
|
|
509
|
+
width, height, unit, dpi = options["width"], options["height"], options["unit"], options["dpi"]
|
|
510
|
+
|
|
511
|
+
if "インチ" in unit:
|
|
512
|
+
return width, height
|
|
513
|
+
elif "ミリメートル" in unit:
|
|
514
|
+
return width / 25.4, height / 25.4
|
|
515
|
+
elif "センチメートル" in unit:
|
|
516
|
+
return width / 2.54, height / 2.54
|
|
517
|
+
elif "ピクセル" in unit:
|
|
518
|
+
return width / dpi, height / dpi
|
|
519
|
+
|
|
520
|
+
return 8, 6
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""ヘルプメニュー(リファレンス、診断情報、アップデートの確認)。"""
|
|
2
|
+
import logging
|
|
3
|
+
import webbrowser
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from PySide6.QtWidgets import QMessageBox
|
|
7
|
+
|
|
8
|
+
from graphica.gui import notify
|
|
9
|
+
from graphica.gui.dialogs import HelpDialog, CalcHelpDialog, AboutDialog, ShortcutsDialog
|
|
10
|
+
from graphica.gui.task_runner import TaskRunner
|
|
11
|
+
from graphica.core.diagnostics import build_diagnostic_bundle
|
|
12
|
+
from graphica.core.update_check import check_for_update
|
|
13
|
+
from graphica.core.version import __version__
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HelpMixin:
|
|
19
|
+
def _on_show_about(self):
|
|
20
|
+
dialog = AboutDialog(self)
|
|
21
|
+
dialog.exec()
|
|
22
|
+
|
|
23
|
+
def _on_show_help(self):
|
|
24
|
+
"""プロットを操作しながら見られるよう、非モーダルで開く。"""
|
|
25
|
+
if getattr(self, 'help_dialog', None) is not None:
|
|
26
|
+
self.help_dialog.close()
|
|
27
|
+
# close() だけでは C++ のオブジェクトが残ってリークする
|
|
28
|
+
self.help_dialog.deleteLater()
|
|
29
|
+
self.help_dialog = HelpDialog(self)
|
|
30
|
+
self.help_dialog.show()
|
|
31
|
+
self.help_dialog.raise_()
|
|
32
|
+
self.help_dialog.activateWindow()
|
|
33
|
+
|
|
34
|
+
def _on_show_calc_help(self):
|
|
35
|
+
"""プロットを操作しながら見られるよう、非モーダルで開く。"""
|
|
36
|
+
if getattr(self, 'calc_help_dialog', None) is not None:
|
|
37
|
+
self.calc_help_dialog.close()
|
|
38
|
+
self.calc_help_dialog.deleteLater()
|
|
39
|
+
self.calc_help_dialog = CalcHelpDialog(self)
|
|
40
|
+
self.calc_help_dialog.show()
|
|
41
|
+
self.calc_help_dialog.raise_()
|
|
42
|
+
self.calc_help_dialog.activateWindow()
|
|
43
|
+
|
|
44
|
+
def _on_show_shortcuts(self):
|
|
45
|
+
dialog = ShortcutsDialog(self._collect_menu_actions, self)
|
|
46
|
+
dialog.exec()
|
|
47
|
+
|
|
48
|
+
def _on_export_diagnostic_bundle(self):
|
|
49
|
+
"""ログ・環境・設定・プラグインの状態を1つの zip にする(不具合の報告用)。"""
|
|
50
|
+
default_name = f"graphica_diagnostics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
|
51
|
+
file_path, _ = notify.get_save_file_name(
|
|
52
|
+
self, "診断情報をエクスポート", default_name, "Zip Files (*.zip)"
|
|
53
|
+
)
|
|
54
|
+
if not file_path:
|
|
55
|
+
return
|
|
56
|
+
if not file_path.lower().endswith('.zip'):
|
|
57
|
+
file_path += '.zip'
|
|
58
|
+
|
|
59
|
+
settings_dict = {key: self.settings.value(key) for key in self.settings.allKeys()}
|
|
60
|
+
try:
|
|
61
|
+
build_diagnostic_bundle(file_path, settings_dict=settings_dict)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.exception("診断情報のエクスポートに失敗しました。")
|
|
64
|
+
notify.warning(self, "エクスポートエラー", f"診断情報のエクスポート中にエラーが発生しました:\n{e}")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
notify.information(self, "エクスポート完了", f"診断情報を書き出しました:\n{file_path}")
|
|
68
|
+
|
|
69
|
+
# アップデートの確認は公開 API への匿名の GET 1回だけで、何も送らない。起動時は失敗しても黙り、手動では知らせる
|
|
70
|
+
|
|
71
|
+
def _start_startup_update_check(self):
|
|
72
|
+
"""新しい版が無いか失敗したら何も出さない。"""
|
|
73
|
+
if self._update_check_task_runner is not None:
|
|
74
|
+
return # 手動の確認が動いている
|
|
75
|
+
runner = TaskRunner(check_for_update, __version__, parent=self)
|
|
76
|
+
runner.succeeded.connect(self._on_startup_update_check_succeeded)
|
|
77
|
+
runner.failed.connect(self._on_startup_update_check_failed)
|
|
78
|
+
self._update_check_task_runner = runner
|
|
79
|
+
runner.start()
|
|
80
|
+
|
|
81
|
+
def _on_startup_update_check_succeeded(self, update_info):
|
|
82
|
+
self._update_check_task_runner = None
|
|
83
|
+
if update_info is not None:
|
|
84
|
+
self._show_update_available_dialog(update_info)
|
|
85
|
+
|
|
86
|
+
def _on_startup_update_check_failed(self, _message):
|
|
87
|
+
logger.info("起動時のアップデート確認に失敗しました(オフライン等の可能性): %s", _message)
|
|
88
|
+
self._update_check_task_runner = None
|
|
89
|
+
|
|
90
|
+
def _on_check_for_update(self):
|
|
91
|
+
"""「アップデートを確認...」メニューの処理。手動確認は失敗時もエラーを表示する。"""
|
|
92
|
+
if self._update_check_task_runner is not None:
|
|
93
|
+
notify.information(self, "アップデートの確認", "確認中です。完了までお待ちください。")
|
|
94
|
+
return
|
|
95
|
+
runner = TaskRunner(check_for_update, __version__, parent=self)
|
|
96
|
+
runner.succeeded.connect(self._on_manual_update_check_succeeded)
|
|
97
|
+
runner.failed.connect(self._on_manual_update_check_failed)
|
|
98
|
+
self._update_check_task_runner = runner
|
|
99
|
+
runner.start()
|
|
100
|
+
|
|
101
|
+
def _on_manual_update_check_succeeded(self, update_info):
|
|
102
|
+
self._update_check_task_runner = None
|
|
103
|
+
if update_info is not None:
|
|
104
|
+
self._show_update_available_dialog(update_info)
|
|
105
|
+
else:
|
|
106
|
+
notify.information(self, "アップデートの確認", f"お使いのバージョン({__version__})は最新です。")
|
|
107
|
+
|
|
108
|
+
def _on_manual_update_check_failed(self, message):
|
|
109
|
+
self._update_check_task_runner = None
|
|
110
|
+
logger.warning("アップデート確認に失敗しました: %s", message)
|
|
111
|
+
notify.warning(
|
|
112
|
+
self, "アップデートの確認",
|
|
113
|
+
f"アップデート情報の取得に失敗しました。ネットワーク接続を確認してください。\n\n{message}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _show_update_available_dialog(self, update_info):
|
|
117
|
+
"""新しいバージョンが見つかった場合の通知(起動時自動確認・手動確認で共通)。"""
|
|
118
|
+
tag = update_info.get('tag_name', '')
|
|
119
|
+
html_url = update_info.get('html_url', '')
|
|
120
|
+
reply = notify.information(
|
|
121
|
+
self, "新しいバージョンがあります",
|
|
122
|
+
f"新しいバージョン({tag})が公開されています。現在のバージョン: {__version__}\n\n"
|
|
123
|
+
"ダウンロードページを開きますか?",
|
|
124
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
125
|
+
)
|
|
126
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
127
|
+
webbrowser.open(html_url)
|