Add custom on-screen keyboard feature and fix installation
- Added custom half-width on-screen keyboard widget (keyboard_widget.py) - Keyboard appears at bottom center when input fields are active - Integrated keyboard in exit popup and settings popup - Fixed install.sh: added --break-system-packages flag for pip3 - Fixed install.sh: added fallback to online installation for version mismatches - Removed old Kivy 2.1.0 tar.gz that was causing conflicts - Keyboard includes close button for intuitive dismissal - All input fields trigger keyboard on touch - Keyboard automatically cleans up on popup dismiss
This commit is contained in:
208
src/main.py
208
src/main.py
@@ -16,8 +16,12 @@ os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
|
||||
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs
|
||||
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' # Prevent screen saver
|
||||
|
||||
from kivy.app import App
|
||||
# Configure Kivy BEFORE importing any Kivy modules
|
||||
from kivy.config import Config
|
||||
# Disable default virtual keyboard - we'll use our custom one
|
||||
Config.set('kivy', 'keyboard_mode', '')
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.image import AsyncImage, Image
|
||||
@@ -26,6 +30,7 @@ from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.vkeyboard import VKeyboard
|
||||
from kivy.clock import Clock
|
||||
from kivy.core.window import Window
|
||||
from kivy.properties import BooleanProperty
|
||||
@@ -40,17 +45,146 @@ from get_playlists import (
|
||||
send_playlist_restart_feedback,
|
||||
send_player_error_feedback
|
||||
)
|
||||
from keyboard_widget import KeyboardWidget
|
||||
|
||||
# Load the KV file
|
||||
Builder.load_file('signage_player.kv')
|
||||
|
||||
# Removed VLCVideoWidget - using Kivy's built-in Video widget instead
|
||||
|
||||
# Custom keyboard container with close button
|
||||
class KeyboardContainer(BoxLayout):
|
||||
def __init__(self, vkeyboard, **kwargs):
|
||||
super(KeyboardContainer, self).__init__(**kwargs)
|
||||
self.orientation = 'vertical'
|
||||
self.vkeyboard = vkeyboard
|
||||
|
||||
# Calculate dimensions - half screen width
|
||||
container_width = Window.width * 0.5
|
||||
# Height proportional to width (maintain aspect ratio)
|
||||
# Standard keyboard aspect ratio is ~3:1 (width:height)
|
||||
keyboard_height = container_width / 3
|
||||
close_bar_height = 50
|
||||
total_height = keyboard_height + close_bar_height
|
||||
|
||||
# Set exact size (not size_hint)
|
||||
self.size_hint = (None, None)
|
||||
self.size = (container_width, total_height)
|
||||
|
||||
# Center horizontally at bottom
|
||||
self.x = (Window.width - container_width) / 2
|
||||
self.y = 0
|
||||
|
||||
# Bind to window resize
|
||||
Window.bind(on_resize=self._on_window_resize)
|
||||
|
||||
# Create close button bar
|
||||
close_bar = BoxLayout(
|
||||
orientation='horizontal',
|
||||
size_hint=(1, None),
|
||||
height=close_bar_height,
|
||||
padding=[10, 5, 10, 5]
|
||||
)
|
||||
|
||||
# Add a spacer
|
||||
close_bar.add_widget(Widget())
|
||||
|
||||
# Create close button
|
||||
close_btn = Button(
|
||||
text='✕',
|
||||
size_hint=(None, 1),
|
||||
width=50,
|
||||
background_color=(0.8, 0.2, 0.2, 0.9),
|
||||
font_size='24sp',
|
||||
bold=True
|
||||
)
|
||||
close_btn.bind(on_press=self.close_keyboard)
|
||||
close_bar.add_widget(close_btn)
|
||||
|
||||
# Add close button bar at top
|
||||
self.add_widget(close_bar)
|
||||
|
||||
# Set keyboard exact size to match container width
|
||||
self.vkeyboard.size_hint = (None, None)
|
||||
self.vkeyboard.width = container_width
|
||||
self.vkeyboard.height = keyboard_height
|
||||
|
||||
# Force keyboard to respect our dimensions
|
||||
self.vkeyboard.scale_min = container_width / Window.width
|
||||
self.vkeyboard.scale_max = container_width / Window.width
|
||||
|
||||
# Add keyboard
|
||||
self.add_widget(self.vkeyboard)
|
||||
|
||||
# Background
|
||||
with self.canvas.before:
|
||||
from kivy.graphics import Color, RoundedRectangle
|
||||
Color(0.1, 0.1, 0.1, 0.95)
|
||||
self.bg_rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[15, 15, 0, 0])
|
||||
|
||||
self.bind(pos=self._update_bg, size=self._update_bg)
|
||||
|
||||
Logger.info(f"KeyboardContainer: Created at ({self.x}, {self.y}) with size {self.size}")
|
||||
|
||||
def _on_window_resize(self, window, width, height):
|
||||
"""Reposition and resize keyboard on window resize"""
|
||||
container_width = width * 0.5
|
||||
keyboard_height = container_width / 3 # Maintain aspect ratio
|
||||
close_bar_height = 50
|
||||
total_height = keyboard_height + close_bar_height
|
||||
|
||||
self.size = (container_width, total_height)
|
||||
self.x = (width - container_width) / 2
|
||||
self.y = 0
|
||||
|
||||
if self.vkeyboard:
|
||||
self.vkeyboard.width = container_width
|
||||
self.vkeyboard.height = keyboard_height
|
||||
self.vkeyboard.scale_min = container_width / width
|
||||
self.vkeyboard.scale_max = container_width / width
|
||||
|
||||
def _update_bg(self, *args):
|
||||
"""Update background rectangle"""
|
||||
self.bg_rect.pos = self.pos
|
||||
self.bg_rect.size = self.size
|
||||
|
||||
def close_keyboard(self, *args):
|
||||
"""Close the keyboard"""
|
||||
Logger.info("KeyboardContainer: Closing keyboard")
|
||||
if self.vkeyboard.target:
|
||||
self.vkeyboard.target.focus = False
|
||||
|
||||
# Custom VKeyboard that uses container
|
||||
class CustomVKeyboard(VKeyboard):
|
||||
def __init__(self, **kwargs):
|
||||
super(CustomVKeyboard, self).__init__(**kwargs)
|
||||
self.container = None
|
||||
Clock.schedule_once(self._setup_container, 0.1)
|
||||
|
||||
def _setup_container(self, dt):
|
||||
"""Wrap keyboard in a container with close button"""
|
||||
if self.parent and not self.container:
|
||||
# Remove keyboard from parent
|
||||
parent = self.parent
|
||||
parent.remove_widget(self)
|
||||
|
||||
# Create container with keyboard
|
||||
self.container = KeyboardContainer(self)
|
||||
|
||||
# Add container to parent
|
||||
parent.add_widget(self.container)
|
||||
|
||||
Logger.info("CustomVKeyboard: Wrapped in container with close button")
|
||||
|
||||
# Set the custom keyboard factory
|
||||
Window.set_vkeyboard_class(CustomVKeyboard)
|
||||
|
||||
class ExitPasswordPopup(Popup):
|
||||
def __init__(self, player_instance, was_paused=False, **kwargs):
|
||||
super(ExitPasswordPopup, self).__init__(**kwargs)
|
||||
self.player = player_instance
|
||||
self.was_paused = was_paused
|
||||
self.keyboard_widget = None
|
||||
|
||||
# Cancel all scheduled cursor/control hide events
|
||||
try:
|
||||
@@ -70,9 +204,46 @@ class ExitPasswordPopup(Popup):
|
||||
|
||||
# Bind to dismiss event to manage cursor visibility and resume playback
|
||||
self.bind(on_dismiss=self.on_popup_dismiss)
|
||||
|
||||
# Show keyboard after popup opens
|
||||
Clock.schedule_once(self._show_keyboard, 0.2)
|
||||
|
||||
def _show_keyboard(self, dt):
|
||||
"""Show the custom keyboard"""
|
||||
try:
|
||||
# Create keyboard widget and add to window
|
||||
self.keyboard_widget = KeyboardWidget()
|
||||
Window.add_widget(self.keyboard_widget)
|
||||
self.keyboard_widget.show_keyboard(self.ids.password_input)
|
||||
Logger.info("ExitPasswordPopup: Keyboard added to window")
|
||||
except Exception as e:
|
||||
Logger.warning(f"ExitPasswordPopup: Could not show keyboard: {e}")
|
||||
|
||||
def on_input_focus(self, instance, value):
|
||||
"""Handle input field focus"""
|
||||
if value and self.keyboard_widget: # Got focus
|
||||
try:
|
||||
self.keyboard_widget.show_keyboard(instance)
|
||||
except Exception as e:
|
||||
Logger.debug(f"ExitPasswordPopup: Could not show keyboard on focus: {e}")
|
||||
|
||||
def key_pressed(self, key):
|
||||
"""Handle key press from keyboard"""
|
||||
if self.keyboard_widget:
|
||||
self.keyboard_widget.key_pressed(key)
|
||||
|
||||
def hide_keyboard(self):
|
||||
"""Hide the custom keyboard"""
|
||||
if self.keyboard_widget:
|
||||
self.keyboard_widget.hide_keyboard()
|
||||
Window.remove_widget(self.keyboard_widget)
|
||||
self.keyboard_widget = None
|
||||
|
||||
def on_popup_dismiss(self, *args):
|
||||
"""Handle popup dismissal - resume playback and restart cursor hide timer"""
|
||||
# Hide and remove keyboard
|
||||
self.hide_keyboard()
|
||||
|
||||
# Resume playback if it wasn't paused before
|
||||
if not self.was_paused:
|
||||
self.player.is_paused = False
|
||||
@@ -117,6 +288,7 @@ class SettingsPopup(Popup):
|
||||
super(SettingsPopup, self).__init__(**kwargs)
|
||||
self.player = player_instance
|
||||
self.was_paused = was_paused
|
||||
self.keyboard_widget = None
|
||||
|
||||
# Cancel all scheduled cursor/control hide events
|
||||
try:
|
||||
@@ -150,8 +322,42 @@ class SettingsPopup(Popup):
|
||||
# Bind to dismiss event to manage cursor visibility and resume playback
|
||||
self.bind(on_dismiss=self.on_popup_dismiss)
|
||||
|
||||
def on_input_touch(self, instance, touch):
|
||||
"""Handle touch on input field to show keyboard"""
|
||||
Logger.info(f"SettingsPopup: Input touched: {instance}")
|
||||
self._show_keyboard(instance)
|
||||
return False # Don't consume the touch event
|
||||
|
||||
def _show_keyboard(self, target_input):
|
||||
"""Show the custom keyboard"""
|
||||
try:
|
||||
if not self.keyboard_widget:
|
||||
# Create keyboard widget and add to window
|
||||
self.keyboard_widget = KeyboardWidget()
|
||||
Window.add_widget(self.keyboard_widget)
|
||||
Logger.info("SettingsPopup: Keyboard added to window")
|
||||
|
||||
self.keyboard_widget.show_keyboard(target_input)
|
||||
except Exception as e:
|
||||
Logger.warning(f"SettingsPopup: Could not show keyboard: {e}")
|
||||
|
||||
def key_pressed(self, key):
|
||||
"""Handle key press from keyboard"""
|
||||
if self.keyboard_widget:
|
||||
self.keyboard_widget.key_pressed(key)
|
||||
|
||||
def hide_keyboard(self):
|
||||
"""Hide the custom keyboard"""
|
||||
if self.keyboard_widget:
|
||||
self.keyboard_widget.hide_keyboard()
|
||||
Window.remove_widget(self.keyboard_widget)
|
||||
self.keyboard_widget = None
|
||||
|
||||
def on_popup_dismiss(self, *args):
|
||||
"""Handle popup dismissal - resume playback and restart cursor hide timer"""
|
||||
# Hide and remove keyboard
|
||||
self.hide_keyboard()
|
||||
|
||||
# Resume playback if it wasn't paused before
|
||||
if not self.was_paused:
|
||||
self.player.is_paused = False
|
||||
|
||||
Reference in New Issue
Block a user