drag+drop support

This commit is contained in:
2026-09-10 11:18:00 -07:00
parent cd0b55df34
commit 613d2b9103
4 changed files with 162 additions and 15 deletions
+105 -7
View File
@@ -108,21 +108,116 @@ class ProjectManager:
# =========================================================================
# File & Folder Opening Dialogs
# =========================================================================
def is_valid_snirf(self, path: str) -> bool:
"""Fast header check to verify HDF5/SNIRF signature and ignore corrupt/shadow files."""
try:
if os.path.getsize(path) < 8:
return False
with open(path, "rb") as f:
return f.read(8) == b"\x89HDF\r\n\x1a\n"
except Exception:
return False
def open_file_dialog(self) -> None:
"""Opens dialog to pick a single .snirf file."""
file_path, _ = QFileDialog.getOpenFileName(
self.app, "Open File", "", "SNIRF Files (*.snirf);;All Files (*)"
)
if file_path:
self._load_files_into_pipeline([os.path.normpath(file_path)])
if not file_path:
return
norm_path = os.path.normpath(file_path)
filename = os.path.basename(norm_path)
# 1. Check specifically for macOS shadow files
if filename.startswith("._"):
QMessageBox.warning(
self.app,
"Invalid SNIRF File",
f"'{filename}' is a macOS system shadow file (Apple Double resource fork), not a actual data file."
)
return
# 2. Check for general header validity / corruption
if not self.is_valid_snirf(norm_path):
QMessageBox.warning(
self.app,
"Invalid SNIRF File",
f"'{filename}' is not a valid SNIRF file or has a corrupted header."
)
return
self._load_files_into_pipeline([norm_path])
def open_folder_dialog(self)-> None:
"""Recursively finds all .snirf files in a selected directory."""
folder_path = QFileDialog.getExistingDirectory(self.app, "Select Folder", "")
if folder_path:
snirf_files = [os.path.normpath(str(f)) for f in Path(folder_path).rglob("*.snirf")]
self._load_files_into_pipeline(snirf_files)
if not folder_path:
return
# Automatically filter out shadow files and invalid headers in batch mode
snirf_files = [
os.path.normpath(str(f))
for f in Path(folder_path).rglob("*.snirf")
if self.is_valid_snirf(str(f))
]
if not snirf_files:
QMessageBox.information(
self.app,
"No Valid Files",
"No valid .snirf files were found in the selected directory."
)
return
def load_dropped_files(self, file_paths: List[str]) -> None:
"""Public entry point for handling files or folders dropped onto the UI."""
if not file_paths:
return
# 1. Single file drop: Give explicit feedback like open_file_dialog
if len(file_paths) == 1:
norm_path = os.path.normpath(file_paths[0])
filename = os.path.basename(norm_path)
if filename.startswith("._"):
QMessageBox.warning(
self.app,
"Invalid SNIRF File",
f"'{filename}' is a macOS system shadow file (Apple Double resource fork), not an actual data file."
)
return
if not self.is_valid_snirf(norm_path):
QMessageBox.warning(
self.app,
"Invalid SNIRF File",
f"'{filename}' is not a valid SNIRF file or has a corrupted header."
)
return
valid_files = [norm_path]
# 2. Batch drop: Silently filter out shadow/corrupt files like open_folder_dialog
else:
valid_files = [
os.path.normpath(p)
for p in file_paths
if self.is_valid_snirf(os.path.normpath(p))
]
if not valid_files:
QMessageBox.information(
self.app,
"No Valid Files",
"None of the dropped items were valid .snirf files."
)
return
# Delegate to internal pipeline
self._load_files_into_pipeline(valid_files)
def _load_files_into_pipeline(self, file_paths: List[str]) -> None:
"""Loads .snirf files into UI using chunked batches and background workers."""
app = self.app
@@ -146,8 +241,11 @@ class ProjectManager:
if not hasattr(app, "metadata_cache"):
app.metadata_cache = {}
# Filter out files already in the UI to avoid duplicates
new_files = [p for p in file_paths if p not in app.selected_paths]
# Filter out duplicates AND non-SNIRF files (including macOS ._ shadow files)
new_files = [
p for p in file_paths
if p not in app.selected_paths and self.is_valid_snirf(p)
]
if not new_files:
return