From 66ec6271c66c0e61b9a01cedf846b26208488a5a Mon Sep 17 00:00:00 2001 From: Steven Schlechte Date: Thu, 24 Jul 2025 14:58:16 +0200 Subject: [PATCH 1/3] added option to add complete directory and multiple files --- OTAnalytics/adapter_ui/dummy_viewmodel.py | 8 ++-- OTAnalytics/adapter_ui/ui_factory.py | 4 +- .../customtkinter_gui/ctk_ui_factory.py | 4 +- .../nicegui_gui/dialogs/file_picker.py | 4 +- .../plugin_ui/nicegui_gui/ui_factory.py | 46 +++++++++++++++---- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/OTAnalytics/adapter_ui/dummy_viewmodel.py b/OTAnalytics/adapter_ui/dummy_viewmodel.py index 0764b7e6c..00d83b41d 100644 --- a/OTAnalytics/adapter_ui/dummy_viewmodel.py +++ b/OTAnalytics/adapter_ui/dummy_viewmodel.py @@ -561,15 +561,15 @@ async def add_video(self) -> None: for ext in clean_extensions: extension_options[ext] = [ext] - track_files = await self._ui_factory.askopenfilenames( + video_files = await self._ui_factory.askopenfilenames( title="Load video files", filetypes=[("video file", SUPPORTED_VIDEO_FILE_TYPES)], extension_options=extension_options, ) - if not track_files: + if not video_files: return - logger().info(f"Video files to load: {track_files}") - paths = [Path(file) for file in track_files] + logger().info(f"Video files to load: {video_files}") + paths = [Path(file) for file in video_files] self._application.add_videos(files=paths) def remove_videos(self) -> None: diff --git a/OTAnalytics/adapter_ui/ui_factory.py b/OTAnalytics/adapter_ui/ui_factory.py index 7d4845f39..2f687699f 100644 --- a/OTAnalytics/adapter_ui/ui_factory.py +++ b/OTAnalytics/adapter_ui/ui_factory.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from datetime import datetime from pathlib import Path -from typing import Iterable, Literal +from typing import Iterable from OTAnalytics.adapter_ui.file_export_dto import ExportFileDto from OTAnalytics.adapter_ui.flow_dto import FlowDto @@ -45,7 +45,7 @@ async def askopenfilenames( title: str, filetypes: Iterable[tuple[str, str | list[str] | tuple[str, ...]]], extension_options: dict[str, list[str] | None] | None = None, - ) -> Literal[""] | tuple[str, ...]: + ) -> list[Path]: raise NotImplementedError @abstractmethod diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py b/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py index 9c4292b41..4a5ea142e 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py @@ -1,7 +1,7 @@ from datetime import datetime from pathlib import Path from tkinter.filedialog import askopenfilename, askopenfilenames -from typing import Iterable, Literal +from typing import Any, Iterable from OTAnalytics.adapter_ui.file_export_dto import ExportFileDto from OTAnalytics.adapter_ui.flow_dto import FlowDto @@ -57,7 +57,7 @@ async def askopenfilenames( title: str, filetypes: Iterable[tuple[str, str | list[str] | tuple[str, ...]]], extension_options: dict[str, list[str] | None] | None = None, - ) -> Literal[""] | tuple[str, ...]: + ) -> Any: return askopenfilenames(title=title, filetypes=filetypes) async def askopenfilename( diff --git a/OTAnalytics/plugin_ui/nicegui_gui/dialogs/file_picker.py b/OTAnalytics/plugin_ui/nicegui_gui/dialogs/file_picker.py index 1f81370ea..8467be6d6 100644 --- a/OTAnalytics/plugin_ui/nicegui_gui/dialogs/file_picker.py +++ b/OTAnalytics/plugin_ui/nicegui_gui/dialogs/file_picker.py @@ -52,7 +52,7 @@ def __init__( extensions or None for "All Files". If None, uses default options. """ super().__init__() - + self.multiple = multiple self.path: Path = directory.expanduser() self.upper_limit: Path = directory.expanduser() self.show_hidden_files: bool = show_hidden_files @@ -241,7 +241,7 @@ async def _handle_ok(self) -> None: paths: List[Path] = [self._map_to_domain(r[PATH]) for r in rows] # If no rows are selected and we're in directory-only mode, use the current directory # noqa - if not paths and self.show_only_directories: + if not paths and (self.show_only_directories or self.multiple): paths = [self.path] self.submit(paths) diff --git a/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py b/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py index 68f396e96..17b4685b6 100644 --- a/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py +++ b/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py @@ -1,7 +1,7 @@ import asyncio from datetime import datetime from pathlib import Path -from typing import Any, Iterable, Literal +from typing import Any, Iterable from OTAnalytics.adapter_ui.file_export_dto import ExportFileDto from OTAnalytics.adapter_ui.flow_dto import FlowDto @@ -14,6 +14,7 @@ CountingSpecificationDto, ) from OTAnalytics.application.application import CancelAddFlow +from OTAnalytics.application.logger import logger from OTAnalytics.application.resources.resource_manager import ResourceManager from OTAnalytics.application.use_cases.generate_flows import FlowNameGenerator from OTAnalytics.domain.geometry import RelativeOffsetCoordinate @@ -27,6 +28,7 @@ from OTAnalytics.plugin_ui.nicegui_gui.dialogs.file_chooser_dialog import ( FileChooserDialog, ) +from OTAnalytics.plugin_ui.nicegui_gui.dialogs.file_picker import LocalFilePicker from OTAnalytics.plugin_ui.nicegui_gui.nicegui.elements.dialog import DialogResult @@ -88,24 +90,50 @@ async def askopenfilenames( title: str, filetypes: Iterable[tuple[str, str | list[str] | tuple[str, ...]]], extension_options: dict[str, list[str] | None] | None = None, - ) -> Literal[""] | tuple[str, ...]: + ) -> list[Path]: # For now, we'll just support selecting a single file # In a real implementation, this would allow selecting multiple files # Convert list/tuple of extensions to individual (desc, ext) pairs converted_filetypes = [] for desc, ext in filetypes: if isinstance(ext, str): - converted_filetypes.append((desc, ext)) + converted_filetypes.append(ext) elif isinstance(ext, (list, tuple)) and ext: # Use the first extension from the list/tuple - converted_filetypes.append((desc, ext[0])) + converted_filetypes.append(ext[0]) - file_path = await self.askopenfilename( - title, converted_filetypes, "", extension_options + file_paths = await LocalFilePicker( + directory=Path.home(), + show_hidden_files=False, + show_files_only_of_type=None, # Show all files + show_only_directories=False, + extension_options=extension_options, + multiple=True, ) - if file_path: - return (file_path,) - return "" + return self.get_all_files_with_correct_file_ending_in_directory( + file_paths, converted_filetypes + ) + + def get_all_files_with_correct_file_ending_in_directory( + self, files: Iterable[Path], file_types: list[str] + ) -> list[Path]: + files_to_save: set[Path] = set() + for file_type in file_types: + for file in files: + file_to_save = file.expanduser() + if file_to_save.is_dir(): + files_in_directory = file_to_save.rglob(file_type) + files_to_save.update(files_in_directory) + continue + + if not file_to_save.exists() or file_to_save.suffix != file_type: + logger().warning( + f"Ottrk file'{file_to_save}' does not exist. Skipping file." + ) + continue + + files_to_save.add(file_to_save) + return list(files_to_save) async def ask_for_save_file_path( self, From e3e401ce980965a465c02837ae8252beb2cdaf0e Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Mon, 22 Sep 2025 14:06:55 +0200 Subject: [PATCH 2/3] Extract utility method into helper package --- OTAnalytics/helpers/file_helpers.py | 26 ++++++++++++++++++ .../plugin_ui/nicegui_gui/ui_factory.py | 27 +++---------------- 2 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 OTAnalytics/helpers/file_helpers.py diff --git a/OTAnalytics/helpers/file_helpers.py b/OTAnalytics/helpers/file_helpers.py new file mode 100644 index 000000000..1b216121b --- /dev/null +++ b/OTAnalytics/helpers/file_helpers.py @@ -0,0 +1,26 @@ +from pathlib import Path +from typing import Iterable + +from OTAnalytics.application.logger import logger + + +def get_all_files_with_correct_file_ending_in_directory( + files: Iterable[Path], file_types: list[str] +) -> list[Path]: + files_to_save: set[Path] = set() + for file_type in file_types: + for file in files: + file_to_save = file.expanduser() + if file_to_save.is_dir(): + files_in_directory = file_to_save.rglob(file_type) + files_to_save.update(files_in_directory) + continue + + if not file_to_save.exists() or file_to_save.suffix != file_type: + logger().warning( + f"Ottrk file'{file_to_save}' does not exist. Skipping file." + ) + continue + + files_to_save.add(file_to_save) + return list(files_to_save) diff --git a/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py b/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py index 17b4685b6..167c4825c 100644 --- a/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py +++ b/OTAnalytics/plugin_ui/nicegui_gui/ui_factory.py @@ -14,10 +14,12 @@ CountingSpecificationDto, ) from OTAnalytics.application.application import CancelAddFlow -from OTAnalytics.application.logger import logger from OTAnalytics.application.resources.resource_manager import ResourceManager from OTAnalytics.application.use_cases.generate_flows import FlowNameGenerator from OTAnalytics.domain.geometry import RelativeOffsetCoordinate +from OTAnalytics.helpers.file_helpers import ( + get_all_files_with_correct_file_ending_in_directory, +) from OTAnalytics.plugin_ui.nicegui_gui.dialogs.edit_flow_dialog import EditFlowDialog from OTAnalytics.plugin_ui.nicegui_gui.dialogs.edit_section_dialog import ( EditSectionDialog, @@ -110,31 +112,10 @@ async def askopenfilenames( extension_options=extension_options, multiple=True, ) - return self.get_all_files_with_correct_file_ending_in_directory( + return get_all_files_with_correct_file_ending_in_directory( file_paths, converted_filetypes ) - def get_all_files_with_correct_file_ending_in_directory( - self, files: Iterable[Path], file_types: list[str] - ) -> list[Path]: - files_to_save: set[Path] = set() - for file_type in file_types: - for file in files: - file_to_save = file.expanduser() - if file_to_save.is_dir(): - files_in_directory = file_to_save.rglob(file_type) - files_to_save.update(files_in_directory) - continue - - if not file_to_save.exists() or file_to_save.suffix != file_type: - logger().warning( - f"Ottrk file'{file_to_save}' does not exist. Skipping file." - ) - continue - - files_to_save.add(file_to_save) - return list(files_to_save) - async def ask_for_save_file_path( self, title: str, From 711a99d461327620b63ebfdfd15d0e347ac68913 Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Mon, 22 Sep 2025 14:23:22 +0200 Subject: [PATCH 3/3] Move type conversion into plugin --- OTAnalytics/adapter_ui/dummy_viewmodel.py | 3 +-- OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OTAnalytics/adapter_ui/dummy_viewmodel.py b/OTAnalytics/adapter_ui/dummy_viewmodel.py index 53609dc87..15b567273 100644 --- a/OTAnalytics/adapter_ui/dummy_viewmodel.py +++ b/OTAnalytics/adapter_ui/dummy_viewmodel.py @@ -571,8 +571,7 @@ async def add_video(self) -> None: if not video_files: return logger().info(f"Video files to load: {video_files}") - paths = [Path(file) for file in video_files] - self._application.add_videos(files=paths) + self._application.add_videos(files=video_files) def remove_videos(self) -> None: self._application.remove_videos() diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py b/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py index 4a5ea142e..dde44ca94 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/ctk_ui_factory.py @@ -1,7 +1,7 @@ from datetime import datetime from pathlib import Path from tkinter.filedialog import askopenfilename, askopenfilenames -from typing import Any, Iterable +from typing import Iterable from OTAnalytics.adapter_ui.file_export_dto import ExportFileDto from OTAnalytics.adapter_ui.flow_dto import FlowDto @@ -57,8 +57,9 @@ async def askopenfilenames( title: str, filetypes: Iterable[tuple[str, str | list[str] | tuple[str, ...]]], extension_options: dict[str, list[str] | None] | None = None, - ) -> Any: - return askopenfilenames(title=title, filetypes=filetypes) + ) -> list[Path]: + video_files = askopenfilenames(title=title, filetypes=filetypes) + return [Path(file) for file in video_files] async def askopenfilename( self,