New day, new build
This commit is contained in:
parent
d32b83f790
commit
f564cea776
2 changed files with 184 additions and 75 deletions
142
textmenu.py
142
textmenu.py
|
|
@ -83,7 +83,36 @@ class TextMenu:
|
|||
self._timeout_alarm = None
|
||||
|
||||
self.last_click_time = 0
|
||||
self.last_click_pos = (0, 0) # Store last click position (x, y)
|
||||
self.last_clicked_index = -1 # Track which menu item was last clicked
|
||||
self.double_click_threshold = 0.3 # seconds
|
||||
self.position_threshold = 5 # pixels tolerance for position matching
|
||||
|
||||
# Calculate the effective content width (accounting for borders and internal padding)
|
||||
content_width = width - 5 # subtract 3 to account for borders and internal padding
|
||||
|
||||
# Function to center text by adding spaces to the left only, supporting newlines
|
||||
def center_text(text, total_width):
|
||||
if '\n' in text:
|
||||
# Split text into lines and center each line individually
|
||||
lines = text.split('\n')
|
||||
centered_lines = []
|
||||
for line in lines:
|
||||
if len(line) >= total_width:
|
||||
centered_lines.append(line)
|
||||
else:
|
||||
total_padding = total_width - len(line)
|
||||
left_padding = total_padding // 2
|
||||
centered_lines.append(' ' * left_padding + line)
|
||||
return '\n'.join(centered_lines)
|
||||
else:
|
||||
# Single line text
|
||||
text_len = len(text)
|
||||
if text_len >= total_width:
|
||||
return text
|
||||
total_padding = total_width - text_len
|
||||
left_padding = total_padding // 2
|
||||
return ' ' * left_padding + text
|
||||
|
||||
# Custom button class that handles enter/space differently
|
||||
class SelectableButton(urwid.Button):
|
||||
|
|
@ -92,6 +121,8 @@ class TextMenu:
|
|||
def keypress(self, size, key):
|
||||
if key in ('enter', ' '):
|
||||
# For enter/space, trigger activation without generating click signal
|
||||
# Also cancel timeout here to ensure immediate response
|
||||
self._parent_menu._cancel_timeout()
|
||||
self._emit('activate')
|
||||
return None
|
||||
return super().keypress(size, key)
|
||||
|
|
@ -99,21 +130,39 @@ class TextMenu:
|
|||
# Buttons with proper click handling
|
||||
self.menu_widgets = []
|
||||
for i, c in enumerate(choices):
|
||||
btn = SelectableButton(c)
|
||||
# Add a newline prefix for better spacing, then center the text
|
||||
spaced_text = '\n' + c # Add newline prefix
|
||||
centered_text = center_text(spaced_text, content_width)
|
||||
btn = SelectableButton(centered_text)
|
||||
btn._parent_menu = self # Reference to parent menu for timeout control
|
||||
|
||||
# Store original text without the newline prefix for return purposes
|
||||
original_choice = c
|
||||
|
||||
def make_click_handler(choice_text, index):
|
||||
def click_handler(button):
|
||||
# Handle mouse clicks with double-click detection
|
||||
now = time.time()
|
||||
|
||||
# Check if this is a double-click
|
||||
if now - self.last_click_time < self.double_click_threshold:
|
||||
# Double-click detected - return the selection
|
||||
# Store mouse coordinates when this click handler runs
|
||||
current_pos = (getattr(self, 'mouse_x', 0), getattr(self, 'mouse_y', 0))
|
||||
|
||||
# Check if this is a double-click (same item, timing, and position match)
|
||||
is_same_item = (index == self.last_clicked_index)
|
||||
is_timely = (now - self.last_click_time < self.double_click_threshold)
|
||||
pos_match = (abs(current_pos[0] - self.last_click_pos[0]) <= self.position_threshold and
|
||||
abs(current_pos[1] - self.last_click_pos[1]) <= self.position_threshold)
|
||||
|
||||
if is_same_item and is_timely and pos_match:
|
||||
# Double-click detected on the same item - return the selection
|
||||
self._cancel_timeout()
|
||||
self.selected = choice_text
|
||||
self.selected = choice_text # Use original text without prefix/newline
|
||||
raise urwid.ExitMainLoop()
|
||||
else:
|
||||
# Single click - just focus the item (like keyboard navigation)
|
||||
# Single click or click on different item - just focus the item
|
||||
self._cancel_timeout()
|
||||
self.last_clicked_index = index
|
||||
self.last_click_pos = current_pos
|
||||
self.list_walker.set_focus(index)
|
||||
|
||||
self.last_click_time = now
|
||||
|
|
@ -122,18 +171,56 @@ class TextMenu:
|
|||
def make_activate_handler(choice_text):
|
||||
def activate_handler(button):
|
||||
# Handle enter/space activation
|
||||
# Timeout already canceled in button's keypress method
|
||||
# But call it again to be safe and ensure consistency with click handler
|
||||
self._cancel_timeout()
|
||||
self.selected = choice_text
|
||||
self.selected = choice_text # Use original text without prefix/newline
|
||||
raise urwid.ExitMainLoop()
|
||||
return activate_handler
|
||||
|
||||
urwid.connect_signal(btn, 'click', make_click_handler(c, i))
|
||||
urwid.connect_signal(btn, 'activate', make_activate_handler(c))
|
||||
self.menu_widgets.append(urwid.AttrMap(btn, None, focus_map='selected'))
|
||||
urwid.connect_signal(btn, 'click', make_click_handler(original_choice, i)) # Use original text for selection
|
||||
urwid.connect_signal(btn, 'activate', make_activate_handler(original_choice)) # Use original text for selection
|
||||
|
||||
attr_mapped_btn = urwid.AttrMap(btn, None, focus_map='selected')
|
||||
self.menu_widgets.append(attr_mapped_btn)
|
||||
|
||||
# Scrollable ListBox
|
||||
# Custom ListBox that intercepts navigation keys and mouse events
|
||||
class TimedListBox(urwid.ListBox):
|
||||
def keypress(self, size, key):
|
||||
# Cancel timeout for navigation keys and mouse wheel
|
||||
if key in ('up', 'down', 'page up', 'page down', 'home', 'end'):
|
||||
self.parent_obj._cancel_timeout()
|
||||
|
||||
# Process the key normally
|
||||
result = super().keypress(size, key)
|
||||
|
||||
# Return the result (None if handled, otherwise the unhandled key)
|
||||
return result
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
# Store mouse coordinates for double-click detection
|
||||
if event == 'mouse press':
|
||||
# Store the mouse coordinates when a click occurs
|
||||
self.parent_obj.mouse_x = col
|
||||
self.parent_obj.mouse_y = row
|
||||
|
||||
# Handle mouse wheel events
|
||||
if button == 4: # Mouse wheel up
|
||||
self.parent_obj._cancel_timeout()
|
||||
self.keypress(size, 'up')
|
||||
return True
|
||||
elif button == 5: # Mouse wheel down
|
||||
self.parent_obj._cancel_timeout()
|
||||
self.keypress(size, 'down')
|
||||
return True
|
||||
|
||||
# Call parent mouse_event method for other mouse events
|
||||
return super().mouse_event(size, event, button, col, row, focus)
|
||||
|
||||
# Scrollable ListBox with timeout handling
|
||||
self.list_walker = urwid.SimpleFocusListWalker(self.menu_widgets)
|
||||
self.listbox = urwid.ListBox(self.list_walker)
|
||||
self.listbox = TimedListBox(self.list_walker)
|
||||
self.listbox.parent_obj = self # Provide access to parent methods
|
||||
self.listbox = urwid.AttrMap(self.listbox, 'inside')
|
||||
|
||||
# Fix height
|
||||
|
|
@ -160,7 +247,7 @@ class TextMenu:
|
|||
self.loop = urwid.MainLoop(
|
||||
self.frame,
|
||||
palette=self.palette,
|
||||
unhandled_input=self._unhandled_input,
|
||||
#unhandled_input=self._unhandled_input,
|
||||
handle_mouse=True
|
||||
)
|
||||
|
||||
|
|
@ -171,33 +258,6 @@ class TextMenu:
|
|||
self._timeout_alarm = None
|
||||
self.linebox.set_title(self.title) # restore original title
|
||||
|
||||
# Keyboard or mouse input - handle ALL input types
|
||||
def _unhandled_input(self, key):
|
||||
# Cancel timeout on ANY input
|
||||
self._cancel_timeout()
|
||||
|
||||
# Handle Enter key immediately (like double-click behavior)
|
||||
if key in ('enter', ' '):
|
||||
focus_widget, _ = self.listbox.get_focus()
|
||||
if focus_widget:
|
||||
# Get the button text from the nested widgets
|
||||
btn_text = focus_widget.original_widget.base_widget.get_label()
|
||||
self.selected = btn_text
|
||||
raise urwid.ExitMainLoop()
|
||||
elif key == 'esc':
|
||||
self.selected = None
|
||||
raise urwid.ExitMainLoop()
|
||||
# Handle navigation keys that should stop timer but not select
|
||||
elif key in ('up', 'down', 'page up', 'page down'):
|
||||
# Timer already cancelled above, just continue
|
||||
# Navigation already changes focus automatically
|
||||
pass
|
||||
# Handle mouse events that should stop timer
|
||||
elif isinstance(key, tuple) and len(key) >= 3 and key[0].startswith('mouse'):
|
||||
# This catches mouse events like ('mouse press', 1, x, y)
|
||||
# Timer already cancelled above, continue with normal processing
|
||||
pass
|
||||
|
||||
# Timeout exit
|
||||
def _timeout_exit(self, loop, user_data=None):
|
||||
self.selected = None
|
||||
|
|
@ -215,7 +275,7 @@ class TextMenu:
|
|||
|
||||
self.linebox.set_title(f"{self.title} ({math.ceil(remaining)}s)")
|
||||
|
||||
# Schedule next update in 0.1s for precise timing
|
||||
# Schedule next update in 0.1s for precise tracking
|
||||
self._timeout_alarm = loop.set_alarm_in(0.1, self._update_timer)
|
||||
|
||||
def show(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue