Coverage for paintmaskanimator/__main__.py: 0%

78 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1"""Application entry point.""" 

2from .dependency_check import require_runtime_dependencies 

3 

4# Must run before anything pulls in Qt, so a missing PySide6/numpy produces a 

5# readable dialog instead of a traceback that vanishes with the console. 

6require_runtime_dependencies() 

7 

8import sys # noqa: E402 

9from pathlib import Path # noqa: E402 

10 

11from PySide6.QtCore import QTimer # noqa: E402 

12from PySide6.QtGui import QIcon # noqa: E402 

13from PySide6.QtWidgets import QApplication, QMessageBox # noqa: E402 

14 

15from . import i18n # noqa: E402 

16from .i18n import tr # noqa: E402 

17from .constants import APP_DISPLAY_NAME, APP_VERSION # noqa: E402 

18from .main_window import MainWindow # noqa: E402 

19 

20 

21def _show_unhandled_exception(exc_type, exc_value, exc_tb): 

22 import traceback 

23 details = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) 

24 # Prefer the per-user config dir (always writable, unlike an installed 

25 # Program Files app dir); fall back to next to the executable. 

26 log_path = None 

27 try: 

28 from . import config 

29 import time 

30 log_path = config.config_dir() / "crash.log" 

31 with open(log_path, "a", encoding="utf-8") as handle: 

32 handle.write(f"\n===== {time.strftime('%Y-%m-%d %H:%M:%S')} =====\n") 

33 handle.write(details) 

34 except Exception: 

35 try: 

36 app_file = Path(sys.executable) if getattr(sys, "frozen", False) else Path(__file__) 

37 log_path = app_file.with_name("oekaki_animator_crash.log") 

38 log_path.write_text(details, encoding="utf-8") 

39 except Exception: 

40 log_path = None 

41 text = tr("起動中または実行中にエラーが発生しました。") 

42 if log_path: 

43 text += tr("\n\n詳細を保存しました:\n{path}").format(path=log_path) 

44 text += f"\n\n{exc_value}" 

45 app = QApplication.instance() 

46 # Never open another modal error dialog while Qt is shutting down. Doing so 

47 # can create an endless loop of errors from already-destroyed widgets. 

48 if app is not None and not app.closingDown(): 

49 try: 

50 QMessageBox.critical(None, tr("{NAME} エラー").format(NAME=APP_DISPLAY_NAME), text) 

51 except RuntimeError: 

52 print(details) 

53 else: 

54 print(details) 

55 

56 

57def _apply_startup_preferences(): 

58 """Apply the Preferences that must be in place before the window exists.""" 

59 from . import constants, preferences 

60 from .preferences_dialog import apply_undo_preferences 

61 constants.CANVAS_WIDTH, constants.CANVAS_HEIGHT = preferences.new_canvas_size() 

62 apply_undo_preferences() 

63 

64 

65def main(): 

66 from .logging_setup import configure_logging 

67 configure_logging() 

68 sys.excepthook = _show_unhandled_exception 

69 app = QApplication(sys.argv) 

70 # Before anything else runs the event loop: macOS sends the double-clicked 

71 # file as an event right after launch. 

72 from .file_open import ProjectFileOpener, openable_paths_from_argv 

73 file_opener = ProjectFileOpener(app, openable_paths_from_argv(sys.argv)) 

74 app.setApplicationName(APP_DISPLAY_NAME) 

75 app.setApplicationVersion(APP_VERSION) 

76 # ウィンドウ・Dock・タスクバーのアイコン。配布版は実行ファイル側にも 

77 # 同じアイコンを埋め込む(scripts/make_app_icons.py)。 

78 icon_path = Path(__file__).with_name("assets") / "app_icon.png" 

79 if icon_path.exists(): 

80 app.setWindowIcon(QIcon(str(icon_path))) 

81 # Before any widget is built: Qt resolves tr() at call time, but menus and 

82 # dialogs constructed earlier would keep the source-language strings. 

83 i18n.install_preferred(app) 

84 from . import theme, tooltip 

85 theme.apply_theme(app) 

86 tooltip.install(app) 

87 _apply_startup_preferences() 

88 window = MainWindow() 

89 window.show() 

90 file_opener.attach(window) 

91 QTimer.singleShot(0, window.fit_canvas) 

92 # 新しい版があればメニューバーで知らせる(起動の少しあとに裏で確かめる)。 

93 window.update_notice.start() 

94 return app.exec() 

95 

96 

97if __name__ == '__main__': 

98 try: 

99 raise SystemExit(main()) 

100 except SystemExit: 

101 raise 

102 except Exception: 

103 exc_type, exc_value, exc_tb = sys.exc_info() 

104 if exc_type is not None and exc_value is not None: 

105 sys.excepthook(exc_type, exc_value, exc_tb) 

106 raise SystemExit(1) from None