diff --git a/install.sh b/install.sh index 76f12c1..5acd80b 100644 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/src/keyboard_widget.py b/src/keyboard_widget.py new file mode 100644 index 0000000..8e039a2 --- /dev/null +++ b/src/keyboard_widget.py @@ -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}") diff --git a/src/main.py b/src/main.py index 7c5c2d2..b27aff7 100644 --- a/src/main.py +++ b/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 diff --git a/src/signage_player.kv b/src/signage_player.kv index aecb273..d095cb4 100644 --- a/src/signage_player.kv +++ b/src/signage_player.kv @@ -1,5 +1,193 @@ #:kivy 2.1.0 +# Custom On-Screen Keyboard Widget +: + 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 + : 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