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

1"""ヘルプメニュー(リファレンス、診断情報、アップデートの確認)。""" 

2import logging 

3import os 

4import webbrowser 

5from datetime import datetime 

6 

7from PySide6.QtCore import Qt 

8from PySide6.QtWidgets import QDialog, QProgressDialog 

9 

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__ 

17 

18logger = logging.getLogger(__name__) 

19 

20 

21class HelpMixin: 

22 def _on_show_about(self): 

23 dialog = AboutDialog(self) 

24 dialog.exec() 

25 

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() 

36 

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() 

46 

47 def _on_show_shortcuts(self): 

48 dialog = ShortcutsDialog(self._collect_menu_actions, self) 

49 dialog.exec() 

50 

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' 

61 

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 

69 

70 notify.information(self, "エクスポート完了", f"診断情報を書き出しました:\n{file_path}") 

71 

72 # アップデートの確認は公開 API への匿名の GET 1回だけで、何も送らない。起動時は失敗しても黙り、手動では知らせる 

73 

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() 

83 

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) 

88 

89 def _on_startup_update_check_failed(self, _message): 

90 logger.info("起動時のアップデート確認に失敗しました(オフライン等の可能性): %s", _message) 

91 self._update_check_task_runner = None 

92 

93 def _on_check_plugin_updates(self): 

94 """「プラグインの更新を確認...」(ヘルプメニュー・環境設定のプラグインのタブ)。""" 

95 from graphica.gui import plugin_update_flow 

96 plugin_update_flow.check_now(self) 

97 

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() 

108 

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__})は最新です。") 

115 

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 ) 

123 

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', '')) 

137 

138 # --- アプリの中での更新(Windows のインストーラー版) --- 

139 

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() 

159 

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())) 

165 

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) 

172 

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)) 

179 

180 def _install_downloaded_update(self, installer_path): 

181 """Graphica を閉じてから(未保存の確認もここ)インストーラーを起動する。閉じるのをやめたら更新もしない。 

182 

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))