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

1""" 

2Base mixin providing shared application state access 

3""" 

4 

5from typing import Any, Optional, cast 

6from PyQt6.QtWidgets import QStatusBar, QMessageBox, QWidget 

7 

8 

9class ApplicationStateMixin: 

10 """ 

11 Base mixin providing access to shared application state. 

12 

13 This mixin provides properties and helper methods for accessing 

14 core application objects that are shared across all operation mixins. 

15 

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 """ 

22 

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 

29 

30 @project.setter 

31 def project(self, value): 

32 """Set the current project""" 

33 self._project = value 

34 

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 

41 

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) 

48 

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 

55 

56 # Common helper methods 

57 

58 def _get_most_visible_page_index(self): 

59 """ 

60 Determine which page is most visible in the current viewport. 

61 

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 

67 

68 # Get viewport dimensions 

69 viewport_height = self.gl_widget.height() 

70 viewport_center_y = viewport_height / 2 

71 

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 

75 

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) 

81 

82 # Calculate distance from viewport center 

83 distance = abs(page_center_y_offset - viewport_center_y) 

84 

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 

92 

93 return best_page_index 

94 

95 def get_current_page(self): 

96 """ 

97 Get currently visible page (most visible in viewport). 

98 

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 

104 

105 index = self._get_most_visible_page_index() 

106 if 0 <= index < len(self.project.pages): 

107 return self.project.pages[index] 

108 

109 return None 

110 

111 def get_current_page_index(self) -> int: 

112 """ 

113 Get current page index. 

114 

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) 

121 

122 def show_status(self, message: str, timeout: int = 2000): 

123 """ 

124 Show message in status bar. 

125 

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) 

132 

133 def show_error(self, title: str, message: str): 

134 """ 

135 Show error dialog. 

136 

137 Args: 

138 title: Dialog title 

139 message: Error message 

140 """ 

141 QMessageBox.critical(cast(QWidget, self), title, message) 

142 

143 def show_warning(self, title: str, message: str): 

144 """ 

145 Show warning dialog. 

146 

147 Args: 

148 title: Dialog title 

149 message: Warning message 

150 """ 

151 QMessageBox.warning(cast(QWidget, self), title, message) 

152 

153 def show_info(self, title: str, message: str): 

154 """ 

155 Show information dialog. 

156 

157 Args: 

158 title: Dialog title 

159 message: Information message 

160 """ 

161 QMessageBox.information(cast(QWidget, self), title, message) 

162 

163 def require_page(self, show_warning: bool = True) -> bool: 

164 """ 

165 Check if a page is available and optionally show warning. 

166 

167 Args: 

168 show_warning: Whether to show warning dialog if no page exists 

169 

170 Returns: 

171 True if page exists, False otherwise 

172 """ 

173 current_page = self.get_current_page() 

174 

175 if current_page is None: 

176 if show_warning: 

177 self.show_warning("No Page", "Please create a page first.") 

178 return False 

179 

180 return True 

181 

182 def require_selection(self, min_count: int = 1, show_warning: bool = True) -> bool: 

183 """ 

184 Check if required number of elements are selected. 

185 

186 Args: 

187 min_count: Minimum number of selected elements required 

188 show_warning: Whether to show warning dialog if requirement not met 

189 

190 Returns: 

191 True if requirements met, False otherwise 

192 """ 

193 selected_count = len(self.gl_widget.selected_elements) 

194 

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 

202 

203 return True 

204 

205 def update_view(self): 

206 """Trigger GL widget update to refresh the view""" 

207 if self.gl_widget: 

208 self.gl_widget.update() 

209 

210 # Update scrollbars to reflect new content 

211 if hasattr(self, "update_scrollbars"): 

212 self.update_scrollbars()