pomera-ai-commander 1.1.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1199 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_directory.py +549 -0
- 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/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +540 -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/find_replace_diff.py +334 -0
- 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 +2699 -2345
- package/core/memento.py +275 -0
- 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/migrate_data.py +127 -0
- package/package.json +64 -57
- package/pomera.py +7883 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +33 -0
- package/scripts/Dockerfile.alpine +43 -0
- package/scripts/Dockerfile.gui-test +54 -0
- package/scripts/Dockerfile.linux +43 -0
- package/scripts/Dockerfile.test-linux +80 -0
- package/scripts/Dockerfile.ubuntu +39 -0
- package/scripts/README.md +53 -0
- package/scripts/build-all.bat +113 -0
- package/scripts/build-docker.bat +53 -0
- package/scripts/build-docker.sh +55 -0
- package/scripts/build-optimized.bat +101 -0
- package/scripts/build.sh +78 -0
- package/scripts/docker-compose.test.yml +27 -0
- package/scripts/docker-compose.yml +32 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/requirements-minimal.txt +33 -0
- package/scripts/test-linux-simple.bat +28 -0
- package/scripts/validate-release-workflow.py +450 -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 +1817 -1072
- 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 +2289 -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 +978 -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
package/core/widget_cache.py
CHANGED
|
@@ -1,498 +1,498 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Widget Cache - Caches tool widgets to avoid recreation on tool switching.
|
|
3
|
-
|
|
4
|
-
Instead of destroying and recreating widgets on every tool switch,
|
|
5
|
-
this cache hides/shows widgets as needed, improving performance.
|
|
6
|
-
|
|
7
|
-
Author: Pomera AI Commander Team
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import tkinter as tk
|
|
11
|
-
from tkinter import ttk
|
|
12
|
-
from typing import Dict, Optional, Callable, Any, Set, List
|
|
13
|
-
import logging
|
|
14
|
-
import weakref
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from enum import Enum
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class CacheStrategy(Enum):
|
|
23
|
-
"""Widget caching strategies."""
|
|
24
|
-
ALWAYS = "always" # Always cache (default)
|
|
25
|
-
ON_DEMAND = "on_demand" # Cache only when explicitly requested
|
|
26
|
-
NEVER = "never" # Never cache (always recreate)
|
|
27
|
-
LRU = "lru" # Least Recently Used eviction
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass
|
|
31
|
-
class CachedWidget:
|
|
32
|
-
"""
|
|
33
|
-
Information about a cached widget.
|
|
34
|
-
|
|
35
|
-
Attributes:
|
|
36
|
-
widget: The actual Tkinter widget
|
|
37
|
-
tool_name: Name of the tool this widget belongs to
|
|
38
|
-
created_at: Timestamp when widget was created
|
|
39
|
-
last_shown: Timestamp when widget was last shown
|
|
40
|
-
show_count: Number of times widget has been shown
|
|
41
|
-
needs_refresh: Whether widget needs to be refreshed on next show
|
|
42
|
-
"""
|
|
43
|
-
widget: tk.Widget
|
|
44
|
-
tool_name: str
|
|
45
|
-
created_at: float = 0.0
|
|
46
|
-
last_shown: float = 0.0
|
|
47
|
-
show_count: int = 0
|
|
48
|
-
needs_refresh: bool = False
|
|
49
|
-
|
|
50
|
-
def mark_shown(self) -> None:
|
|
51
|
-
"""Mark widget as shown."""
|
|
52
|
-
import time
|
|
53
|
-
self.last_shown = time.time()
|
|
54
|
-
self.show_count += 1
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class WidgetCache:
|
|
58
|
-
"""
|
|
59
|
-
Caches tool widgets to improve tool switching performance.
|
|
60
|
-
|
|
61
|
-
Instead of destroying and recreating widgets on every tool switch,
|
|
62
|
-
this cache hides/shows widgets as needed. This significantly improves
|
|
63
|
-
the user experience when switching between tools frequently.
|
|
64
|
-
|
|
65
|
-
Usage:
|
|
66
|
-
cache = WidgetCache(parent_frame)
|
|
67
|
-
|
|
68
|
-
# Register widget factories
|
|
69
|
-
cache.register_factory("Case Tool", lambda: create_case_tool_widget())
|
|
70
|
-
cache.register_factory("AI Tools", lambda: create_ai_tools_widget())
|
|
71
|
-
|
|
72
|
-
# Switch to a tool (creates widget if not cached, shows it)
|
|
73
|
-
cache.show("Case Tool")
|
|
74
|
-
|
|
75
|
-
# Switch to another tool (hides Case Tool, shows AI Tools)
|
|
76
|
-
cache.show("AI Tools")
|
|
77
|
-
|
|
78
|
-
# Force refresh a widget on next show
|
|
79
|
-
cache.invalidate("Case Tool")
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
def __init__(self,
|
|
83
|
-
parent_frame: tk.Frame,
|
|
84
|
-
strategy: CacheStrategy = CacheStrategy.ALWAYS,
|
|
85
|
-
max_cached: int = 20,
|
|
86
|
-
logger: Optional[logging.Logger] = None):
|
|
87
|
-
"""
|
|
88
|
-
Initialize the widget cache.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
parent_frame: The frame where tool widgets are displayed
|
|
92
|
-
strategy: Caching strategy to use
|
|
93
|
-
max_cached: Maximum number of widgets to cache (for LRU)
|
|
94
|
-
logger: Logger instance
|
|
95
|
-
"""
|
|
96
|
-
self.parent_frame = parent_frame
|
|
97
|
-
self.strategy = strategy
|
|
98
|
-
self.max_cached = max_cached
|
|
99
|
-
self.logger = logger or logging.getLogger(__name__)
|
|
100
|
-
|
|
101
|
-
# Widget storage
|
|
102
|
-
self._cache: Dict[str, CachedWidget] = {}
|
|
103
|
-
self._factories: Dict[str, Callable[[], tk.Widget]] = {}
|
|
104
|
-
self._current_tool: Optional[str] = None
|
|
105
|
-
|
|
106
|
-
# Tools that should never be cached
|
|
107
|
-
self._never_cache: Set[str] = set()
|
|
108
|
-
|
|
109
|
-
# Callbacks
|
|
110
|
-
self._on_widget_created: Optional[Callable[[str, tk.Widget], None]] = None
|
|
111
|
-
self._on_widget_shown: Optional[Callable[[str, tk.Widget], None]] = None
|
|
112
|
-
self._on_widget_hidden: Optional[Callable[[str, tk.Widget], None]] = None
|
|
113
|
-
|
|
114
|
-
# Statistics
|
|
115
|
-
self._stats = {
|
|
116
|
-
'cache_hits': 0,
|
|
117
|
-
'cache_misses': 0,
|
|
118
|
-
'widgets_created': 0,
|
|
119
|
-
'widgets_destroyed': 0
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
def register_factory(self,
|
|
123
|
-
tool_name: str,
|
|
124
|
-
factory: Callable[[], tk.Widget],
|
|
125
|
-
never_cache: bool = False) -> None:
|
|
126
|
-
"""
|
|
127
|
-
Register a factory function for creating a tool's widget.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
tool_name: Name of the tool
|
|
131
|
-
factory: Function that creates and returns the widget
|
|
132
|
-
never_cache: If True, widget will always be recreated
|
|
133
|
-
"""
|
|
134
|
-
self._factories[tool_name] = factory
|
|
135
|
-
if never_cache:
|
|
136
|
-
self._never_cache.add(tool_name)
|
|
137
|
-
self.logger.debug(f"Registered factory for: {tool_name}")
|
|
138
|
-
|
|
139
|
-
def unregister_factory(self, tool_name: str) -> bool:
|
|
140
|
-
"""
|
|
141
|
-
Unregister a factory function.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
tool_name: Name of the tool
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
True if factory was found and removed
|
|
148
|
-
"""
|
|
149
|
-
if tool_name in self._factories:
|
|
150
|
-
del self._factories[tool_name]
|
|
151
|
-
self._never_cache.discard(tool_name)
|
|
152
|
-
return True
|
|
153
|
-
return False
|
|
154
|
-
|
|
155
|
-
def get_or_create(self, tool_name: str) -> Optional[tk.Widget]:
|
|
156
|
-
"""
|
|
157
|
-
Get a cached widget or create it if not cached.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
tool_name: Name of the tool
|
|
161
|
-
|
|
162
|
-
Returns:
|
|
163
|
-
The tool's widget, or None if no factory registered
|
|
164
|
-
"""
|
|
165
|
-
# Check if we should never cache this tool
|
|
166
|
-
should_cache = (
|
|
167
|
-
self.strategy != CacheStrategy.NEVER and
|
|
168
|
-
tool_name not in self._never_cache
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# Return cached widget if available and valid
|
|
172
|
-
if tool_name in self._cache and should_cache:
|
|
173
|
-
cached = self._cache[tool_name]
|
|
174
|
-
if not cached.needs_refresh:
|
|
175
|
-
self._stats['cache_hits'] += 1
|
|
176
|
-
self.logger.debug(f"Cache hit for: {tool_name}")
|
|
177
|
-
return cached.widget
|
|
178
|
-
else:
|
|
179
|
-
# Widget needs refresh, destroy and recreate
|
|
180
|
-
self._destroy_widget(tool_name)
|
|
181
|
-
|
|
182
|
-
self._stats['cache_misses'] += 1
|
|
183
|
-
|
|
184
|
-
# Check if we have a factory
|
|
185
|
-
if tool_name not in self._factories:
|
|
186
|
-
self.logger.warning(f"No factory registered for tool: {tool_name}")
|
|
187
|
-
return None
|
|
188
|
-
|
|
189
|
-
# Enforce max cache size (LRU eviction)
|
|
190
|
-
if self.strategy == CacheStrategy.LRU and len(self._cache) >= self.max_cached:
|
|
191
|
-
self._evict_lru()
|
|
192
|
-
|
|
193
|
-
# Create new widget
|
|
194
|
-
try:
|
|
195
|
-
widget = self._factories[tool_name]()
|
|
196
|
-
|
|
197
|
-
if should_cache:
|
|
198
|
-
import time
|
|
199
|
-
self._cache[tool_name] = CachedWidget(
|
|
200
|
-
widget=widget,
|
|
201
|
-
tool_name=tool_name,
|
|
202
|
-
created_at=time.time()
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
self._stats['widgets_created'] += 1
|
|
206
|
-
self.logger.debug(f"Created widget for: {tool_name}")
|
|
207
|
-
|
|
208
|
-
# Callback
|
|
209
|
-
if self._on_widget_created:
|
|
210
|
-
self._on_widget_created(tool_name, widget)
|
|
211
|
-
|
|
212
|
-
return widget
|
|
213
|
-
|
|
214
|
-
except Exception as e:
|
|
215
|
-
self.logger.error(f"Failed to create widget for {tool_name}: {e}")
|
|
216
|
-
return None
|
|
217
|
-
|
|
218
|
-
def show(self, tool_name: str) -> bool:
|
|
219
|
-
"""
|
|
220
|
-
Show the widget for the specified tool, hiding others.
|
|
221
|
-
|
|
222
|
-
Args:
|
|
223
|
-
tool_name: Name of the tool to show
|
|
224
|
-
|
|
225
|
-
Returns:
|
|
226
|
-
True if successful, False otherwise
|
|
227
|
-
"""
|
|
228
|
-
# Hide current widget
|
|
229
|
-
if self._current_tool and self._current_tool != tool_name:
|
|
230
|
-
self._hide_current()
|
|
231
|
-
|
|
232
|
-
# Get or create the new widget
|
|
233
|
-
widget = self.get_or_create(tool_name)
|
|
234
|
-
if widget is None:
|
|
235
|
-
return False
|
|
236
|
-
|
|
237
|
-
# Show the new widget
|
|
238
|
-
try:
|
|
239
|
-
widget.pack(fill=tk.BOTH, expand=True)
|
|
240
|
-
self._current_tool = tool_name
|
|
241
|
-
|
|
242
|
-
# Update cached widget stats
|
|
243
|
-
if tool_name in self._cache:
|
|
244
|
-
self._cache[tool_name].mark_shown()
|
|
245
|
-
|
|
246
|
-
# Callback
|
|
247
|
-
if self._on_widget_shown:
|
|
248
|
-
self._on_widget_shown(tool_name, widget)
|
|
249
|
-
|
|
250
|
-
self.logger.debug(f"Showing widget: {tool_name}")
|
|
251
|
-
return True
|
|
252
|
-
|
|
253
|
-
except tk.TclError as e:
|
|
254
|
-
self.logger.error(f"Failed to show widget {tool_name}: {e}")
|
|
255
|
-
# Widget might be destroyed, remove from cache
|
|
256
|
-
self._cache.pop(tool_name, None)
|
|
257
|
-
return False
|
|
258
|
-
|
|
259
|
-
def hide(self, tool_name: str) -> bool:
|
|
260
|
-
"""
|
|
261
|
-
Hide a specific tool's widget.
|
|
262
|
-
|
|
263
|
-
Args:
|
|
264
|
-
tool_name: Name of the tool to hide
|
|
265
|
-
|
|
266
|
-
Returns:
|
|
267
|
-
True if widget was hidden
|
|
268
|
-
"""
|
|
269
|
-
if tool_name not in self._cache:
|
|
270
|
-
return False
|
|
271
|
-
|
|
272
|
-
try:
|
|
273
|
-
self._cache[tool_name].widget.pack_forget()
|
|
274
|
-
|
|
275
|
-
if self._current_tool == tool_name:
|
|
276
|
-
self._current_tool = None
|
|
277
|
-
|
|
278
|
-
# Callback
|
|
279
|
-
if self._on_widget_hidden:
|
|
280
|
-
self._on_widget_hidden(tool_name, self._cache[tool_name].widget)
|
|
281
|
-
|
|
282
|
-
return True
|
|
283
|
-
|
|
284
|
-
except tk.TclError:
|
|
285
|
-
# Widget might be destroyed
|
|
286
|
-
self._cache.pop(tool_name, None)
|
|
287
|
-
return False
|
|
288
|
-
|
|
289
|
-
def _hide_current(self) -> None:
|
|
290
|
-
"""Hide the currently shown widget."""
|
|
291
|
-
if self._current_tool and self._current_tool in self._cache:
|
|
292
|
-
try:
|
|
293
|
-
self._cache[self._current_tool].widget.pack_forget()
|
|
294
|
-
|
|
295
|
-
if self._on_widget_hidden:
|
|
296
|
-
self._on_widget_hidden(
|
|
297
|
-
self._current_tool,
|
|
298
|
-
self._cache[self._current_tool].widget
|
|
299
|
-
)
|
|
300
|
-
except tk.TclError:
|
|
301
|
-
# Widget might be destroyed
|
|
302
|
-
self._cache.pop(self._current_tool, None)
|
|
303
|
-
|
|
304
|
-
def invalidate(self, tool_name: str) -> None:
|
|
305
|
-
"""
|
|
306
|
-
Mark a widget for refresh (will be recreated on next show).
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
tool_name: Name of the tool to invalidate
|
|
310
|
-
"""
|
|
311
|
-
if tool_name in self._cache:
|
|
312
|
-
self._cache[tool_name].needs_refresh = True
|
|
313
|
-
self.logger.debug(f"Invalidated cache for: {tool_name}")
|
|
314
|
-
|
|
315
|
-
def destroy(self, tool_name: str) -> bool:
|
|
316
|
-
"""
|
|
317
|
-
Destroy a cached widget immediately.
|
|
318
|
-
|
|
319
|
-
Args:
|
|
320
|
-
tool_name: Name of the tool
|
|
321
|
-
|
|
322
|
-
Returns:
|
|
323
|
-
True if widget was found and destroyed
|
|
324
|
-
"""
|
|
325
|
-
return self._destroy_widget(tool_name)
|
|
326
|
-
|
|
327
|
-
def _destroy_widget(self, tool_name: str) -> bool:
|
|
328
|
-
"""Internal method to destroy a widget."""
|
|
329
|
-
if tool_name not in self._cache:
|
|
330
|
-
return False
|
|
331
|
-
|
|
332
|
-
try:
|
|
333
|
-
self._cache[tool_name].widget.destroy()
|
|
334
|
-
except tk.TclError:
|
|
335
|
-
pass # Already destroyed
|
|
336
|
-
|
|
337
|
-
del self._cache[tool_name]
|
|
338
|
-
self._stats['widgets_destroyed'] += 1
|
|
339
|
-
|
|
340
|
-
if self._current_tool == tool_name:
|
|
341
|
-
self._current_tool = None
|
|
342
|
-
|
|
343
|
-
self.logger.debug(f"Destroyed widget: {tool_name}")
|
|
344
|
-
return True
|
|
345
|
-
|
|
346
|
-
def _evict_lru(self) -> None:
|
|
347
|
-
"""Evict the least recently used widget."""
|
|
348
|
-
if not self._cache:
|
|
349
|
-
return
|
|
350
|
-
|
|
351
|
-
# Don't evict current widget
|
|
352
|
-
candidates = [
|
|
353
|
-
(name, cached) for name, cached in self._cache.items()
|
|
354
|
-
if name != self._current_tool
|
|
355
|
-
]
|
|
356
|
-
|
|
357
|
-
if not candidates:
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
# Find LRU
|
|
361
|
-
lru_name, _ = min(candidates, key=lambda x: x[1].last_shown)
|
|
362
|
-
self._destroy_widget(lru_name)
|
|
363
|
-
self.logger.debug(f"Evicted LRU widget: {lru_name}")
|
|
364
|
-
|
|
365
|
-
def clear(self) -> None:
|
|
366
|
-
"""Clear all cached widgets."""
|
|
367
|
-
for tool_name in list(self._cache.keys()):
|
|
368
|
-
self._destroy_widget(tool_name)
|
|
369
|
-
self._current_tool = None
|
|
370
|
-
self.logger.debug("Cleared all cached widgets")
|
|
371
|
-
|
|
372
|
-
def refresh_all(self) -> None:
|
|
373
|
-
"""Mark all widgets for refresh."""
|
|
374
|
-
for cached in self._cache.values():
|
|
375
|
-
cached.needs_refresh = True
|
|
376
|
-
self.logger.debug("Marked all widgets for refresh")
|
|
377
|
-
|
|
378
|
-
def is_cached(self, tool_name: str) -> bool:
|
|
379
|
-
"""Check if a tool's widget is cached."""
|
|
380
|
-
return tool_name in self._cache
|
|
381
|
-
|
|
382
|
-
def get_widget(self, tool_name: str) -> Optional[tk.Widget]:
|
|
383
|
-
"""
|
|
384
|
-
Get a cached widget without creating it.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
tool_name: Name of the tool
|
|
388
|
-
|
|
389
|
-
Returns:
|
|
390
|
-
The widget if cached, None otherwise
|
|
391
|
-
"""
|
|
392
|
-
if tool_name in self._cache:
|
|
393
|
-
return self._cache[tool_name].widget
|
|
394
|
-
return None
|
|
395
|
-
|
|
396
|
-
@property
|
|
397
|
-
def cached_tools(self) -> List[str]:
|
|
398
|
-
"""Get list of currently cached tool names."""
|
|
399
|
-
return list(self._cache.keys())
|
|
400
|
-
|
|
401
|
-
@property
|
|
402
|
-
def current_tool(self) -> Optional[str]:
|
|
403
|
-
"""Get the currently displayed tool name."""
|
|
404
|
-
return self._current_tool
|
|
405
|
-
|
|
406
|
-
@property
|
|
407
|
-
def factory_count(self) -> int:
|
|
408
|
-
"""Get number of registered factories."""
|
|
409
|
-
return len(self._factories)
|
|
410
|
-
|
|
411
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
412
|
-
"""Get cache statistics."""
|
|
413
|
-
total_requests = self._stats['cache_hits'] + self._stats['cache_misses']
|
|
414
|
-
hit_rate = (
|
|
415
|
-
self._stats['cache_hits'] / total_requests * 100
|
|
416
|
-
if total_requests > 0 else 0
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
return {
|
|
420
|
-
**self._stats,
|
|
421
|
-
'cached_widgets': len(self._cache),
|
|
422
|
-
'registered_factories': len(self._factories),
|
|
423
|
-
'current_tool': self._current_tool,
|
|
424
|
-
'hit_rate_percent': round(hit_rate, 2),
|
|
425
|
-
'strategy': self.strategy.value
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
def get_cache_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
|
|
429
|
-
"""
|
|
430
|
-
Get information about a cached widget.
|
|
431
|
-
|
|
432
|
-
Args:
|
|
433
|
-
tool_name: Name of the tool
|
|
434
|
-
|
|
435
|
-
Returns:
|
|
436
|
-
Dictionary with cache info, or None if not cached
|
|
437
|
-
"""
|
|
438
|
-
if tool_name not in self._cache:
|
|
439
|
-
return None
|
|
440
|
-
|
|
441
|
-
cached = self._cache[tool_name]
|
|
442
|
-
return {
|
|
443
|
-
'tool_name': cached.tool_name,
|
|
444
|
-
'created_at': cached.created_at,
|
|
445
|
-
'last_shown': cached.last_shown,
|
|
446
|
-
'show_count': cached.show_count,
|
|
447
|
-
'needs_refresh': cached.needs_refresh
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
# Callback setters
|
|
451
|
-
def set_on_widget_created(self, callback: Callable[[str, tk.Widget], None]) -> None:
|
|
452
|
-
"""Set callback for when a widget is created."""
|
|
453
|
-
self._on_widget_created = callback
|
|
454
|
-
|
|
455
|
-
def set_on_widget_shown(self, callback: Callable[[str, tk.Widget], None]) -> None:
|
|
456
|
-
"""Set callback for when a widget is shown."""
|
|
457
|
-
self._on_widget_shown = callback
|
|
458
|
-
|
|
459
|
-
def set_on_widget_hidden(self, callback: Callable[[str, tk.Widget], None]) -> None:
|
|
460
|
-
"""Set callback for when a widget is hidden."""
|
|
461
|
-
self._on_widget_hidden = callback
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
# Global instance
|
|
465
|
-
_widget_cache: Optional[WidgetCache] = None
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
def get_widget_cache() -> Optional[WidgetCache]:
|
|
469
|
-
"""Get the global widget cache instance."""
|
|
470
|
-
return _widget_cache
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def init_widget_cache(parent_frame: tk.Frame,
|
|
474
|
-
strategy: CacheStrategy = CacheStrategy.ALWAYS,
|
|
475
|
-
max_cached: int = 20) -> WidgetCache:
|
|
476
|
-
"""
|
|
477
|
-
Initialize the global widget cache.
|
|
478
|
-
|
|
479
|
-
Args:
|
|
480
|
-
parent_frame: Parent frame for widgets
|
|
481
|
-
strategy: Caching strategy
|
|
482
|
-
max_cached: Maximum cached widgets (for LRU)
|
|
483
|
-
|
|
484
|
-
Returns:
|
|
485
|
-
Initialized WidgetCache
|
|
486
|
-
"""
|
|
487
|
-
global _widget_cache
|
|
488
|
-
_widget_cache = WidgetCache(parent_frame, strategy, max_cached)
|
|
489
|
-
return _widget_cache
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
def shutdown_widget_cache() -> None:
|
|
493
|
-
"""Shutdown the global widget cache."""
|
|
494
|
-
global _widget_cache
|
|
495
|
-
if _widget_cache is not None:
|
|
496
|
-
_widget_cache.clear()
|
|
497
|
-
_widget_cache = None
|
|
498
|
-
|
|
1
|
+
"""
|
|
2
|
+
Widget Cache - Caches tool widgets to avoid recreation on tool switching.
|
|
3
|
+
|
|
4
|
+
Instead of destroying and recreating widgets on every tool switch,
|
|
5
|
+
this cache hides/shows widgets as needed, improving performance.
|
|
6
|
+
|
|
7
|
+
Author: Pomera AI Commander Team
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import tkinter as tk
|
|
11
|
+
from tkinter import ttk
|
|
12
|
+
from typing import Dict, Optional, Callable, Any, Set, List
|
|
13
|
+
import logging
|
|
14
|
+
import weakref
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CacheStrategy(Enum):
|
|
23
|
+
"""Widget caching strategies."""
|
|
24
|
+
ALWAYS = "always" # Always cache (default)
|
|
25
|
+
ON_DEMAND = "on_demand" # Cache only when explicitly requested
|
|
26
|
+
NEVER = "never" # Never cache (always recreate)
|
|
27
|
+
LRU = "lru" # Least Recently Used eviction
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CachedWidget:
|
|
32
|
+
"""
|
|
33
|
+
Information about a cached widget.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
widget: The actual Tkinter widget
|
|
37
|
+
tool_name: Name of the tool this widget belongs to
|
|
38
|
+
created_at: Timestamp when widget was created
|
|
39
|
+
last_shown: Timestamp when widget was last shown
|
|
40
|
+
show_count: Number of times widget has been shown
|
|
41
|
+
needs_refresh: Whether widget needs to be refreshed on next show
|
|
42
|
+
"""
|
|
43
|
+
widget: tk.Widget
|
|
44
|
+
tool_name: str
|
|
45
|
+
created_at: float = 0.0
|
|
46
|
+
last_shown: float = 0.0
|
|
47
|
+
show_count: int = 0
|
|
48
|
+
needs_refresh: bool = False
|
|
49
|
+
|
|
50
|
+
def mark_shown(self) -> None:
|
|
51
|
+
"""Mark widget as shown."""
|
|
52
|
+
import time
|
|
53
|
+
self.last_shown = time.time()
|
|
54
|
+
self.show_count += 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WidgetCache:
|
|
58
|
+
"""
|
|
59
|
+
Caches tool widgets to improve tool switching performance.
|
|
60
|
+
|
|
61
|
+
Instead of destroying and recreating widgets on every tool switch,
|
|
62
|
+
this cache hides/shows widgets as needed. This significantly improves
|
|
63
|
+
the user experience when switching between tools frequently.
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
cache = WidgetCache(parent_frame)
|
|
67
|
+
|
|
68
|
+
# Register widget factories
|
|
69
|
+
cache.register_factory("Case Tool", lambda: create_case_tool_widget())
|
|
70
|
+
cache.register_factory("AI Tools", lambda: create_ai_tools_widget())
|
|
71
|
+
|
|
72
|
+
# Switch to a tool (creates widget if not cached, shows it)
|
|
73
|
+
cache.show("Case Tool")
|
|
74
|
+
|
|
75
|
+
# Switch to another tool (hides Case Tool, shows AI Tools)
|
|
76
|
+
cache.show("AI Tools")
|
|
77
|
+
|
|
78
|
+
# Force refresh a widget on next show
|
|
79
|
+
cache.invalidate("Case Tool")
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self,
|
|
83
|
+
parent_frame: tk.Frame,
|
|
84
|
+
strategy: CacheStrategy = CacheStrategy.ALWAYS,
|
|
85
|
+
max_cached: int = 20,
|
|
86
|
+
logger: Optional[logging.Logger] = None):
|
|
87
|
+
"""
|
|
88
|
+
Initialize the widget cache.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
parent_frame: The frame where tool widgets are displayed
|
|
92
|
+
strategy: Caching strategy to use
|
|
93
|
+
max_cached: Maximum number of widgets to cache (for LRU)
|
|
94
|
+
logger: Logger instance
|
|
95
|
+
"""
|
|
96
|
+
self.parent_frame = parent_frame
|
|
97
|
+
self.strategy = strategy
|
|
98
|
+
self.max_cached = max_cached
|
|
99
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
100
|
+
|
|
101
|
+
# Widget storage
|
|
102
|
+
self._cache: Dict[str, CachedWidget] = {}
|
|
103
|
+
self._factories: Dict[str, Callable[[], tk.Widget]] = {}
|
|
104
|
+
self._current_tool: Optional[str] = None
|
|
105
|
+
|
|
106
|
+
# Tools that should never be cached
|
|
107
|
+
self._never_cache: Set[str] = set()
|
|
108
|
+
|
|
109
|
+
# Callbacks
|
|
110
|
+
self._on_widget_created: Optional[Callable[[str, tk.Widget], None]] = None
|
|
111
|
+
self._on_widget_shown: Optional[Callable[[str, tk.Widget], None]] = None
|
|
112
|
+
self._on_widget_hidden: Optional[Callable[[str, tk.Widget], None]] = None
|
|
113
|
+
|
|
114
|
+
# Statistics
|
|
115
|
+
self._stats = {
|
|
116
|
+
'cache_hits': 0,
|
|
117
|
+
'cache_misses': 0,
|
|
118
|
+
'widgets_created': 0,
|
|
119
|
+
'widgets_destroyed': 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def register_factory(self,
|
|
123
|
+
tool_name: str,
|
|
124
|
+
factory: Callable[[], tk.Widget],
|
|
125
|
+
never_cache: bool = False) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Register a factory function for creating a tool's widget.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
tool_name: Name of the tool
|
|
131
|
+
factory: Function that creates and returns the widget
|
|
132
|
+
never_cache: If True, widget will always be recreated
|
|
133
|
+
"""
|
|
134
|
+
self._factories[tool_name] = factory
|
|
135
|
+
if never_cache:
|
|
136
|
+
self._never_cache.add(tool_name)
|
|
137
|
+
self.logger.debug(f"Registered factory for: {tool_name}")
|
|
138
|
+
|
|
139
|
+
def unregister_factory(self, tool_name: str) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Unregister a factory function.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
tool_name: Name of the tool
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if factory was found and removed
|
|
148
|
+
"""
|
|
149
|
+
if tool_name in self._factories:
|
|
150
|
+
del self._factories[tool_name]
|
|
151
|
+
self._never_cache.discard(tool_name)
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def get_or_create(self, tool_name: str) -> Optional[tk.Widget]:
|
|
156
|
+
"""
|
|
157
|
+
Get a cached widget or create it if not cached.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
tool_name: Name of the tool
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The tool's widget, or None if no factory registered
|
|
164
|
+
"""
|
|
165
|
+
# Check if we should never cache this tool
|
|
166
|
+
should_cache = (
|
|
167
|
+
self.strategy != CacheStrategy.NEVER and
|
|
168
|
+
tool_name not in self._never_cache
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Return cached widget if available and valid
|
|
172
|
+
if tool_name in self._cache and should_cache:
|
|
173
|
+
cached = self._cache[tool_name]
|
|
174
|
+
if not cached.needs_refresh:
|
|
175
|
+
self._stats['cache_hits'] += 1
|
|
176
|
+
self.logger.debug(f"Cache hit for: {tool_name}")
|
|
177
|
+
return cached.widget
|
|
178
|
+
else:
|
|
179
|
+
# Widget needs refresh, destroy and recreate
|
|
180
|
+
self._destroy_widget(tool_name)
|
|
181
|
+
|
|
182
|
+
self._stats['cache_misses'] += 1
|
|
183
|
+
|
|
184
|
+
# Check if we have a factory
|
|
185
|
+
if tool_name not in self._factories:
|
|
186
|
+
self.logger.warning(f"No factory registered for tool: {tool_name}")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
# Enforce max cache size (LRU eviction)
|
|
190
|
+
if self.strategy == CacheStrategy.LRU and len(self._cache) >= self.max_cached:
|
|
191
|
+
self._evict_lru()
|
|
192
|
+
|
|
193
|
+
# Create new widget
|
|
194
|
+
try:
|
|
195
|
+
widget = self._factories[tool_name]()
|
|
196
|
+
|
|
197
|
+
if should_cache:
|
|
198
|
+
import time
|
|
199
|
+
self._cache[tool_name] = CachedWidget(
|
|
200
|
+
widget=widget,
|
|
201
|
+
tool_name=tool_name,
|
|
202
|
+
created_at=time.time()
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self._stats['widgets_created'] += 1
|
|
206
|
+
self.logger.debug(f"Created widget for: {tool_name}")
|
|
207
|
+
|
|
208
|
+
# Callback
|
|
209
|
+
if self._on_widget_created:
|
|
210
|
+
self._on_widget_created(tool_name, widget)
|
|
211
|
+
|
|
212
|
+
return widget
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self.logger.error(f"Failed to create widget for {tool_name}: {e}")
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
def show(self, tool_name: str) -> bool:
|
|
219
|
+
"""
|
|
220
|
+
Show the widget for the specified tool, hiding others.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
tool_name: Name of the tool to show
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if successful, False otherwise
|
|
227
|
+
"""
|
|
228
|
+
# Hide current widget
|
|
229
|
+
if self._current_tool and self._current_tool != tool_name:
|
|
230
|
+
self._hide_current()
|
|
231
|
+
|
|
232
|
+
# Get or create the new widget
|
|
233
|
+
widget = self.get_or_create(tool_name)
|
|
234
|
+
if widget is None:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
# Show the new widget
|
|
238
|
+
try:
|
|
239
|
+
widget.pack(fill=tk.BOTH, expand=True)
|
|
240
|
+
self._current_tool = tool_name
|
|
241
|
+
|
|
242
|
+
# Update cached widget stats
|
|
243
|
+
if tool_name in self._cache:
|
|
244
|
+
self._cache[tool_name].mark_shown()
|
|
245
|
+
|
|
246
|
+
# Callback
|
|
247
|
+
if self._on_widget_shown:
|
|
248
|
+
self._on_widget_shown(tool_name, widget)
|
|
249
|
+
|
|
250
|
+
self.logger.debug(f"Showing widget: {tool_name}")
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
except tk.TclError as e:
|
|
254
|
+
self.logger.error(f"Failed to show widget {tool_name}: {e}")
|
|
255
|
+
# Widget might be destroyed, remove from cache
|
|
256
|
+
self._cache.pop(tool_name, None)
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
def hide(self, tool_name: str) -> bool:
|
|
260
|
+
"""
|
|
261
|
+
Hide a specific tool's widget.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
tool_name: Name of the tool to hide
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if widget was hidden
|
|
268
|
+
"""
|
|
269
|
+
if tool_name not in self._cache:
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
self._cache[tool_name].widget.pack_forget()
|
|
274
|
+
|
|
275
|
+
if self._current_tool == tool_name:
|
|
276
|
+
self._current_tool = None
|
|
277
|
+
|
|
278
|
+
# Callback
|
|
279
|
+
if self._on_widget_hidden:
|
|
280
|
+
self._on_widget_hidden(tool_name, self._cache[tool_name].widget)
|
|
281
|
+
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
except tk.TclError:
|
|
285
|
+
# Widget might be destroyed
|
|
286
|
+
self._cache.pop(tool_name, None)
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
def _hide_current(self) -> None:
|
|
290
|
+
"""Hide the currently shown widget."""
|
|
291
|
+
if self._current_tool and self._current_tool in self._cache:
|
|
292
|
+
try:
|
|
293
|
+
self._cache[self._current_tool].widget.pack_forget()
|
|
294
|
+
|
|
295
|
+
if self._on_widget_hidden:
|
|
296
|
+
self._on_widget_hidden(
|
|
297
|
+
self._current_tool,
|
|
298
|
+
self._cache[self._current_tool].widget
|
|
299
|
+
)
|
|
300
|
+
except tk.TclError:
|
|
301
|
+
# Widget might be destroyed
|
|
302
|
+
self._cache.pop(self._current_tool, None)
|
|
303
|
+
|
|
304
|
+
def invalidate(self, tool_name: str) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Mark a widget for refresh (will be recreated on next show).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
tool_name: Name of the tool to invalidate
|
|
310
|
+
"""
|
|
311
|
+
if tool_name in self._cache:
|
|
312
|
+
self._cache[tool_name].needs_refresh = True
|
|
313
|
+
self.logger.debug(f"Invalidated cache for: {tool_name}")
|
|
314
|
+
|
|
315
|
+
def destroy(self, tool_name: str) -> bool:
|
|
316
|
+
"""
|
|
317
|
+
Destroy a cached widget immediately.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
tool_name: Name of the tool
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
True if widget was found and destroyed
|
|
324
|
+
"""
|
|
325
|
+
return self._destroy_widget(tool_name)
|
|
326
|
+
|
|
327
|
+
def _destroy_widget(self, tool_name: str) -> bool:
|
|
328
|
+
"""Internal method to destroy a widget."""
|
|
329
|
+
if tool_name not in self._cache:
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
self._cache[tool_name].widget.destroy()
|
|
334
|
+
except tk.TclError:
|
|
335
|
+
pass # Already destroyed
|
|
336
|
+
|
|
337
|
+
del self._cache[tool_name]
|
|
338
|
+
self._stats['widgets_destroyed'] += 1
|
|
339
|
+
|
|
340
|
+
if self._current_tool == tool_name:
|
|
341
|
+
self._current_tool = None
|
|
342
|
+
|
|
343
|
+
self.logger.debug(f"Destroyed widget: {tool_name}")
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
def _evict_lru(self) -> None:
|
|
347
|
+
"""Evict the least recently used widget."""
|
|
348
|
+
if not self._cache:
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# Don't evict current widget
|
|
352
|
+
candidates = [
|
|
353
|
+
(name, cached) for name, cached in self._cache.items()
|
|
354
|
+
if name != self._current_tool
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
if not candidates:
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
# Find LRU
|
|
361
|
+
lru_name, _ = min(candidates, key=lambda x: x[1].last_shown)
|
|
362
|
+
self._destroy_widget(lru_name)
|
|
363
|
+
self.logger.debug(f"Evicted LRU widget: {lru_name}")
|
|
364
|
+
|
|
365
|
+
def clear(self) -> None:
|
|
366
|
+
"""Clear all cached widgets."""
|
|
367
|
+
for tool_name in list(self._cache.keys()):
|
|
368
|
+
self._destroy_widget(tool_name)
|
|
369
|
+
self._current_tool = None
|
|
370
|
+
self.logger.debug("Cleared all cached widgets")
|
|
371
|
+
|
|
372
|
+
def refresh_all(self) -> None:
|
|
373
|
+
"""Mark all widgets for refresh."""
|
|
374
|
+
for cached in self._cache.values():
|
|
375
|
+
cached.needs_refresh = True
|
|
376
|
+
self.logger.debug("Marked all widgets for refresh")
|
|
377
|
+
|
|
378
|
+
def is_cached(self, tool_name: str) -> bool:
|
|
379
|
+
"""Check if a tool's widget is cached."""
|
|
380
|
+
return tool_name in self._cache
|
|
381
|
+
|
|
382
|
+
def get_widget(self, tool_name: str) -> Optional[tk.Widget]:
|
|
383
|
+
"""
|
|
384
|
+
Get a cached widget without creating it.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
tool_name: Name of the tool
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The widget if cached, None otherwise
|
|
391
|
+
"""
|
|
392
|
+
if tool_name in self._cache:
|
|
393
|
+
return self._cache[tool_name].widget
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def cached_tools(self) -> List[str]:
|
|
398
|
+
"""Get list of currently cached tool names."""
|
|
399
|
+
return list(self._cache.keys())
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def current_tool(self) -> Optional[str]:
|
|
403
|
+
"""Get the currently displayed tool name."""
|
|
404
|
+
return self._current_tool
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def factory_count(self) -> int:
|
|
408
|
+
"""Get number of registered factories."""
|
|
409
|
+
return len(self._factories)
|
|
410
|
+
|
|
411
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
412
|
+
"""Get cache statistics."""
|
|
413
|
+
total_requests = self._stats['cache_hits'] + self._stats['cache_misses']
|
|
414
|
+
hit_rate = (
|
|
415
|
+
self._stats['cache_hits'] / total_requests * 100
|
|
416
|
+
if total_requests > 0 else 0
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
**self._stats,
|
|
421
|
+
'cached_widgets': len(self._cache),
|
|
422
|
+
'registered_factories': len(self._factories),
|
|
423
|
+
'current_tool': self._current_tool,
|
|
424
|
+
'hit_rate_percent': round(hit_rate, 2),
|
|
425
|
+
'strategy': self.strategy.value
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
def get_cache_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
|
|
429
|
+
"""
|
|
430
|
+
Get information about a cached widget.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
tool_name: Name of the tool
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Dictionary with cache info, or None if not cached
|
|
437
|
+
"""
|
|
438
|
+
if tool_name not in self._cache:
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
cached = self._cache[tool_name]
|
|
442
|
+
return {
|
|
443
|
+
'tool_name': cached.tool_name,
|
|
444
|
+
'created_at': cached.created_at,
|
|
445
|
+
'last_shown': cached.last_shown,
|
|
446
|
+
'show_count': cached.show_count,
|
|
447
|
+
'needs_refresh': cached.needs_refresh
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# Callback setters
|
|
451
|
+
def set_on_widget_created(self, callback: Callable[[str, tk.Widget], None]) -> None:
|
|
452
|
+
"""Set callback for when a widget is created."""
|
|
453
|
+
self._on_widget_created = callback
|
|
454
|
+
|
|
455
|
+
def set_on_widget_shown(self, callback: Callable[[str, tk.Widget], None]) -> None:
|
|
456
|
+
"""Set callback for when a widget is shown."""
|
|
457
|
+
self._on_widget_shown = callback
|
|
458
|
+
|
|
459
|
+
def set_on_widget_hidden(self, callback: Callable[[str, tk.Widget], None]) -> None:
|
|
460
|
+
"""Set callback for when a widget is hidden."""
|
|
461
|
+
self._on_widget_hidden = callback
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Global instance
|
|
465
|
+
_widget_cache: Optional[WidgetCache] = None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def get_widget_cache() -> Optional[WidgetCache]:
|
|
469
|
+
"""Get the global widget cache instance."""
|
|
470
|
+
return _widget_cache
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def init_widget_cache(parent_frame: tk.Frame,
|
|
474
|
+
strategy: CacheStrategy = CacheStrategy.ALWAYS,
|
|
475
|
+
max_cached: int = 20) -> WidgetCache:
|
|
476
|
+
"""
|
|
477
|
+
Initialize the global widget cache.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
parent_frame: Parent frame for widgets
|
|
481
|
+
strategy: Caching strategy
|
|
482
|
+
max_cached: Maximum cached widgets (for LRU)
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Initialized WidgetCache
|
|
486
|
+
"""
|
|
487
|
+
global _widget_cache
|
|
488
|
+
_widget_cache = WidgetCache(parent_frame, strategy, max_cached)
|
|
489
|
+
return _widget_cache
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def shutdown_widget_cache() -> None:
|
|
493
|
+
"""Shutdown the global widget cache."""
|
|
494
|
+
global _widget_cache
|
|
495
|
+
if _widget_cache is not None:
|
|
496
|
+
_widget_cache.clear()
|
|
497
|
+
_widget_cache = None
|
|
498
|
+
|