From df76cb9b365b55d209ece935c341c829e4f8ecc0 Mon Sep 17 00:00:00 2001 From: claudi Date: Thu, 12 Mar 2026 09:07:14 +0100 Subject: [PATCH] feat: Add toolbar icon configuration and update handling for Agravity --- build/brands/agravity.json | 6 ++- build/brands/template.jsonc | 6 ++- build/scripts/brand_config.py | 28 ++++++++++++++ build/scripts/build_macos.sh | 4 ++ build/scripts/build_windows.py | 4 ++ docs/BRANDING_AND_RELEASES.md | 19 ++++++++++ src/webdrop_bridge/ui/main_window.py | 56 +++++++++++++++++++++------- tests/unit/test_brand_config.py | 4 ++ 8 files changed, 112 insertions(+), 15 deletions(-) diff --git a/build/brands/agravity.json b/build/brands/agravity.json index 6848ea3..3eaf961 100644 --- a/build/brands/agravity.json +++ b/build/brands/agravity.json @@ -14,5 +14,9 @@ "icon_icns": "resources/icons/app.icns", "dialog_bmp": "resources/icons/background.bmp", "banner_bmp": "resources/icons/banner.bmp", - "license_rtf": "resources/license.rtf" + "license_rtf": "resources/license.rtf", + "toolbar_icon_home": "resources/icons/home.ico", + "toolbar_icon_reload": "resources/icons/reload.ico", + "toolbar_icon_open": "resources/icons/open.ico", + "toolbar_icon_openwith": "resources/icons/openwith.ico" } \ No newline at end of file diff --git a/build/brands/template.jsonc b/build/brands/template.jsonc index b089411..d71b703 100644 --- a/build/brands/template.jsonc +++ b/build/brands/template.jsonc @@ -16,5 +16,9 @@ "icon_icns": "resources/icons/app.icns", "dialog_bmp": "resources/icons/background.bmp", "banner_bmp": "resources/icons/banner.bmp", - "license_rtf": "resources/license.rtf" + "license_rtf": "resources/license.rtf", + "toolbar_icon_home": "resources/icons/home.ico", + "toolbar_icon_reload": "resources/icons/reload.ico", + "toolbar_icon_open": "resources/icons/open.ico", + "toolbar_icon_openwith": "resources/icons/openwith.ico" } \ No newline at end of file diff --git a/build/scripts/brand_config.py b/build/scripts/brand_config.py index 3987c9c..8d886b1 100644 --- a/build/scripts/brand_config.py +++ b/build/scripts/brand_config.py @@ -29,6 +29,10 @@ class BrandConfig: dialog_bmp: Path banner_bmp: Path license_rtf: Path + toolbar_icon_home: str + toolbar_icon_reload: str + toolbar_icon_open: str + toolbar_icon_openwith: str def windows_installer_name(self, version: str) -> str: return f"{self.asset_prefix}-{version}-win-x64.msi" @@ -58,6 +62,10 @@ DEFAULT_BRAND_VALUES: dict[str, Any] = { "dialog_bmp": "resources/icons/background.bmp", "banner_bmp": "resources/icons/banner.bmp", "license_rtf": "resources/license.rtf", + "toolbar_icon_home": "resources/icons/home.ico", + "toolbar_icon_reload": "resources/icons/reload.ico", + "toolbar_icon_open": "resources/icons/open.ico", + "toolbar_icon_openwith": "resources/icons/openwith.ico", } DEFAULT_BRAND_ID = str(DEFAULT_BRAND_VALUES["brand_id"]) @@ -125,6 +133,18 @@ def load_brand_config( dialog_bmp=resolve_asset("dialog_bmp"), banner_bmp=resolve_asset("banner_bmp"), license_rtf=resolve_asset("license_rtf"), + toolbar_icon_home=str( + values.get("toolbar_icon_home", DEFAULT_BRAND_VALUES["toolbar_icon_home"]) + ), + toolbar_icon_reload=str( + values.get("toolbar_icon_reload", DEFAULT_BRAND_VALUES["toolbar_icon_reload"]) + ), + toolbar_icon_open=str( + values.get("toolbar_icon_open", DEFAULT_BRAND_VALUES["toolbar_icon_open"]) + ), + toolbar_icon_openwith=str( + values.get("toolbar_icon_openwith", DEFAULT_BRAND_VALUES["toolbar_icon_openwith"]) + ), ) @@ -272,6 +292,10 @@ def cli_env(args: argparse.Namespace) -> int: "WEBDROP_UPDATE_CHANNEL": brand.update_channel, "WEBDROP_ICON_ICO": str(brand.icon_ico), "WEBDROP_ICON_ICNS": str(brand.icon_icns), + "WEBDROP_TOOLBAR_ICON_HOME": brand.toolbar_icon_home, + "WEBDROP_TOOLBAR_ICON_RELOAD": brand.toolbar_icon_reload, + "WEBDROP_TOOLBAR_ICON_OPEN": brand.toolbar_icon_open, + "WEBDROP_TOOLBAR_ICON_OPENWITH": brand.toolbar_icon_openwith, } for key, value in assignments.items(): print(f'export {key}="{value}"') @@ -324,6 +348,10 @@ def cli_show(args: argparse.Namespace) -> int: "config_dir_name": brand.config_dir_name, "msi_upgrade_code": brand.msi_upgrade_code, "update_channel": brand.update_channel, + "toolbar_icon_home": brand.toolbar_icon_home, + "toolbar_icon_reload": brand.toolbar_icon_reload, + "toolbar_icon_open": brand.toolbar_icon_open, + "toolbar_icon_openwith": brand.toolbar_icon_openwith, }, indent=2, ) diff --git a/build/scripts/build_macos.sh b/build/scripts/build_macos.sh index ce16e4f..63b7534 100644 --- a/build/scripts/build_macos.sh +++ b/build/scripts/build_macos.sh @@ -211,6 +211,10 @@ build_executable() { echo "BRAND_ID=\"$WEBDROP_BRAND_ID\"" echo "APP_CONFIG_DIR_NAME=\"$WEBDROP_CONFIG_DIR_NAME\"" echo "UPDATE_CHANNEL=\"$WEBDROP_UPDATE_CHANNEL\"" + echo "TOOLBAR_ICON_HOME=\"$WEBDROP_TOOLBAR_ICON_HOME\"" + echo "TOOLBAR_ICON_RELOAD=\"$WEBDROP_TOOLBAR_ICON_RELOAD\"" + echo "TOOLBAR_ICON_OPEN=\"$WEBDROP_TOOLBAR_ICON_OPEN\"" + echo "TOOLBAR_ICON_OPENWITH=\"$WEBDROP_TOOLBAR_ICON_OPENWITH\"" } >> "$BUNDLED_ENV_FILE" # Export env file for spec file to pick up diff --git a/build/scripts/build_windows.py b/build/scripts/build_windows.py index eac5978..8ae8a20 100644 --- a/build/scripts/build_windows.py +++ b/build/scripts/build_windows.py @@ -113,6 +113,10 @@ class WindowsBuilder: "BRAND_ID": self.brand.brand_id, "APP_CONFIG_DIR_NAME": self.brand.config_dir_name, "UPDATE_CHANNEL": self.brand.update_channel, + "TOOLBAR_ICON_HOME": self.brand.toolbar_icon_home, + "TOOLBAR_ICON_RELOAD": self.brand.toolbar_icon_reload, + "TOOLBAR_ICON_OPEN": self.brand.toolbar_icon_open, + "TOOLBAR_ICON_OPENWITH": self.brand.toolbar_icon_openwith, } output_env = self.temp_dir / ".env" diff --git a/docs/BRANDING_AND_RELEASES.md b/docs/BRANDING_AND_RELEASES.md index 4e46d26..4354dc3 100644 --- a/docs/BRANDING_AND_RELEASES.md +++ b/docs/BRANDING_AND_RELEASES.md @@ -106,8 +106,20 @@ These can point at brand-specific files or default shared files: - `banner_bmp` - `license_rtf` +Optional toolbar icon overrides: + +- `toolbar_icon_home` +- `toolbar_icon_reload` +- `toolbar_icon_open` +- `toolbar_icon_openwith` + If a referenced asset path does not exist, the helper falls back to the default asset defined in `build/scripts/brand_config.py`. +For toolbar icons, the runtime looks for the configured paths in packaged and development layouts. If an icon is missing: + +- Home falls back to a standard Qt home icon +- Reload/Open/OpenWith keep their existing icon behavior + ### Identity Rules Treat these values as long-lived product identity once a brand has shipped: @@ -163,6 +175,13 @@ Relevant runtime config keys include: - `update_channel` - `update_manifest_name` +Toolbar icon env overrides (useful for packaged branding): + +- `TOOLBAR_ICON_HOME` +- `TOOLBAR_ICON_RELOAD` +- `TOOLBAR_ICON_OPEN` +- `TOOLBAR_ICON_OPENWITH` + The current example in `config.example.json` shows the Agravity runtime setup. When adding a new brand, make sure the runtime config matches the packaging manifest at least for: diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index c4f9967..7b7bb5a 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -3,6 +3,7 @@ import asyncio import json import logging +import os import re import subprocess import sys @@ -1386,16 +1387,13 @@ 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_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_HOME", "resources/icons/home.ico") + ) home_icon = ( QIcon(str(home_icon_path)) - if home_icon_path.exists() + if home_icon_path is not None else self.style().standardIcon(self.style().StandardPixmap.SP_DirHomeIcon) ) home_action = toolbar.addAction(home_icon, "") @@ -1404,15 +1402,19 @@ class MainWindow(QMainWindow): # 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(): + reload_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_RELOAD", "resources/icons/reload.ico") + ) + if reload_icon_path is not None: 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(): + open_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_OPEN", "resources/icons/open.ico") + ) + if open_icon_path is not None: 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) @@ -1422,8 +1424,10 @@ class MainWindow(QMainWindow): # 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(): + open_with_icon_path = self._resolve_toolbar_icon_path( + os.getenv("TOOLBAR_ICON_OPENWITH", "resources/icons/openwith.ico") + ) + if open_with_icon_path is not None: 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 @@ -1467,6 +1471,32 @@ class MainWindow(QMainWindow): dev_tools_action.setToolTip(tr("toolbar.tooltip.dev_tools")) dev_tools_action.triggered.connect(self._open_developer_tools) + def _resolve_toolbar_icon_path(self, configured_path: str) -> Path | None: + """Resolve configured toolbar icon path in both dev and packaged layouts.""" + icon_path = Path(configured_path) + + candidates: list[Path] = [] + if icon_path.is_absolute(): + candidates.append(icon_path) + else: + if hasattr(sys, "_MEIPASS"): + meipass = Path(sys._MEIPASS) # type: ignore[attr-defined] + candidates.append(meipass / icon_path) + + exe_dir = Path(sys.executable).resolve().parent + candidates.append(exe_dir / icon_path) + candidates.append(exe_dir / "_internal" / icon_path) + + project_root = Path(__file__).parent.parent.parent.parent + candidates.append(project_root / icon_path) + + for candidate in candidates: + if candidate.exists(): + return candidate + + logger.warning(f"Toolbar icon not found for configured path: {configured_path}") + return None + def _open_log_file(self) -> None: """Open the application log file in the system default text editor. diff --git a/tests/unit/test_brand_config.py b/tests/unit/test_brand_config.py index fa5dd5e..9016fef 100644 --- a/tests/unit/test_brand_config.py +++ b/tests/unit/test_brand_config.py @@ -25,6 +25,10 @@ def test_load_agravity_brand_config(): assert brand.display_name == "Agravity Bridge" assert brand.asset_prefix == "AgravityBridge" assert brand.exe_name == "AgravityBridge" + assert brand.toolbar_icon_home == "resources/icons/home.ico" + assert brand.toolbar_icon_reload == "resources/icons/reload.ico" + assert brand.toolbar_icon_open == "resources/icons/open.ico" + assert brand.toolbar_icon_openwith == "resources/icons/openwith.ico" assert brand.windows_installer_name("0.8.4") == "AgravityBridge-0.8.4-win-x64.msi"