diff --git a/internal_filesystem/lib/mpos/__init__.py b/internal_filesystem/lib/mpos/__init__.py index 1b2cd401..09511e1b 100644 --- a/internal_filesystem/lib/mpos/__init__.py +++ b/internal_filesystem/lib/mpos/__init__.py @@ -39,7 +39,7 @@ from .ui.topmenu import open_bar, close_bar, open_drawer, drawer_open, NOTIFICAT from .ui.focus import save_and_clear_current_focusgroup from .ui.gesture_navigation import handle_back_swipe, handle_top_swipe from .ui.util import shutdown, set_foreground_app, get_foreground_app -from .ui.anim import smooth_show, smooth_hide +from .ui.widget_animator import WidgetAnimator from .ui import focus_direction # Utility modules @@ -78,7 +78,7 @@ __all__ = [ "save_and_clear_current_focusgroup", "handle_back_swipe", "handle_top_swipe", "shutdown", "set_foreground_app", "get_foreground_app", - "smooth_show", "smooth_hide", + "WidgetAnimator", "focus_direction", # Testing utilities "wait_for_render", "capture_screenshot", "simulate_click", "get_widget_coords", diff --git a/internal_filesystem/lib/mpos/ui/__init__.py b/internal_filesystem/lib/mpos/ui/__init__.py index dd94e407..5f844906 100644 --- a/internal_filesystem/lib/mpos/ui/__init__.py +++ b/internal_filesystem/lib/mpos/ui/__init__.py @@ -11,7 +11,7 @@ from .event import get_event_name, print_event from .util import shutdown, set_foreground_app, get_foreground_app from .setting_activity import SettingActivity from .settings_activity import SettingsActivity -from .anim import smooth_show, smooth_hide +from .widget_animator import WidgetAnimator from . import focus_direction # main_display is assigned by board-specific initialization code @@ -28,6 +28,6 @@ __all__ = [ "shutdown", "set_foreground_app", "get_foreground_app", "SettingActivity", "SettingsActivity", - "smooth_show", "smooth_hide", + "WidgetAnimator", "focus_direction" ] diff --git a/internal_filesystem/lib/mpos/ui/anim.py b/internal_filesystem/lib/mpos/ui/anim.py deleted file mode 100644 index faeedfff..00000000 --- a/internal_filesystem/lib/mpos/ui/anim.py +++ /dev/null @@ -1,181 +0,0 @@ -import lvgl as lv - - -def safe_widget_access(callback): - """ - Wrapper to safely access a widget, catching LvReferenceError. - - If the widget has been deleted, the callback is silently skipped. - This prevents crashes when animations try to access deleted widgets. - - Args: - callback: Function to call (should access a widget) - - Returns: - None (always, even if callback returns a value) - """ - try: - callback() - except Exception as e: - # Check if it's an LvReferenceError (widget was deleted) - if "LvReferenceError" in str(type(e).__name__) or "Referenced object was deleted" in str(e): - # Widget was deleted - silently ignore - pass - else: - # Some other error - re-raise it - raise - - -class WidgetAnimator: - -# def __init__(self): -# self.animations = {} # Store animations for each widget - -# def stop_animation(self, widget): -# """Stop any running animation for the widget.""" -# if widget in self.animations: -# self.animations[widget].delete() -# del self.animations[widget] - - - # show_widget and hide_widget could have a (lambda) callback that sets the final state (eg: drawer_open) at the end - @staticmethod - def show_widget(widget, anim_type="fade", duration=500, delay=0): - lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches - anim = lv.anim_t() - anim.init() - anim.set_var(widget) - anim.set_delay(delay) - anim.set_duration(duration) - # Clear HIDDEN flag to make widget visible for animation: - anim.set_start_cb(lambda *args: safe_widget_access(lambda: widget.remove_flag(lv.obj.FLAG.HIDDEN))) - - if anim_type == "fade": - # Create fade-in animation (opacity from 0 to 255) - anim.set_values(0, 255) - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_style_opa(value, 0))) - anim.set_path_cb(lv.anim_t.path_ease_in_out) - # Ensure opacity is reset after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_style_opa(255, 0))) - elif anim_type == "slide_down": - print("doing slide_down") - # Create slide-down animation (y from -height to original y) - original_y = widget.get_y() - height = widget.get_height() - anim.set_values(original_y - height, original_y) - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value))) - anim.set_path_cb(lv.anim_t.path_ease_in_out) - # Reset y position after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_y(original_y))) - else: # "slide_up": - # Create slide-up animation (y from +height to original y) - # Seems to cause scroll bars to be added somehow if done to a keyboard at the bottom of the screen... - original_y = widget.get_y() - height = widget.get_height() - anim.set_values(original_y + height, original_y) - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value))) - anim.set_path_cb(lv.anim_t.path_ease_in_out) - # Reset y position after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_y(original_y))) - - anim.start() - return anim - - @staticmethod - def hide_widget(widget, anim_type="fade", duration=500, delay=0, hide=True): - lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches - anim = lv.anim_t() - anim.init() - anim.set_var(widget) - anim.set_duration(duration) - anim.set_delay(delay) - - """Hide a widget with an animation (fade or slide).""" - if anim_type == "fade": - # Create fade-out animation (opacity from 255 to 0) - anim.set_values(255, 0) - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_style_opa(value, 0))) - anim.set_path_cb(lv.anim_t.path_ease_in_out) - # Set HIDDEN flag after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, hide=hide))) - elif anim_type == "slide_down": - # Create slide-down animation (y from original y to +height) - # Seems to cause scroll bars to be added somehow if done to a keyboard at the bottom of the screen... - original_y = widget.get_y() - height = widget.get_height() - anim.set_values(original_y, original_y + height) - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value))) - anim.set_path_cb(lv.anim_t.path_ease_in_out) - # Set HIDDEN flag after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, original_y, hide))) - else: # "slide_up": - print("hide with slide_up") - # Create slide-up animation (y from original y to -height) - original_y = widget.get_y() - height = widget.get_height() - anim.set_values(original_y, original_y - height) - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value))) - anim.set_path_cb(lv.anim_t.path_ease_in_out) - # Set HIDDEN flag after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, original_y, hide))) - - anim.start() - return anim - - @staticmethod - def change_widget(widget, anim_type="interpolate", duration=5000, delay=0, begin_value=0, end_value=100, display_change=None): - """ - Animate a widget's text by interpolating between begin_value and end_value. - - Args: - widget: The widget to animate (should have set_text method) - anim_type: Type of animation (currently "interpolate" is supported) - duration: Animation duration in milliseconds - delay: Animation delay in milliseconds - begin_value: Starting value for interpolation - end_value: Ending value for interpolation - display_change: callback to display the change in the UI - - Returns: - The animation object - """ - lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches - anim = lv.anim_t() - anim.init() - anim.set_var(widget) - anim.set_delay(delay) - anim.set_duration(duration) - - if anim_type == "interpolate": - print(f"Create interpolation animation (value from {begin_value} to {end_value})") - anim.set_values(begin_value, end_value) - if display_change is not None: - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: display_change(value))) - # Ensure final value is set after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: display_change(end_value))) - else: - anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_text(str(value)))) - # Ensure final value is set after animation - anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_text(str(end_value)))) - anim.set_path_cb(lv.anim_t.path_ease_in_out) - else: - print(f"change_widget: unknown anim_type {anim_type}") - return - - anim.start() - return anim - - @staticmethod - def hide_complete_cb(widget, original_y=None, hide=True): - #print("hide_complete_cb") - if hide: - widget.add_flag(lv.obj.FLAG.HIDDEN) - if original_y: - widget.set_y(original_y) # in case it shifted slightly due to rounding etc - - -def smooth_show(widget, duration=500, delay=0): - return WidgetAnimator.show_widget(widget, anim_type="fade", duration=duration, delay=delay) - -def smooth_hide(widget, hide=True, duration=500, delay=0): - return WidgetAnimator.hide_widget(widget, anim_type="fade", duration=duration, delay=delay, hide=hide) diff --git a/internal_filesystem/lib/mpos/ui/camera_settings.py b/internal_filesystem/lib/mpos/ui/camera_settings.py index 843b69cc..f3598f03 100644 --- a/internal_filesystem/lib/mpos/ui/camera_settings.py +++ b/internal_filesystem/lib/mpos/ui/camera_settings.py @@ -3,7 +3,7 @@ import lvgl as lv from ..config import SharedPreferences from ..app.activity import Activity from .display import DisplayMetrics -from . import anim +from .widget_animator import WidgetAnimator class CameraSettingsActivity(Activity): @@ -354,11 +354,11 @@ class CameraSettingsActivity(Activity): def exposure_ctrl_changed(e=None): is_auto = aec_checkbox.get_state() & lv.STATE.CHECKED if is_auto: - anim.smooth_hide(me_cont, duration=1000) - anim.smooth_show(ae_cont, delay=1000) + WidgetAnimator.smooth_hide(me_cont, duration=1000) + WidgetAnimator.smooth_show(ae_cont, delay=1000) else: - anim.smooth_hide(ae_cont, duration=1000) - anim.smooth_show(me_cont, delay=1000) + WidgetAnimator.smooth_hide(ae_cont, duration=1000) + WidgetAnimator.smooth_show(me_cont, delay=1000) aec_checkbox.add_event_cb(exposure_ctrl_changed, lv.EVENT.VALUE_CHANGED, None) exposure_ctrl_changed() @@ -382,9 +382,9 @@ class CameraSettingsActivity(Activity): is_auto = agc_checkbox.get_state() & lv.STATE.CHECKED gain_slider = self.ui_controls["agc_gain"] if is_auto: - anim.smooth_hide(agc_cont, duration=1000) + WidgetAnimator.smooth_hide(agc_cont, duration=1000) else: - anim.smooth_show(agc_cont, duration=1000) + WidgetAnimator.smooth_show(agc_cont, duration=1000) agc_checkbox.add_event_cb(gain_ctrl_changed, lv.EVENT.VALUE_CHANGED, None) gain_ctrl_changed() @@ -414,9 +414,9 @@ class CameraSettingsActivity(Activity): def whitebal_changed(e=None): is_auto = wbcheckbox.get_state() & lv.STATE.CHECKED if is_auto: - anim.smooth_hide(wb_cont, duration=1000) + WidgetAnimator.smooth_hide(wb_cont, duration=1000) else: - anim.smooth_show(wb_cont, duration=1000) + WidgetAnimator.smooth_show(wb_cont, duration=1000) wbcheckbox.add_event_cb(whitebal_changed, lv.EVENT.VALUE_CHANGED, None) whitebal_changed() diff --git a/internal_filesystem/lib/mpos/ui/gesture_navigation.py b/internal_filesystem/lib/mpos/ui/gesture_navigation.py index 71ff0eff..61ad9455 100644 --- a/internal_filesystem/lib/mpos/ui/gesture_navigation.py +++ b/internal_filesystem/lib/mpos/ui/gesture_navigation.py @@ -1,6 +1,6 @@ import lvgl as lv from lvgl import LvReferenceError -from .anim import smooth_show, smooth_hide +from .widget_animator import WidgetAnimator from .view import back_screen from mpos.ui import topmenu as topmenu from .display import DisplayMetrics @@ -50,12 +50,12 @@ def _back_swipe_cb(event): should_show = not is_short_movement(dx, dy) if should_show != backbutton_visible: backbutton_visible = should_show - smooth_show(backbutton) if should_show else smooth_hide(backbutton) + WidgetAnimator.smooth_show(backbutton) if should_show else WidgetAnimator.smooth_hide(backbutton) backbutton.set_pos(round(x / 10), back_start_y) elif event_code == lv.EVENT.RELEASED: if backbutton_visible: backbutton_visible = False - smooth_hide(backbutton) + WidgetAnimator.smooth_hide(backbutton) if x > DisplayMetrics.width() / 5: if topmenu.drawer_open : topmenu.close_drawer() @@ -89,12 +89,12 @@ def _top_swipe_cb(event): should_show = not is_short_movement(dx, dy) if should_show != downbutton_visible: downbutton_visible = should_show - smooth_show(downbutton) if should_show else smooth_hide(downbutton) + WidgetAnimator.smooth_show(downbutton) if should_show else WidgetAnimator.smooth_hide(downbutton) downbutton.set_pos(down_start_x, round(y / 10)) elif event_code == lv.EVENT.RELEASED: if downbutton_visible: downbutton_visible = False - smooth_hide(downbutton) + WidgetAnimator.smooth_hide(downbutton) dx = abs(x - down_start_x) dy = abs(y - down_start_y) if y > DisplayMetrics.height() / 5: diff --git a/internal_filesystem/lib/mpos/ui/keyboard.py b/internal_filesystem/lib/mpos/ui/keyboard.py index 0566ba81..e8f000bc 100644 --- a/internal_filesystem/lib/mpos/ui/keyboard.py +++ b/internal_filesystem/lib/mpos/ui/keyboard.py @@ -17,6 +17,7 @@ Usage: import lvgl as lv import mpos.ui.theme +from .widget_animator import WidgetAnimator class MposKeyboard: """ @@ -247,7 +248,7 @@ class MposKeyboard: def show_keyboard(self): self._saved_scroll_y = self._parent.get_scroll_y() - mpos.ui.anim.smooth_show(self._keyboard, duration=500) + WidgetAnimator.smooth_show(self._keyboard, duration=500) # Scroll to view on a timer because it will be hidden initially lv.timer_create(self.scroll_after_show, 250, None).set_repeat_count(1) # When this is done from a timer, focus styling is not applied so the user doesn't see which button is selected. @@ -259,7 +260,7 @@ class MposKeyboard: self.focus_on_keyboard() def hide_keyboard(self): - mpos.ui.anim.smooth_hide(self._keyboard, duration=500) + WidgetAnimator.smooth_hide(self._keyboard, duration=500) # Do this after the hide so the scrollbars disappear automatically if not needed scroll_timer = lv.timer_create(self.scroll_back_after_hide,550,None).set_repeat_count(1) diff --git a/internal_filesystem/lib/mpos/ui/setting_activity.py b/internal_filesystem/lib/mpos/ui/setting_activity.py index 20f1c7bd..94555a4f 100644 --- a/internal_filesystem/lib/mpos/ui/setting_activity.py +++ b/internal_filesystem/lib/mpos/ui/setting_activity.py @@ -3,7 +3,7 @@ import lvgl as lv from ..app.activity import Activity from .camera_activity import CameraActivity from .display import DisplayMetrics -from . import anim +from .widget_animator import WidgetAnimator from ..camera_manager import CameraManager """ @@ -134,7 +134,7 @@ class SettingActivity(Activity): def onStop(self, screen): if self.keyboard: - anim.smooth_hide(self.keyboard) + WidgetAnimator.smooth_hide(self.keyboard) def radio_event_handler(self, event): print("radio_event_handler called") diff --git a/internal_filesystem/lib/mpos/ui/topmenu.py b/internal_filesystem/lib/mpos/ui/topmenu.py index 83e8a53f..17ddef99 100644 --- a/internal_filesystem/lib/mpos/ui/topmenu.py +++ b/internal_filesystem/lib/mpos/ui/topmenu.py @@ -5,7 +5,7 @@ import mpos.battery_voltage from .display_metrics import DisplayMetrics from .util import (get_foreground_app) from . import focus_direction -from .anim import WidgetAnimator +from .widget_animator import WidgetAnimator NOTIFICATION_BAR_HEIGHT=24 diff --git a/internal_filesystem/lib/mpos/ui/widget_animator.py b/internal_filesystem/lib/mpos/ui/widget_animator.py new file mode 100644 index 00000000..6faaa275 --- /dev/null +++ b/internal_filesystem/lib/mpos/ui/widget_animator.py @@ -0,0 +1,225 @@ +import lvgl as lv + + +class WidgetAnimator: + """ + Utility for creating smooth, non-blocking animations on LVGL widgets. + + Provides fade, slide, and value interpolation animations with automatic + cleanup and safe widget access handling. + """ + + @staticmethod + def _safe_widget_access(callback): + """ + Wrapper to safely access a widget, catching LvReferenceError. + + If the widget has been deleted, the callback is silently skipped. + This prevents crashes when animations try to access deleted widgets. + + Args: + callback: Function to call (should access a widget) + + Returns: + None (always, even if callback returns a value) + """ + try: + callback() + except Exception as e: + # Check if it's an LvReferenceError (widget was deleted) + if "LvReferenceError" in str(type(e).__name__) or "Referenced object was deleted" in str(e): + # Widget was deleted - silently ignore + pass + else: + # Some other error - re-raise it + raise + + @staticmethod + def show_widget(widget, anim_type="fade", duration=500, delay=0): + """ + Show a widget with an animation. + + Args: + widget (lv.obj): The widget to show + anim_type (str): Animation type - "fade", "slide_down", or "slide_up" (default: "fade") + duration (int): Animation duration in milliseconds (default: 500) + delay (int): Animation delay in milliseconds (default: 0) + + Returns: + The animation object + """ + lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches + anim = lv.anim_t() + anim.init() + anim.set_var(widget) + anim.set_delay(delay) + anim.set_duration(duration) + # Clear HIDDEN flag to make widget visible for animation: + anim.set_start_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.remove_flag(lv.obj.FLAG.HIDDEN))) + + if anim_type == "fade": + # Create fade-in animation (opacity from 0 to 255) + anim.set_values(0, 255) + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_style_opa(value, 0))) + anim.set_path_cb(lv.anim_t.path_ease_in_out) + # Ensure opacity is reset after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_style_opa(255, 0))) + elif anim_type == "slide_down": + # Create slide-down animation (y from -height to original y) + original_y = widget.get_y() + height = widget.get_height() + anim.set_values(original_y - height, original_y) + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value))) + anim.set_path_cb(lv.anim_t.path_ease_in_out) + # Reset y position after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_y(original_y))) + else: # "slide_up" + # Create slide-up animation (y from +height to original y) + original_y = widget.get_y() + height = widget.get_height() + anim.set_values(original_y + height, original_y) + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value))) + anim.set_path_cb(lv.anim_t.path_ease_in_out) + # Reset y position after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_y(original_y))) + + anim.start() + return anim + + @staticmethod + def hide_widget(widget, anim_type="fade", duration=500, delay=0, hide=True): + """ + Hide a widget with an animation. + + Args: + widget (lv.obj): The widget to hide + anim_type (str): Animation type - "fade", "slide_down", or "slide_up" (default: "fade") + duration (int): Animation duration in milliseconds (default: 500) + delay (int): Animation delay in milliseconds (default: 0) + hide (bool): If True, adds HIDDEN flag after animation. If False, only animates opacity/position (default: True) + + Returns: + The animation object + """ + lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches + anim = lv.anim_t() + anim.init() + anim.set_var(widget) + anim.set_duration(duration) + anim.set_delay(delay) + + if anim_type == "fade": + # Create fade-out animation (opacity from 255 to 0) + anim.set_values(255, 0) + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_style_opa(value, 0))) + anim.set_path_cb(lv.anim_t.path_ease_in_out) + # Set HIDDEN flag after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: WidgetAnimator._hide_complete_cb(widget, hide=hide))) + elif anim_type == "slide_down": + # Create slide-down animation (y from original y to +height) + original_y = widget.get_y() + height = widget.get_height() + anim.set_values(original_y, original_y + height) + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value))) + anim.set_path_cb(lv.anim_t.path_ease_in_out) + # Set HIDDEN flag after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: WidgetAnimator._hide_complete_cb(widget, original_y, hide))) + else: # "slide_up" + # Create slide-up animation (y from original y to -height) + original_y = widget.get_y() + height = widget.get_height() + anim.set_values(original_y, original_y - height) + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value))) + anim.set_path_cb(lv.anim_t.path_ease_in_out) + # Set HIDDEN flag after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: WidgetAnimator._hide_complete_cb(widget, original_y, hide))) + + anim.start() + return anim + + @staticmethod + def change_widget(widget, anim_type="interpolate", duration=5000, delay=0, begin_value=0, end_value=100, display_change=None): + """ + Animate a widget's text by interpolating between begin_value and end_value. + + Args: + widget: The widget to animate (should have set_text method) + anim_type: Type of animation (currently "interpolate" is supported) + duration: Animation duration in milliseconds + delay: Animation delay in milliseconds + begin_value: Starting value for interpolation + end_value: Ending value for interpolation + display_change: callback to display the change in the UI + + Returns: + The animation object + """ + lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches + anim = lv.anim_t() + anim.init() + anim.set_var(widget) + anim.set_delay(delay) + anim.set_duration(duration) + + if anim_type == "interpolate": + anim.set_values(begin_value, end_value) + if display_change is not None: + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: display_change(value))) + # Ensure final value is set after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: display_change(end_value))) + else: + anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_text(str(value)))) + # Ensure final value is set after animation + anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_text(str(end_value)))) + anim.set_path_cb(lv.anim_t.path_ease_in_out) + else: + return + + anim.start() + return anim + + @staticmethod + def smooth_show(widget, duration=500, delay=0): + """ + Fade in a widget (shorthand for show_widget with fade animation). + + Args: + widget: The widget to show + duration: Animation duration in milliseconds (default: 500) + delay: Animation delay in milliseconds (default: 0) + + Returns: + The animation object + """ + return WidgetAnimator.show_widget(widget, anim_type="fade", duration=duration, delay=delay) + + @staticmethod + def smooth_hide(widget, hide=True, duration=500, delay=0): + """ + Fade out a widget (shorthand for hide_widget with fade animation). + + Args: + widget: The widget to hide + hide: If True, adds HIDDEN flag after animation (default: True) + duration: Animation duration in milliseconds (default: 500) + delay: Animation delay in milliseconds (default: 0) + + Returns: + The animation object + """ + return WidgetAnimator.hide_widget(widget, anim_type="fade", duration=duration, delay=delay, hide=hide) + + @staticmethod + def _hide_complete_cb(widget, original_y=None, hide=True): + """ + Internal callback for hide animation completion. + + Args: + widget: The widget being hidden + original_y: Original Y position (for slide animations) + hide: Whether to add HIDDEN flag + """ + if hide: + widget.add_flag(lv.obj.FLAG.HIDDEN) + if original_y: + widget.set_y(original_y) # in case it shifted slightly due to rounding etc diff --git a/tests/test_graphical_animation_deleted_widget.py b/tests/test_graphical_animation_deleted_widget.py index 0de85aee..46e4cabf 100644 --- a/tests/test_graphical_animation_deleted_widget.py +++ b/tests/test_graphical_animation_deleted_widget.py @@ -17,7 +17,7 @@ Usage: import unittest import lvgl as lv -import mpos.ui.anim +from mpos.ui.widget_animator import WidgetAnimator import time from mpos import wait_for_render @@ -57,7 +57,7 @@ class TestAnimationDeletedWidget(unittest.TestCase): # Start fade-in animation (500ms duration) print("Starting smooth_show animation...") - mpos.ui.anim.smooth_show(widget) + WidgetAnimator.smooth_show(widget) # Give animation time to start wait_for_render(2) @@ -97,7 +97,7 @@ class TestAnimationDeletedWidget(unittest.TestCase): # Start fade-out animation print("Starting smooth_hide animation...") - mpos.ui.anim.smooth_hide(widget) + WidgetAnimator.smooth_hide(widget) # Give animation time to start wait_for_render(2) @@ -144,7 +144,7 @@ class TestAnimationDeletedWidget(unittest.TestCase): # User clicks textarea - keyboard shows with animation print("Showing keyboard with animation...") - mpos.ui.anim.smooth_show(keyboard) + WidgetAnimator.smooth_show(keyboard) # Give animation time to start wait_for_render(2) @@ -189,7 +189,7 @@ class TestAnimationDeletedWidget(unittest.TestCase): # Start animations on all widgets print("Starting animations on 5 widgets...") for w in widgets: - mpos.ui.anim.smooth_show(w) + WidgetAnimator.smooth_show(w) wait_for_render(2) diff --git a/tests/test_graphical_keyboard_animation.py b/tests/test_graphical_keyboard_animation.py index adeb6f8b..569049fe 100644 --- a/tests/test_graphical_keyboard_animation.py +++ b/tests/test_graphical_keyboard_animation.py @@ -1,8 +1,8 @@ """ -Test MposKeyboard animation support (show/hide with mpos.ui.anim). +Test MposKeyboard animation support (show/hide with WidgetAnimator). This test reproduces the bug where MposKeyboard is missing methods -required by mpos.ui.anim.smooth_show() and smooth_hide(). +required by WidgetAnimator.smooth_show() and smooth_hide(). Usage: Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_animation.py @@ -12,7 +12,7 @@ Usage: import unittest import lvgl as lv import time -import mpos.ui.anim +from mpos.ui.widget_animator import WidgetAnimator from base import KeyboardTestBase @@ -23,7 +23,7 @@ class TestKeyboardAnimation(KeyboardTestBase): """ Test that MposKeyboard has set_style_opa method. - This method is required by mpos.ui.anim for fade animations. + This method is required by WidgetAnimator for fade animations. """ print("Testing that MposKeyboard has set_style_opa...") @@ -62,7 +62,7 @@ class TestKeyboardAnimation(KeyboardTestBase): # This should work without raising AttributeError try: - mpos.ui.anim.smooth_show(self.keyboard) + WidgetAnimator.smooth_show(self.keyboard) self.wait_for_render(100) print("smooth_show called successfully") except AttributeError as e: @@ -91,7 +91,7 @@ class TestKeyboardAnimation(KeyboardTestBase): # This should work without raising AttributeError try: - mpos.ui.anim.smooth_hide(self.keyboard) + WidgetAnimator.smooth_hide(self.keyboard) print("smooth_hide called successfully") except AttributeError as e: self.fail(f"smooth_hide raised AttributeError: {e}\n" @@ -117,7 +117,7 @@ class TestKeyboardAnimation(KeyboardTestBase): # Show keyboard (simulates textarea click) try: - mpos.ui.anim.smooth_show(self.keyboard) + WidgetAnimator.smooth_show(self.keyboard) self.wait_for_render(100) except AttributeError as e: self.fail(f"Failed during smooth_show: {e}") @@ -127,7 +127,7 @@ class TestKeyboardAnimation(KeyboardTestBase): # Hide keyboard (simulates pressing Enter) try: - mpos.ui.anim.smooth_hide(self.keyboard) + WidgetAnimator.smooth_hide(self.keyboard) self.wait_for_render(100) except AttributeError as e: self.fail(f"Failed during smooth_hide: {e}")