Coverage for pyPhotoAlbum/mixins/base.py: 99%
86 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
1"""
2Base mixin providing shared application state access
3"""
5from typing import Any, Optional, cast
6from PyQt6.QtWidgets import QStatusBar, QMessageBox, QWidget
9class ApplicationStateMixin:
10 """
11 Base mixin providing access to shared application state.
13 This mixin provides properties and helper methods for accessing
14 core application objects that are shared across all operation mixins.
16 Required attributes (must be set by MainWindow):
17 _project: Project instance
18 _gl_widget: GLWidget instance
19 _status_bar: QStatusBar instance
20 _template_manager: TemplateManager instance
21 """
23 @property
24 def project(self):
25 """Access to current project"""
26 if not hasattr(self, "_project"):
27 raise AttributeError("MainWindow must set _project attribute")
28 return self._project
30 @project.setter
31 def project(self, value):
32 """Set the current project"""
33 self._project = value
35 @property
36 def gl_widget(self):
37 """Access to GL rendering widget"""
38 if not hasattr(self, "_gl_widget"):
39 raise AttributeError("MainWindow must set _gl_widget attribute")
40 return self._gl_widget
42 @property
43 def status_bar(self) -> QStatusBar:
44 """Access to status bar"""
45 if not hasattr(self, "_status_bar"):
46 raise AttributeError("MainWindow must set _status_bar attribute")
47 return cast(QStatusBar, self._status_bar)
49 @property
50 def template_manager(self):
51 """Access to template manager"""
52 if not hasattr(self, "_template_manager"):
53 raise AttributeError("MainWindow must set _template_manager attribute")
54 return self._template_manager
56 # Common helper methods
58 def _get_most_visible_page_index(self):
59 """
60 Determine which page is most visible in the current viewport.
62 Returns:
63 int: Index of the most visible page
64 """
65 if not hasattr(self.gl_widget, "_page_renderers") or not self.gl_widget._page_renderers:
66 return self.gl_widget.current_page_index
68 # Get viewport dimensions
69 viewport_height = self.gl_widget.height()
70 viewport_center_y = viewport_height / 2
72 # Find which page's center is closest to viewport center
73 min_distance = float("inf")
74 best_page_index = self.gl_widget.current_page_index
76 for renderer, page in self.gl_widget._page_renderers:
77 # Get page center Y position in screen coordinates
78 page_height_mm = page.layout.size[1]
79 page_height_px = page_height_mm * self.project.working_dpi / 25.4
80 page_center_y_offset = renderer.screen_y + (page_height_px * self.gl_widget.zoom_level / 2)
82 # Calculate distance from viewport center
83 distance = abs(page_center_y_offset - viewport_center_y)
85 if distance < min_distance:
86 min_distance = distance
87 # Find the page index in project.pages
88 try:
89 best_page_index = self.project.pages.index(page)
90 except ValueError:
91 pass
93 return best_page_index
95 def get_current_page(self):
96 """
97 Get currently visible page (most visible in viewport).
99 Returns:
100 Page instance or None if no page is selected
101 """
102 if not self.project or not self.project.pages:
103 return None
105 index = self._get_most_visible_page_index()
106 if 0 <= index < len(self.project.pages):
107 return self.project.pages[index]
109 return None
111 def get_current_page_index(self) -> int:
112 """
113 Get current page index.
115 Returns:
116 Current page index, or -1 if no page
117 """
118 if not self.project or not self.project.pages:
119 return -1
120 return int(self.gl_widget.current_page_index)
122 def show_status(self, message: str, timeout: int = 2000):
123 """
124 Show message in status bar.
126 Args:
127 message: Message to display
128 timeout: Display duration in milliseconds
129 """
130 if self.status_bar:
131 self.status_bar.showMessage(message, timeout)
133 def show_error(self, title: str, message: str):
134 """
135 Show error dialog.
137 Args:
138 title: Dialog title
139 message: Error message
140 """
141 QMessageBox.critical(cast(QWidget, self), title, message)
143 def show_warning(self, title: str, message: str):
144 """
145 Show warning dialog.
147 Args:
148 title: Dialog title
149 message: Warning message
150 """
151 QMessageBox.warning(cast(QWidget, self), title, message)
153 def show_info(self, title: str, message: str):
154 """
155 Show information dialog.
157 Args:
158 title: Dialog title
159 message: Information message
160 """
161 QMessageBox.information(cast(QWidget, self), title, message)
163 def require_page(self, show_warning: bool = True) -> bool:
164 """
165 Check if a page is available and optionally show warning.
167 Args:
168 show_warning: Whether to show warning dialog if no page exists
170 Returns:
171 True if page exists, False otherwise
172 """
173 current_page = self.get_current_page()
175 if current_page is None:
176 if show_warning:
177 self.show_warning("No Page", "Please create a page first.")
178 return False
180 return True
182 def require_selection(self, min_count: int = 1, show_warning: bool = True) -> bool:
183 """
184 Check if required number of elements are selected.
186 Args:
187 min_count: Minimum number of selected elements required
188 show_warning: Whether to show warning dialog if requirement not met
190 Returns:
191 True if requirements met, False otherwise
192 """
193 selected_count = len(self.gl_widget.selected_elements)
195 if selected_count < min_count:
196 if show_warning:
197 if min_count == 1:
198 self.show_info("No Selection", "Please select an element.")
199 else:
200 self.show_info("Selection Required", f"Please select at least {min_count} elements.")
201 return False
203 return True
205 def update_view(self):
206 """Trigger GL widget update to refresh the view"""
207 if self.gl_widget:
208 self.gl_widget.update()
210 # Update scrollbars to reflect new content
211 if hasattr(self, "update_scrollbars"):
212 self.update_scrollbars()