Coverage for paintmaskanimator/file_open.py: 100%

46 statements  

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

1"""Open files handed to the app by the OS (double-click in Finder or Explorer, 

2or a file dropped on the app icon). 

3 

4``.pman`` projects open as the document; ``.clip`` files are imported as a 

5CLIP STUDIO animation, the same way an in-window drop does it. 

6 

7Windows passes the file on the command line; macOS sends a ``QFileOpenEvent`` 

8instead, both at launch and while the app is already running. Events can 

9arrive before the main window exists, so they are queued until ``attach``. 

10""" 

11from pathlib import Path 

12 

13from PySide6.QtCore import QEvent, QObject, QTimer 

14 

15PROJECT_SUFFIX = ".pman" 

16CLIP_SUFFIX = ".clip" 

17#: What the OS may hand us. Order is irrelevant; only the last file opens. 

18OPENABLE_SUFFIXES = (PROJECT_SUFFIX, CLIP_SUFFIX) 

19 

20 

21def openable_paths_from_argv(argv): 

22 """The openable files named on the command line (``argv[0]`` skipped).""" 

23 return [ 

24 arg for arg in argv[1:] 

25 if Path(arg).suffix.lower() in OPENABLE_SUFFIXES and Path(arg).is_file() 

26 ] 

27 

28 

29class ProjectFileOpener(QObject): 

30 """Application-wide filter that turns file-open requests into opens.""" 

31 

32 def __init__(self, app, paths=()): 

33 super().__init__(app) 

34 self._window = None 

35 self._pending = list(paths) 

36 app.installEventFilter(self) 

37 

38 def eventFilter(self, watched, event): 

39 if event.type() == QEvent.Type.FileOpen: 

40 path = event.file() 

41 if Path(path).suffix.lower() in OPENABLE_SUFFIXES: 

42 self._request(path) 

43 return True 

44 return False 

45 

46 def attach(self, window): 

47 """Start opening into ``window``; queued files open once it is idle. 

48 

49 Scheduled after the window's own startup work (the autosave restore 

50 prompt), so a launch file never silently replaces restored work. 

51 """ 

52 self._window = window 

53 QTimer.singleShot(0, self._open_pending_at_launch) 

54 

55 def _request(self, path): 

56 window = self._window 

57 if window is None: 

58 self._pending.append(path) 

59 else: 

60 # Already running: the canvas may hold unsaved work. 

61 self._open_asking(window, path) 

62 

63 def _open_pending_at_launch(self): 

64 pending, self._pending = self._pending, [] 

65 window = self._window 

66 if not pending or window is None: 

67 return 

68 # Only the last one: the window holds a single project. 

69 path = pending[-1] 

70 if self._is_clip(path): 

71 # The importer asks by itself, and stays quiet on a pristine canvas. 

72 window.importer.clip_animation(path) 

73 elif window.current_project_path is None: 

74 window.project.open(path) 

75 else: 

76 # An autosave was restored; ask before replacing it. 

77 window.project.open_dropped(path) 

78 

79 def _open_asking(self, window, path): 

80 if self._is_clip(path): 

81 window.importer.clip_animation(path) 

82 else: 

83 window.project.open_dropped(path) 

84 

85 @staticmethod 

86 def _is_clip(path): 

87 return Path(path).suffix.lower() == CLIP_SUFFIX