diff --git a/build/scripts/build_windows.py b/build/scripts/build_windows.py index 42d68fa..67b960e 100644 --- a/build/scripts/build_windows.py +++ b/build/scripts/build_windows.py @@ -243,10 +243,6 @@ class WindowsBuilder: if not self._create_wix_source(): return False - # Ensure toolbar icons are present in bundled resources before harvesting. - if not self._ensure_toolbar_icons_in_bundle(): - return False - # Harvest application files using Heat print(f" Harvesting application files...") dist_folder = self.dist_dir / "WebDropBridge" @@ -356,36 +352,6 @@ class WindowsBuilder: return True - def _ensure_toolbar_icons_in_bundle(self) -> bool: - """Ensure toolbar icon files exist in the bundled app folder. - - This guarantees WiX Heat harvest includes these icons in the MSI, - even if a previous PyInstaller run omitted them. - """ - src_icons_dir = self.project_root / "resources" / "icons" - bundle_icons_dir = self.dist_dir / "WebDropBridge" / "_internal" / "resources" / "icons" - required_icons = ["home.ico", "reload.ico", "open.ico", "openwith.ico"] - - try: - bundle_icons_dir.mkdir(parents=True, exist_ok=True) - - for icon_name in required_icons: - src = src_icons_dir / icon_name - dst = bundle_icons_dir / icon_name - - if not src.exists(): - print(f"❌ Required icon not found: {src}") - return False - - if not dst.exists() or src.stat().st_mtime > dst.stat().st_mtime: - shutil.copy2(src, dst) - print(f" ✓ Ensured toolbar icon in bundle: {icon_name}") - - return True - except Exception as e: - print(f"❌ Failed to ensure toolbar icons in bundle: {e}") - return False - def _create_wix_source(self) -> bool: """Create WiX source file for MSI generation. diff --git a/resources/icons/home.ico b/resources/icons/home.ico deleted file mode 100644 index e080469..0000000 Binary files a/resources/icons/home.ico and /dev/null differ diff --git a/resources/icons/open.ico b/resources/icons/open.ico deleted file mode 100644 index 1b6fed8..0000000 Binary files a/resources/icons/open.ico and /dev/null differ diff --git a/resources/icons/openwith.ico b/resources/icons/openwith.ico deleted file mode 100644 index 3facabc..0000000 Binary files a/resources/icons/openwith.ico and /dev/null differ diff --git a/resources/icons/reload.ico b/resources/icons/reload.ico deleted file mode 100644 index 1f1023d..0000000 Binary files a/resources/icons/reload.ico and /dev/null differ diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 2bfc78e..c6db37e 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -4,7 +4,6 @@ import asyncio import json import logging import re -import subprocess import sys from datetime import datetime from pathlib import Path @@ -251,12 +250,6 @@ class OpenDropZone(QWidget): self.setMinimumSize(QSize(44, 44)) self.setMaximumSize(QSize(48, 48)) - def set_icon(self, icon: QIcon) -> None: - """Set the displayed icon for the drop zone widget.""" - if icon.isNull(): - return - self._icon_label.setPixmap(icon.pixmap(QSize(32, 32))) - # ------------------------------------------------------------------ # Drop handling # ------------------------------------------------------------------ @@ -302,46 +295,6 @@ class OpenDropZone(QWidget): logger.debug(f"OpenDropZone: skipping non-local URL {url.toString()}") -class OpenWithDropZone(OpenDropZone): - """Drop target widget that opens files via an app chooser dialog. - - When a file is dropped, this widget emits ``file_open_with_requested`` for - each local file so the main window can invoke platform-specific - "Open With" behavior. - - Signals: - file_open_with_requested (str): Emitted with the local file path. - """ - - file_open_with_requested = Signal(str) - - def __init__(self, parent: Optional[QWidget] = None) -> None: - """Initialize the OpenWithDropZone widget. - - Args: - parent: Parent widget. - """ - super().__init__(parent) - self._icon_label.setToolTip("Drop a file here to choose which app should open it") - - def dropEvent(self, event) -> None: # type: ignore[override] - """Emit dropped local files for app-chooser handling.""" - self._icon_label.setStyleSheet(self._NORMAL_STYLE) - mime = event.mimeData() - if not mime.hasUrls(): - event.ignore() - return - - event.acceptProposedAction() - for url in mime.urls(): - if url.isLocalFile(): - file_path = url.toLocalFile() - logger.info(f"OpenWithDropZone: request app chooser for '{file_path}'") - self.file_open_with_requested.emit(file_path) - else: - logger.debug(f"OpenWithDropZone: skipping non-local URL {url.toString()}") - - class _DragBridge(QObject): """JavaScript bridge for drag operations via QWebChannel. @@ -1118,78 +1071,6 @@ class MainWindow(QMainWindow): f"File: {file_path}\nError: {error}", ) - def _on_file_open_with_requested(self, file_path: str) -> None: - """Handle a file dropped on the OpenWithDropZone. - - Args: - file_path: Local file path to open using an app chooser. - """ - if self._open_with_app_chooser(file_path): - self.statusBar().showMessage(f"Choose app for: {Path(file_path).name}", 4000) - logger.info(f"Opened app chooser for '{file_path}'") - return - - logger.warning(f"Could not open app chooser for '{file_path}'") - QMessageBox.warning( - self, - "Open With Error", - "Could not open an application chooser for this file on your platform.", - ) - - def _open_with_app_chooser(self, file_path: str) -> bool: - """Open OS-specific app chooser for a local file. - - Args: - file_path: Local file path. - - Returns: - True if the chooser command was started successfully, False otherwise. - """ - try: - normalized_path = str(Path(file_path)) - if not Path(normalized_path).exists(): - logger.warning(f"Open-with target does not exist: {normalized_path}") - return False - - if sys.platform.startswith("win"): - # First try the native shell "openas" verb. - import ctypes - - result = ctypes.windll.shell32.ShellExecuteW( - None, "openas", normalized_path, None, None, 1 - ) - if result > 32: - return True - - logger.warning(f"ShellExecuteW(openas) failed with code {result}; trying fallback") - - # Fallback for systems where openas verb is not available/reliable. - subprocess.Popen(["rundll32.exe", "shell32.dll,OpenAs_RunDLL", normalized_path]) - return True - - if sys.platform == "darwin": - # Prompt for an app and open the file with the selected app. - script = ( - "on run argv\n" - "set targetFile to POSIX file (item 1 of argv)\n" - "set chosenApp to choose application\n" - 'tell application "Finder" to open targetFile using chosenApp\n' - "end run" - ) - result = subprocess.run( - ["osascript", "-e", script, file_path], - check=False, - capture_output=True, - text=True, - ) - return result.returncode == 0 - - logger.warning(f"Open-with chooser not implemented for platform: {sys.platform}") - return False - except Exception as e: - logger.warning(f"Failed to open app chooser for '{file_path}': {e}") - return False - def _on_download_requested(self, download: QWebEngineDownloadRequest) -> None: """Handle download requests from the embedded web view. @@ -1382,52 +1263,25 @@ class MainWindow(QMainWindow): # Separator toolbar.addSeparator() - if hasattr(sys, "_MEIPASS"): - icons_dir = Path(sys._MEIPASS) / "resources" / "icons" # type: ignore[attr-defined] - else: - icons_dir = Path(__file__).parent.parent.parent.parent / "resources" / "icons" - # Home button - home_icon_path = icons_dir / "home.ico" - home_icon = ( - QIcon(str(home_icon_path)) - if home_icon_path.exists() - else self.style().standardIcon(self.style().StandardPixmap.SP_DirHomeIcon) + home_action = toolbar.addAction( + self.style().standardIcon(self.style().StandardPixmap.SP_DirHomeIcon), "" ) - home_action = toolbar.addAction(home_icon, "") home_action.setToolTip("Home") home_action.triggered.connect(self._navigate_home) # Refresh button refresh_action = self.web_view.pageAction(self.web_view.page().WebAction.Reload) - reload_icon_path = icons_dir / "reload.ico" - if reload_icon_path.exists(): - refresh_action.setIcon(QIcon(str(reload_icon_path))) toolbar.addAction(refresh_action) # Open-with-default-app drop zone (right of Reload) self._open_drop_zone = OpenDropZone() - open_icon_path = icons_dir / "open.ico" - if open_icon_path.exists(): - self._open_drop_zone.set_icon(QIcon(str(open_icon_path))) self._open_drop_zone.file_opened.connect(self._on_file_opened_via_drop) self._open_drop_zone.file_open_failed.connect(self._on_file_open_failed_via_drop) open_drop_action = QWidgetAction(toolbar) open_drop_action.setDefaultWidget(self._open_drop_zone) toolbar.addAction(open_drop_action) - # Open-with chooser drop zone (right of Open-with-default-app) - self._open_with_drop_zone = OpenWithDropZone() - open_with_icon_path = icons_dir / "openwith.ico" - if open_with_icon_path.exists(): - self._open_with_drop_zone.set_icon(QIcon(str(open_with_icon_path))) - self._open_with_drop_zone.file_open_with_requested.connect( - self._on_file_open_with_requested - ) - open_with_drop_action = QWidgetAction(toolbar) - open_with_drop_action.setDefaultWidget(self._open_with_drop_zone) - toolbar.addAction(open_with_drop_action) - # Add stretch spacer to push help buttons to the right spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) @@ -1613,10 +1467,6 @@ class MainWindow(QMainWindow): f"Bridges web-based drag-and-drop workflows with native file operations " f"for professional desktop applications.
" f"
" - f"Toolbar Drop Zones:
" - f"Open icon: Opens dropped files with the system default app.
" - f"Open-with icon: Shows an app chooser for dropped files.
" - f"
" f"Product of:
" f"hörl Information Management GmbH
" f"Silberburgstraße 126
" diff --git a/tests/unit/test_main_window.py b/tests/unit/test_main_window.py index 161be8a..01eaa5a 100644 --- a/tests/unit/test_main_window.py +++ b/tests/unit/test_main_window.py @@ -149,74 +149,3 @@ class TestMainWindowURLWhitelist: # web_view should have allowed_urls configured assert window.web_view.allowed_urls == sample_config.allowed_urls - - -class TestMainWindowOpenWith: - """Test Open With chooser behavior.""" - - def test_open_with_app_chooser_windows(self, qtbot, sample_config): - """Windows should use ShellExecuteW with the openas verb.""" - window = MainWindow(sample_config) - qtbot.addWidget(window) - - test_file = sample_config.allowed_roots[0] / "open_with_test.txt" - test_file.write_text("test") - - with patch("webdrop_bridge.ui.main_window.sys.platform", "win32"): - with patch("ctypes.windll.shell32.ShellExecuteW", return_value=33) as mock_shell: - assert window._open_with_app_chooser(str(test_file)) is True - mock_shell.assert_called_once_with( - None, - "openas", - str(test_file), - None, - None, - 1, - ) - - def test_open_with_app_chooser_windows_shellexecute_failure(self, qtbot, sample_config): - """Windows should fall back to OpenAs_RunDLL when ShellExecuteW fails.""" - window = MainWindow(sample_config) - qtbot.addWidget(window) - - test_file = sample_config.allowed_roots[0] / "open_with_fallback.txt" - test_file.write_text("test") - - with patch("webdrop_bridge.ui.main_window.sys.platform", "win32"): - with patch("ctypes.windll.shell32.ShellExecuteW", return_value=31): - with patch("webdrop_bridge.ui.main_window.subprocess.Popen") as mock_popen: - assert window._open_with_app_chooser(str(test_file)) is True - mock_popen.assert_called_once_with( - ["rundll32.exe", "shell32.dll,OpenAs_RunDLL", str(test_file)] - ) - - def test_open_with_app_chooser_missing_file(self, qtbot, sample_config): - """Missing files should fail before platform-specific invocation.""" - window = MainWindow(sample_config) - qtbot.addWidget(window) - - with patch("webdrop_bridge.ui.main_window.sys.platform", "win32"): - assert window._open_with_app_chooser("C:/tmp/does_not_exist.txt") is False - - def test_open_with_app_chooser_macos_success(self, qtbot, sample_config): - """macOS should return True when osascript exits successfully.""" - window = MainWindow(sample_config) - qtbot.addWidget(window) - - test_file = sample_config.allowed_roots[0] / "open_with_macos.txt" - test_file.write_text("test") - - class _Result: - returncode = 0 - - with patch("webdrop_bridge.ui.main_window.sys.platform", "darwin"): - with patch("webdrop_bridge.ui.main_window.subprocess.run", return_value=_Result()): - assert window._open_with_app_chooser(str(test_file)) is True - - def test_open_with_app_chooser_unsupported_platform(self, qtbot, sample_config): - """Unsupported platforms should return False.""" - window = MainWindow(sample_config) - qtbot.addWidget(window) - - with patch("webdrop_bridge.ui.main_window.sys.platform", "linux"): - assert window._open_with_app_chooser("/tmp/test.txt") is False