pomera-ai-commander 0.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,313 +1,313 @@
1
- """
2
- Context Menu Module for Text Widgets
3
-
4
- This module provides a reusable right-click context menu for text widgets and entry fields
5
- with standard operations: Cut, Copy, Paste, Select All, and Delete.
6
-
7
- Features:
8
- - Automatic detection of selected text
9
- - Smart menu item enabling/disabling based on context
10
- - Support for Text, Entry, and ScrolledText widgets
11
- - Keyboard shortcuts displayed in menu
12
- - Cross-platform compatibility
13
-
14
- Author: Promera AI Commander
15
- """
16
-
17
- import tkinter as tk
18
- from tkinter import ttk, scrolledtext
19
- from typing import Optional, Union
20
- import platform
21
-
22
-
23
- class TextContextMenu:
24
- """
25
- Context menu manager for text widgets.
26
-
27
- Provides right-click context menu with Cut, Copy, Paste, Select All, and Delete
28
- operations. Automatically enables/disables menu items based on selection state
29
- and clipboard content.
30
- """
31
-
32
- def __init__(self, widget: Union[tk.Text, tk.Entry, scrolledtext.ScrolledText]):
33
- """
34
- Initialize context menu for a text widget.
35
-
36
- Args:
37
- widget: The text widget to attach the context menu to
38
- """
39
- self.widget = widget
40
- self.menu = None
41
- self._create_menu()
42
- self._bind_events()
43
-
44
- def _create_menu(self):
45
- """Create the context menu with standard operations."""
46
- self.menu = tk.Menu(self.widget, tearoff=0)
47
-
48
- # Determine keyboard shortcuts based on platform
49
- if platform.system() == "Darwin": # macOS
50
- cut_accel = "Cmd+X"
51
- copy_accel = "Cmd+C"
52
- paste_accel = "Cmd+V"
53
- select_all_accel = "Cmd+A"
54
- else: # Windows/Linux
55
- cut_accel = "Ctrl+X"
56
- copy_accel = "Ctrl+C"
57
- paste_accel = "Ctrl+V"
58
- select_all_accel = "Ctrl+A"
59
-
60
- # Add menu items
61
- self.menu.add_command(
62
- label="Cut",
63
- command=self._cut,
64
- accelerator=cut_accel
65
- )
66
- self.menu.add_command(
67
- label="Copy",
68
- command=self._copy,
69
- accelerator=copy_accel
70
- )
71
- self.menu.add_command(
72
- label="Paste",
73
- command=self._paste,
74
- accelerator=paste_accel
75
- )
76
- self.menu.add_separator()
77
- self.menu.add_command(
78
- label="Select All",
79
- command=self._select_all,
80
- accelerator=select_all_accel
81
- )
82
- self.menu.add_command(
83
- label="Delete",
84
- command=self._delete
85
- )
86
-
87
- def _bind_events(self):
88
- """Bind right-click event to show menu."""
89
- # Right-click on Windows/Linux
90
- self.widget.bind("<Button-3>", self._show_menu)
91
-
92
- # Right-click on macOS (also bind Button-2 for compatibility)
93
- if platform.system() == "Darwin":
94
- self.widget.bind("<Button-2>", self._show_menu)
95
- self.widget.bind("<Control-Button-1>", self._show_menu)
96
-
97
- def _show_menu(self, event):
98
- """
99
- Show the context menu at the cursor position.
100
-
101
- Args:
102
- event: The mouse event
103
- """
104
- # Update menu item states based on current context
105
- self._update_menu_states()
106
-
107
- # Show menu at cursor position
108
- try:
109
- self.menu.tk_popup(event.x_root, event.y_root)
110
- finally:
111
- self.menu.grab_release()
112
-
113
- def _update_menu_states(self):
114
- """Update menu item states based on selection and clipboard."""
115
- has_selection = self._has_selection()
116
- is_readonly = self._is_readonly()
117
- has_clipboard = self._has_clipboard_content()
118
-
119
- # Cut: enabled if has selection and not readonly
120
- if has_selection and not is_readonly:
121
- self.menu.entryconfig("Cut", state="normal")
122
- else:
123
- self.menu.entryconfig("Cut", state="disabled")
124
-
125
- # Copy: enabled if has selection
126
- if has_selection:
127
- self.menu.entryconfig("Copy", state="normal")
128
- else:
129
- self.menu.entryconfig("Copy", state="disabled")
130
-
131
- # Paste: enabled if clipboard has content and not readonly
132
- if has_clipboard and not is_readonly:
133
- self.menu.entryconfig("Paste", state="normal")
134
- else:
135
- self.menu.entryconfig("Paste", state="disabled")
136
-
137
- # Select All: always enabled if widget has content
138
- if self._has_content():
139
- self.menu.entryconfig("Select All", state="normal")
140
- else:
141
- self.menu.entryconfig("Select All", state="disabled")
142
-
143
- # Delete: enabled if has selection and not readonly
144
- if has_selection and not is_readonly:
145
- self.menu.entryconfig("Delete", state="normal")
146
- else:
147
- self.menu.entryconfig("Delete", state="disabled")
148
-
149
- def _has_selection(self) -> bool:
150
- """Check if widget has selected text."""
151
- try:
152
- if isinstance(self.widget, tk.Text):
153
- return bool(self.widget.tag_ranges("sel"))
154
- elif isinstance(self.widget, tk.Entry):
155
- return self.widget.selection_present()
156
- return False
157
- except:
158
- return False
159
-
160
- def _is_readonly(self) -> bool:
161
- """Check if widget is read-only."""
162
- try:
163
- if isinstance(self.widget, tk.Text):
164
- state = str(self.widget.cget("state"))
165
- return state == "disabled"
166
- elif isinstance(self.widget, tk.Entry):
167
- state = str(self.widget.cget("state"))
168
- return state == "disabled" or state == "readonly"
169
- return False
170
- except:
171
- return False
172
-
173
- def _has_clipboard_content(self) -> bool:
174
- """Check if clipboard has content."""
175
- try:
176
- self.widget.clipboard_get()
177
- return True
178
- except:
179
- return False
180
-
181
- def _has_content(self) -> bool:
182
- """Check if widget has any content."""
183
- try:
184
- if isinstance(self.widget, tk.Text):
185
- content = self.widget.get("1.0", tk.END).strip()
186
- return bool(content)
187
- elif isinstance(self.widget, tk.Entry):
188
- content = self.widget.get().strip()
189
- return bool(content)
190
- return False
191
- except:
192
- return False
193
-
194
- def _cut(self):
195
- """Cut selected text to clipboard."""
196
- try:
197
- if isinstance(self.widget, tk.Text):
198
- if self.widget.tag_ranges("sel"):
199
- self.widget.event_generate("<<Cut>>")
200
- elif isinstance(self.widget, tk.Entry):
201
- if self.widget.selection_present():
202
- self.widget.event_generate("<<Cut>>")
203
- except Exception as e:
204
- print(f"Error in cut operation: {e}")
205
-
206
- def _copy(self):
207
- """Copy selected text to clipboard."""
208
- try:
209
- if isinstance(self.widget, tk.Text):
210
- if self.widget.tag_ranges("sel"):
211
- self.widget.event_generate("<<Copy>>")
212
- elif isinstance(self.widget, tk.Entry):
213
- if self.widget.selection_present():
214
- self.widget.event_generate("<<Copy>>")
215
- except Exception as e:
216
- print(f"Error in copy operation: {e}")
217
-
218
- def _paste(self):
219
- """Paste clipboard content at cursor position."""
220
- try:
221
- self.widget.event_generate("<<Paste>>")
222
- except Exception as e:
223
- print(f"Error in paste operation: {e}")
224
-
225
- def _select_all(self):
226
- """Select all text in widget."""
227
- try:
228
- if isinstance(self.widget, tk.Text):
229
- self.widget.tag_add("sel", "1.0", tk.END)
230
- self.widget.mark_set("insert", "1.0")
231
- self.widget.see("insert")
232
- elif isinstance(self.widget, tk.Entry):
233
- self.widget.select_range(0, tk.END)
234
- self.widget.icursor(tk.END)
235
- except Exception as e:
236
- print(f"Error in select all operation: {e}")
237
-
238
- def _delete(self):
239
- """Delete selected text."""
240
- try:
241
- if isinstance(self.widget, tk.Text):
242
- if self.widget.tag_ranges("sel"):
243
- self.widget.delete("sel.first", "sel.last")
244
- elif isinstance(self.widget, tk.Entry):
245
- if self.widget.selection_present():
246
- self.widget.delete("sel.first", "sel.last")
247
- except Exception as e:
248
- print(f"Error in delete operation: {e}")
249
-
250
-
251
- def add_context_menu(widget: Union[tk.Text, tk.Entry, scrolledtext.ScrolledText]) -> TextContextMenu:
252
- """
253
- Add a context menu to a text widget.
254
-
255
- This is a convenience function that creates and attaches a context menu
256
- to the specified widget.
257
-
258
- Args:
259
- widget: The text widget to add context menu to
260
-
261
- Returns:
262
- TextContextMenu instance
263
-
264
- Example:
265
- >>> text_widget = tk.Text(parent)
266
- >>> context_menu = add_context_menu(text_widget)
267
- """
268
- return TextContextMenu(widget)
269
-
270
-
271
- def add_context_menu_to_children(parent: tk.Widget, widget_types: Optional[tuple] = None):
272
- """
273
- Recursively add context menus to all text widgets in a parent widget.
274
-
275
- Args:
276
- parent: Parent widget to search for text widgets
277
- widget_types: Tuple of widget types to add context menu to.
278
- Defaults to (tk.Text, tk.Entry, scrolledtext.ScrolledText)
279
-
280
- Example:
281
- >>> # Add context menus to all text widgets in a frame
282
- >>> add_context_menu_to_children(my_frame)
283
- """
284
- if widget_types is None:
285
- widget_types = (tk.Text, tk.Entry)
286
-
287
- try:
288
- for child in parent.winfo_children():
289
- # Add context menu if it's a text widget
290
- if isinstance(child, widget_types):
291
- # Check if context menu already exists
292
- if not hasattr(child, '_context_menu'):
293
- child._context_menu = add_context_menu(child)
294
-
295
- # Recursively process children
296
- if hasattr(child, 'winfo_children'):
297
- add_context_menu_to_children(child, widget_types)
298
- except Exception as e:
299
- print(f"Error adding context menus to children: {e}")
300
-
301
-
302
- # Convenience function for backward compatibility
303
- def setup_text_context_menu(widget: Union[tk.Text, tk.Entry]) -> TextContextMenu:
304
- """
305
- Setup context menu for a text widget (alias for add_context_menu).
306
-
307
- Args:
308
- widget: The text widget
309
-
310
- Returns:
311
- TextContextMenu instance
312
- """
313
- return add_context_menu(widget)
1
+ """
2
+ Context Menu Module for Text Widgets
3
+
4
+ This module provides a reusable right-click context menu for text widgets and entry fields
5
+ with standard operations: Cut, Copy, Paste, Select All, and Delete.
6
+
7
+ Features:
8
+ - Automatic detection of selected text
9
+ - Smart menu item enabling/disabling based on context
10
+ - Support for Text, Entry, and ScrolledText widgets
11
+ - Keyboard shortcuts displayed in menu
12
+ - Cross-platform compatibility
13
+
14
+ Author: Promera AI Commander
15
+ """
16
+
17
+ import tkinter as tk
18
+ from tkinter import ttk, scrolledtext
19
+ from typing import Optional, Union
20
+ import platform
21
+
22
+
23
+ class TextContextMenu:
24
+ """
25
+ Context menu manager for text widgets.
26
+
27
+ Provides right-click context menu with Cut, Copy, Paste, Select All, and Delete
28
+ operations. Automatically enables/disables menu items based on selection state
29
+ and clipboard content.
30
+ """
31
+
32
+ def __init__(self, widget: Union[tk.Text, tk.Entry, scrolledtext.ScrolledText]):
33
+ """
34
+ Initialize context menu for a text widget.
35
+
36
+ Args:
37
+ widget: The text widget to attach the context menu to
38
+ """
39
+ self.widget = widget
40
+ self.menu = None
41
+ self._create_menu()
42
+ self._bind_events()
43
+
44
+ def _create_menu(self):
45
+ """Create the context menu with standard operations."""
46
+ self.menu = tk.Menu(self.widget, tearoff=0)
47
+
48
+ # Determine keyboard shortcuts based on platform
49
+ if platform.system() == "Darwin": # macOS
50
+ cut_accel = "Cmd+X"
51
+ copy_accel = "Cmd+C"
52
+ paste_accel = "Cmd+V"
53
+ select_all_accel = "Cmd+A"
54
+ else: # Windows/Linux
55
+ cut_accel = "Ctrl+X"
56
+ copy_accel = "Ctrl+C"
57
+ paste_accel = "Ctrl+V"
58
+ select_all_accel = "Ctrl+A"
59
+
60
+ # Add menu items
61
+ self.menu.add_command(
62
+ label="Cut",
63
+ command=self._cut,
64
+ accelerator=cut_accel
65
+ )
66
+ self.menu.add_command(
67
+ label="Copy",
68
+ command=self._copy,
69
+ accelerator=copy_accel
70
+ )
71
+ self.menu.add_command(
72
+ label="Paste",
73
+ command=self._paste,
74
+ accelerator=paste_accel
75
+ )
76
+ self.menu.add_separator()
77
+ self.menu.add_command(
78
+ label="Select All",
79
+ command=self._select_all,
80
+ accelerator=select_all_accel
81
+ )
82
+ self.menu.add_command(
83
+ label="Delete",
84
+ command=self._delete
85
+ )
86
+
87
+ def _bind_events(self):
88
+ """Bind right-click event to show menu."""
89
+ # Right-click on Windows/Linux
90
+ self.widget.bind("<Button-3>", self._show_menu)
91
+
92
+ # Right-click on macOS (also bind Button-2 for compatibility)
93
+ if platform.system() == "Darwin":
94
+ self.widget.bind("<Button-2>", self._show_menu)
95
+ self.widget.bind("<Control-Button-1>", self._show_menu)
96
+
97
+ def _show_menu(self, event):
98
+ """
99
+ Show the context menu at the cursor position.
100
+
101
+ Args:
102
+ event: The mouse event
103
+ """
104
+ # Update menu item states based on current context
105
+ self._update_menu_states()
106
+
107
+ # Show menu at cursor position
108
+ try:
109
+ self.menu.tk_popup(event.x_root, event.y_root)
110
+ finally:
111
+ self.menu.grab_release()
112
+
113
+ def _update_menu_states(self):
114
+ """Update menu item states based on selection and clipboard."""
115
+ has_selection = self._has_selection()
116
+ is_readonly = self._is_readonly()
117
+ has_clipboard = self._has_clipboard_content()
118
+
119
+ # Cut: enabled if has selection and not readonly
120
+ if has_selection and not is_readonly:
121
+ self.menu.entryconfig("Cut", state="normal")
122
+ else:
123
+ self.menu.entryconfig("Cut", state="disabled")
124
+
125
+ # Copy: enabled if has selection
126
+ if has_selection:
127
+ self.menu.entryconfig("Copy", state="normal")
128
+ else:
129
+ self.menu.entryconfig("Copy", state="disabled")
130
+
131
+ # Paste: enabled if clipboard has content and not readonly
132
+ if has_clipboard and not is_readonly:
133
+ self.menu.entryconfig("Paste", state="normal")
134
+ else:
135
+ self.menu.entryconfig("Paste", state="disabled")
136
+
137
+ # Select All: always enabled if widget has content
138
+ if self._has_content():
139
+ self.menu.entryconfig("Select All", state="normal")
140
+ else:
141
+ self.menu.entryconfig("Select All", state="disabled")
142
+
143
+ # Delete: enabled if has selection and not readonly
144
+ if has_selection and not is_readonly:
145
+ self.menu.entryconfig("Delete", state="normal")
146
+ else:
147
+ self.menu.entryconfig("Delete", state="disabled")
148
+
149
+ def _has_selection(self) -> bool:
150
+ """Check if widget has selected text."""
151
+ try:
152
+ if isinstance(self.widget, tk.Text):
153
+ return bool(self.widget.tag_ranges("sel"))
154
+ elif isinstance(self.widget, tk.Entry):
155
+ return self.widget.selection_present()
156
+ return False
157
+ except:
158
+ return False
159
+
160
+ def _is_readonly(self) -> bool:
161
+ """Check if widget is read-only."""
162
+ try:
163
+ if isinstance(self.widget, tk.Text):
164
+ state = str(self.widget.cget("state"))
165
+ return state == "disabled"
166
+ elif isinstance(self.widget, tk.Entry):
167
+ state = str(self.widget.cget("state"))
168
+ return state == "disabled" or state == "readonly"
169
+ return False
170
+ except:
171
+ return False
172
+
173
+ def _has_clipboard_content(self) -> bool:
174
+ """Check if clipboard has content."""
175
+ try:
176
+ self.widget.clipboard_get()
177
+ return True
178
+ except:
179
+ return False
180
+
181
+ def _has_content(self) -> bool:
182
+ """Check if widget has any content."""
183
+ try:
184
+ if isinstance(self.widget, tk.Text):
185
+ content = self.widget.get("1.0", tk.END).strip()
186
+ return bool(content)
187
+ elif isinstance(self.widget, tk.Entry):
188
+ content = self.widget.get().strip()
189
+ return bool(content)
190
+ return False
191
+ except:
192
+ return False
193
+
194
+ def _cut(self):
195
+ """Cut selected text to clipboard."""
196
+ try:
197
+ if isinstance(self.widget, tk.Text):
198
+ if self.widget.tag_ranges("sel"):
199
+ self.widget.event_generate("<<Cut>>")
200
+ elif isinstance(self.widget, tk.Entry):
201
+ if self.widget.selection_present():
202
+ self.widget.event_generate("<<Cut>>")
203
+ except Exception as e:
204
+ print(f"Error in cut operation: {e}")
205
+
206
+ def _copy(self):
207
+ """Copy selected text to clipboard."""
208
+ try:
209
+ if isinstance(self.widget, tk.Text):
210
+ if self.widget.tag_ranges("sel"):
211
+ self.widget.event_generate("<<Copy>>")
212
+ elif isinstance(self.widget, tk.Entry):
213
+ if self.widget.selection_present():
214
+ self.widget.event_generate("<<Copy>>")
215
+ except Exception as e:
216
+ print(f"Error in copy operation: {e}")
217
+
218
+ def _paste(self):
219
+ """Paste clipboard content at cursor position."""
220
+ try:
221
+ self.widget.event_generate("<<Paste>>")
222
+ except Exception as e:
223
+ print(f"Error in paste operation: {e}")
224
+
225
+ def _select_all(self):
226
+ """Select all text in widget."""
227
+ try:
228
+ if isinstance(self.widget, tk.Text):
229
+ self.widget.tag_add("sel", "1.0", tk.END)
230
+ self.widget.mark_set("insert", "1.0")
231
+ self.widget.see("insert")
232
+ elif isinstance(self.widget, tk.Entry):
233
+ self.widget.select_range(0, tk.END)
234
+ self.widget.icursor(tk.END)
235
+ except Exception as e:
236
+ print(f"Error in select all operation: {e}")
237
+
238
+ def _delete(self):
239
+ """Delete selected text."""
240
+ try:
241
+ if isinstance(self.widget, tk.Text):
242
+ if self.widget.tag_ranges("sel"):
243
+ self.widget.delete("sel.first", "sel.last")
244
+ elif isinstance(self.widget, tk.Entry):
245
+ if self.widget.selection_present():
246
+ self.widget.delete("sel.first", "sel.last")
247
+ except Exception as e:
248
+ print(f"Error in delete operation: {e}")
249
+
250
+
251
+ def add_context_menu(widget: Union[tk.Text, tk.Entry, scrolledtext.ScrolledText]) -> TextContextMenu:
252
+ """
253
+ Add a context menu to a text widget.
254
+
255
+ This is a convenience function that creates and attaches a context menu
256
+ to the specified widget.
257
+
258
+ Args:
259
+ widget: The text widget to add context menu to
260
+
261
+ Returns:
262
+ TextContextMenu instance
263
+
264
+ Example:
265
+ >>> text_widget = tk.Text(parent)
266
+ >>> context_menu = add_context_menu(text_widget)
267
+ """
268
+ return TextContextMenu(widget)
269
+
270
+
271
+ def add_context_menu_to_children(parent: tk.Widget, widget_types: Optional[tuple] = None):
272
+ """
273
+ Recursively add context menus to all text widgets in a parent widget.
274
+
275
+ Args:
276
+ parent: Parent widget to search for text widgets
277
+ widget_types: Tuple of widget types to add context menu to.
278
+ Defaults to (tk.Text, tk.Entry, scrolledtext.ScrolledText)
279
+
280
+ Example:
281
+ >>> # Add context menus to all text widgets in a frame
282
+ >>> add_context_menu_to_children(my_frame)
283
+ """
284
+ if widget_types is None:
285
+ widget_types = (tk.Text, tk.Entry)
286
+
287
+ try:
288
+ for child in parent.winfo_children():
289
+ # Add context menu if it's a text widget
290
+ if isinstance(child, widget_types):
291
+ # Check if context menu already exists
292
+ if not hasattr(child, '_context_menu'):
293
+ child._context_menu = add_context_menu(child)
294
+
295
+ # Recursively process children
296
+ if hasattr(child, 'winfo_children'):
297
+ add_context_menu_to_children(child, widget_types)
298
+ except Exception as e:
299
+ print(f"Error adding context menus to children: {e}")
300
+
301
+
302
+ # Convenience function for backward compatibility
303
+ def setup_text_context_menu(widget: Union[tk.Text, tk.Entry]) -> TextContextMenu:
304
+ """
305
+ Setup context menu for a text widget (alias for add_context_menu).
306
+
307
+ Args:
308
+ widget: The text widget
309
+
310
+ Returns:
311
+ TextContextMenu instance
312
+ """
313
+ return add_context_menu(widget)