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:
Kiwy Signage Player
2025-12-04 22:17:57 +02:00
parent 744681bb20
commit 07b7e96edd
4 changed files with 579 additions and 5 deletions

View File

@@ -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