pomera-ai-commander 0.1.0 → 1.2.1

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 (191) 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 +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. 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()