Source code for gpype.frontend.main_app

from __future__ import annotations

import os
import platform
import subprocess
import sys
from pathlib import Path

from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QGridLayout, QMainWindow, QWidget

from .widgets.base.widget import Widget


[docs] class MainApp: """Main application class for g.Pype frontend applications. Provides framework for creating PyQt6-based applications with widget management, window configuration, and lifecycle handling. Uses composition for better flexibility and testability. """ #: Default window geometry configuration DEFAULT_POSITION = [100, 100, 700, 400] # [x, y, width, height] #: Default grid size (rows, cols) DEFAULT_GRID_SIZE = [3, 3] #: Application icon path ICON_PATH = Path("resources") / "gtec.ico"
[docs] def __init__( self, caption: str = "g.Pype Application", position: list[int] = None, grid_size: list[int] = None, app=None, prevent_sleep: bool = True, ): """Initialize the main application with window and widget management. Args: caption: Window title text displayed in the title bar. position: Window geometry as [x, y, width, height] list. Uses DEFAULT_POSITION if None. grid_size: Grid dimensions as [rows, cols] list. Uses DEFAULT_GRID_SIZE if None. app: Existing QApplication instance for testing or integration. Creates new QApplication if None. prevent_sleep: Whether to prevent system sleep/power saving. Default True for real-time applications. """ # Create or use existing QApplication (composition over inheritance) # This allows for better testability and flexibility self._app = app or QApplication([]) # Initialize widget collection for lifecycle management self._widgets: list[Widget] = [] # Store configuration self._grid_size = grid_size or MainApp.DEFAULT_GRID_SIZE self._grid_rows, self._grid_cols = self._grid_size self._prevent_sleep = prevent_sleep self._sleep_prevention_active = False # Create and configure main window self._window = QMainWindow() self._window.setWindowTitle(caption) # Set application icon if file exists icon_path = Path(__file__).parent / MainApp.ICON_PATH if icon_path.exists(): self._window.setWindowIcon(QIcon(str(icon_path))) # Configure window geometry if position is None: position = MainApp.DEFAULT_POSITION self._window.setGeometry(*position) # Create central widget and layout system # QMainWindow requires a central widget to contain other widgets central_widget = QWidget() self._window.setCentralWidget(central_widget) # Create grid layout for widget arrangement (MATLAB subplot-style) self._layout = QGridLayout() central_widget.setLayout(self._layout) # Connect cleanup handler for graceful shutdown self._app.aboutToQuit.connect(self._on_quit)
[docs] def add_widget(self, widget: Widget, grid_positions: list[int] = None): """Add a widget to the application layout and management system. Registers the widget for lifecycle management and adds it to the main window's grid layout. Widget will be automatically started during run() and terminated during shutdown. Args: widget: Widget instance to add to the application. Must inherit from the base Widget class. grid_positions: List of grid positions (1-indexed) to span. For a 3x3 grid: [1,2,3] spans top row, [1,4,7] spans left col. If None, adds to next available position. """ # Register widget for lifecycle management self._widgets.append(widget) if grid_positions is None: # Auto-placement: find next available position self._layout.addWidget(widget.widget) else: # Manual placement: convert positions to grid coordinates min_pos = min(grid_positions) max_pos = max(grid_positions) # Convert 1-indexed positions to 0-indexed row/col start_row = (min_pos - 1) // self._grid_cols start_col = (min_pos - 1) % self._grid_cols end_row = (max_pos - 1) // self._grid_cols end_col = (max_pos - 1) % self._grid_cols # Calculate span row_span = end_row - start_row + 1 col_span = end_col - start_col + 1 # Add widget to grid layout with specified span self._layout.addWidget( widget.widget, start_row, start_col, row_span, col_span )
def _enable_sleep_prevention(self): """Enable system sleep prevention based on the current platform.""" if not self._prevent_sleep or self._sleep_prevention_active: return system = platform.system() try: if system == "Windows": self._prevent_sleep_windows() elif system == "Darwin": # macOS self._prevent_sleep_macos() else: print(f"Sleep prevention not implemented for {system}") return self._sleep_prevention_active = True except Exception as e: print(f"Failed to enable sleep prevention: {e}") def _disable_sleep_prevention(self): """Disable system sleep prevention and restore normal power mgmt.""" if not self._sleep_prevention_active: return system = platform.system() try: if system == "Windows": self._restore_sleep_windows() elif system == "Darwin": # macOS self._restore_sleep_macos() self._sleep_prevention_active = False except Exception as e: print(f"Failed to disable sleep prevention: {e}") def _prevent_sleep_windows(self): """Prevent sleep on Windows using SetThreadExecutionState.""" try: import ctypes from ctypes import wintypes # Constants for SetThreadExecutionState ES_CONTINUOUS = 0x80000000 ES_SYSTEM_REQUIRED = 0x00000001 ES_DISPLAY_REQUIRED = 0x00000002 ES_AWAYMODE_REQUIRED = 0x00000040 # Prevent system sleep and display sleep execution_state = ( ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED | ES_AWAYMODE_REQUIRED ) kernel32 = ctypes.windll.kernel32 kernel32.SetThreadExecutionState.argtypes = [wintypes.DWORD] kernel32.SetThreadExecutionState.restype = wintypes.DWORD result = kernel32.SetThreadExecutionState(execution_state) if not result: raise RuntimeError("SetThreadExecutionState failed") except ImportError: raise RuntimeError("Windows API not available") def _restore_sleep_windows(self): """Restore normal sleep behavior on Windows.""" try: import ctypes from ctypes import wintypes # ES_CONTINUOUS without other flags restores normal behavior ES_CONTINUOUS = 0x80000000 kernel32 = ctypes.windll.kernel32 kernel32.SetThreadExecutionState.argtypes = [wintypes.DWORD] kernel32.SetThreadExecutionState.restype = wintypes.DWORD kernel32.SetThreadExecutionState(ES_CONTINUOUS) except ImportError: pass # Silently fail if Windows API not available def _prevent_sleep_macos(self): """Prevent sleep on macOS using caffeinate or IOKit.""" try: import subprocess # Try to use caffeinate command (available on macOS 10.8+) self._caffeinate_process = subprocess.Popen( ["caffeinate", "-d", "-i", "-m", "-s"] ) except (FileNotFoundError, subprocess.SubprocessError): try: # Fallback to IOKit (requires pyobjc) self._prevent_sleep_macos_iokit() except ImportError: raise RuntimeError( "macOS sleep prevention requires caffeinate command " "or pyobjc library" ) def _prevent_sleep_macos_iokit(self): """Prevent sleep on macOS using IOKit (requires pyobjc).""" try: import Cocoa # noqa: F401 from CoreFoundation import kCFStringEncodingUTF8 # noqa: F401 # Create assertion to prevent sleep reason = Cocoa.CFStringCreateWithCString( None, "g.Pype Application", kCFStringEncodingUTF8 ) # Import IOKit functions from IOKit import IOPMAssertionCreateWithName # noqa: F401 from IOKit import kIOPMAssertionTypeNoDisplaySleep success, self._sleep_assertion_id = IOPMAssertionCreateWithName( kIOPMAssertionTypeNoDisplaySleep, 255, # kIOPMAssertionLevelOn reason, None, ) if not success: raise RuntimeError("Failed to create IOKit assertion") except ImportError: raise ImportError("pyobjc library required for IOKit method") def _restore_sleep_macos(self): """Restore normal sleep behavior on macOS.""" # Terminate caffeinate process if running if hasattr(self, "_caffeinate_process"): try: self._caffeinate_process.terminate() self._caffeinate_process.wait(timeout=5) except (AttributeError, subprocess.TimeoutExpired): try: self._caffeinate_process.kill() except AttributeError: pass finally: delattr(self, "_caffeinate_process") # Release IOKit assertion if created if hasattr(self, "_sleep_assertion_id"): try: from IOKit import IOPMAssertionRelease # noqa: F401 IOPMAssertionRelease(self._sleep_assertion_id) except ImportError: pass finally: delattr(self, "_sleep_assertion_id") def _on_quit(self): """Handle application shutdown cleanup. Called automatically when the QApplication is about to quit. Ensures all registered widgets are properly terminated and restores normal sleep behavior. """ # Disable sleep prevention before terminating widgets self._disable_sleep_prevention() # Terminate all widgets gracefully for widget in self._widgets: widget.terminate()
[docs] def run(self) -> int: """Start the application and enter the main event loop. Shows the main window, starts all registered widgets, enables sleep prevention if configured, and enters the Qt event loop. Blocks until the application is closed. Returns: int: Application exit code. 0 indicates successful execution, non-zero values indicate errors or abnormal termination. """ # Enable sleep prevention if configured if self._prevent_sleep: self._enable_sleep_prevention() # Show the main window self._window.show() # Start all registered widgets for widget in self._widgets: widget.run() # Enter the Qt event loop (blocks until application closes) return self._app.exec()