1 """

2 Base mixin providing shared application state access

3 """

4

5 from typing import Any, Optional, cast

6 from PyQt6.QtWidgets import QStatusBar, QMessageBox, QWidget

7

8

9 class 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()