You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

875 lines
34 KiB

#!/usr/bin/env python3
"""
User Profile Manager for Games
A PySDL2-based profile management system with gamepad-only controls
Features:
- Create new user profiles
- Edit existing profiles
- Delete profiles
- Select active profile
- JSON-based storage
- Gamepad navigation only
"""
import os
import json
import time
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from datetime import datetime
import sdl2
import sdl2.ext
from sdl2.ext.compat import byteify
@dataclass
class UserProfile:
"""User profile data structure"""
name: str
created_date: str
last_played: str
games_played: int = 0
total_score: int = 0
best_score: int = 0
settings: Dict[str, Any] = None
achievements: List[str] = None
def __post_init__(self):
if self.settings is None:
self.settings = {
"difficulty": "normal",
"sound_volume": 50,
"music_volume": 50,
"screen_shake": True,
"auto_save": True
}
if self.achievements is None:
self.achievements = []
class ProfileManager:
"""Main profile management system"""
def __init__(self, profiles_file: str = "user_profiles.json"):
self.profiles_file = profiles_file
self.profiles: Dict[str, UserProfile] = {}
self.active_profile: Optional[str] = None
# UI State
self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile
self.selected_index = 0
self.input_text = ""
self.input_active = False
self.editing_field = None
# Virtual keyboard state
self.vk_cursor_x = 0 # Virtual keyboard cursor X
self.vk_cursor_y = 0 # Virtual keyboard cursor Y
self.virtual_keyboard = [
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'],
['U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3'],
['4', '5', '6', '7', '8', '9', ' ', '.', '-', '_'],
['<DEL', 'SPACE', 'DONE', 'CANCEL']
]
# SDL2 Setup
self.window = None
self.renderer = None
self.font_manager = None
self.running = True
# Gamepad state
self.gamepad = None
self.button_states = {}
self.last_button_time = {}
# Colors
self.colors = {
'white': sdl2.ext.Color(255, 255, 255),
'black': sdl2.ext.Color(0, 0, 0),
'gray': sdl2.ext.Color(128, 128, 128),
'light_gray': sdl2.ext.Color(200, 200, 200),
'dark_gray': sdl2.ext.Color(64, 64, 64),
'blue': sdl2.ext.Color(70, 130, 200),
'light_blue': sdl2.ext.Color(120, 180, 255),
'green': sdl2.ext.Color(50, 200, 50),
'light_green': sdl2.ext.Color(100, 255, 100),
'red': sdl2.ext.Color(200, 50, 50),
'yellow': sdl2.ext.Color(255, 220, 0),
'orange': sdl2.ext.Color(255, 165, 0),
'purple': sdl2.ext.Color(150, 50, 200)
}
self.load_profiles()
def init_sdl(self):
"""Initialize SDL2 components"""
sdl2.ext.init(joystick=True)
# Create window
self.window = sdl2.ext.Window(
title="Profile Manager",
size=(640, 480)
)
self.window.show()
# Create renderer
self.renderer = sdl2.ext.Renderer(
self.window,
flags=sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC
)
# Initialize font system - create multiple sizes
font_path = "assets/decterm.ttf" if os.path.exists("assets/decterm.ttf") else None
self.fonts = {
'title': sdl2.ext.FontManager(font_path=font_path, size=36),
'large': sdl2.ext.FontManager(font_path=font_path, size=28),
'medium': sdl2.ext.FontManager(font_path=font_path, size=22),
'small': sdl2.ext.FontManager(font_path=font_path, size=18),
'tiny': sdl2.ext.FontManager(font_path=font_path, size=14)
}
# Initialize gamepad
self.init_gamepad()
def init_gamepad(self):
"""Initialize gamepad support"""
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER)
num_joysticks = sdl2.SDL_NumJoysticks()
if num_joysticks > 0:
self.gamepad = sdl2.SDL_JoystickOpen(0)
print(f"Gamepad detected: {sdl2.SDL_JoystickName(self.gamepad).decode()}")
else:
print("No gamepad detected - using keyboard fallback")
def load_profiles(self):
"""Load profiles from JSON file"""
if os.path.exists(self.profiles_file):
try:
with open(self.profiles_file, 'r') as f:
data = json.load(f)
self.profiles = {
name: UserProfile(**profile_data)
for name, profile_data in data.get('profiles', {}).items()
}
self.active_profile = data.get('active_profile')
except (json.JSONDecodeError, KeyError) as e:
print(f"Error loading profiles: {e}")
self.profiles = {}
def save_profiles(self):
"""Save profiles to JSON file"""
data = {
'profiles': {
name: asdict(profile)
for name, profile in self.profiles.items()
},
'active_profile': self.active_profile
}
try:
with open(self.profiles_file, 'w') as f:
json.dump(data, f, indent=2)
except IOError as e:
print(f"Error saving profiles: {e}")
def create_profile(self, name: str) -> bool:
"""Create a new profile"""
if name in self.profiles or not name.strip():
return False
now = datetime.now().isoformat()
profile = UserProfile(
name=name.strip(),
created_date=now,
last_played=now
)
self.profiles[name.strip()] = profile
self.save_profiles()
return True
def delete_profile(self, name: str) -> bool:
"""Delete a profile"""
if name in self.profiles:
del self.profiles[name]
if self.active_profile == name:
self.active_profile = None
self.save_profiles()
return True
return False
def set_active_profile(self, name: str) -> bool:
"""Set the active profile"""
if name in self.profiles:
self.active_profile = name
self.profiles[name].last_played = datetime.now().isoformat()
self.save_profiles()
return True
return False
def handle_gamepad_input(self):
"""Handle gamepad input with debouncing"""
if not self.gamepad:
return
current_time = time.time()
# D-pad navigation
hat_state = sdl2.SDL_JoystickGetHat(self.gamepad, 0)
# Up
if hat_state & sdl2.SDL_HAT_UP:
if self.can_process_input('up', current_time):
self.navigate_up()
# Down
if hat_state & sdl2.SDL_HAT_DOWN:
if self.can_process_input('down', current_time):
self.navigate_down()
# Left
if hat_state & sdl2.SDL_HAT_LEFT:
if self.can_process_input('left', current_time):
self.navigate_left()
# Right
if hat_state & sdl2.SDL_HAT_RIGHT:
if self.can_process_input('right', current_time):
self.navigate_right()
# Buttons
button_count = sdl2.SDL_JoystickNumButtons(self.gamepad)
for i in range(min(button_count, 16)): # Limit to reasonable number
if sdl2.SDL_JoystickGetButton(self.gamepad, i):
if self.can_process_input(f'button_{i}', current_time):
self.handle_button_press(i)
def can_process_input(self, input_key: str, current_time: float) -> bool:
"""Check if enough time has passed to process input (debouncing)"""
delay = 0.15 # 150ms delay
if input_key not in self.last_button_time:
self.last_button_time[input_key] = current_time
return True
if current_time - self.last_button_time[input_key] > delay:
self.last_button_time[input_key] = current_time
return True
return False
def handle_button_press(self, button: int):
"""Handle gamepad button presses"""
# Button mappings (common gamepad layout)
# 0: A/Cross - Confirm
# 1: B/Circle - Back
# 2: X/Square - Delete/Special
# 3: Y/Triangle - Menu
if button == 0: # A/Confirm
self.handle_confirm()
elif button == 1: # B/Back
self.handle_back()
elif button == 2: # X/Delete
self.handle_delete()
elif button == 3: # Y/Menu
self.handle_menu()
def navigate_up(self):
"""Navigate up in current screen"""
if self.current_screen == "main_menu":
self.selected_index = max(0, self.selected_index - 1)
elif self.current_screen == "profile_list":
self.selected_index = max(0, self.selected_index - 1)
elif self.current_screen == "edit_profile":
self.selected_index = max(0, self.selected_index - 1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
self.vk_cursor_y = max(0, self.vk_cursor_y - 1)
# Adjust x cursor if current row is shorter
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1
self.vk_cursor_x = min(self.vk_cursor_x, max_x)
def navigate_down(self):
"""Navigate down in current screen"""
if self.current_screen == "main_menu":
max_index = 3 # Create, Select, Settings, Exit (0-3)
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "profile_list":
max_index = len(self.profiles) # Profiles + Back
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "edit_profile":
max_index = 5 # 4 settings + save + back (0-5)
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
max_y = len(self.virtual_keyboard) - 1
self.vk_cursor_y = min(max_y, self.vk_cursor_y + 1)
# Adjust x cursor if current row is shorter
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1
self.vk_cursor_x = min(self.vk_cursor_x, max_x)
def navigate_left(self):
"""Navigate left (for adjusting values)"""
if self.current_screen == "edit_profile":
self.adjust_setting(-1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
self.vk_cursor_x = max(0, self.vk_cursor_x - 1)
def navigate_right(self):
"""Navigate right (for adjusting values)"""
if self.current_screen == "edit_profile":
self.adjust_setting(1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1
self.vk_cursor_x = min(max_x, self.vk_cursor_x + 1)
def handle_confirm(self):
"""Handle confirm action (A button)"""
if self.current_screen == "main_menu":
if self.selected_index == 0: # Create Profile
self.current_screen = "create_profile"
self.input_text = ""
self.input_active = True
elif self.selected_index == 1: # Select Profile
self.current_screen = "profile_list"
self.selected_index = 0
elif self.selected_index == 2: # Settings
if self.active_profile:
self.current_screen = "edit_profile"
self.selected_index = 0
elif self.selected_index == 3: # Exit
self.running = False
elif self.current_screen == "profile_list":
profile_names = list(self.profiles.keys())
if self.selected_index < len(profile_names):
# Select profile
profile_name = profile_names[self.selected_index]
self.set_active_profile(profile_name)
self.current_screen = "main_menu"
self.selected_index = 0
else: # Back option
self.current_screen = "main_menu"
self.selected_index = 1
elif self.current_screen == "create_profile":
if self.input_active:
# Handle virtual keyboard selection
self.handle_virtual_keyboard_input()
else:
# Start text input mode
self.input_active = True
self.vk_cursor_x = 0
self.vk_cursor_y = 0
elif self.current_screen == "edit_profile":
if self.selected_index == 4: # Save (index 4)
self.save_profiles()
self.current_screen = "main_menu"
self.selected_index = 0
elif self.selected_index == 5: # Back (index 5)
self.current_screen = "main_menu"
self.selected_index = 0
def handle_back(self):
"""Handle back action (B button)"""
if self.current_screen == "main_menu":
self.running = False
elif self.current_screen in ["profile_list", "create_profile", "edit_profile"]:
self.current_screen = "main_menu"
self.selected_index = 0
self.input_active = False
def handle_delete(self):
"""Handle delete action (X button)"""
if self.current_screen == "profile_list":
profile_names = list(self.profiles.keys())
if self.selected_index < len(profile_names):
profile_name = profile_names[self.selected_index]
self.delete_profile(profile_name)
self.selected_index = min(self.selected_index, len(self.profiles) - 1)
elif self.current_screen == "create_profile":
# Delete character from input
if self.input_text:
self.input_text = self.input_text[:-1]
def handle_menu(self):
"""Handle menu action (Y button)"""
pass # Reserved for future use
def handle_virtual_keyboard_input(self):
"""Handle virtual keyboard character selection"""
if self.vk_cursor_y >= len(self.virtual_keyboard):
return
row = self.virtual_keyboard[self.vk_cursor_y]
if self.vk_cursor_x >= len(row):
return
selected_char = row[self.vk_cursor_x]
if selected_char == '<DEL':
# Delete last character
if self.input_text:
self.input_text = self.input_text[:-1]
elif selected_char == 'SPACE':
# Add space
if len(self.input_text) < 20:
self.input_text += ' '
elif selected_char == 'DONE':
# Finish input
if self.input_text.strip():
if self.create_profile(self.input_text):
self.current_screen = "main_menu"
self.selected_index = 0
self.input_active = False
elif selected_char == 'CANCEL':
# Cancel input
self.input_text = ""
self.input_active = False
else:
# Add character
if len(self.input_text) < 20:
self.input_text += selected_char
def adjust_setting(self, direction: int):
"""Adjust setting value left/right"""
if self.current_screen == "edit_profile" and self.active_profile:
profile = self.profiles[self.active_profile]
if self.selected_index == 0: # Difficulty
difficulties = ["easy", "normal", "hard", "expert"]
current = difficulties.index(profile.settings["difficulty"])
new_index = (current + direction) % len(difficulties)
profile.settings["difficulty"] = difficulties[new_index]
elif self.selected_index == 1: # Sound Volume
profile.settings["sound_volume"] = max(0, min(100,
profile.settings["sound_volume"] + direction * 5))
elif self.selected_index == 2: # Music Volume
profile.settings["music_volume"] = max(0, min(100,
profile.settings["music_volume"] + direction * 5))
elif self.selected_index == 3: # Screen Shake
profile.settings["screen_shake"] = not profile.settings["screen_shake"]
def render(self):
"""Main rendering method"""
# Clear with dark background
self.renderer.clear(self.colors['black'])
# Draw subtle background pattern/gradient
for y in range(0, 480, 20):
alpha = int(20 * (1 - y / 480)) # Fade effect
if alpha > 5:
color = sdl2.ext.Color(alpha, alpha, alpha * 2)
self.renderer.draw_line((0, y, 640, y), color)
if self.current_screen == "main_menu":
self.render_main_menu()
elif self.current_screen == "profile_list":
self.render_profile_list()
elif self.current_screen == "create_profile":
self.render_create_profile()
elif self.current_screen == "edit_profile":
self.render_edit_profile()
self.renderer.present()
def render_main_menu(self):
"""Render main menu screen with improved layout"""
# Draw background gradient effect
self.renderer.fill((0, 0, 640, 120), self.colors['dark_gray'])
# Title
title = "Profile Manager"
self.draw_text(title, 320, 20, self.colors['light_blue'], 'title', center=True)
# Subtitle with active profile
if self.active_profile:
active_text = f"Active: {self.active_profile}"
self.draw_text(active_text, 320, 60, self.colors['light_green'], 'medium', center=True)
else:
self.draw_text("No active profile", 320, 60, self.colors['yellow'], 'medium', center=True)
# Menu panel
panel_x, panel_y = 120, 140
panel_width, panel_height = 400, 220
self.draw_panel(panel_x, panel_y, panel_width, panel_height,
self.colors['dark_gray'], self.colors['gray'])
# Menu items with proper spacing
menu_items = [
"Create Profile",
"Select Profile",
"Edit Settings",
"Exit"
]
button_width = 280
button_height = 35
button_x = panel_x + 60
start_y = panel_y + 20
for i, item in enumerate(menu_items):
button_y = start_y + i * (button_height + 10)
selected = (i == self.selected_index)
self.draw_button(item, button_x, button_y, button_width, button_height, selected)
# Controls help panel
help_panel_y = 380
self.draw_panel(10, help_panel_y, 620, 80,
self.colors['black'], self.colors['dark_gray'])
self.draw_text("Controls:", 20, help_panel_y + 8,
self.colors['light_blue'], 'small')
self.draw_text("↑↓ Navigate Enter Confirm Escape Back", 20, help_panel_y + 30,
self.colors['light_gray'], 'tiny')
def render_profile_list(self):
"""Render profile selection screen with improved layout"""
# Header
self.renderer.fill((0, 0, 640, 70), self.colors['dark_gray'])
self.draw_text("Select Profile", 320, 15, self.colors['light_blue'], 'title', center=True)
profile_names = list(self.profiles.keys())
if not profile_names:
# No profiles message
self.draw_panel(120, 100, 400, 150, self.colors['dark_gray'], self.colors['gray'])
self.draw_text("No profiles found", 320, 150, self.colors['yellow'], 'large', center=True)
self.draw_text("Create one first", 320, 180,
self.colors['light_gray'], 'medium', center=True)
# Back button
self.draw_button("← Back", 270, 300, 100, 30, True)
else:
# Profile list panel
panel_height = min(280, len(profile_names) * 55 + 60)
self.draw_panel(30, 80, 580, panel_height,
self.colors['black'], self.colors['gray'])
# Profile entries
for i, name in enumerate(profile_names):
profile = self.profiles[name]
entry_y = 95 + i * 50
entry_selected = (i == self.selected_index)
# Profile entry background
entry_color = self.colors['blue'] if entry_selected else self.colors['dark_gray']
border_color = self.colors['light_blue'] if entry_selected else self.colors['gray']
self.draw_panel(40, entry_y, 560, 40, entry_color, border_color)
# Profile name
name_color = self.colors['white'] if entry_selected else self.colors['light_gray']
self.draw_text(name, 50, entry_y + 5, name_color, 'medium')
# Profile stats (compact)
stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}"
stats_color = self.colors['light_gray'] if entry_selected else self.colors['gray']
self.draw_text(stats_text, 50, entry_y + 22, stats_color, 'tiny')
# Active indicator
if name == self.active_profile:
self.draw_text("", 580, entry_y + 12,
self.colors['light_green'], 'small')
# Back button
back_y = 95 + len(profile_names) * 50 + 10
back_selected = (self.selected_index == len(profile_names))
self.draw_button("← Back", 270, back_y, 100, 30, back_selected)
# Instructions
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Enter: Select • Escape: Back • Delete: Remove",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_create_profile(self):
"""Render profile creation screen with virtual keyboard"""
# Header
self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray'])
self.draw_text("Create Profile", 320, 15, self.colors['light_blue'], 'title', center=True)
# Input field
input_x, input_y = 120, 90
input_width, input_height = 400, 30
input_bg = self.colors['white'] if self.input_active else self.colors['light_gray']
input_border = self.colors['blue'] if self.input_active else self.colors['gray']
self.draw_panel(input_x, input_y, input_width, input_height, input_bg, input_border, 2)
# Input text
display_text = self.input_text if self.input_text else "Profile Name"
text_color = self.colors['black'] if self.input_text else self.colors['gray']
self.draw_text(display_text, input_x + 10, input_y + 8, text_color, 'medium')
if self.input_active:
# Virtual keyboard
self.render_virtual_keyboard()
else:
# Start input instruction
self.draw_text("Press Enter to start typing", 320, 150,
self.colors['yellow'], 'medium', center=True)
# Back button
self.draw_button("← Back", 270, 200, 100, 30, True)
# Instructions
if not self.input_active:
self.draw_panel(50, 400, 540, 60, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Enter: Start Input • Escape: Back", 320, 420,
self.colors['light_gray'], 'small', center=True)
def render_virtual_keyboard(self):
"""Render the virtual keyboard"""
kb_start_x = 50
kb_start_y = 150
key_width = 45
key_height = 30
for row_idx, row in enumerate(self.virtual_keyboard):
row_y = kb_start_y + row_idx * (key_height + 5)
# Special handling for bottom row (commands)
if row_idx == len(self.virtual_keyboard) - 1:
# Bottom row with command keys
key_widths = [80, 80, 80, 80] # Wider keys for commands
x_offset = 140 # Center the bottom row
else:
key_widths = [key_width] * len(row)
x_offset = kb_start_x
current_x = x_offset
for col_idx, char in enumerate(row):
selected = (row_idx == self.vk_cursor_y and col_idx == self.vk_cursor_x)
# Key background
if selected:
self.draw_panel(current_x, row_y, key_widths[col_idx], key_height,
self.colors['blue'], self.colors['light_blue'])
text_color = self.colors['white']
else:
self.draw_panel(current_x, row_y, key_widths[col_idx], key_height,
self.colors['dark_gray'], self.colors['gray'])
text_color = self.colors['light_gray']
# Key text
display_char = char
if char == '<DEL':
display_char = 'DEL'
elif char == 'SPACE':
display_char = 'SPC'
text_x = current_x + key_widths[col_idx] // 2
text_y = row_y + 8
self.draw_text(display_char, text_x, text_y, text_color, 'tiny', center=True)
current_x += key_widths[col_idx] + 5
# Instructions for virtual keyboard
self.draw_panel(50, 420, 540, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Arrows: Navigate • Enter: Select • Escape: Cancel",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_edit_profile(self):
"""Render profile editing screen with improved layout"""
if not self.active_profile:
self.current_screen = "main_menu"
return
profile = self.profiles[self.active_profile]
# Header
self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray'])
title = f"Edit: {profile.name}"
self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True)
# Profile stats
stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}"
self.draw_text(stats_text, 320, 50, self.colors['light_green'], 'small', center=True)
# Settings panel
panel_height = 280
self.draw_panel(60, 90, 520, panel_height, self.colors['dark_gray'], self.colors['gray'])
# Settings title
self.draw_text("Settings", 320, 105, self.colors['white'], 'large', center=True)
settings_items = [
("Difficulty", f"{profile.settings['difficulty'].title()}"),
("Sound Vol", f"{profile.settings['sound_volume']}%"),
("Music Vol", f"{profile.settings['music_volume']}%"),
("Screen Shake", "On" if profile.settings['screen_shake'] else "Off"),
]
start_y = 130
item_height = 35
for i, (label, value) in enumerate(settings_items):
item_y = start_y + i * item_height
selected = (i == self.selected_index)
# Setting item background
if selected:
self.draw_panel(80, item_y, 480, 25, self.colors['blue'], self.colors['light_blue'])
text_color = self.colors['white']
value_color = self.colors['light_green']
# Navigation arrows for selected item
self.draw_text("", 70, item_y + 5, self.colors['white'], 'small')
self.draw_text("", 570, item_y + 5, self.colors['white'], 'small')
else:
text_color = self.colors['light_gray']
value_color = self.colors['yellow']
# Setting label and value
self.draw_text(label, 90, item_y + 5, text_color, 'medium')
self.draw_text(value, 500, item_y + 5, value_color, 'medium')
# Action buttons
button_y = 310
save_selected = (self.selected_index == len(settings_items))
back_selected = (self.selected_index == len(settings_items) + 1)
self.draw_button("Save", 200, button_y, 80, 30, save_selected)
self.draw_button("← Back", 320, button_y, 80, 30, back_selected)
# Instructions
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Left/Right: Adjust • Enter: Save/Back • Escape: Cancel",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def draw_text(self, text: str, x: int, y: int, color, font_type='medium', center: bool = False):
"""Draw text on screen with improved styling"""
if not text:
return
factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
font = self.fonts.get(font_type, self.fonts['medium'])
text_sprite = factory.from_text(text, color=color, fontmanager=font)
if center:
x = x - text_sprite.size[0] // 2
text_sprite.position = (x, y)
self.renderer.copy(text_sprite, dstrect=text_sprite.position)
return text_sprite.size
def draw_panel(self, x: int, y: int, width: int, height: int, bg_color=None, border_color=None, border_width=2):
"""Draw a styled panel/box"""
if bg_color:
self.renderer.fill((x, y, width, height), bg_color)
if border_color:
# Draw border
for i in range(border_width):
self.renderer.draw_rect((x + i, y + i, width - 2*i, height - 2*i), border_color)
def draw_button(self, text: str, x: int, y: int, width: int, height: int, selected: bool = False):
"""Draw a styled button"""
# Button colors based on state
if selected:
bg_color = self.colors['blue']
border_color = self.colors['light_blue']
text_color = self.colors['white']
else:
bg_color = self.colors['dark_gray']
border_color = self.colors['gray']
text_color = self.colors['light_gray']
# Draw button background and border
self.draw_panel(x, y, width, height, bg_color, border_color)
# Draw button text centered
text_x = x + width // 2
text_y = y + height // 2 - 12 # Approximate text height offset
self.draw_text(text, text_x, text_y, text_color, 'medium', center=True)
def handle_keyboard_input(self, event):
"""Handle keyboard input for navigation only (no text input)"""
key = event.key.keysym.sym
# Navigation mode only
if key == sdl2.SDLK_UP:
self.navigate_up()
elif key == sdl2.SDLK_DOWN:
self.navigate_down()
elif key == sdl2.SDLK_LEFT:
self.navigate_left()
elif key == sdl2.SDLK_RIGHT:
self.navigate_right()
elif key == sdl2.SDLK_RETURN or key == sdl2.SDLK_SPACE:
self.handle_confirm()
elif key == sdl2.SDLK_ESCAPE:
self.handle_back()
elif key == sdl2.SDLK_DELETE or key == sdl2.SDLK_BACKSPACE:
self.handle_delete()
elif key == sdl2.SDLK_TAB:
self.handle_menu()
def run(self):
"""Main application loop"""
self.init_sdl()
# Use SDL2's built-in timing instead of Clock
target_fps = 30 # Lower FPS for better performance on smaller screens
frame_time = 1000 // target_fps # milliseconds per frame
while self.running:
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
self.running = False
elif event.type == sdl2.SDL_KEYDOWN:
self.handle_keyboard_input(event)
# Handle gamepad input (if available)
self.handle_gamepad_input()
# Render frame
frame_start = sdl2.SDL_GetTicks()
self.render()
# Cap framerate
frame_duration = sdl2.SDL_GetTicks() - frame_start
if frame_duration < frame_time:
sdl2.SDL_Delay(frame_time - frame_duration)
# Cleanup
if self.gamepad:
sdl2.SDL_JoystickClose(self.gamepad)
sdl2.ext.quit()
def main():
"""Entry point"""
print("Starting Profile Manager...")
print("Controls:")
print(" Arrow Keys: Navigate menus")
print(" Enter/Space: Confirm/Select")
print(" Escape: Back/Cancel")
print(" Delete/Backspace: Delete")
print(" Tab: Menu (reserved)")
print(" Keyboard typing: Text input when creating profiles")
print("")
print("Gamepad Controls (if connected):")
print(" D-Pad: Navigate menus")
print(" A Button: Confirm/Select")
print(" B Button: Back/Cancel")
print(" X Button: Delete")
manager = ProfileManager()
manager.run()
if __name__ == "__main__":
main()