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.
- package/LICENSE +21 -21
- package/README.md +105 -680
- package/bin/pomera-ai-commander.js +62 -62
- package/core/__init__.py +65 -65
- package/core/app_context.py +482 -482
- package/core/async_text_processor.py +421 -421
- package/core/backup_manager.py +655 -655
- package/core/backup_recovery_manager.py +1033 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_validator.py +1066 -1066
- package/core/database_connection_manager.py +744 -744
- package/core/database_curl_settings_manager.py +608 -608
- package/core/database_promera_ai_settings_manager.py +446 -446
- package/core/database_schema.py +411 -411
- package/core/database_schema_manager.py +395 -395
- package/core/database_settings_manager.py +1507 -1507
- package/core/database_settings_manager_interface.py +456 -456
- package/core/dialog_manager.py +734 -734
- package/core/efficient_line_numbers.py +510 -510
- package/core/error_handler.py +746 -746
- package/core/error_service.py +431 -431
- package/core/event_consolidator.py +511 -511
- package/core/mcp/__init__.py +43 -43
- package/core/mcp/protocol.py +288 -288
- package/core/mcp/schema.py +251 -251
- package/core/mcp/server_stdio.py +299 -299
- package/core/mcp/tool_registry.py +2372 -2345
- package/core/memory_efficient_text_widget.py +711 -711
- package/core/migration_manager.py +914 -914
- package/core/migration_test_suite.py +1085 -1085
- package/core/migration_validator.py +1143 -1143
- package/core/optimized_find_replace.py +714 -714
- package/core/optimized_pattern_engine.py +424 -424
- package/core/optimized_search_highlighter.py +552 -552
- package/core/performance_monitor.py +674 -674
- package/core/persistence_manager.py +712 -712
- package/core/progressive_stats_calculator.py +632 -632
- package/core/regex_pattern_cache.py +529 -529
- package/core/regex_pattern_library.py +350 -350
- package/core/search_operation_manager.py +434 -434
- package/core/settings_defaults_registry.py +1087 -1087
- package/core/settings_integrity_validator.py +1111 -1111
- package/core/settings_serializer.py +557 -557
- package/core/settings_validator.py +1823 -1823
- package/core/smart_stats_calculator.py +709 -709
- package/core/statistics_update_manager.py +619 -619
- package/core/stats_config_manager.py +858 -858
- package/core/streaming_text_handler.py +723 -723
- package/core/task_scheduler.py +596 -596
- package/core/update_pattern_library.py +168 -168
- package/core/visibility_monitor.py +596 -596
- package/core/widget_cache.py +498 -498
- package/mcp.json +51 -61
- package/package.json +61 -57
- package/pomera.py +7482 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +32 -0
- package/tools/__init__.py +4 -4
- package/tools/ai_tools.py +2891 -2891
- package/tools/ascii_art_generator.py +352 -352
- package/tools/base64_tools.py +183 -183
- package/tools/base_tool.py +511 -511
- package/tools/case_tool.py +308 -308
- package/tools/column_tools.py +395 -395
- package/tools/cron_tool.py +884 -884
- package/tools/curl_history.py +600 -600
- package/tools/curl_processor.py +1207 -1207
- package/tools/curl_settings.py +502 -502
- package/tools/curl_tool.py +5467 -5467
- package/tools/diff_viewer.py +1071 -1071
- package/tools/email_extraction_tool.py +248 -248
- package/tools/email_header_analyzer.py +425 -425
- package/tools/extraction_tools.py +250 -250
- package/tools/find_replace.py +1750 -1750
- package/tools/folder_file_reporter.py +1463 -1463
- package/tools/folder_file_reporter_adapter.py +480 -480
- package/tools/generator_tools.py +1216 -1216
- package/tools/hash_generator.py +255 -255
- package/tools/html_tool.py +656 -656
- package/tools/jsonxml_tool.py +729 -729
- package/tools/line_tools.py +419 -419
- package/tools/markdown_tools.py +561 -561
- package/tools/mcp_widget.py +1417 -1417
- package/tools/notes_widget.py +973 -973
- package/tools/number_base_converter.py +372 -372
- package/tools/regex_extractor.py +571 -571
- package/tools/slug_generator.py +310 -310
- package/tools/sorter_tools.py +458 -458
- package/tools/string_escape_tool.py +392 -392
- package/tools/text_statistics_tool.py +365 -365
- package/tools/text_wrapper.py +430 -430
- package/tools/timestamp_converter.py +421 -421
- package/tools/tool_loader.py +710 -710
- package/tools/translator_tools.py +522 -522
- package/tools/url_link_extractor.py +261 -261
- package/tools/url_parser.py +204 -204
- package/tools/whitespace_tools.py +355 -355
- package/tools/word_frequency_counter.py +146 -146
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- 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()
|