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

@@ -83,13 +83,18 @@ if [ "$OFFLINE_MODE" = true ] && [ -d "$WHEELS_DIR" ] && [ "$(ls -A $WHEELS_DIR/
echo "Installing from offline Python wheels..."
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
echo "Python packages installed from offline repository"
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"
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
# Online: Use pip from PyPI
echo "Installing from PyPI..."
pip3 install -r requirements.txt
pip3 install --break-system-packages -r requirements.txt
echo "Python packages installed successfully"
fi

160
src/keyboard_widget.py Normal file
View 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}")

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

View File

@@ -1,5 +1,193 @@
#: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>:
size: root.screen_width, root.screen_height
canvas.before:
@@ -126,6 +314,9 @@
font_size: sp(16)
size_hint_y: None
height: dp(40)
write_tab: False
readonly: True
on_focus: root.on_input_focus(self, self.focus)
Label:
id: error_label
@@ -180,6 +371,8 @@
size_hint_x: 0.7
multiline: False
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
BoxLayout:
@@ -200,6 +393,8 @@
size_hint_x: 0.7
multiline: False
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
BoxLayout:
@@ -220,6 +415,8 @@
size_hint_x: 0.7
multiline: False
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
BoxLayout:
@@ -240,6 +437,8 @@
size_hint_x: 0.7
multiline: False
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
BoxLayout:
@@ -260,6 +459,8 @@
size_hint_x: 0.7
multiline: False
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
BoxLayout:
@@ -281,6 +482,8 @@
multiline: False
font_size: sp(14)
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:
size_hint_y: 0.05