pomera-ai-commander 1.2.5 → 1.2.8

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.
@@ -0,0 +1,197 @@
1
+ """
2
+ Collapsible Panel Widget - Reusable collapsible container for UI sections.
3
+
4
+ This module provides a collapsible panel widget that can be used to hide/show
5
+ sections of the UI, saving screen space while keeping functionality accessible.
6
+
7
+ Author: Pomera AI Commander Team
8
+ """
9
+
10
+ import tkinter as tk
11
+ from tkinter import ttk
12
+ from typing import Optional, Callable
13
+ import logging
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CollapsiblePanel(ttk.Frame):
20
+ """
21
+ A reusable collapsible panel widget.
22
+
23
+ Features:
24
+ - Toggle button with chevron icon (▼/▲)
25
+ - Smooth collapse/expand animation
26
+ - State persistence via callback
27
+ - Keyboard shortcut support
28
+ - Optional title display
29
+
30
+ Usage:
31
+ panel = CollapsiblePanel(
32
+ parent,
33
+ title="Options",
34
+ collapsed=False,
35
+ on_state_change=lambda collapsed: save_state(collapsed)
36
+ )
37
+ # Add content to panel.content_frame
38
+ ttk.Label(panel.content_frame, text="Panel content").pack()
39
+ """
40
+
41
+ # Animation settings
42
+ ANIMATION_DURATION_MS = 200
43
+ ANIMATION_STEPS = 10
44
+
45
+ def __init__(
46
+ self,
47
+ parent: tk.Widget,
48
+ title: str = "",
49
+ collapsed: bool = False,
50
+ on_state_change: Optional[Callable[[bool], None]] = None,
51
+ show_title: bool = True,
52
+ **kwargs
53
+ ):
54
+ """
55
+ Initialize the collapsible panel.
56
+
57
+ Args:
58
+ parent: Parent widget
59
+ title: Panel title text
60
+ collapsed: Initial collapsed state
61
+ on_state_change: Callback when collapse state changes, receives bool
62
+ show_title: Whether to show the title text
63
+ **kwargs: Additional keyword arguments for ttk.Frame
64
+ """
65
+ super().__init__(parent, **kwargs)
66
+
67
+ self.title = title
68
+ self._collapsed = collapsed
69
+ self._on_state_change = on_state_change
70
+ self._show_title = show_title
71
+ self._animation_id: Optional[str] = None
72
+ self._content_height: int = 0
73
+
74
+ self._create_widgets()
75
+ self._apply_initial_state()
76
+
77
+ def _create_widgets(self) -> None:
78
+ """Create the panel widgets."""
79
+ # Header frame with toggle button
80
+ self.header_frame = ttk.Frame(self)
81
+ self.header_frame.pack(fill=tk.X)
82
+
83
+ # Toggle button with chevron
84
+ self.toggle_btn = ttk.Button(
85
+ self.header_frame,
86
+ text=self._get_toggle_text(),
87
+ command=self.toggle,
88
+ width=3
89
+ )
90
+ self.toggle_btn.pack(side=tk.LEFT, padx=(0, 5))
91
+
92
+ # Title label with inline shortcut hint
93
+ if self._show_title and self.title:
94
+ self.title_label = ttk.Label(
95
+ self.header_frame,
96
+ text=f"{self.title} (Ctrl+Shift+H)",
97
+ font=("TkDefaultFont", 9, "bold")
98
+ )
99
+ self.title_label.pack(side=tk.LEFT)
100
+ # Make title clickable too
101
+ self.title_label.bind("<Button-1>", lambda e: self.toggle())
102
+
103
+ # Content frame (what gets collapsed)
104
+ self.content_frame = ttk.Frame(self)
105
+ if not self._collapsed:
106
+ self.content_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
107
+
108
+ def _get_toggle_text(self) -> str:
109
+ """Get the toggle button text based on collapsed state."""
110
+ return "▲" if self._collapsed else "▼"
111
+
112
+ def _apply_initial_state(self) -> None:
113
+ """Apply the initial collapsed state."""
114
+ if self._collapsed:
115
+ self.content_frame.pack_forget()
116
+ self.toggle_btn.configure(text=self._get_toggle_text())
117
+
118
+ @property
119
+ def collapsed(self) -> bool:
120
+ """Get the current collapsed state."""
121
+ return self._collapsed
122
+
123
+ @collapsed.setter
124
+ def collapsed(self, value: bool) -> None:
125
+ """Set the collapsed state."""
126
+ if value != self._collapsed:
127
+ self._collapsed = value
128
+ self._update_state()
129
+
130
+ def toggle(self) -> None:
131
+ """Toggle the collapsed state."""
132
+ self._collapsed = not self._collapsed
133
+ self._update_state()
134
+
135
+ # Notify callback
136
+ if self._on_state_change:
137
+ try:
138
+ self._on_state_change(self._collapsed)
139
+ except Exception as e:
140
+ logger.warning(f"Error in on_state_change callback: {e}")
141
+
142
+ def _update_state(self) -> None:
143
+ """Update the visual state based on collapsed flag."""
144
+ # Cancel any running animation
145
+ if self._animation_id:
146
+ self.after_cancel(self._animation_id)
147
+ self._animation_id = None
148
+
149
+ # Update toggle button
150
+ self.toggle_btn.configure(text=self._get_toggle_text())
151
+
152
+ # Show/hide content
153
+ if self._collapsed:
154
+ self.content_frame.pack_forget()
155
+ else:
156
+ self.content_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
157
+
158
+ logger.debug(f"Panel '{self.title}' collapsed={self._collapsed}")
159
+
160
+ def expand(self) -> None:
161
+ """Expand the panel if collapsed."""
162
+ if self._collapsed:
163
+ self.toggle()
164
+
165
+ def collapse(self) -> None:
166
+ """Collapse the panel if expanded."""
167
+ if not self._collapsed:
168
+ self.toggle()
169
+
170
+ def set_content_widget(self, widget: tk.Widget) -> None:
171
+ """
172
+ Set a widget as the panel content.
173
+
174
+ Args:
175
+ widget: Widget to place inside the content frame
176
+ """
177
+ # Clear existing content
178
+ for child in self.content_frame.winfo_children():
179
+ child.destroy()
180
+
181
+ # Add new content
182
+ widget.pack(in_=self.content_frame, fill=tk.BOTH, expand=True)
183
+
184
+ def bind_shortcut(self, root: tk.Tk, shortcut: str = "<Control-Shift-H>") -> None:
185
+ """
186
+ Bind a keyboard shortcut to toggle the panel.
187
+
188
+ Args:
189
+ root: Root window to bind the shortcut to
190
+ shortcut: Key sequence (default: Ctrl+Shift+H)
191
+ """
192
+ root.bind_all(shortcut, lambda e: self.toggle())
193
+ logger.debug(f"Bound shortcut {shortcut} to panel '{self.title}'")
194
+
195
+
196
+ # Module availability flag for import checking
197
+ COLLAPSIBLE_PANEL_AVAILABLE = True
@@ -45,12 +45,17 @@ class NestedSettingsProxy:
45
45
  def __getitem__(self, key: str) -> Any:
46
46
  """Handle nested access like settings["tool_settings"]["Tool Name"]."""
47
47
  if key not in self._data:
48
- # For tool_settings, create empty tool settings when accessed
48
+ # For tool_settings, first try to load existing settings from database
49
49
  if self.parent_key == "tool_settings":
50
- # Initialize empty tool settings
51
- self._data[key] = {}
52
- # Also save to database
53
- self.settings_manager.set_tool_setting(key, "initialized", True)
50
+ existing_settings = self.settings_manager.get_tool_settings(key)
51
+ if existing_settings and not (len(existing_settings) == 1 and 'initialized' in existing_settings):
52
+ # Found real settings in database - use them
53
+ self._data[key] = existing_settings
54
+ else:
55
+ # No existing settings - create empty tool settings
56
+ self._data[key] = {}
57
+ # Save initialized marker to database
58
+ self.settings_manager.set_tool_setting(key, "initialized", True)
54
59
  else:
55
60
  raise KeyError(f"Key '{key}' not found in {self.parent_key}")
56
61
 
@@ -91,10 +96,16 @@ class NestedSettingsProxy:
91
96
  def get(self, key: str, default: Any = None) -> Any:
92
97
  """Handle nested_settings.get("key", default) calls."""
93
98
  if key not in self._data and self.parent_key == "tool_settings":
94
- # For tool_settings, create empty tool settings when accessed via get()
95
- self._data[key] = {}
96
- # Also save to database
97
- self.settings_manager.set_tool_setting(key, "initialized", True)
99
+ # For tool_settings, first try to load existing settings from database
100
+ existing_settings = self.settings_manager.get_tool_settings(key)
101
+ if existing_settings and not (len(existing_settings) == 1 and 'initialized' in existing_settings):
102
+ # Found real settings in database - use them
103
+ self._data[key] = existing_settings
104
+ else:
105
+ # No existing settings - create empty tool settings
106
+ self._data[key] = {}
107
+ # Save initialized marker to database
108
+ self.settings_manager.set_tool_setting(key, "initialized", True)
98
109
 
99
110
  value = self._data.get(key, default)
100
111
 
@@ -324,8 +324,9 @@ class MigrationManager:
324
324
  """
325
325
  try:
326
326
  with self.connection_manager.transaction() as conn:
327
- # Clear existing data
328
- self._clear_all_tables(conn)
327
+ # NOTE: Do NOT clear tables - use INSERT OR REPLACE for upsert semantics
328
+ # The _clear_all_tables() call was removed because it caused data loss
329
+ # when save_settings() was called with incomplete/empty data
329
330
 
330
331
  # Migrate core settings
331
332
  self._migrate_core_settings(conn, json_data)
@@ -432,7 +433,7 @@ class MigrationManager:
432
433
  serialized_value = self.converter.serialize_value(value)
433
434
 
434
435
  conn.execute(
435
- "INSERT INTO core_settings (key, value, data_type) VALUES (?, ?, ?)",
436
+ "INSERT OR REPLACE INTO core_settings (key, value, data_type) VALUES (?, ?, ?)",
436
437
  (key, serialized_value, data_type)
437
438
  )
438
439
 
@@ -454,7 +455,7 @@ class MigrationManager:
454
455
  serialized_value = self.converter.serialize_value(value)
455
456
 
456
457
  conn.execute(
457
- "INSERT INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
458
+ "INSERT OR REPLACE INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
458
459
  (tool_name, setting_path, serialized_value, data_type)
459
460
  )
460
461
  else:
@@ -463,7 +464,7 @@ class MigrationManager:
463
464
  serialized_value = self.converter.serialize_value(tool_config)
464
465
 
465
466
  conn.execute(
466
- "INSERT INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
467
+ "INSERT OR REPLACE INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
467
468
  (tool_name, 'value', serialized_value, data_type)
468
469
  )
469
470
 
@@ -480,7 +481,7 @@ class MigrationManager:
480
481
  input_tabs = json_data['input_tabs']
481
482
  for i, content in enumerate(input_tabs):
482
483
  conn.execute(
483
- "INSERT INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
484
+ "INSERT OR REPLACE INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
484
485
  ('input', i, content or '')
485
486
  )
486
487
 
@@ -489,7 +490,7 @@ class MigrationManager:
489
490
  output_tabs = json_data['output_tabs']
490
491
  for i, content in enumerate(output_tabs):
491
492
  conn.execute(
492
- "INSERT INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
493
+ "INSERT OR REPLACE INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
493
494
  ('output', i, content or '')
494
495
  )
495
496
 
@@ -511,7 +512,7 @@ class MigrationManager:
511
512
  serialized_value = self.converter.serialize_value(value)
512
513
 
513
514
  conn.execute(
514
- "INSERT INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
515
+ "INSERT OR REPLACE INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
515
516
  (category, setting_key, serialized_value, data_type)
516
517
  )
517
518
  else:
@@ -520,7 +521,7 @@ class MigrationManager:
520
521
  serialized_value = self.converter.serialize_value(settings)
521
522
 
522
523
  conn.execute(
523
- "INSERT INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
524
+ "INSERT OR REPLACE INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
524
525
  (category, 'value', serialized_value, data_type)
525
526
  )
526
527
 
@@ -539,7 +540,7 @@ class MigrationManager:
539
540
  serialized_value = self.converter.serialize_value(value)
540
541
 
541
542
  conn.execute(
542
- "INSERT INTO font_settings (font_type, property, value, data_type) VALUES (?, ?, ?, ?)",
543
+ "INSERT OR REPLACE INTO font_settings (font_type, property, value, data_type) VALUES (?, ?, ?, ?)",
543
544
  (font_type, property_name, serialized_value, data_type)
544
545
  )
545
546
 
@@ -560,7 +561,7 @@ class MigrationManager:
560
561
  self.logger.debug(f"Inserting dialog setting: {category}.{property_name} = {value} (type: {data_type})")
561
562
 
562
563
  conn.execute(
563
- "INSERT INTO dialog_settings (category, property, value, data_type) VALUES (?, ?, ?, ?)",
564
+ "INSERT OR REPLACE INTO dialog_settings (category, property, value, data_type) VALUES (?, ?, ?, ?)",
564
565
  (category, property_name, serialized_value, data_type)
565
566
  )
566
567
 
@@ -833,6 +833,14 @@ class SettingsDefaultsRegistry:
833
833
  "description": "Error messages for critical issues (cannot be disabled)",
834
834
  "examples": ["File not found", "Network error", "Invalid configuration"]
835
835
  }
836
+ },
837
+ # UI Layout Settings for tool search and collapsible panels
838
+ "ui_layout": {
839
+ "options_panel_collapsed": False,
840
+ "search_bar_collapsed": False,
841
+ "favorite_tools": [],
842
+ "recent_tools": [],
843
+ "recent_tools_max": 10
836
844
  }
837
845
  }
838
846