""" Base mixin providing shared application state access """ from typing import Any, Optional, cast from PyQt6.QtWidgets import QStatusBar, QMessageBox, QWidget class ApplicationStateMixin: """ Base mixin providing access to shared application state. This mixin provides properties and helper methods for accessing core application objects that are shared across all operation mixins. Required attributes (must be set by MainWindow): _project: Project instance _gl_widget: GLWidget instance _status_bar: QStatusBar instance _template_manager: TemplateManager instance """ @property def project(self): """Access to current project""" if not hasattr(self, "_project"): raise AttributeError("MainWindow must set _project attribute") return self._project @project.setter def project(self, value): """Set the current project""" self._project = value @property def gl_widget(self): """Access to GL rendering widget""" if not hasattr(self, "_gl_widget"): raise AttributeError("MainWindow must set _gl_widget attribute") return self._gl_widget @property def status_bar(self) -> QStatusBar: """Access to status bar""" if not hasattr(self, "_status_bar"): raise AttributeError("MainWindow must set _status_bar attribute") return cast(QStatusBar, self._status_bar) @property def template_manager(self): """Access to template manager""" if not hasattr(self, "_template_manager"): raise AttributeError("MainWindow must set _template_manager attribute") return self._template_manager # Common helper methods def _get_most_visible_page_index(self): """ Determine which page is most visible in the current viewport. Returns: int: Index of the most visible page """ if not hasattr(self.gl_widget, "_page_renderers") or not self.gl_widget._page_renderers: return self.gl_widget.current_page_index # Get viewport dimensions viewport_height = self.gl_widget.height() viewport_center_y = viewport_height / 2 # Find which page's center is closest to viewport center min_distance = float("inf") best_page_index = self.gl_widget.current_page_index for renderer, page in self.gl_widget._page_renderers: # Get page center Y position in screen coordinates page_height_mm = page.layout.size[1] page_height_px = page_height_mm * self.project.working_dpi / 25.4 page_center_y_offset = renderer.screen_y + (page_height_px * self.gl_widget.zoom_level / 2) # Calculate distance from viewport center distance = abs(page_center_y_offset - viewport_center_y) if distance < min_distance: min_distance = distance # Find the page index in project.pages try: best_page_index = self.project.pages.index(page) except ValueError: pass return best_page_index def get_current_page(self): """ Get currently visible page (most visible in viewport). Returns: Page instance or None if no page is selected """ if not self.project or not self.project.pages: return None index = self._get_most_visible_page_index() if 0 <= index < len(self.project.pages): return self.project.pages[index] return None def get_current_page_index(self) -> int: """ Get current page index. Returns: Current page index, or -1 if no page """ if not self.project or not self.project.pages: return -1 return int(self.gl_widget.current_page_index) def show_status(self, message: str, timeout: int = 2000): """ Show message in status bar. Args: message: Message to display timeout: Display duration in milliseconds """ if self.status_bar: self.status_bar.showMessage(message, timeout) def show_error(self, title: str, message: str): """ Show error dialog. Args: title: Dialog title message: Error message """ QMessageBox.critical(cast(QWidget, self), title, message) def show_warning(self, title: str, message: str): """ Show warning dialog. Args: title: Dialog title message: Warning message """ QMessageBox.warning(cast(QWidget, self), title, message) def show_info(self, title: str, message: str): """ Show information dialog. Args: title: Dialog title message: Information message """ QMessageBox.information(cast(QWidget, self), title, message) def require_page(self, show_warning: bool = True) -> bool: """ Check if a page is available and optionally show warning. Args: show_warning: Whether to show warning dialog if no page exists Returns: True if page exists, False otherwise """ current_page = self.get_current_page() if current_page is None: if show_warning: self.show_warning("No Page", "Please create a page first.") return False return True def require_selection(self, min_count: int = 1, show_warning: bool = True) -> bool: """ Check if required number of elements are selected. Args: min_count: Minimum number of selected elements required show_warning: Whether to show warning dialog if requirement not met Returns: True if requirements met, False otherwise """ selected_count = len(self.gl_widget.selected_elements) if selected_count < min_count: if show_warning: if min_count == 1: self.show_info("No Selection", "Please select an element.") else: self.show_info("Selection Required", f"Please select at least {min_count} elements.") return False return True def update_view(self): """Trigger GL widget update to refresh the view""" if self.gl_widget: self.gl_widget.update() # Update scrollbars to reflect new content if hasattr(self, "update_scrollbars"): self.update_scrollbars()