claude's gae

This commit is contained in:
2026-01-20 16:05:31 +01:00
parent e00c47a56f
commit afa053f006
6 changed files with 258 additions and 149 deletions
+182 -15
View File
@@ -66,6 +66,12 @@ class UnifiedDropdown(ctk.CTkFrame):
self.item_buttons: List[ctk.CTkButton] = []
self.item_tooltips: List[ToolTip] = []
# Search state
self.all_items: List[Dict] = [] # Store complete unfiltered list
self.filtered_items: List[Dict] = [] # Currently filtered items
self.search_entry: Optional[ctk.CTkEntry] = None
self.search_query: str = ""
# Popup window
self.popup_window: Optional[tk.Toplevel] = None
self.popup_frame: Optional[ctk.CTkScrollableFrame] = None
@@ -133,7 +139,10 @@ class UnifiedDropdown(ctk.CTkFrame):
Args:
items: List of item dictionaries
"""
self.items = items
# Store complete list and initialize filtered list
self.all_items = items
self.filtered_items = items.copy()
self.items = self.filtered_items
self.current_index = -1
# Update count label
@@ -202,6 +211,7 @@ class UnifiedDropdown(ctk.CTkFrame):
self.popup_window = tk.Toplevel(self)
self.popup_window.wm_overrideredirect(True) # Remove window decorations
self.popup_window.wm_attributes("-topmost", True) # Always on top
self.popup_window.wm_resizable(False, False) # Prevent resizing
# Set background color to prevent white flash
appearance_mode = ctk.get_appearance_mode()
@@ -213,6 +223,20 @@ class UnifiedDropdown(ctk.CTkFrame):
self.popup_window.withdraw() # Hide initially to prevent flash in top-left corner
# Create search entry at top of popup
self.search_entry = ctk.CTkEntry(
self.popup_window,
placeholder_text="Search...",
height=32,
font=ctk.CTkFont(size=13)
)
self.search_entry.pack(fill="x", padx=5, pady=(5, 0))
self.search_entry.focus_set()
# Bind search events
self.search_entry.bind("<KeyRelease>", self._on_search_changed)
self.search_entry.bind("<Escape>", lambda e: self._close_dropdown())
# Calculate dynamic height based on number of items
# Show first 10 items (or all if less than 10)
# Each item: 40px button + 2px borders (1px top + 1px bottom) + 4px padding (2px top + 2px bottom) = 46px per item
@@ -220,25 +244,27 @@ class UnifiedDropdown(ctk.CTkFrame):
max_visible_items = 10
items_to_show = min(len(self.items), max_visible_items)
# Height calculation: (items × 46px) + extra padding for frame borders
# Add 20px padding: 10px top + 10px bottom (5px pack padding + 5px extra for scrollbar/borders)
calculated_height = (items_to_show * item_height) + 20
# Height calculation: (items × 46px) + extra padding for frame borders + search box height
# Add 62px total: 42px for search box (32px height + 5px top padding + 5px spacing) + 20px for frame borders
calculated_height = (items_to_show * item_height) + 62
self._popup_height = calculated_height
# Set window size explicitly to ensure proper height
# Set window size explicitly and fix dimensions
self.popup_window.wm_geometry(f"{self.button_width}x{self._popup_height}")
self.popup_window.wm_minsize(self.button_width, self._popup_height)
self.popup_window.wm_maxsize(self.button_width, self._popup_height)
# Create scrollable frame for items with fixed width
# Frame height = window height - padding (10px pack padding total)
frame_height = self._popup_height - 10
# Frame height = window height - padding - search box height (10px pack padding total + 42px search)
frame_height = self._popup_height - 52
self.popup_frame = ctk.CTkScrollableFrame(
self.popup_window,
width=self.button_width - 20,
height=frame_height
)
self.popup_frame.pack(fill="both", expand=False, padx=5, pady=5)
self.popup_frame.pack(fill="both", expand=True, padx=5, pady=5)
# Configure scroll speed to match main window (40px per scroll unit = 2x faster)
self.popup_frame._parent_canvas.configure(yscrollincrement=40)
@@ -275,7 +301,8 @@ class UnifiedDropdown(ctk.CTkFrame):
border_width=1,
border_color="gray50",
hover_color=("gray70", "gray30"),
anchor="w"
anchor="w",
text_color=("gray10", "gray90")
)
btn.grid(row=idx, column=0, padx=5, pady=pady_val, sticky="ew")
self.popup_frame.grid_columnconfigure(0, weight=1)
@@ -312,19 +339,30 @@ class UnifiedDropdown(ctk.CTkFrame):
# Position popup
self._position_popup()
# Bind keyboard events (use correct Tkinter key names)
# Bind keyboard events to search entry for navigation
self.search_entry.bind("<Down>", lambda e: self._focus_first_item())
self.search_entry.bind("<Up>", lambda e: self._navigate_end())
self.search_entry.bind("<Next>", self._navigate_page_down) # PageDown in Tkinter
self.search_entry.bind("<Prior>", self._navigate_page_up) # PageUp in Tkinter
self.search_entry.bind("<Home>", self._navigate_home)
self.search_entry.bind("<End>", self._navigate_end)
self.search_entry.bind("<Return>", self._confirm_selection)
# Bind keyboard events to popup window as well (for when focus moves)
self.popup_window.bind("<Escape>", lambda e: self._close_dropdown())
self.popup_window.bind("<Down>", self._navigate_down)
self.popup_window.bind("<Up>", self._navigate_up)
self.popup_window.bind("<Next>", self._navigate_page_down) # PageDown in Tkinter
self.popup_window.bind("<Prior>", self._navigate_page_up) # PageUp in Tkinter
self.popup_window.bind("<Next>", self._navigate_page_down)
self.popup_window.bind("<Prior>", self._navigate_page_up)
self.popup_window.bind("<Home>", self._navigate_home)
self.popup_window.bind("<End>", self._navigate_end)
self.popup_window.bind("<Return>", self._confirm_selection)
# Removed FocusOut binding - it causes race conditions with button clicks
# Set focus to popup
self.popup_window.focus_set()
# Bind any printable character to redirect focus to search entry
self.popup_window.bind("<Key>", self._redirect_to_search)
# Keep focus on search entry (don't override with popup_window.focus_set())
# Focus was already set on search_entry at line 233
# Bind click outside to close
self.popup_window.bind("<Button-1>", self._check_click_outside, add="+")
@@ -335,6 +373,12 @@ class UnifiedDropdown(ctk.CTkFrame):
# Set flag to prevent race conditions
self._closing = True
# Clean up search state
self.search_entry = None
self.search_query = ""
self.filtered_items = self.all_items.copy()
self.items = self.filtered_items
# Clean up tooltips
for tooltip in self.item_tooltips:
tooltip.destroy()
@@ -560,6 +604,129 @@ class UnifiedDropdown(ctk.CTkFrame):
# Note: CTkScrollableFrame uses internal canvas
btn.update_idletasks()
def _on_search_changed(self, event=None):
"""Handle search query change and filter items."""
query = self.search_entry.get().strip().lower()
self.search_query = query
if not query:
# Show all items
self.filtered_items = self.all_items.copy()
else:
# Filter items based on display text (case-insensitive)
self.filtered_items = [
item for item in self.all_items
if query in self._get_display_text(item).lower()
]
# Rebuild popup with filtered items
self._rebuild_popup_items()
def _rebuild_popup_items(self):
"""Rebuild popup item list with filtered items."""
if not self.popup_frame:
return
# Clear existing buttons and tooltips
for btn in self.item_buttons:
btn.destroy()
self.item_buttons.clear()
for tooltip in self.item_tooltips:
tooltip.destroy()
self.item_tooltips.clear()
# Update items reference
self.items = self.filtered_items
# Show "no results" if empty
if not self.filtered_items:
no_results_label = ctk.CTkLabel(
self.popup_frame,
text="No matching items found",
font=ctk.CTkFont(size=14),
text_color="gray"
)
no_results_label.grid(row=0, column=0, padx=10, pady=20)
return
# Recreate buttons
for idx, item in enumerate(self.filtered_items):
display_text = self._get_display_text(item)
max_chars = 60
truncated = display_text if len(display_text) <= max_chars else display_text[:max_chars] + "..."
# Add extra padding for first and last items
if idx == 0:
pady_val = (3, 2)
elif idx == len(self.filtered_items) - 1:
pady_val = (2, 3)
else:
pady_val = 2
btn = ctk.CTkButton(
self.popup_frame,
text=truncated,
command=lambda i=idx: self._select_item(i, close_popup=True),
font=ctk.CTkFont(size=14),
height=40,
fg_color="transparent",
border_width=1,
border_color="gray50",
hover_color=("gray70", "gray30"),
anchor="w",
text_color=("gray10", "gray90")
)
btn.grid(row=idx, column=0, padx=5, pady=pady_val, sticky="ew")
self.popup_frame.grid_columnconfigure(0, weight=1)
# Add tooltip if truncated
if len(display_text) > max_chars:
tooltip = ToolTip(btn, display_text, delay=500)
self.item_tooltips.append(tooltip)
# Bind mouse wheel
def popup_scroll(event):
if event.delta > 0:
self.popup_frame._parent_canvas.yview_scroll(-1, "units")
else:
self.popup_frame._parent_canvas.yview_scroll(1, "units")
return "break"
btn.bind("<MouseWheel>", popup_scroll, add="+")
self.item_buttons.append(btn)
# Reset current index and highlight first item
self.current_index = 0
self._highlight_item(0)
def _focus_first_item(self):
"""Move focus from search to first item."""
if self.item_buttons:
self.current_index = 0
self._highlight_item(0)
self.popup_window.focus_set()
def _redirect_to_search(self, event):
"""Redirect keyboard input to search entry."""
# Ignore special keys that are already handled
if event.keysym in ['Escape', 'Down', 'Up', 'Next', 'Prior', 'Home', 'End', 'Return',
'Left', 'Right', 'Tab', 'Shift_L', 'Shift_R', 'Control_L',
'Control_R', 'Alt_L', 'Alt_R']:
return
# Focus search entry if not already focused
if self.search_entry and str(self.focus_get()) != str(self.search_entry):
self.search_entry.focus_set()
# Insert the character that was typed
if len(event.char) == 1 and event.char.isprintable():
# Get current cursor position
current_pos = self.search_entry.index(tk.INSERT)
# Insert character at cursor position
self.search_entry.insert(current_pos, event.char)
# Trigger search update
self._on_search_changed()
def get_selected(self) -> Optional[Dict]:
"""
Get the currently selected item.