Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/Loading Screen #936

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions openadapt/app/tray.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,28 @@
usage: `python -m openadapt.app.tray` or `poetry run app`
"""

import sys
import asyncio
import os

# Fix for Windows ProactorEventLoop
if sys.platform == "win32":
try:
if isinstance(
asyncio.get_event_loop_policy(), asyncio.WindowsProactorEventLoopPolicy
):
# Use WindowsSelectorEventLoopPolicy instead
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
except Exception as e:
print(f"Failed to set event loop policy: {e}")

from datetime import datetime
from functools import partial
from pprint import pformat
from threading import Thread
from typing import Any, Callable
import inspect
import multiprocessing
import os
import sys

from pyqttoast import Toast, ToastButtonAlignment, ToastIcon, ToastPosition, ToastPreset
from PySide6.QtCore import QMargins, QObject, QSize, Qt, QThread, Signal
Expand All @@ -33,7 +46,7 @@
QWidget,
)

from openadapt.app import quick_record, stop_record, FPATH
from openadapt.app import FPATH, quick_record, stop_record
from openadapt.app.dashboard.run import cleanup as cleanup_dashboard
from openadapt.app.dashboard.run import run as run_dashboard
from openadapt.build_utils import is_running_from_executable
Expand Down Expand Up @@ -109,8 +122,9 @@ class SystemTrayIcon:

def __init__(self) -> None:
"""Initialize the system tray icon."""
self.app = QApplication([])

self.app = QApplication.instance()
if not self.app:
self.app = QApplication([])
if sys.platform == "darwin":
# hide Dock icon while allowing focus on dialogs
# (must come after QApplication())
Expand Down
128 changes: 118 additions & 10 deletions openadapt/entrypoint.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,138 @@
"""Entrypoint for OpenAdapt."""

import multiprocessing
import sys
import requests

if __name__ == "__main__":
# This needs to be called before any code that uses multiprocessing
multiprocessing.freeze_support()
from PySide6.QtCore import QObject, QTimer, Signal
from PySide6.QtWidgets import QApplication

from openadapt.build_utils import redirect_stdout_stderr
from openadapt.error_reporting import configure_error_reporting
from openadapt.config import config
from openadapt.custom_logger import logger
from openadapt.splash_screen import LoadingScreen


def run_openadapt() -> None:
"""Run OpenAdapt."""
with redirect_stdout_stderr():
class LoadingManager(QObject):
"""Manages the loading stages and progress updates."""

progress_updated = Signal(int, str)
loading_complete = Signal()

def __init__(self, splash_screen: LoadingScreen, app: QApplication) -> None:
"""Initialize the loading manager."""
super().__init__()
self.splash = splash_screen
self.app = app
self.progress_updated.connect(self._update_progress)
self.loading_complete.connect(self._on_loading_complete)
self.dashboard_check_attempts = 0
self.is_ready = False
self.dashboard_timer = None

def _update_progress(self, value: int, message: str) -> None:
"""Update progress bar and process events."""
if not self.is_ready:
self.splash.update_progress(value)
self.splash.update_status(message)
self.app.processEvents()
logger.debug(f"Progress: {value}% - {message}")

def _on_loading_complete(self) -> None:
"""Handle completion of loading sequence."""
self.is_ready = True
if self.dashboard_timer:
self.dashboard_timer.stop()
QTimer.singleShot(500, self.splash.hide)
QTimer.singleShot(600, self.app.quit)

def check_dashboard(self) -> None:
"""Check if dashboard is responsive and handle loading completion."""
try:
from openadapt.alembic.context_loader import load_alembic_context
from openadapt.app import tray
url = f"http://localhost:{config.DASHBOARD_CLIENT_PORT}"
response = requests.get(url, timeout=1)
if response.status_code == 200:
self.progress_updated.emit(100, "Ready!")
self.loading_complete.emit()
return
except requests.RequestException:
pass

def start_dashboard_monitoring(self) -> None:
"""Start dashboard monitoring using Qt timer."""
self.dashboard_timer = QTimer(self)
self.dashboard_timer.timeout.connect(self.check_dashboard)
self.dashboard_timer.start(100) # Check every 100ms

def start_loading_sequence(self) -> bool:
"""Execute the loading sequence with visible progress updates."""
try:
# Initial setup - 0%
self.progress_updated.emit(0, "Initializing...")

# Configuration - 20%
self.progress_updated.emit(20, "Loading configuration...")
from openadapt.config import print_config

print_config()

# Error reporting setup - 40%
self.progress_updated.emit(40, "Configuring error reporting...")
from openadapt.error_reporting import configure_error_reporting

configure_error_reporting()

# Database context - 60%
self.progress_updated.emit(60, "Loading database context...")
from openadapt.alembic.context_loader import load_alembic_context

load_alembic_context()
tray._run()

# System tray setup - 80%
self.progress_updated.emit(80, "Starting system tray...")

return True

except Exception as e:
logger.error(f"Loading sequence failed: {e}")
return False


def run_tray() -> None:
"""Run the openadapt tray."""
from openadapt.app import tray

tray._run()


def run_openadapt() -> None:
"""Run OpenAdapt."""
with redirect_stdout_stderr():
try:
app = QApplication(sys.argv)
splash = LoadingScreen()
splash.show()

loading_manager = LoadingManager(splash, app)

if not loading_manager.start_loading_sequence():
raise Exception("Loading sequence failed")

tray_process = multiprocessing.Process(target=run_tray, daemon=False)
tray_process.start()

if not tray_process.is_alive():
raise Exception("Tray process failed to start")

loading_manager.start_dashboard_monitoring()

app.exec()

except Exception as exc:
logger.exception(exc)
if "tray_process" in locals() and tray_process and tray_process.is_alive():
tray_process.terminate()
sys.exit(1)


if __name__ == "__main__":
Expand Down
91 changes: 91 additions & 0 deletions openadapt/splash_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Modern minimal splash screen for OpenAdapt with improved responsiveness."""

from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QColor, QPixmap
from PySide6.QtWidgets import QApplication, QLabel, QProgressBar, QSplashScreen


class LoadingScreen(QSplashScreen):
"""A minimal splash screen for."""

def __init__(self) -> None:
"""Initialize the loading screen."""
pixmap = QPixmap(400, 100)
pixmap.fill(QColor(0, 0, 0, 180))

super().__init__(pixmap)

self.title_label = QLabel("OpenAdapt", self)
self.title_label.setGeometry(0, 15, 400, 30)
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet(
"""
QLabel {
color: #FFFFFF;
font-family: Arial;
font-size: 20px;
font-weight: bold;
}
"""
)

self.progress_bar = QProgressBar(self)
self.progress_bar.setGeometry(50, 55, 300, 6)
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setTextVisible(False)

self.status_label = QLabel(self)
self.status_label.setGeometry(0, 70, 400, 20)
self.status_label.setAlignment(Qt.AlignCenter)

self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)

self.progress_bar.setStyleSheet(
"""
QProgressBar {
border: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
QProgressBar::chunk {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #2196F3, stop:1 #64B5F6);
border-radius: 3px;
}
"""
)

self.status_label.setStyleSheet(
"""
QLabel {
color: #CCCCCC;
font-size: 11px;
font-family: Arial;
}
"""
)

def update_status(self, message: str) -> None:
"""Update the status message displayed on the splash screen."""
self.status_label.setText(message)
self.repaint()
QApplication.processEvents()

def update_progress(self, value: int) -> None:
"""Update progress value with immediate visual feedback."""
value = max(0, min(100, value))
# for smooth progress updates
QTimer.singleShot(0, lambda: self._do_update(value))

def _do_update(self, value: int) -> None:
"""Internal method to perform the actual progress update."""
self.progress_bar.setValue(value)
self.repaint()
QApplication.processEvents()

def show(self) -> None:
"""Show the splash screen."""
super().show()
self.raise_()
QApplication.processEvents()
Loading