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
« 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).
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.
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
13from PySide6.QtCore import QEvent, QObject, QTimer
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)
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 ]
29class ProjectFileOpener(QObject):
30 """Application-wide filter that turns file-open requests into opens."""
32 def __init__(self, app, paths=()):
33 super().__init__(app)
34 self._window = None
35 self._pending = list(paths)
36 app.installEventFilter(self)
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
46 def attach(self, window):
47 """Start opening into ``window``; queued files open once it is idle.
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)
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)
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)
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)
85 @staticmethod
86 def _is_clip(path):
87 return Path(path).suffix.lower() == CLIP_SUFFIX