Coverage for graphica/gui/mixins/help_mixin.py: 92.0%
142 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""ヘルプメニュー(リファレンス、診断情報、アップデートの確認)。"""
2import logging
3import os
4import webbrowser
5from datetime import datetime
7from PySide6.QtCore import Qt
8from PySide6.QtWidgets import QDialog, QProgressDialog
10from graphica.core.i18n import tr
11from graphica.gui import dialog_dirs, notify, updater
12from graphica.gui.dialogs import HelpDialog, CalcHelpDialog, AboutDialog, ShortcutsDialog, UpdateDialog
13from graphica.gui.task_runner import TaskRunner
14from graphica.core.diagnostics import build_diagnostic_bundle
15from graphica.core.update_check import check_for_update, pick_asset
16from graphica.core.version import __version__
18logger = logging.getLogger(__name__)
21class HelpMixin:
22 def _on_show_about(self):
23 dialog = AboutDialog(self)
24 dialog.exec()
26 def _on_show_help(self):
27 """プロットを操作しながら見られるよう、非モーダルで開く。"""
28 if getattr(self, 'help_dialog', None) is not None:
29 self.help_dialog.close()
30 # close() だけでは C++ のオブジェクトが残ってリークする
31 self.help_dialog.deleteLater()
32 self.help_dialog = HelpDialog(self)
33 self.help_dialog.show()
34 self.help_dialog.raise_()
35 self.help_dialog.activateWindow()
37 def _on_show_calc_help(self):
38 """プロットを操作しながら見られるよう、非モーダルで開く。"""
39 if getattr(self, 'calc_help_dialog', None) is not None:
40 self.calc_help_dialog.close()
41 self.calc_help_dialog.deleteLater()
42 self.calc_help_dialog = CalcHelpDialog(self)
43 self.calc_help_dialog.show()
44 self.calc_help_dialog.raise_()
45 self.calc_help_dialog.activateWindow()
47 def _on_show_shortcuts(self):
48 dialog = ShortcutsDialog(self._collect_menu_actions, self)
49 dialog.exec()
51 def _on_export_diagnostic_bundle(self):
52 """ログ・環境・設定・プラグインの状態を1つの zip にする(不具合の報告用)。"""
53 default_name = f"graphica_diagnostics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
54 file_path, _ = dialog_dirs.get_save_file_name(
55 self, dialog_dirs.OTHER, "診断情報をエクスポート", default_name, "Zip Files (*.zip)"
56 )
57 if not file_path:
58 return
59 if not file_path.lower().endswith('.zip'):
60 file_path += '.zip'
62 settings_dict = {key: self.settings.value(key) for key in self.settings.allKeys()}
63 try:
64 build_diagnostic_bundle(file_path, settings_dict=settings_dict)
65 except Exception as e:
66 logger.exception("診断情報のエクスポートに失敗しました。")
67 notify.warning(self, "エクスポートエラー", f"診断情報のエクスポート中にエラーが発生しました:\n{e}")
68 return
70 notify.information(self, "エクスポート完了", f"診断情報を書き出しました:\n{file_path}")
72 # アップデートの確認は公開 API への匿名の GET 1回だけで、何も送らない。起動時は失敗しても黙り、手動では知らせる
74 def _start_startup_update_check(self):
75 """新しい版が無いか失敗したら何も出さない。"""
76 if self._update_check_task_runner is not None: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 return # 手動の確認が動いている
78 runner = TaskRunner(check_for_update, __version__, parent=self)
79 runner.succeeded.connect(self._on_startup_update_check_succeeded)
80 runner.failed.connect(self._on_startup_update_check_failed)
81 self._update_check_task_runner = runner
82 runner.start()
84 def _on_startup_update_check_succeeded(self, update_info):
85 self._update_check_task_runner = None
86 if update_info is not None:
87 self._show_update_available_dialog(update_info)
89 def _on_startup_update_check_failed(self, _message):
90 logger.info("起動時のアップデート確認に失敗しました(オフライン等の可能性): %s", _message)
91 self._update_check_task_runner = None
93 def _on_check_plugin_updates(self):
94 """「プラグインの更新を確認...」(ヘルプメニュー・環境設定のプラグインのタブ)。"""
95 from graphica.gui import plugin_update_flow
96 plugin_update_flow.check_now(self)
98 def _on_check_for_update(self):
99 """「アップデートを確認...」メニューの処理。手動確認は失敗時もエラーを表示する。"""
100 if self._update_check_task_runner is not None:
101 notify.information(self, "アップデートの確認", "確認中です。完了までお待ちください。")
102 return
103 runner = TaskRunner(check_for_update, __version__, parent=self)
104 runner.succeeded.connect(self._on_manual_update_check_succeeded)
105 runner.failed.connect(self._on_manual_update_check_failed)
106 self._update_check_task_runner = runner
107 runner.start()
109 def _on_manual_update_check_succeeded(self, update_info):
110 self._update_check_task_runner = None
111 if update_info is not None:
112 self._show_update_available_dialog(update_info)
113 else:
114 notify.information(self, "アップデートの確認", f"お使いのバージョン({__version__})は最新です。")
116 def _on_manual_update_check_failed(self, message):
117 self._update_check_task_runner = None
118 logger.warning("アップデート確認に失敗しました: %s", message)
119 notify.warning(
120 self, "アップデートの確認",
121 f"アップデート情報の取得に失敗しました。ネットワーク接続を確認してください。\n\n{message}"
122 )
124 def _show_update_available_dialog(self, update_info):
125 """新しいバージョンが見つかった場合の通知(起動時自動確認・手動確認で共通)。配布の形に合った更新の方法を出す。"""
126 kind = updater.install_kind()
127 asset = pick_asset(update_info, kind)
128 dialog = UpdateDialog(update_info, __version__, kind, asset is not None, self)
129 if dialog.exec() != QDialog.DialogCode.Accepted:
130 return
131 if dialog.choice == 'install' and asset is not None:
132 self._start_update_download(asset)
133 elif dialog.choice == 'download' and asset is not None:
134 webbrowser.open(asset['url'])
135 elif dialog.choice == 'page': 135 ↛ exitline 135 didn't return from function '_show_update_available_dialog' because the condition on line 135 was always true
136 webbrowser.open(update_info.get('html_url', ''))
138 # --- アプリの中での更新(Windows のインストーラー版) ---
140 def _start_update_download(self, asset):
141 if self._update_download_runner is not None: 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true
142 notify.information(self, tr("アップデート"), tr("ダウンロード中です。完了までお待ちください。"))
143 return
144 dest_path = os.path.join(updater.update_download_dir(), asset['name'])
145 progress = QProgressDialog(tr("新しいバージョンをダウンロードしています..."), tr("キャンセル"),
146 0, max(asset.get('size', 0) // 1024, 1), self)
147 progress.setWindowTitle(tr("アップデート"))
148 progress.setWindowModality(Qt.WindowModality.WindowModal)
149 progress.setMinimumDuration(0)
150 progress.setValue(0)
151 runner = TaskRunner(updater.download_file, asset['url'], dest_path,
152 asset.get('size', 0), asset.get('sha256', ''), parent=self)
153 runner.progress.connect(lambda done, total, _message: self._on_update_download_progress(progress, done, total))
154 progress.canceled.connect(runner.requestInterruption)
155 runner.succeeded.connect(lambda path: self._on_update_downloaded(path, progress))
156 runner.failed.connect(lambda message: self._on_update_download_failed(message, progress))
157 self._update_download_runner = runner
158 runner.start()
160 @staticmethod
161 def _on_update_download_progress(progress, done_kb, total_kb):
162 if total_kb > 0 and progress.maximum() != total_kb:
163 progress.setMaximum(total_kb)
164 progress.setValue(min(done_kb, progress.maximum()))
166 def _on_update_downloaded(self, installer_path, progress):
167 self._update_download_runner = None
168 progress.close()
169 if installer_path is None:
170 return # キャンセル
171 self._install_downloaded_update(installer_path)
173 def _on_update_download_failed(self, message, progress):
174 self._update_download_runner = None
175 progress.close()
176 logger.warning("アップデートのダウンロードに失敗しました: %s", message)
177 notify.warning(self, tr("アップデート"),
178 tr("新しいバージョンをダウンロードできませんでした。\n\n{message}").format(message=message))
180 def _install_downloaded_update(self, installer_path):
181 """Graphica を閉じてから(未保存の確認もここ)インストーラーを起動する。閉じるのをやめたら更新もしない。
183 先にインストーラーを起動すると、閉じるのをやめたときにインストーラーが Graphica を強制的に閉じてしまう。
184 """
185 if not self.window().close():
186 notify.information(
187 self, tr("アップデート"),
188 tr("Graphica を閉じなかったので、更新を取りやめました。ダウンロードしたインストーラーは次の場所にあります:\n{path}")
189 .format(path=installer_path))
190 return
191 try:
192 updater.launch_installer(installer_path)
193 except OSError as e:
194 logger.exception("インストーラーを起動できませんでした")
195 notify.critical(None, tr("アップデート"),
196 tr("インストーラーを起動できませんでした。次のファイルを手で実行してください:\n{path}\n\n{error}")
197 .format(path=installer_path, error=e))