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:
13
install.sh
13
install.sh
@@ -83,13 +83,18 @@ if [ "$OFFLINE_MODE" = true ] && [ -d "$WHEELS_DIR" ] && [ "$(ls -A $WHEELS_DIR/
|
|||||||
echo "Installing from offline Python wheels..."
|
echo "Installing from offline Python wheels..."
|
||||||
echo "Wheel files found: $(ls -1 $WHEELS_DIR/*.whl 2>/dev/null | wc -l)"
|
echo "Wheel files found: $(ls -1 $WHEELS_DIR/*.whl 2>/dev/null | wc -l)"
|
||||||
|
|
||||||
pip3 install --no-index --find-links="$WHEELS_DIR" -r requirements.txt
|
if pip3 install --break-system-packages --no-index --find-links="$WHEELS_DIR" -r requirements.txt 2>&1 | tee /tmp/pip_install.log; then
|
||||||
|
echo "Python packages installed from offline repository"
|
||||||
echo "Python packages installed from offline repository"
|
else
|
||||||
|
echo "Warning: Offline installation failed (possibly due to Python version mismatch)"
|
||||||
|
echo "Falling back to online installation..."
|
||||||
|
pip3 install --break-system-packages -r requirements.txt
|
||||||
|
echo "Python packages installed from PyPI"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# Online: Use pip from PyPI
|
# Online: Use pip from PyPI
|
||||||
echo "Installing from PyPI..."
|
echo "Installing from PyPI..."
|
||||||
pip3 install -r requirements.txt
|
pip3 install --break-system-packages -r requirements.txt
|
||||||
|
|
||||||
echo "Python packages installed successfully"
|
echo "Python packages installed successfully"
|
||||||
fi
|
fi
|
||||||
|
|||||||
160
src/keyboard_widget.py
Normal file
160
src/keyboard_widget.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
Custom Keyboard Widget for Signage Player
|
||||||
|
Provides an on-screen keyboard for text input
|
||||||
|
"""
|
||||||
|
|
||||||
|
from kivy.uix.floatlayout import FloatLayout
|
||||||
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
|
from kivy.uix.button import Button
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.animation import Animation
|
||||||
|
from kivy.logger import Logger
|
||||||
|
from kivy.core.window import Window
|
||||||
|
from kivy.graphics import Color, RoundedRectangle
|
||||||
|
|
||||||
|
class KeyboardWidget(FloatLayout):
|
||||||
|
"""Custom on-screen keyboard widget"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(KeyboardWidget, self).__init__(**kwargs)
|
||||||
|
self.target_input = None
|
||||||
|
self.size_hint = (None, None)
|
||||||
|
|
||||||
|
# Calculate size - half screen width
|
||||||
|
self.width = Window.width * 0.5
|
||||||
|
self.height = self.width / 3
|
||||||
|
|
||||||
|
# Position at bottom center
|
||||||
|
self.x = (Window.width - self.width) / 2
|
||||||
|
self.y = 0
|
||||||
|
|
||||||
|
# Start hidden
|
||||||
|
self.opacity = 0
|
||||||
|
|
||||||
|
# Create the keyboard UI
|
||||||
|
self._build_keyboard()
|
||||||
|
|
||||||
|
# Bind window resize
|
||||||
|
Window.bind(on_resize=self._on_window_resize)
|
||||||
|
|
||||||
|
Logger.info(f"KeyboardWidget: Initialized at ({self.x}, {self.y}) with size {self.width}x{self.height}")
|
||||||
|
|
||||||
|
def _build_keyboard(self):
|
||||||
|
"""Build the keyboard UI"""
|
||||||
|
# Background
|
||||||
|
with self.canvas.before:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Main layout
|
||||||
|
main_layout = BoxLayout(orientation='vertical', padding=5, spacing=5)
|
||||||
|
main_layout.size = self.size
|
||||||
|
main_layout.pos = self.pos
|
||||||
|
|
||||||
|
# Close button bar
|
||||||
|
close_bar = BoxLayout(orientation='horizontal', size_hint=(1, None), height=40, padding=[5, 0])
|
||||||
|
close_bar.add_widget(Widget())
|
||||||
|
close_btn = Button(text='✕', size_hint=(None, 1), width=40,
|
||||||
|
background_color=(0.8, 0.2, 0.2, 0.9), font_size='20sp', bold=True)
|
||||||
|
close_btn.bind(on_press=lambda x: self.hide_keyboard())
|
||||||
|
close_bar.add_widget(close_btn)
|
||||||
|
main_layout.add_widget(close_bar)
|
||||||
|
|
||||||
|
# Number row
|
||||||
|
self._add_key_row(main_layout, ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'])
|
||||||
|
|
||||||
|
# Top letter row
|
||||||
|
self._add_key_row(main_layout, ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'])
|
||||||
|
|
||||||
|
# Middle letter row (with offset)
|
||||||
|
middle_row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||||
|
middle_row.add_widget(Widget(size_hint=(0.5, 1)))
|
||||||
|
for letter in ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L']:
|
||||||
|
btn = Button(text=letter)
|
||||||
|
btn.bind(on_press=lambda x, l=letter: self.key_pressed(l.lower()))
|
||||||
|
middle_row.add_widget(btn)
|
||||||
|
middle_row.add_widget(Widget(size_hint=(0.5, 1)))
|
||||||
|
main_layout.add_widget(middle_row)
|
||||||
|
|
||||||
|
# Bottom letter row (with offset)
|
||||||
|
bottom_row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||||
|
bottom_row.add_widget(Widget(size_hint=(1, 1)))
|
||||||
|
for letter in ['Z', 'X', 'C', 'V', 'B', 'N', 'M']:
|
||||||
|
btn = Button(text=letter)
|
||||||
|
btn.bind(on_press=lambda x, l=letter: self.key_pressed(l.lower()))
|
||||||
|
bottom_row.add_widget(btn)
|
||||||
|
bottom_row.add_widget(Widget(size_hint=(1, 1)))
|
||||||
|
main_layout.add_widget(bottom_row)
|
||||||
|
|
||||||
|
# Space and backspace row
|
||||||
|
last_row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||||
|
backspace_btn = Button(text='←', size_hint=(0.3, 1), font_size='24sp')
|
||||||
|
backspace_btn.bind(on_press=lambda x: self.key_pressed('backspace'))
|
||||||
|
last_row.add_widget(backspace_btn)
|
||||||
|
space_btn = Button(text='Space', size_hint=(0.7, 1))
|
||||||
|
space_btn.bind(on_press=lambda x: self.key_pressed(' '))
|
||||||
|
last_row.add_widget(space_btn)
|
||||||
|
main_layout.add_widget(last_row)
|
||||||
|
|
||||||
|
self.add_widget(main_layout)
|
||||||
|
|
||||||
|
def _add_key_row(self, parent, keys):
|
||||||
|
"""Add a row of keys"""
|
||||||
|
row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||||
|
for key in keys:
|
||||||
|
btn = Button(text=key)
|
||||||
|
btn.bind(on_press=lambda x, k=key: self.key_pressed(k.lower() if k.isalpha() else k))
|
||||||
|
row.add_widget(btn)
|
||||||
|
parent.add_widget(row)
|
||||||
|
|
||||||
|
def _update_bg(self, *args):
|
||||||
|
"""Update background rectangle"""
|
||||||
|
self.bg_rect.pos = self.pos
|
||||||
|
self.bg_rect.size = self.size
|
||||||
|
|
||||||
|
def _on_window_resize(self, window, width, height):
|
||||||
|
"""Handle window resize"""
|
||||||
|
self.width = width * 0.5
|
||||||
|
self.height = self.width / 3
|
||||||
|
self.x = (width - self.width) / 2
|
||||||
|
self.y = 0
|
||||||
|
|
||||||
|
def show_keyboard(self, target_input):
|
||||||
|
"""Show the keyboard for a specific TextInput"""
|
||||||
|
self.target_input = target_input
|
||||||
|
Logger.info(f"KeyboardWidget: Showing keyboard for {target_input}")
|
||||||
|
|
||||||
|
# Animate keyboard appearing
|
||||||
|
anim = Animation(opacity=1, duration=0.2)
|
||||||
|
anim.start(self)
|
||||||
|
|
||||||
|
def hide_keyboard(self):
|
||||||
|
"""Hide the keyboard"""
|
||||||
|
Logger.info("KeyboardWidget: Hiding keyboard")
|
||||||
|
|
||||||
|
# Animate keyboard disappearing
|
||||||
|
anim = Animation(opacity=0, duration=0.2)
|
||||||
|
anim.start(self)
|
||||||
|
|
||||||
|
# Clear target
|
||||||
|
if self.target_input:
|
||||||
|
self.target_input.focus = False
|
||||||
|
self.target_input = None
|
||||||
|
|
||||||
|
def key_pressed(self, key):
|
||||||
|
"""Handle key press"""
|
||||||
|
if not self.target_input:
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == 'backspace':
|
||||||
|
# Remove last character
|
||||||
|
if self.target_input.text:
|
||||||
|
self.target_input.text = self.target_input.text[:-1]
|
||||||
|
else:
|
||||||
|
# Add character
|
||||||
|
self.target_input.text += key
|
||||||
|
|
||||||
|
Logger.debug(f"KeyboardWidget: Key pressed '{key}', current text: {self.target_input.text}")
|
||||||
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['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs
|
||||||
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' # Prevent screen saver
|
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
|
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.widget import Widget
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
from kivy.uix.image import AsyncImage, Image
|
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.button import Button
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.textinput import TextInput
|
from kivy.uix.textinput import TextInput
|
||||||
|
from kivy.uix.vkeyboard import VKeyboard
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
from kivy.properties import BooleanProperty
|
from kivy.properties import BooleanProperty
|
||||||
@@ -40,17 +45,146 @@ from get_playlists import (
|
|||||||
send_playlist_restart_feedback,
|
send_playlist_restart_feedback,
|
||||||
send_player_error_feedback
|
send_player_error_feedback
|
||||||
)
|
)
|
||||||
|
from keyboard_widget import KeyboardWidget
|
||||||
|
|
||||||
# Load the KV file
|
# Load the KV file
|
||||||
Builder.load_file('signage_player.kv')
|
Builder.load_file('signage_player.kv')
|
||||||
|
|
||||||
# Removed VLCVideoWidget - using Kivy's built-in Video widget instead
|
# 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):
|
class ExitPasswordPopup(Popup):
|
||||||
def __init__(self, player_instance, was_paused=False, **kwargs):
|
def __init__(self, player_instance, was_paused=False, **kwargs):
|
||||||
super(ExitPasswordPopup, self).__init__(**kwargs)
|
super(ExitPasswordPopup, self).__init__(**kwargs)
|
||||||
self.player = player_instance
|
self.player = player_instance
|
||||||
self.was_paused = was_paused
|
self.was_paused = was_paused
|
||||||
|
self.keyboard_widget = None
|
||||||
|
|
||||||
# Cancel all scheduled cursor/control hide events
|
# Cancel all scheduled cursor/control hide events
|
||||||
try:
|
try:
|
||||||
@@ -70,9 +204,46 @@ class ExitPasswordPopup(Popup):
|
|||||||
|
|
||||||
# Bind to dismiss event to manage cursor visibility and resume playback
|
# Bind to dismiss event to manage cursor visibility and resume playback
|
||||||
self.bind(on_dismiss=self.on_popup_dismiss)
|
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):
|
def on_popup_dismiss(self, *args):
|
||||||
"""Handle popup dismissal - resume playback and restart cursor hide timer"""
|
"""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
|
# Resume playback if it wasn't paused before
|
||||||
if not self.was_paused:
|
if not self.was_paused:
|
||||||
self.player.is_paused = False
|
self.player.is_paused = False
|
||||||
@@ -117,6 +288,7 @@ class SettingsPopup(Popup):
|
|||||||
super(SettingsPopup, self).__init__(**kwargs)
|
super(SettingsPopup, self).__init__(**kwargs)
|
||||||
self.player = player_instance
|
self.player = player_instance
|
||||||
self.was_paused = was_paused
|
self.was_paused = was_paused
|
||||||
|
self.keyboard_widget = None
|
||||||
|
|
||||||
# Cancel all scheduled cursor/control hide events
|
# Cancel all scheduled cursor/control hide events
|
||||||
try:
|
try:
|
||||||
@@ -150,8 +322,42 @@ class SettingsPopup(Popup):
|
|||||||
# Bind to dismiss event to manage cursor visibility and resume playback
|
# Bind to dismiss event to manage cursor visibility and resume playback
|
||||||
self.bind(on_dismiss=self.on_popup_dismiss)
|
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):
|
def on_popup_dismiss(self, *args):
|
||||||
"""Handle popup dismissal - resume playback and restart cursor hide timer"""
|
"""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
|
# Resume playback if it wasn't paused before
|
||||||
if not self.was_paused:
|
if not self.was_paused:
|
||||||
self.player.is_paused = False
|
self.player.is_paused = False
|
||||||
|
|||||||
@@ -1,5 +1,193 @@
|
|||||||
#:kivy 2.1.0
|
#:kivy 2.1.0
|
||||||
|
|
||||||
|
# Custom On-Screen Keyboard Widget
|
||||||
|
<CustomKeyboard@FloatLayout>:
|
||||||
|
size_hint: None, None
|
||||||
|
width: root.parent.width * 0.5 if root.parent else dp(600)
|
||||||
|
height: self.width / 3
|
||||||
|
pos: (root.parent.width - self.width) / 2 if root.parent else 0, 0
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: 0.1, 0.1, 0.1, 0.95
|
||||||
|
RoundedRectangle:
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
radius: [dp(15), dp(15), 0, 0]
|
||||||
|
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
padding: dp(5)
|
||||||
|
spacing: dp(5)
|
||||||
|
|
||||||
|
# Close button bar
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, None
|
||||||
|
height: dp(40)
|
||||||
|
padding: [dp(5), 0]
|
||||||
|
|
||||||
|
Widget:
|
||||||
|
|
||||||
|
Button:
|
||||||
|
text: '✕'
|
||||||
|
size_hint: None, 1
|
||||||
|
width: dp(40)
|
||||||
|
background_color: 0.8, 0.2, 0.2, 0.9
|
||||||
|
font_size: sp(20)
|
||||||
|
bold: True
|
||||||
|
on_press: root.parent.hide_keyboard() if root.parent and hasattr(root.parent, 'hide_keyboard') else None
|
||||||
|
|
||||||
|
# Number row
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, 1
|
||||||
|
spacing: dp(3)
|
||||||
|
Button:
|
||||||
|
text: '1'
|
||||||
|
on_press: root.parent.key_pressed('1') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '2'
|
||||||
|
on_press: root.parent.key_pressed('2') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '3'
|
||||||
|
on_press: root.parent.key_pressed('3') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '4'
|
||||||
|
on_press: root.parent.key_pressed('4') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '5'
|
||||||
|
on_press: root.parent.key_pressed('5') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '6'
|
||||||
|
on_press: root.parent.key_pressed('6') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '7'
|
||||||
|
on_press: root.parent.key_pressed('7') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '8'
|
||||||
|
on_press: root.parent.key_pressed('8') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '9'
|
||||||
|
on_press: root.parent.key_pressed('9') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: '0'
|
||||||
|
on_press: root.parent.key_pressed('0') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
|
||||||
|
# Top letter row (QWERTYUIOP)
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, 1
|
||||||
|
spacing: dp(3)
|
||||||
|
Button:
|
||||||
|
text: 'Q'
|
||||||
|
on_press: root.parent.key_pressed('q') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'W'
|
||||||
|
on_press: root.parent.key_pressed('w') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'E'
|
||||||
|
on_press: root.parent.key_pressed('e') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'R'
|
||||||
|
on_press: root.parent.key_pressed('r') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'T'
|
||||||
|
on_press: root.parent.key_pressed('t') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'Y'
|
||||||
|
on_press: root.parent.key_pressed('y') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'U'
|
||||||
|
on_press: root.parent.key_pressed('u') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'I'
|
||||||
|
on_press: root.parent.key_pressed('i') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'O'
|
||||||
|
on_press: root.parent.key_pressed('o') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'P'
|
||||||
|
on_press: root.parent.key_pressed('p') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
|
||||||
|
# Middle letter row (ASDFGHJKL)
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, 1
|
||||||
|
spacing: dp(3)
|
||||||
|
Widget:
|
||||||
|
size_hint: 0.5, 1
|
||||||
|
Button:
|
||||||
|
text: 'A'
|
||||||
|
on_press: root.parent.key_pressed('a') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'S'
|
||||||
|
on_press: root.parent.key_pressed('s') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'D'
|
||||||
|
on_press: root.parent.key_pressed('d') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'F'
|
||||||
|
on_press: root.parent.key_pressed('f') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'G'
|
||||||
|
on_press: root.parent.key_pressed('g') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'H'
|
||||||
|
on_press: root.parent.key_pressed('h') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'J'
|
||||||
|
on_press: root.parent.key_pressed('j') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'K'
|
||||||
|
on_press: root.parent.key_pressed('k') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'L'
|
||||||
|
on_press: root.parent.key_pressed('l') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Widget:
|
||||||
|
size_hint: 0.5, 1
|
||||||
|
|
||||||
|
# Bottom letter row (ZXCVBNM)
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, 1
|
||||||
|
spacing: dp(3)
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 1
|
||||||
|
Button:
|
||||||
|
text: 'Z'
|
||||||
|
on_press: root.parent.key_pressed('z') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'X'
|
||||||
|
on_press: root.parent.key_pressed('x') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'C'
|
||||||
|
on_press: root.parent.key_pressed('c') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'V'
|
||||||
|
on_press: root.parent.key_pressed('v') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'B'
|
||||||
|
on_press: root.parent.key_pressed('b') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'N'
|
||||||
|
on_press: root.parent.key_pressed('n') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'M'
|
||||||
|
on_press: root.parent.key_pressed('m') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 1
|
||||||
|
|
||||||
|
# Bottom row (Space, Backspace)
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, 1
|
||||||
|
spacing: dp(3)
|
||||||
|
Button:
|
||||||
|
text: '←'
|
||||||
|
size_hint: 0.3, 1
|
||||||
|
font_size: sp(24)
|
||||||
|
on_press: root.parent.key_pressed('backspace') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
Button:
|
||||||
|
text: 'Space'
|
||||||
|
size_hint: 0.7, 1
|
||||||
|
on_press: root.parent.key_pressed(' ') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||||
|
|
||||||
<SignagePlayer@FloatLayout>:
|
<SignagePlayer@FloatLayout>:
|
||||||
size: root.screen_width, root.screen_height
|
size: root.screen_width, root.screen_height
|
||||||
canvas.before:
|
canvas.before:
|
||||||
@@ -126,6 +314,9 @@
|
|||||||
font_size: sp(16)
|
font_size: sp(16)
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
height: dp(40)
|
height: dp(40)
|
||||||
|
write_tab: False
|
||||||
|
readonly: True
|
||||||
|
on_focus: root.on_input_focus(self, self.focus)
|
||||||
|
|
||||||
Label:
|
Label:
|
||||||
id: error_label
|
id: error_label
|
||||||
@@ -180,6 +371,8 @@
|
|||||||
size_hint_x: 0.7
|
size_hint_x: 0.7
|
||||||
multiline: False
|
multiline: False
|
||||||
font_size: sp(14)
|
font_size: sp(14)
|
||||||
|
write_tab: False
|
||||||
|
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||||
|
|
||||||
# Screen name
|
# Screen name
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
@@ -200,6 +393,8 @@
|
|||||||
size_hint_x: 0.7
|
size_hint_x: 0.7
|
||||||
multiline: False
|
multiline: False
|
||||||
font_size: sp(14)
|
font_size: sp(14)
|
||||||
|
write_tab: False
|
||||||
|
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||||
|
|
||||||
# Quickconnect key
|
# Quickconnect key
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
@@ -220,6 +415,8 @@
|
|||||||
size_hint_x: 0.7
|
size_hint_x: 0.7
|
||||||
multiline: False
|
multiline: False
|
||||||
font_size: sp(14)
|
font_size: sp(14)
|
||||||
|
write_tab: False
|
||||||
|
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||||
|
|
||||||
# Orientation
|
# Orientation
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
@@ -240,6 +437,8 @@
|
|||||||
size_hint_x: 0.7
|
size_hint_x: 0.7
|
||||||
multiline: False
|
multiline: False
|
||||||
font_size: sp(14)
|
font_size: sp(14)
|
||||||
|
write_tab: False
|
||||||
|
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||||
|
|
||||||
# Touch
|
# Touch
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
@@ -260,6 +459,8 @@
|
|||||||
size_hint_x: 0.7
|
size_hint_x: 0.7
|
||||||
multiline: False
|
multiline: False
|
||||||
font_size: sp(14)
|
font_size: sp(14)
|
||||||
|
write_tab: False
|
||||||
|
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||||
|
|
||||||
# Resolution
|
# Resolution
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
@@ -281,6 +482,8 @@
|
|||||||
multiline: False
|
multiline: False
|
||||||
font_size: sp(14)
|
font_size: sp(14)
|
||||||
hint_text: '1920x1080 or auto'
|
hint_text: '1920x1080 or auto'
|
||||||
|
write_tab: False
|
||||||
|
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||||
|
|
||||||
Widget:
|
Widget:
|
||||||
size_hint_y: 0.05
|
size_hint_y: 0.05
|
||||||
|
|||||||
Reference in New Issue
Block a user