266 lines
9.3 KiB
Python
266 lines
9.3 KiB
Python
"""
|
|
Improved Text Input Widget System for McRogueFace
|
|
Uses proper parent-child frame structure and handles keyboard input correctly
|
|
"""
|
|
|
|
import mcrfpy
|
|
|
|
|
|
class FocusManager:
|
|
"""Manages focus across multiple widgets"""
|
|
def __init__(self):
|
|
self.widgets = []
|
|
self.focused_widget = None
|
|
self.focus_index = -1
|
|
# Global keyboard state
|
|
self.shift_pressed = False
|
|
self.caps_lock = False
|
|
|
|
def register(self, widget):
|
|
"""Register a widget"""
|
|
self.widgets.append(widget)
|
|
if self.focused_widget is None:
|
|
self.focus(widget)
|
|
|
|
def focus(self, widget):
|
|
"""Set focus to widget"""
|
|
if self.focused_widget:
|
|
self.focused_widget.on_blur()
|
|
|
|
self.focused_widget = widget
|
|
self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
|
|
|
|
if widget:
|
|
widget.on_focus()
|
|
|
|
def focus_next(self):
|
|
"""Focus next widget"""
|
|
if not self.widgets:
|
|
return
|
|
self.focus_index = (self.focus_index + 1) % len(self.widgets)
|
|
self.focus(self.widgets[self.focus_index])
|
|
|
|
def focus_prev(self):
|
|
"""Focus previous widget"""
|
|
if not self.widgets:
|
|
return
|
|
self.focus_index = (self.focus_index - 1) % len(self.widgets)
|
|
self.focus(self.widgets[self.focus_index])
|
|
|
|
def handle_key(self, key, state):
|
|
"""Send key to focused widget"""
|
|
# Track shift state
|
|
if key == "LShift" or key == "RShift":
|
|
self.shift_pressed = True
|
|
return True
|
|
elif key == "start": # Key release for shift
|
|
self.shift_pressed = False
|
|
return True
|
|
elif key == "CapsLock":
|
|
self.caps_lock = not self.caps_lock
|
|
return True
|
|
|
|
if self.focused_widget:
|
|
return self.focused_widget.handle_key(key, self.shift_pressed, self.caps_lock)
|
|
return False
|
|
|
|
|
|
class TextInput:
|
|
"""Text input field widget with proper parent-child structure"""
|
|
def __init__(self, x, y, width, height=24, label="", placeholder="", on_change=None):
|
|
self.x = x
|
|
self.y = y
|
|
self.width = width
|
|
self.height = height
|
|
self.label = label
|
|
self.placeholder = placeholder
|
|
self.on_change = on_change
|
|
|
|
# Text state
|
|
self.text = ""
|
|
self.cursor_pos = 0
|
|
self.focused = False
|
|
|
|
# Create the widget structure
|
|
self._create_ui()
|
|
|
|
def _create_ui(self):
|
|
"""Create UI components with proper parent-child structure"""
|
|
# Parent frame that contains everything
|
|
self.parent_frame = mcrfpy.Frame(self.x, self.y - (20 if self.label else 0),
|
|
self.width, self.height + (20 if self.label else 0))
|
|
self.parent_frame.fill_color = (0, 0, 0, 0) # Transparent parent
|
|
|
|
# Input frame (relative to parent)
|
|
self.frame = mcrfpy.Frame(0, 20 if self.label else 0, self.width, self.height)
|
|
self.frame.fill_color = (255, 255, 255, 255)
|
|
self.frame.outline_color = (128, 128, 128, 255)
|
|
self.frame.outline = 2
|
|
|
|
# Label (relative to parent)
|
|
if self.label:
|
|
self.label_text = mcrfpy.Caption(self.label, 0, 0)
|
|
self.label_text.fill_color = (255, 255, 255, 255)
|
|
self.parent_frame.children.append(self.label_text)
|
|
|
|
# Text content (relative to input frame)
|
|
self.text_display = mcrfpy.Caption("", 4, 4)
|
|
self.text_display.fill_color = (0, 0, 0, 255)
|
|
|
|
# Placeholder text (relative to input frame)
|
|
if self.placeholder:
|
|
self.placeholder_text = mcrfpy.Caption(self.placeholder, 4, 4)
|
|
self.placeholder_text.fill_color = (180, 180, 180, 255)
|
|
self.frame.children.append(self.placeholder_text)
|
|
|
|
# Cursor (relative to input frame)
|
|
# Experiment: replacing cursor frame with an inline text character
|
|
#self.cursor = mcrfpy.Frame(4, 4, 2, self.height - 8)
|
|
#self.cursor.fill_color = (0, 0, 0, 255)
|
|
#self.cursor.visible = False
|
|
|
|
# Add children to input frame
|
|
self.frame.children.append(self.text_display)
|
|
#self.frame.children.append(self.cursor)
|
|
|
|
# Add input frame to parent
|
|
self.parent_frame.children.append(self.frame)
|
|
|
|
# Click handler on the input frame
|
|
self.frame.click = self._on_click
|
|
|
|
def _on_click(self, x, y, button, state):
|
|
"""Handle mouse clicks"""
|
|
print(f"{x=} {y=} {button=} {state=}")
|
|
if button == "left" and hasattr(self, '_focus_manager'):
|
|
self._focus_manager.focus(self)
|
|
|
|
def on_focus(self):
|
|
"""Called when focused"""
|
|
self.focused = True
|
|
self.frame.outline_color = (0, 120, 255, 255)
|
|
self.frame.outline = 3
|
|
#self.cursor.visible = True
|
|
self._update_display()
|
|
|
|
def on_blur(self):
|
|
"""Called when focus lost"""
|
|
self.focused = False
|
|
self.frame.outline_color = (128, 128, 128, 255)
|
|
self.frame.outline = 2
|
|
#self.cursor.visible = False
|
|
self._update_display()
|
|
|
|
def handle_key(self, key, shift_pressed, caps_lock):
|
|
"""Process keyboard input with shift state"""
|
|
if not self.focused:
|
|
return False
|
|
|
|
old_text = self.text
|
|
handled = True
|
|
|
|
# Special key mappings for shifted characters
|
|
shift_map = {
|
|
"1": "!", "2": "@", "3": "#", "4": "$", "5": "%",
|
|
"6": "^", "7": "&", "8": "*", "9": "(", "0": ")",
|
|
"-": "_", "=": "+", "[": "{", "]": "}", "\\": "|",
|
|
";": ":", "'": '"', ",": "<", ".": ">", "/": "?",
|
|
"`": "~"
|
|
}
|
|
|
|
# Navigation and editing keys
|
|
if key == "BackSpace":
|
|
if self.cursor_pos > 0:
|
|
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
|
self.cursor_pos -= 1
|
|
elif key == "Delete":
|
|
if self.cursor_pos < len(self.text):
|
|
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
|
elif key == "Left":
|
|
self.cursor_pos = max(0, self.cursor_pos - 1)
|
|
elif key == "Right":
|
|
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
|
elif key == "Home":
|
|
self.cursor_pos = 0
|
|
elif key == "End":
|
|
self.cursor_pos = len(self.text)
|
|
elif key == "Space":
|
|
self._insert_at_cursor(" ")
|
|
elif key in ("Tab", "Return"):
|
|
handled = False # Let parent handle
|
|
# Handle number keys with "Num" prefix
|
|
elif key.startswith("Num") and len(key) == 4:
|
|
num = key[3] # Get the digit after "Num"
|
|
if shift_pressed and num in shift_map:
|
|
self._insert_at_cursor(shift_map[num])
|
|
else:
|
|
self._insert_at_cursor(num)
|
|
# Handle single character keys
|
|
elif len(key) == 1:
|
|
char = key
|
|
# Apply shift transformations
|
|
if shift_pressed:
|
|
if char in shift_map:
|
|
char = shift_map[char]
|
|
elif char.isalpha():
|
|
char = char.upper()
|
|
else:
|
|
# Apply caps lock for letters
|
|
if char.isalpha():
|
|
if caps_lock:
|
|
char = char.upper()
|
|
else:
|
|
char = char.lower()
|
|
self._insert_at_cursor(char)
|
|
else:
|
|
# Unhandled key - print for debugging
|
|
print(f"[TextInput] Unhandled key: '{key}' (shift={shift_pressed}, caps={caps_lock})")
|
|
handled = False
|
|
|
|
# Update if changed
|
|
if old_text != self.text:
|
|
self._update_display()
|
|
if self.on_change:
|
|
self.on_change(self.text)
|
|
elif handled:
|
|
self._update_cursor()
|
|
|
|
return handled
|
|
|
|
def _insert_at_cursor(self, char):
|
|
"""Insert a character at the cursor position"""
|
|
self.text = self.text[:self.cursor_pos] + char + self.text[self.cursor_pos:]
|
|
self.cursor_pos += 1
|
|
|
|
def _update_display(self):
|
|
"""Update visual state"""
|
|
# Show/hide placeholder
|
|
if hasattr(self, 'placeholder_text'):
|
|
self.placeholder_text.visible = (self.text == "" and not self.focused)
|
|
|
|
# Update text
|
|
self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:]
|
|
self._update_cursor()
|
|
|
|
def _update_cursor(self):
|
|
"""Update cursor position"""
|
|
if self.focused:
|
|
# Estimate position (10 pixels per character)
|
|
#self.cursor.x = 4 + (self.cursor_pos * 10)
|
|
self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:]
|
|
pass
|
|
|
|
def set_text(self, text):
|
|
"""Set text programmatically"""
|
|
self.text = text
|
|
self.cursor_pos = len(text)
|
|
self._update_display()
|
|
|
|
def get_text(self):
|
|
"""Get current text"""
|
|
return self.text
|
|
|
|
def add_to_scene(self, scene):
|
|
"""Add only the parent frame to scene"""
|
|
scene.append(self.parent_frame)
|