pomera-ai-commander 1.1.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1199 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_directory.py +549 -0
  12. package/core/data_validator.py +1066 -1066
  13. package/core/database_connection_manager.py +744 -744
  14. package/core/database_curl_settings_manager.py +608 -608
  15. package/core/database_promera_ai_settings_manager.py +446 -446
  16. package/core/database_schema.py +411 -411
  17. package/core/database_schema_manager.py +395 -395
  18. package/core/database_settings_manager.py +1507 -1507
  19. package/core/database_settings_manager_interface.py +456 -456
  20. package/core/dialog_manager.py +734 -734
  21. package/core/diff_utils.py +239 -0
  22. package/core/efficient_line_numbers.py +540 -510
  23. package/core/error_handler.py +746 -746
  24. package/core/error_service.py +431 -431
  25. package/core/event_consolidator.py +511 -511
  26. package/core/mcp/__init__.py +43 -43
  27. package/core/mcp/find_replace_diff.py +334 -0
  28. package/core/mcp/protocol.py +288 -288
  29. package/core/mcp/schema.py +251 -251
  30. package/core/mcp/server_stdio.py +299 -299
  31. package/core/mcp/tool_registry.py +2699 -2345
  32. package/core/memento.py +275 -0
  33. package/core/memory_efficient_text_widget.py +711 -711
  34. package/core/migration_manager.py +914 -914
  35. package/core/migration_test_suite.py +1085 -1085
  36. package/core/migration_validator.py +1143 -1143
  37. package/core/optimized_find_replace.py +714 -714
  38. package/core/optimized_pattern_engine.py +424 -424
  39. package/core/optimized_search_highlighter.py +552 -552
  40. package/core/performance_monitor.py +674 -674
  41. package/core/persistence_manager.py +712 -712
  42. package/core/progressive_stats_calculator.py +632 -632
  43. package/core/regex_pattern_cache.py +529 -529
  44. package/core/regex_pattern_library.py +350 -350
  45. package/core/search_operation_manager.py +434 -434
  46. package/core/settings_defaults_registry.py +1087 -1087
  47. package/core/settings_integrity_validator.py +1111 -1111
  48. package/core/settings_serializer.py +557 -557
  49. package/core/settings_validator.py +1823 -1823
  50. package/core/smart_stats_calculator.py +709 -709
  51. package/core/statistics_update_manager.py +619 -619
  52. package/core/stats_config_manager.py +858 -858
  53. package/core/streaming_text_handler.py +723 -723
  54. package/core/task_scheduler.py +596 -596
  55. package/core/update_pattern_library.py +168 -168
  56. package/core/visibility_monitor.py +596 -596
  57. package/core/widget_cache.py +498 -498
  58. package/mcp.json +51 -61
  59. package/migrate_data.py +127 -0
  60. package/package.json +64 -57
  61. package/pomera.py +7883 -7482
  62. package/pomera_mcp_server.py +183 -144
  63. package/requirements.txt +33 -0
  64. package/scripts/Dockerfile.alpine +43 -0
  65. package/scripts/Dockerfile.gui-test +54 -0
  66. package/scripts/Dockerfile.linux +43 -0
  67. package/scripts/Dockerfile.test-linux +80 -0
  68. package/scripts/Dockerfile.ubuntu +39 -0
  69. package/scripts/README.md +53 -0
  70. package/scripts/build-all.bat +113 -0
  71. package/scripts/build-docker.bat +53 -0
  72. package/scripts/build-docker.sh +55 -0
  73. package/scripts/build-optimized.bat +101 -0
  74. package/scripts/build.sh +78 -0
  75. package/scripts/docker-compose.test.yml +27 -0
  76. package/scripts/docker-compose.yml +32 -0
  77. package/scripts/postinstall.js +62 -0
  78. package/scripts/requirements-minimal.txt +33 -0
  79. package/scripts/test-linux-simple.bat +28 -0
  80. package/scripts/validate-release-workflow.py +450 -0
  81. package/tools/__init__.py +4 -4
  82. package/tools/ai_tools.py +2891 -2891
  83. package/tools/ascii_art_generator.py +352 -352
  84. package/tools/base64_tools.py +183 -183
  85. package/tools/base_tool.py +511 -511
  86. package/tools/case_tool.py +308 -308
  87. package/tools/column_tools.py +395 -395
  88. package/tools/cron_tool.py +884 -884
  89. package/tools/curl_history.py +600 -600
  90. package/tools/curl_processor.py +1207 -1207
  91. package/tools/curl_settings.py +502 -502
  92. package/tools/curl_tool.py +5467 -5467
  93. package/tools/diff_viewer.py +1817 -1072
  94. package/tools/email_extraction_tool.py +248 -248
  95. package/tools/email_header_analyzer.py +425 -425
  96. package/tools/extraction_tools.py +250 -250
  97. package/tools/find_replace.py +2289 -1750
  98. package/tools/folder_file_reporter.py +1463 -1463
  99. package/tools/folder_file_reporter_adapter.py +480 -480
  100. package/tools/generator_tools.py +1216 -1216
  101. package/tools/hash_generator.py +255 -255
  102. package/tools/html_tool.py +656 -656
  103. package/tools/jsonxml_tool.py +729 -729
  104. package/tools/line_tools.py +419 -419
  105. package/tools/markdown_tools.py +561 -561
  106. package/tools/mcp_widget.py +1417 -1417
  107. package/tools/notes_widget.py +978 -973
  108. package/tools/number_base_converter.py +372 -372
  109. package/tools/regex_extractor.py +571 -571
  110. package/tools/slug_generator.py +310 -310
  111. package/tools/sorter_tools.py +458 -458
  112. package/tools/string_escape_tool.py +392 -392
  113. package/tools/text_statistics_tool.py +365 -365
  114. package/tools/text_wrapper.py +430 -430
  115. package/tools/timestamp_converter.py +421 -421
  116. package/tools/tool_loader.py +710 -710
  117. package/tools/translator_tools.py +522 -522
  118. package/tools/url_link_extractor.py +261 -261
  119. package/tools/url_parser.py +204 -204
  120. package/tools/whitespace_tools.py +355 -355
  121. package/tools/word_frequency_counter.py +146 -146
  122. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  123. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  124. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  125. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  126. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  127. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  128. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  129. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  131. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  132. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  134. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  135. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  136. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  137. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  138. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  139. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  140. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  141. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  142. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  143. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  144. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  145. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  146. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  147. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  148. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  149. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  150. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  151. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  152. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  153. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  154. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  155. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  156. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  157. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  158. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  159. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  160. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  161. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  162. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  163. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  164. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  165. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  166. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  167. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  168. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  169. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  170. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  192. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  193. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  194. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  195. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  196. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  197. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  198. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  199. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  200. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  201. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  202. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  203. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  204. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  205. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  206. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  207. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  208. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  209. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  210. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  211. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  212. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  213. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,596 +1,596 @@
1
- """
2
- Visibility Monitor System for Pomera AI Commander.
3
-
4
- This module provides a dedicated system for tracking component visibility states,
5
- including tab visibility, window focus, and minimization states. It enables
6
- automatic detection of when statistics bars are hidden to skip unnecessary calculations.
7
-
8
- Requirements addressed:
9
- - 3.1: Skip statistics updates for inactive tabs
10
- - 3.2: Pause statistics updates when application window is minimized
11
- - 3.3: Skip calculations when statistics bars are not visible
12
- """
13
-
14
- import tkinter as tk
15
- import threading
16
- import weakref
17
- from typing import Dict, Optional, Callable, Set, Any
18
- from dataclasses import dataclass, field
19
- from enum import Enum
20
- import time
21
-
22
-
23
- class VisibilityState(Enum):
24
- """Visibility states for components."""
25
- VISIBLE_ACTIVE = "visible_active" # Currently visible and active
26
- VISIBLE_INACTIVE = "visible_inactive" # Visible but not active
27
- HIDDEN = "hidden" # Not visible (hidden tab)
28
- MINIMIZED = "minimized" # Window is minimized
29
- UNKNOWN = "unknown" # State not yet determined
30
-
31
-
32
- class WindowState(Enum):
33
- """Window states."""
34
- NORMAL = "normal"
35
- MINIMIZED = "minimized"
36
- MAXIMIZED = "maximized"
37
- WITHDRAWN = "withdrawn"
38
-
39
-
40
- @dataclass
41
- class ComponentVisibility:
42
- """Visibility information for a component."""
43
- component_id: str
44
- widget_ref: weakref.ref
45
- visibility_state: VisibilityState = VisibilityState.UNKNOWN
46
- is_tab_visible: bool = True
47
- is_widget_visible: bool = True
48
- is_window_focused: bool = True
49
- last_state_change: float = field(default_factory=time.time)
50
- state_change_count: int = 0
51
-
52
- @property
53
- def widget(self):
54
- """Get the actual widget from the weak reference."""
55
- return self.widget_ref() if self.widget_ref else None
56
-
57
- @property
58
- def is_valid(self) -> bool:
59
- """Check if the widget still exists."""
60
- return self.widget is not None
61
-
62
- @property
63
- def time_since_state_change(self) -> float:
64
- """Get time since last state change in seconds."""
65
- return time.time() - self.last_state_change
66
-
67
- def update_state(self, new_state: VisibilityState) -> bool:
68
- """
69
- Update the visibility state.
70
-
71
- Args:
72
- new_state: New visibility state
73
-
74
- Returns:
75
- True if state changed, False otherwise
76
- """
77
- if self.visibility_state != new_state:
78
- self.visibility_state = new_state
79
- self.last_state_change = time.time()
80
- self.state_change_count += 1
81
- return True
82
- return False
83
-
84
-
85
- class VisibilityMonitor:
86
- """
87
- Monitors component visibility states including tab visibility, window focus,
88
- and minimization states.
89
-
90
- This class provides automatic visibility state tracking and callbacks for
91
- state changes, enabling optimizations like skipping calculations for hidden
92
- components.
93
- """
94
-
95
- def __init__(self):
96
- """Initialize the visibility monitor."""
97
- # Component registry
98
- self.components: Dict[str, ComponentVisibility] = {}
99
-
100
- # Window state tracking
101
- self.window_state = WindowState.NORMAL
102
- self.window_focused = True
103
-
104
- # Tab tracking
105
- self.active_tabs: Set[str] = set()
106
- self.tab_to_components: Dict[str, Set[str]] = {}
107
-
108
- # Callbacks for state changes
109
- self.state_change_callbacks: Dict[str, list] = {}
110
-
111
- # Global visibility change callbacks
112
- self.global_callbacks: list = []
113
-
114
- # Thread safety
115
- self.lock = threading.RLock()
116
-
117
- # Tkinter root reference
118
- self._tk_root: Optional[tk.Tk] = None
119
-
120
- # Statistics
121
- self.stats = {
122
- 'total_state_changes': 0,
123
- 'components_tracked': 0,
124
- 'callbacks_executed': 0
125
- }
126
-
127
- def set_tk_root(self, root: tk.Tk):
128
- """
129
- Set the Tkinter root and bind window events.
130
-
131
- Args:
132
- root: Tkinter root window
133
- """
134
- self._tk_root = root
135
-
136
- # Bind window state events
137
- try:
138
- root.bind("<Unmap>", self._on_window_unmapped)
139
- root.bind("<Map>", self._on_window_mapped)
140
- root.bind("<FocusIn>", self._on_window_focus_in)
141
- root.bind("<FocusOut>", self._on_window_focus_out)
142
- except tk.TclError as e:
143
- print(f"Warning: Could not bind window events: {e}")
144
-
145
- def register_component(self, component_id: str, widget: tk.Widget,
146
- tab_id: Optional[str] = None,
147
- initial_state: VisibilityState = VisibilityState.VISIBLE_ACTIVE) -> None:
148
- """
149
- Register a component for visibility tracking.
150
-
151
- Args:
152
- component_id: Unique identifier for the component
153
- widget: The widget to monitor
154
- tab_id: Optional tab identifier if component is in a tab
155
- initial_state: Initial visibility state
156
- """
157
- with self.lock:
158
- # Create component visibility info
159
- comp_vis = ComponentVisibility(
160
- component_id=component_id,
161
- widget_ref=weakref.ref(widget),
162
- visibility_state=initial_state
163
- )
164
-
165
- self.components[component_id] = comp_vis
166
-
167
- # Track tab association
168
- if tab_id:
169
- if tab_id not in self.tab_to_components:
170
- self.tab_to_components[tab_id] = set()
171
- self.tab_to_components[tab_id].add(component_id)
172
-
173
- self.stats['components_tracked'] = len(self.components)
174
-
175
- # Bind widget visibility events
176
- self._bind_widget_events(widget, component_id)
177
-
178
- def unregister_component(self, component_id: str) -> None:
179
- """
180
- Unregister a component from visibility tracking.
181
-
182
- Args:
183
- component_id: Component identifier
184
- """
185
- with self.lock:
186
- # Remove from components
187
- comp_vis = self.components.pop(component_id, None)
188
-
189
- # Remove from tab associations
190
- for tab_id, components in self.tab_to_components.items():
191
- components.discard(component_id)
192
-
193
- # Remove callbacks
194
- self.state_change_callbacks.pop(component_id, None)
195
-
196
- self.stats['components_tracked'] = len(self.components)
197
-
198
- def _bind_widget_events(self, widget: tk.Widget, component_id: str):
199
- """
200
- Bind visibility-related events to a widget.
201
-
202
- Args:
203
- widget: Widget to bind events to
204
- component_id: Component identifier
205
- """
206
- try:
207
- # Bind visibility change events
208
- widget.bind("<Visibility>", lambda e: self._on_widget_visibility_change(component_id, e))
209
- widget.bind("<Unmap>", lambda e: self._on_widget_unmapped(component_id))
210
- widget.bind("<Map>", lambda e: self._on_widget_mapped(component_id))
211
- except tk.TclError:
212
- pass # Some widgets may not support these events
213
-
214
- def set_tab_active(self, tab_id: str, active: bool = True) -> None:
215
- """
216
- Set whether a tab is active (visible).
217
-
218
- Args:
219
- tab_id: Tab identifier
220
- active: True if tab is active, False otherwise
221
- """
222
- with self.lock:
223
- if active:
224
- self.active_tabs.add(tab_id)
225
- else:
226
- self.active_tabs.discard(tab_id)
227
-
228
- # Update all components in this tab
229
- components = self.tab_to_components.get(tab_id, set())
230
- for component_id in components:
231
- self._update_component_visibility(component_id)
232
-
233
- def set_component_visibility(self, component_id: str, visible: bool) -> None:
234
- """
235
- Manually set component visibility.
236
-
237
- Args:
238
- component_id: Component identifier
239
- visible: True if visible, False otherwise
240
- """
241
- with self.lock:
242
- comp_vis = self.components.get(component_id)
243
- if comp_vis:
244
- comp_vis.is_widget_visible = visible
245
- self._update_component_visibility(component_id)
246
-
247
- def get_visibility_state(self, component_id: str) -> Optional[VisibilityState]:
248
- """
249
- Get the current visibility state of a component.
250
-
251
- Args:
252
- component_id: Component identifier
253
-
254
- Returns:
255
- Current visibility state or None if not found
256
- """
257
- with self.lock:
258
- comp_vis = self.components.get(component_id)
259
- return comp_vis.visibility_state if comp_vis else None
260
-
261
- def is_component_visible(self, component_id: str) -> bool:
262
- """
263
- Check if a component is currently visible.
264
-
265
- Args:
266
- component_id: Component identifier
267
-
268
- Returns:
269
- True if component is visible (not hidden or minimized)
270
- """
271
- state = self.get_visibility_state(component_id)
272
- return state in [VisibilityState.VISIBLE_ACTIVE, VisibilityState.VISIBLE_INACTIVE]
273
-
274
- def is_component_active(self, component_id: str) -> bool:
275
- """
276
- Check if a component is currently visible and active.
277
-
278
- Args:
279
- component_id: Component identifier
280
-
281
- Returns:
282
- True if component is visible and active
283
- """
284
- return self.get_visibility_state(component_id) == VisibilityState.VISIBLE_ACTIVE
285
-
286
- def register_state_change_callback(self, component_id: str,
287
- callback: Callable[[str, VisibilityState, VisibilityState], None]) -> None:
288
- """
289
- Register a callback for component state changes.
290
-
291
- Args:
292
- component_id: Component identifier
293
- callback: Callback function(component_id, old_state, new_state)
294
- """
295
- with self.lock:
296
- if component_id not in self.state_change_callbacks:
297
- self.state_change_callbacks[component_id] = []
298
- self.state_change_callbacks[component_id].append(callback)
299
-
300
- def register_global_callback(self, callback: Callable[[str, VisibilityState, VisibilityState], None]) -> None:
301
- """
302
- Register a global callback for any component state change.
303
-
304
- Args:
305
- callback: Callback function(component_id, old_state, new_state)
306
- """
307
- with self.lock:
308
- self.global_callbacks.append(callback)
309
-
310
- def _update_component_visibility(self, component_id: str) -> None:
311
- """
312
- Update the visibility state of a component based on all factors.
313
-
314
- Args:
315
- component_id: Component identifier
316
- """
317
- comp_vis = self.components.get(component_id)
318
- if not comp_vis or not comp_vis.is_valid:
319
- return
320
-
321
- # Determine new state
322
- new_state = self._calculate_visibility_state(comp_vis)
323
-
324
- # Update state and trigger callbacks if changed
325
- old_state = comp_vis.visibility_state
326
- if comp_vis.update_state(new_state):
327
- self.stats['total_state_changes'] += 1
328
- self._trigger_callbacks(component_id, old_state, new_state)
329
-
330
- def _calculate_visibility_state(self, comp_vis: ComponentVisibility) -> VisibilityState:
331
- """
332
- Calculate the visibility state based on all factors.
333
-
334
- Args:
335
- comp_vis: Component visibility info
336
-
337
- Returns:
338
- Calculated visibility state
339
- """
340
- # Check window state first
341
- if self.window_state == WindowState.MINIMIZED:
342
- return VisibilityState.MINIMIZED
343
-
344
- # Check if widget is visible
345
- if not comp_vis.is_widget_visible:
346
- return VisibilityState.HIDDEN
347
-
348
- # Check tab visibility
349
- if not comp_vis.is_tab_visible:
350
- return VisibilityState.HIDDEN
351
-
352
- # Check if window is focused
353
- if not self.window_focused:
354
- return VisibilityState.VISIBLE_INACTIVE
355
-
356
- # Widget is visible and window is focused
357
- return VisibilityState.VISIBLE_ACTIVE
358
-
359
- def _trigger_callbacks(self, component_id: str, old_state: VisibilityState,
360
- new_state: VisibilityState) -> None:
361
- """
362
- Trigger callbacks for a state change.
363
-
364
- Args:
365
- component_id: Component identifier
366
- old_state: Previous visibility state
367
- new_state: New visibility state
368
- """
369
- # Component-specific callbacks
370
- callbacks = self.state_change_callbacks.get(component_id, [])
371
- for callback in callbacks:
372
- try:
373
- callback(component_id, old_state, new_state)
374
- self.stats['callbacks_executed'] += 1
375
- except Exception as e:
376
- print(f"Error in visibility callback for {component_id}: {e}")
377
-
378
- # Global callbacks
379
- for callback in self.global_callbacks:
380
- try:
381
- callback(component_id, old_state, new_state)
382
- self.stats['callbacks_executed'] += 1
383
- except Exception as e:
384
- print(f"Error in global visibility callback: {e}")
385
-
386
- def _on_window_unmapped(self, event=None) -> None:
387
- """Handle window unmap event (minimization)."""
388
- with self.lock:
389
- self.window_state = WindowState.MINIMIZED
390
-
391
- # Update all components
392
- for component_id in list(self.components.keys()):
393
- self._update_component_visibility(component_id)
394
-
395
- def _on_window_mapped(self, event=None) -> None:
396
- """Handle window map event (restoration)."""
397
- with self.lock:
398
- self.window_state = WindowState.NORMAL
399
-
400
- # Update all components
401
- for component_id in list(self.components.keys()):
402
- self._update_component_visibility(component_id)
403
-
404
- def _on_window_focus_in(self, event=None) -> None:
405
- """Handle window focus in event."""
406
- with self.lock:
407
- self.window_focused = True
408
-
409
- # Update all components
410
- for component_id in list(self.components.keys()):
411
- self._update_component_visibility(component_id)
412
-
413
- def _on_window_focus_out(self, event=None) -> None:
414
- """Handle window focus out event."""
415
- with self.lock:
416
- self.window_focused = False
417
-
418
- # Update all components
419
- for component_id in list(self.components.keys()):
420
- self._update_component_visibility(component_id)
421
-
422
- def _on_widget_visibility_change(self, component_id: str, event) -> None:
423
- """
424
- Handle widget visibility change event.
425
-
426
- Args:
427
- component_id: Component identifier
428
- event: Tkinter event
429
- """
430
- with self.lock:
431
- comp_vis = self.components.get(component_id)
432
- if comp_vis:
433
- # Check if widget is actually visible
434
- try:
435
- widget = comp_vis.widget
436
- if widget:
437
- # Widget is visible if it has non-zero dimensions
438
- visible = widget.winfo_viewable()
439
- comp_vis.is_widget_visible = visible
440
- self._update_component_visibility(component_id)
441
- except tk.TclError:
442
- pass
443
-
444
- def _on_widget_unmapped(self, component_id: str) -> None:
445
- """
446
- Handle widget unmap event.
447
-
448
- Args:
449
- component_id: Component identifier
450
- """
451
- with self.lock:
452
- comp_vis = self.components.get(component_id)
453
- if comp_vis:
454
- comp_vis.is_widget_visible = False
455
- self._update_component_visibility(component_id)
456
-
457
- def _on_widget_mapped(self, component_id: str) -> None:
458
- """
459
- Handle widget map event.
460
-
461
- Args:
462
- component_id: Component identifier
463
- """
464
- with self.lock:
465
- comp_vis = self.components.get(component_id)
466
- if comp_vis:
467
- comp_vis.is_widget_visible = True
468
- self._update_component_visibility(component_id)
469
-
470
- def get_component_info(self, component_id: str) -> Optional[Dict[str, Any]]:
471
- """
472
- Get detailed information about a component.
473
-
474
- Args:
475
- component_id: Component identifier
476
-
477
- Returns:
478
- Dictionary with component information or None if not found
479
- """
480
- with self.lock:
481
- comp_vis = self.components.get(component_id)
482
- if not comp_vis:
483
- return None
484
-
485
- return {
486
- 'component_id': comp_vis.component_id,
487
- 'visibility_state': comp_vis.visibility_state.value,
488
- 'is_valid': comp_vis.is_valid,
489
- 'is_tab_visible': comp_vis.is_tab_visible,
490
- 'is_widget_visible': comp_vis.is_widget_visible,
491
- 'is_window_focused': comp_vis.is_window_focused,
492
- 'time_since_state_change': comp_vis.time_since_state_change,
493
- 'state_change_count': comp_vis.state_change_count
494
- }
495
-
496
- def get_all_visible_components(self) -> Set[str]:
497
- """
498
- Get all currently visible component IDs.
499
-
500
- Returns:
501
- Set of visible component IDs
502
- """
503
- with self.lock:
504
- return {
505
- comp_id for comp_id, comp_vis in self.components.items()
506
- if comp_vis.visibility_state in [VisibilityState.VISIBLE_ACTIVE,
507
- VisibilityState.VISIBLE_INACTIVE]
508
- }
509
-
510
- def get_all_active_components(self) -> Set[str]:
511
- """
512
- Get all currently active (visible and focused) component IDs.
513
-
514
- Returns:
515
- Set of active component IDs
516
- """
517
- with self.lock:
518
- return {
519
- comp_id for comp_id, comp_vis in self.components.items()
520
- if comp_vis.visibility_state == VisibilityState.VISIBLE_ACTIVE
521
- }
522
-
523
- def get_statistics(self) -> Dict[str, Any]:
524
- """
525
- Get statistics about visibility monitoring.
526
-
527
- Returns:
528
- Dictionary with statistics
529
- """
530
- with self.lock:
531
- stats = self.stats.copy()
532
-
533
- # Add current state information
534
- stats['window_state'] = self.window_state.value
535
- stats['window_focused'] = self.window_focused
536
- stats['active_tabs'] = len(self.active_tabs)
537
- stats['total_tabs'] = len(self.tab_to_components)
538
-
539
- # Add visibility breakdown
540
- visibility_counts = {}
541
- for comp_vis in self.components.values():
542
- state = comp_vis.visibility_state.value
543
- visibility_counts[state] = visibility_counts.get(state, 0) + 1
544
- stats['visibility_breakdown'] = visibility_counts
545
-
546
- # Add callback information
547
- stats['registered_callbacks'] = sum(len(cbs) for cbs in self.state_change_callbacks.values())
548
- stats['global_callbacks'] = len(self.global_callbacks)
549
-
550
- return stats
551
-
552
- def cleanup_invalid_components(self) -> int:
553
- """
554
- Clean up components whose widgets have been destroyed.
555
-
556
- Returns:
557
- Number of components cleaned up
558
- """
559
- with self.lock:
560
- invalid_ids = [
561
- comp_id for comp_id, comp_vis in self.components.items()
562
- if not comp_vis.is_valid
563
- ]
564
-
565
- for comp_id in invalid_ids:
566
- self.unregister_component(comp_id)
567
-
568
- return len(invalid_ids)
569
-
570
- def force_update_all(self) -> None:
571
- """Force update of all component visibility states."""
572
- with self.lock:
573
- for component_id in list(self.components.keys()):
574
- self._update_component_visibility(component_id)
575
-
576
-
577
- # Global instance for easy access
578
- _global_visibility_monitor: Optional[VisibilityMonitor] = None
579
-
580
-
581
- def get_visibility_monitor() -> VisibilityMonitor:
582
- """Get the global visibility monitor instance."""
583
- global _global_visibility_monitor
584
- if _global_visibility_monitor is None:
585
- _global_visibility_monitor = VisibilityMonitor()
586
- return _global_visibility_monitor
587
-
588
-
589
- def create_visibility_monitor() -> VisibilityMonitor:
590
- """
591
- Create a new visibility monitor instance.
592
-
593
- Returns:
594
- New VisibilityMonitor instance
595
- """
596
- return VisibilityMonitor()
1
+ """
2
+ Visibility Monitor System for Pomera AI Commander.
3
+
4
+ This module provides a dedicated system for tracking component visibility states,
5
+ including tab visibility, window focus, and minimization states. It enables
6
+ automatic detection of when statistics bars are hidden to skip unnecessary calculations.
7
+
8
+ Requirements addressed:
9
+ - 3.1: Skip statistics updates for inactive tabs
10
+ - 3.2: Pause statistics updates when application window is minimized
11
+ - 3.3: Skip calculations when statistics bars are not visible
12
+ """
13
+
14
+ import tkinter as tk
15
+ import threading
16
+ import weakref
17
+ from typing import Dict, Optional, Callable, Set, Any
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ import time
21
+
22
+
23
+ class VisibilityState(Enum):
24
+ """Visibility states for components."""
25
+ VISIBLE_ACTIVE = "visible_active" # Currently visible and active
26
+ VISIBLE_INACTIVE = "visible_inactive" # Visible but not active
27
+ HIDDEN = "hidden" # Not visible (hidden tab)
28
+ MINIMIZED = "minimized" # Window is minimized
29
+ UNKNOWN = "unknown" # State not yet determined
30
+
31
+
32
+ class WindowState(Enum):
33
+ """Window states."""
34
+ NORMAL = "normal"
35
+ MINIMIZED = "minimized"
36
+ MAXIMIZED = "maximized"
37
+ WITHDRAWN = "withdrawn"
38
+
39
+
40
+ @dataclass
41
+ class ComponentVisibility:
42
+ """Visibility information for a component."""
43
+ component_id: str
44
+ widget_ref: weakref.ref
45
+ visibility_state: VisibilityState = VisibilityState.UNKNOWN
46
+ is_tab_visible: bool = True
47
+ is_widget_visible: bool = True
48
+ is_window_focused: bool = True
49
+ last_state_change: float = field(default_factory=time.time)
50
+ state_change_count: int = 0
51
+
52
+ @property
53
+ def widget(self):
54
+ """Get the actual widget from the weak reference."""
55
+ return self.widget_ref() if self.widget_ref else None
56
+
57
+ @property
58
+ def is_valid(self) -> bool:
59
+ """Check if the widget still exists."""
60
+ return self.widget is not None
61
+
62
+ @property
63
+ def time_since_state_change(self) -> float:
64
+ """Get time since last state change in seconds."""
65
+ return time.time() - self.last_state_change
66
+
67
+ def update_state(self, new_state: VisibilityState) -> bool:
68
+ """
69
+ Update the visibility state.
70
+
71
+ Args:
72
+ new_state: New visibility state
73
+
74
+ Returns:
75
+ True if state changed, False otherwise
76
+ """
77
+ if self.visibility_state != new_state:
78
+ self.visibility_state = new_state
79
+ self.last_state_change = time.time()
80
+ self.state_change_count += 1
81
+ return True
82
+ return False
83
+
84
+
85
+ class VisibilityMonitor:
86
+ """
87
+ Monitors component visibility states including tab visibility, window focus,
88
+ and minimization states.
89
+
90
+ This class provides automatic visibility state tracking and callbacks for
91
+ state changes, enabling optimizations like skipping calculations for hidden
92
+ components.
93
+ """
94
+
95
+ def __init__(self):
96
+ """Initialize the visibility monitor."""
97
+ # Component registry
98
+ self.components: Dict[str, ComponentVisibility] = {}
99
+
100
+ # Window state tracking
101
+ self.window_state = WindowState.NORMAL
102
+ self.window_focused = True
103
+
104
+ # Tab tracking
105
+ self.active_tabs: Set[str] = set()
106
+ self.tab_to_components: Dict[str, Set[str]] = {}
107
+
108
+ # Callbacks for state changes
109
+ self.state_change_callbacks: Dict[str, list] = {}
110
+
111
+ # Global visibility change callbacks
112
+ self.global_callbacks: list = []
113
+
114
+ # Thread safety
115
+ self.lock = threading.RLock()
116
+
117
+ # Tkinter root reference
118
+ self._tk_root: Optional[tk.Tk] = None
119
+
120
+ # Statistics
121
+ self.stats = {
122
+ 'total_state_changes': 0,
123
+ 'components_tracked': 0,
124
+ 'callbacks_executed': 0
125
+ }
126
+
127
+ def set_tk_root(self, root: tk.Tk):
128
+ """
129
+ Set the Tkinter root and bind window events.
130
+
131
+ Args:
132
+ root: Tkinter root window
133
+ """
134
+ self._tk_root = root
135
+
136
+ # Bind window state events
137
+ try:
138
+ root.bind("<Unmap>", self._on_window_unmapped)
139
+ root.bind("<Map>", self._on_window_mapped)
140
+ root.bind("<FocusIn>", self._on_window_focus_in)
141
+ root.bind("<FocusOut>", self._on_window_focus_out)
142
+ except tk.TclError as e:
143
+ print(f"Warning: Could not bind window events: {e}")
144
+
145
+ def register_component(self, component_id: str, widget: tk.Widget,
146
+ tab_id: Optional[str] = None,
147
+ initial_state: VisibilityState = VisibilityState.VISIBLE_ACTIVE) -> None:
148
+ """
149
+ Register a component for visibility tracking.
150
+
151
+ Args:
152
+ component_id: Unique identifier for the component
153
+ widget: The widget to monitor
154
+ tab_id: Optional tab identifier if component is in a tab
155
+ initial_state: Initial visibility state
156
+ """
157
+ with self.lock:
158
+ # Create component visibility info
159
+ comp_vis = ComponentVisibility(
160
+ component_id=component_id,
161
+ widget_ref=weakref.ref(widget),
162
+ visibility_state=initial_state
163
+ )
164
+
165
+ self.components[component_id] = comp_vis
166
+
167
+ # Track tab association
168
+ if tab_id:
169
+ if tab_id not in self.tab_to_components:
170
+ self.tab_to_components[tab_id] = set()
171
+ self.tab_to_components[tab_id].add(component_id)
172
+
173
+ self.stats['components_tracked'] = len(self.components)
174
+
175
+ # Bind widget visibility events
176
+ self._bind_widget_events(widget, component_id)
177
+
178
+ def unregister_component(self, component_id: str) -> None:
179
+ """
180
+ Unregister a component from visibility tracking.
181
+
182
+ Args:
183
+ component_id: Component identifier
184
+ """
185
+ with self.lock:
186
+ # Remove from components
187
+ comp_vis = self.components.pop(component_id, None)
188
+
189
+ # Remove from tab associations
190
+ for tab_id, components in self.tab_to_components.items():
191
+ components.discard(component_id)
192
+
193
+ # Remove callbacks
194
+ self.state_change_callbacks.pop(component_id, None)
195
+
196
+ self.stats['components_tracked'] = len(self.components)
197
+
198
+ def _bind_widget_events(self, widget: tk.Widget, component_id: str):
199
+ """
200
+ Bind visibility-related events to a widget.
201
+
202
+ Args:
203
+ widget: Widget to bind events to
204
+ component_id: Component identifier
205
+ """
206
+ try:
207
+ # Bind visibility change events
208
+ widget.bind("<Visibility>", lambda e: self._on_widget_visibility_change(component_id, e))
209
+ widget.bind("<Unmap>", lambda e: self._on_widget_unmapped(component_id))
210
+ widget.bind("<Map>", lambda e: self._on_widget_mapped(component_id))
211
+ except tk.TclError:
212
+ pass # Some widgets may not support these events
213
+
214
+ def set_tab_active(self, tab_id: str, active: bool = True) -> None:
215
+ """
216
+ Set whether a tab is active (visible).
217
+
218
+ Args:
219
+ tab_id: Tab identifier
220
+ active: True if tab is active, False otherwise
221
+ """
222
+ with self.lock:
223
+ if active:
224
+ self.active_tabs.add(tab_id)
225
+ else:
226
+ self.active_tabs.discard(tab_id)
227
+
228
+ # Update all components in this tab
229
+ components = self.tab_to_components.get(tab_id, set())
230
+ for component_id in components:
231
+ self._update_component_visibility(component_id)
232
+
233
+ def set_component_visibility(self, component_id: str, visible: bool) -> None:
234
+ """
235
+ Manually set component visibility.
236
+
237
+ Args:
238
+ component_id: Component identifier
239
+ visible: True if visible, False otherwise
240
+ """
241
+ with self.lock:
242
+ comp_vis = self.components.get(component_id)
243
+ if comp_vis:
244
+ comp_vis.is_widget_visible = visible
245
+ self._update_component_visibility(component_id)
246
+
247
+ def get_visibility_state(self, component_id: str) -> Optional[VisibilityState]:
248
+ """
249
+ Get the current visibility state of a component.
250
+
251
+ Args:
252
+ component_id: Component identifier
253
+
254
+ Returns:
255
+ Current visibility state or None if not found
256
+ """
257
+ with self.lock:
258
+ comp_vis = self.components.get(component_id)
259
+ return comp_vis.visibility_state if comp_vis else None
260
+
261
+ def is_component_visible(self, component_id: str) -> bool:
262
+ """
263
+ Check if a component is currently visible.
264
+
265
+ Args:
266
+ component_id: Component identifier
267
+
268
+ Returns:
269
+ True if component is visible (not hidden or minimized)
270
+ """
271
+ state = self.get_visibility_state(component_id)
272
+ return state in [VisibilityState.VISIBLE_ACTIVE, VisibilityState.VISIBLE_INACTIVE]
273
+
274
+ def is_component_active(self, component_id: str) -> bool:
275
+ """
276
+ Check if a component is currently visible and active.
277
+
278
+ Args:
279
+ component_id: Component identifier
280
+
281
+ Returns:
282
+ True if component is visible and active
283
+ """
284
+ return self.get_visibility_state(component_id) == VisibilityState.VISIBLE_ACTIVE
285
+
286
+ def register_state_change_callback(self, component_id: str,
287
+ callback: Callable[[str, VisibilityState, VisibilityState], None]) -> None:
288
+ """
289
+ Register a callback for component state changes.
290
+
291
+ Args:
292
+ component_id: Component identifier
293
+ callback: Callback function(component_id, old_state, new_state)
294
+ """
295
+ with self.lock:
296
+ if component_id not in self.state_change_callbacks:
297
+ self.state_change_callbacks[component_id] = []
298
+ self.state_change_callbacks[component_id].append(callback)
299
+
300
+ def register_global_callback(self, callback: Callable[[str, VisibilityState, VisibilityState], None]) -> None:
301
+ """
302
+ Register a global callback for any component state change.
303
+
304
+ Args:
305
+ callback: Callback function(component_id, old_state, new_state)
306
+ """
307
+ with self.lock:
308
+ self.global_callbacks.append(callback)
309
+
310
+ def _update_component_visibility(self, component_id: str) -> None:
311
+ """
312
+ Update the visibility state of a component based on all factors.
313
+
314
+ Args:
315
+ component_id: Component identifier
316
+ """
317
+ comp_vis = self.components.get(component_id)
318
+ if not comp_vis or not comp_vis.is_valid:
319
+ return
320
+
321
+ # Determine new state
322
+ new_state = self._calculate_visibility_state(comp_vis)
323
+
324
+ # Update state and trigger callbacks if changed
325
+ old_state = comp_vis.visibility_state
326
+ if comp_vis.update_state(new_state):
327
+ self.stats['total_state_changes'] += 1
328
+ self._trigger_callbacks(component_id, old_state, new_state)
329
+
330
+ def _calculate_visibility_state(self, comp_vis: ComponentVisibility) -> VisibilityState:
331
+ """
332
+ Calculate the visibility state based on all factors.
333
+
334
+ Args:
335
+ comp_vis: Component visibility info
336
+
337
+ Returns:
338
+ Calculated visibility state
339
+ """
340
+ # Check window state first
341
+ if self.window_state == WindowState.MINIMIZED:
342
+ return VisibilityState.MINIMIZED
343
+
344
+ # Check if widget is visible
345
+ if not comp_vis.is_widget_visible:
346
+ return VisibilityState.HIDDEN
347
+
348
+ # Check tab visibility
349
+ if not comp_vis.is_tab_visible:
350
+ return VisibilityState.HIDDEN
351
+
352
+ # Check if window is focused
353
+ if not self.window_focused:
354
+ return VisibilityState.VISIBLE_INACTIVE
355
+
356
+ # Widget is visible and window is focused
357
+ return VisibilityState.VISIBLE_ACTIVE
358
+
359
+ def _trigger_callbacks(self, component_id: str, old_state: VisibilityState,
360
+ new_state: VisibilityState) -> None:
361
+ """
362
+ Trigger callbacks for a state change.
363
+
364
+ Args:
365
+ component_id: Component identifier
366
+ old_state: Previous visibility state
367
+ new_state: New visibility state
368
+ """
369
+ # Component-specific callbacks
370
+ callbacks = self.state_change_callbacks.get(component_id, [])
371
+ for callback in callbacks:
372
+ try:
373
+ callback(component_id, old_state, new_state)
374
+ self.stats['callbacks_executed'] += 1
375
+ except Exception as e:
376
+ print(f"Error in visibility callback for {component_id}: {e}")
377
+
378
+ # Global callbacks
379
+ for callback in self.global_callbacks:
380
+ try:
381
+ callback(component_id, old_state, new_state)
382
+ self.stats['callbacks_executed'] += 1
383
+ except Exception as e:
384
+ print(f"Error in global visibility callback: {e}")
385
+
386
+ def _on_window_unmapped(self, event=None) -> None:
387
+ """Handle window unmap event (minimization)."""
388
+ with self.lock:
389
+ self.window_state = WindowState.MINIMIZED
390
+
391
+ # Update all components
392
+ for component_id in list(self.components.keys()):
393
+ self._update_component_visibility(component_id)
394
+
395
+ def _on_window_mapped(self, event=None) -> None:
396
+ """Handle window map event (restoration)."""
397
+ with self.lock:
398
+ self.window_state = WindowState.NORMAL
399
+
400
+ # Update all components
401
+ for component_id in list(self.components.keys()):
402
+ self._update_component_visibility(component_id)
403
+
404
+ def _on_window_focus_in(self, event=None) -> None:
405
+ """Handle window focus in event."""
406
+ with self.lock:
407
+ self.window_focused = True
408
+
409
+ # Update all components
410
+ for component_id in list(self.components.keys()):
411
+ self._update_component_visibility(component_id)
412
+
413
+ def _on_window_focus_out(self, event=None) -> None:
414
+ """Handle window focus out event."""
415
+ with self.lock:
416
+ self.window_focused = False
417
+
418
+ # Update all components
419
+ for component_id in list(self.components.keys()):
420
+ self._update_component_visibility(component_id)
421
+
422
+ def _on_widget_visibility_change(self, component_id: str, event) -> None:
423
+ """
424
+ Handle widget visibility change event.
425
+
426
+ Args:
427
+ component_id: Component identifier
428
+ event: Tkinter event
429
+ """
430
+ with self.lock:
431
+ comp_vis = self.components.get(component_id)
432
+ if comp_vis:
433
+ # Check if widget is actually visible
434
+ try:
435
+ widget = comp_vis.widget
436
+ if widget:
437
+ # Widget is visible if it has non-zero dimensions
438
+ visible = widget.winfo_viewable()
439
+ comp_vis.is_widget_visible = visible
440
+ self._update_component_visibility(component_id)
441
+ except tk.TclError:
442
+ pass
443
+
444
+ def _on_widget_unmapped(self, component_id: str) -> None:
445
+ """
446
+ Handle widget unmap event.
447
+
448
+ Args:
449
+ component_id: Component identifier
450
+ """
451
+ with self.lock:
452
+ comp_vis = self.components.get(component_id)
453
+ if comp_vis:
454
+ comp_vis.is_widget_visible = False
455
+ self._update_component_visibility(component_id)
456
+
457
+ def _on_widget_mapped(self, component_id: str) -> None:
458
+ """
459
+ Handle widget map event.
460
+
461
+ Args:
462
+ component_id: Component identifier
463
+ """
464
+ with self.lock:
465
+ comp_vis = self.components.get(component_id)
466
+ if comp_vis:
467
+ comp_vis.is_widget_visible = True
468
+ self._update_component_visibility(component_id)
469
+
470
+ def get_component_info(self, component_id: str) -> Optional[Dict[str, Any]]:
471
+ """
472
+ Get detailed information about a component.
473
+
474
+ Args:
475
+ component_id: Component identifier
476
+
477
+ Returns:
478
+ Dictionary with component information or None if not found
479
+ """
480
+ with self.lock:
481
+ comp_vis = self.components.get(component_id)
482
+ if not comp_vis:
483
+ return None
484
+
485
+ return {
486
+ 'component_id': comp_vis.component_id,
487
+ 'visibility_state': comp_vis.visibility_state.value,
488
+ 'is_valid': comp_vis.is_valid,
489
+ 'is_tab_visible': comp_vis.is_tab_visible,
490
+ 'is_widget_visible': comp_vis.is_widget_visible,
491
+ 'is_window_focused': comp_vis.is_window_focused,
492
+ 'time_since_state_change': comp_vis.time_since_state_change,
493
+ 'state_change_count': comp_vis.state_change_count
494
+ }
495
+
496
+ def get_all_visible_components(self) -> Set[str]:
497
+ """
498
+ Get all currently visible component IDs.
499
+
500
+ Returns:
501
+ Set of visible component IDs
502
+ """
503
+ with self.lock:
504
+ return {
505
+ comp_id for comp_id, comp_vis in self.components.items()
506
+ if comp_vis.visibility_state in [VisibilityState.VISIBLE_ACTIVE,
507
+ VisibilityState.VISIBLE_INACTIVE]
508
+ }
509
+
510
+ def get_all_active_components(self) -> Set[str]:
511
+ """
512
+ Get all currently active (visible and focused) component IDs.
513
+
514
+ Returns:
515
+ Set of active component IDs
516
+ """
517
+ with self.lock:
518
+ return {
519
+ comp_id for comp_id, comp_vis in self.components.items()
520
+ if comp_vis.visibility_state == VisibilityState.VISIBLE_ACTIVE
521
+ }
522
+
523
+ def get_statistics(self) -> Dict[str, Any]:
524
+ """
525
+ Get statistics about visibility monitoring.
526
+
527
+ Returns:
528
+ Dictionary with statistics
529
+ """
530
+ with self.lock:
531
+ stats = self.stats.copy()
532
+
533
+ # Add current state information
534
+ stats['window_state'] = self.window_state.value
535
+ stats['window_focused'] = self.window_focused
536
+ stats['active_tabs'] = len(self.active_tabs)
537
+ stats['total_tabs'] = len(self.tab_to_components)
538
+
539
+ # Add visibility breakdown
540
+ visibility_counts = {}
541
+ for comp_vis in self.components.values():
542
+ state = comp_vis.visibility_state.value
543
+ visibility_counts[state] = visibility_counts.get(state, 0) + 1
544
+ stats['visibility_breakdown'] = visibility_counts
545
+
546
+ # Add callback information
547
+ stats['registered_callbacks'] = sum(len(cbs) for cbs in self.state_change_callbacks.values())
548
+ stats['global_callbacks'] = len(self.global_callbacks)
549
+
550
+ return stats
551
+
552
+ def cleanup_invalid_components(self) -> int:
553
+ """
554
+ Clean up components whose widgets have been destroyed.
555
+
556
+ Returns:
557
+ Number of components cleaned up
558
+ """
559
+ with self.lock:
560
+ invalid_ids = [
561
+ comp_id for comp_id, comp_vis in self.components.items()
562
+ if not comp_vis.is_valid
563
+ ]
564
+
565
+ for comp_id in invalid_ids:
566
+ self.unregister_component(comp_id)
567
+
568
+ return len(invalid_ids)
569
+
570
+ def force_update_all(self) -> None:
571
+ """Force update of all component visibility states."""
572
+ with self.lock:
573
+ for component_id in list(self.components.keys()):
574
+ self._update_component_visibility(component_id)
575
+
576
+
577
+ # Global instance for easy access
578
+ _global_visibility_monitor: Optional[VisibilityMonitor] = None
579
+
580
+
581
+ def get_visibility_monitor() -> VisibilityMonitor:
582
+ """Get the global visibility monitor instance."""
583
+ global _global_visibility_monitor
584
+ if _global_visibility_monitor is None:
585
+ _global_visibility_monitor = VisibilityMonitor()
586
+ return _global_visibility_monitor
587
+
588
+
589
+ def create_visibility_monitor() -> VisibilityMonitor:
590
+ """
591
+ Create a new visibility monitor instance.
592
+
593
+ Returns:
594
+ New VisibilityMonitor instance
595
+ """
596
+ return VisibilityMonitor()