diff --git a/CHANGELOG.md b/CHANGELOG.md index c22469f92..502deaea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1102,5 +1102,3 @@ * @omid-aignostics made their first contribution * @idelsink made their first contribution * @dependabot[bot] made their first contribution - - diff --git a/src/aignostics.py b/src/aignostics.py index f591a5a2d..caf2cd103 100644 --- a/src/aignostics.py +++ b/src/aignostics.py @@ -23,7 +23,7 @@ os.environ["LOGFIRE_PYDANTIC_RECORD"] = "off" -from aignostics.constants import SENTRY_INTEGRATIONS # noqa: E402 +from aignostics.constants import SENTRY_INTEGRATIONS, WINDOW_TITLE # noqa: E402 from aignostics.utils import boot, gui_run # noqa: E402 boot(SENTRY_INTEGRATIONS) @@ -80,4 +80,4 @@ else: if pyi_splash and pyi_splash.is_alive(): pyi_splash.update_text("Opening user interface ...") - gui_run(native=True, with_api=False, title="Aignostics Launchpad", icon="🔬") + gui_run(native=True, with_api=False, title=WINDOW_TITLE, icon="🔬") diff --git a/src/aignostics/cli.py b/src/aignostics/cli.py index f2cff89b9..12ace588f 100644 --- a/src/aignostics/cli.py +++ b/src/aignostics/cli.py @@ -7,7 +7,7 @@ import typer from loguru import logger -from .constants import NOTEBOOK_DEFAULT +from .constants import NOTEBOOK_DEFAULT, WINDOW_TITLE from .utils import ( __is_running_in_container__, __python_version__, @@ -27,7 +27,7 @@ def launchpad() -> None: """Open Aignostics Launchpad, the graphical user interface of the Aignostics Platform.""" from .utils import gui_run # noqa: PLC0415 - gui_run(native=True, with_api=False, title="Aignostics Launchpad", icon="🔬") + gui_run(native=True, with_api=False, title=WINDOW_TITLE, icon="🔬") if find_spec("marimo"): diff --git a/src/aignostics/constants.py b/src/aignostics/constants.py index c8bde088f..0a5d4b0ad 100644 --- a/src/aignostics/constants.py +++ b/src/aignostics/constants.py @@ -37,3 +37,5 @@ TEST_APP_APPLICATION_ID = "test-app" WSI_SUPPORTED_FILE_EXTENSIONS = {".dcm", ".tiff", ".tif", ".svs"} WSI_SUPPORTED_FILE_EXTENSIONS_TEST_APP = {".tiff"} + +WINDOW_TITLE = "Aignostics Launchpad" diff --git a/src/aignostics/gui/_frame.py b/src/aignostics/gui/_frame.py index 495b2bd43..f4470f5a9 100644 --- a/src/aignostics/gui/_frame.py +++ b/src/aignostics/gui/_frame.py @@ -11,7 +11,9 @@ from html_sanitizer import Sanitizer from humanize import naturaldelta +from loguru import logger +from aignostics.constants import WINDOW_TITLE from aignostics.utils import __version__, open_user_data_directory from ._theme import theme @@ -84,6 +86,34 @@ def _render_nav_groups() -> None: for item in group.items: _render_nav_item(item) + def _bring_window_to_front() -> None: + """Bring the native window to front after authentication completes. + + Uses platform-specific approaches: + - Windows: Uses ctypes to find window by title and call SetForegroundWindow, + as pywebview's set_always_on_top/show methods don't reliably bring windows + to front and the window handle isn't directly exposed. + - macOS/Linux: Uses pywebview's built-in methods. + """ + if not app.native.main_window: + return + try: + if platform.system() == "Windows": + import ctypes # noqa: PLC0415 + + # Find window by title since pywebview doesn't expose hwnd directly + # FindWindowW(lpClassName, lpWindowName) - use None for class to match any + hwnd = ctypes.windll.user32.FindWindowW(None, WINDOW_TITLE) # type: ignore + if hwnd: + ctypes.windll.user32.SetForegroundWindow(hwnd) # type: ignore + else: + app.native.main_window.set_always_on_top(True) + app.native.main_window.show() + app.native.main_window.set_always_on_top(False) + except Exception as e: + logger.exception(f"Failed to bring window to front: {e}") + # Window operations can fail on some platforms + user_info: UserInfo | None = None launchpad_healthy: bool | None = None @@ -127,6 +157,8 @@ async def _user_info_ui_load() -> None: await ui.context.client.connected() app.storage.tab["user_info"] = user_info _user_info_ui.refresh() + if user_info: + _bring_window_to_front() ui.timer(interval=USERINFO_UPDATE_INTERVAL, callback=_user_info_ui_load, immediate=True) @@ -139,6 +171,8 @@ async def _user_info_ui_relogin() -> None: _user_info_ui.refresh() with contextlib.suppress(Exception): user_info = await run.io_bound(PlatformService.get_user_info, relogin=True) + if user_info: + _bring_window_to_front() app.storage.tab["user_info"] = user_info ui.navigate.reload() diff --git a/tests/aignostics/cli_test.py b/tests/aignostics/cli_test.py index 9c2eda662..932d39313 100644 --- a/tests/aignostics/cli_test.py +++ b/tests/aignostics/cli_test.py @@ -10,6 +10,7 @@ from typer.testing import CliRunner from aignostics.cli import cli +from aignostics.constants import WINDOW_TITLE from aignostics.utils import ( __python_version__, __version__, @@ -205,7 +206,7 @@ def mock_app_mount(path, app_instance): # Check that ui.run was called with the expected parameters assert mock_ui_run_called, "ui.run was not called" - assert mock_ui_run_args["title"] == "Aignostics Launchpad", "title parameter is incorrect" + assert mock_ui_run_args["title"] == WINDOW_TITLE, "title parameter is incorrect" assert mock_ui_run_args["favicon"] == "🔬", "favicon parameter is incorrect" if platform.system() == "Linux": assert mock_ui_run_args["native"] is False, "native parameter should be False on Linux" diff --git a/tests/main.py b/tests/main.py index 020639485..c607dea18 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,7 +1,8 @@ """Start script for pytest.""" +from aignostics.constants import WINDOW_TITLE from aignostics.utils import ( gui_run, ) -gui_run(native=False, with_api=False, title="Aignostics Launchpad", icon="🔬") +gui_run(native=False, with_api=False, title=WINDOW_TITLE, icon="🔬")