diff --git a/docs/pyui-screensaver-customization.md b/docs/pyui-screensaver-customization.md new file mode 100644 index 00000000..54de9e0c --- /dev/null +++ b/docs/pyui-screensaver-customization.md @@ -0,0 +1,69 @@ +# PyUI Screensaver Customization + +## Install Backgrounds + +Copy user screensaver images to: + +```text +/mnt/SDCARD/App/PyUI/screensavers/ +``` + +SMB path: + +```text +\\\sdcard\App\PyUI\screensavers +``` + +Supported formats: `.png`, `.jpg`, `.jpeg`, `.bmp`, `.gif`. + +Theme-specific defaults can also be added as: + +```text +/mnt/SDCARD/Themes//screensaver.png +/mnt/SDCARD/Themes//screensaver.gif +``` + +## Settings + +Open `Settings > Screensaver`. + +- `Timeout`: global idle timeout before screensaver starts. +- `Background image`: choose image/GIF with preview. +- `Background color`: RGB picker with live preview. +- `Overlay opacity`: dim background. +- `Edit layout`: visual widget editor. + +## Visual Layout Editor + +- D-pad: move selected widget. +- `L1` / `R1`: decrease/increase widget size. +- `L2` / `R2`: cycle quick colors. +- `START`: open RGB picker for selected widget color. +- `Y`: change font for clock/date/text, or battery style for battery. +- `X`: show/hide selected widget. +- `SELECT`: switch selected widget. +- `A`: save. +- `B`: cancel. + +Included widget fonts: + +- `PressStart2P-Regular.ttf` +- `VT323-Regular.ttf` +- `Silkscreen-Regular.ttf` +- `PixelifySans.ttf` +- `Orbitron.ttf` +- `Audiowide-Regular.ttf` +- `BungeeShade-Regular.ttf` +- `EmilysCandy-Regular.ttf` + +Battery styles: + +- `percent` +- `blocks` +- `bar` +- `pill` +- `segments` + +## Performance Notes + +Animated GIFs are only loaded after the screensaver timeout and are freed when the user wakes the device. Large 1280x720 GIFs with many frames can still use more memory while active, so prefer short optimized loops. diff --git a/docs/pyui-screensaver-style-preview.png b/docs/pyui-screensaver-style-preview.png new file mode 100644 index 00000000..91c27aec Binary files /dev/null and b/docs/pyui-screensaver-style-preview.png differ diff --git a/fonts/Audiowide-Regular.ttf b/fonts/Audiowide-Regular.ttf new file mode 100644 index 00000000..8b50bedc Binary files /dev/null and b/fonts/Audiowide-Regular.ttf differ diff --git a/fonts/BungeeShade-Regular.ttf b/fonts/BungeeShade-Regular.ttf new file mode 100644 index 00000000..80ada80a Binary files /dev/null and b/fonts/BungeeShade-Regular.ttf differ diff --git a/fonts/EmilysCandy-Regular.ttf b/fonts/EmilysCandy-Regular.ttf new file mode 100644 index 00000000..d3bedc35 Binary files /dev/null and b/fonts/EmilysCandy-Regular.ttf differ diff --git a/fonts/Orbitron.ttf b/fonts/Orbitron.ttf new file mode 100644 index 00000000..1fc54ce7 Binary files /dev/null and b/fonts/Orbitron.ttf differ diff --git a/fonts/PixelifySans.ttf b/fonts/PixelifySans.ttf new file mode 100644 index 00000000..2d7eb388 Binary files /dev/null and b/fonts/PixelifySans.ttf differ diff --git a/fonts/PressStart2P-Regular.ttf b/fonts/PressStart2P-Regular.ttf new file mode 100644 index 00000000..39adf42e Binary files /dev/null and b/fonts/PressStart2P-Regular.ttf differ diff --git a/fonts/Silkscreen-Regular.ttf b/fonts/Silkscreen-Regular.ttf new file mode 100644 index 00000000..8caff819 Binary files /dev/null and b/fonts/Silkscreen-Regular.ttf differ diff --git a/fonts/VT323-Regular.ttf b/fonts/VT323-Regular.ttf new file mode 100644 index 00000000..afa69098 Binary files /dev/null and b/fonts/VT323-Regular.ttf differ diff --git a/main-ui/controller/controller.py b/main-ui/controller/controller.py index 822ad2aa..6bc8b2ec 100644 --- a/main-ui/controller/controller.py +++ b/main-ui/controller/controller.py @@ -99,8 +99,8 @@ def _matches_secret_prefix(): if len(h) < plen: return False - return h[-plen:] == p - + return h[-plen:] == p + @staticmethod def set_last_input(last_input): if(last_input == ControllerInput.LEFT_STICK_UP): @@ -259,7 +259,7 @@ def last_input(): # PyUiLogger.get_logger().info(f"Returning last input: {Controller.last_controller_input}") return Controller.last_controller_input - @staticmethod + @staticmethod def add_button_watcher(button_watcher): Controller.additional_button_watchers.append(button_watcher) @@ -278,9 +278,9 @@ def check_for_hotkey(): if(Controller.get_input(timeout=0.05, called_from_check_for_hotkey=True)): Controller.perform_hotkey(Controller.last_input()) time.sleep(0.1) - was_hotkey = True + was_hotkey = True elif(Controller.non_sdl_input is not None): - was_hotkey = True + was_hotkey = True Controller.perform_hotkey(Controller.non_sdl_input) time.sleep(0.1) @@ -289,7 +289,7 @@ def check_for_hotkey(): Controller.controller_interface.restore_cached_event() Controller.is_check_for_hotkey = False return was_hotkey - + @staticmethod def perform_hotkey(controller_input): PyUiLogger.get_logger().info(f"Performing hotkey for {controller_input}") @@ -298,7 +298,7 @@ def perform_hotkey(controller_input): Device.get_device().raise_lumination() elif(ControllerInput.VOLUME_DOWN == controller_input): Device.get_device().lower_lumination() - + @staticmethod def non_sdl_input_event(controller_input, is_down): TRIGGER_TIME_FOR_HOLD_BUTTONS = 2 @@ -306,7 +306,7 @@ def non_sdl_input_event(controller_input, is_down): if(is_down): if(controller_input in Controller.hold_buttons): if controller_input not in Controller.last_press_time_map: - Controller.last_press_time_map[controller_input] = time.time() + Controller.last_press_time_map[controller_input] = time.time() else: last_press_time_length = time.time() - Controller.last_press_time_map[controller_input] if(last_press_time_length > TRIGGER_TIME_FOR_HOLD_BUTTONS): diff --git a/main-ui/display/display.py b/main-ui/display/display.py index 9b619dc9..7dc68811 100644 --- a/main-ui/display/display.py +++ b/main-ui/display/display.py @@ -29,7 +29,7 @@ def __init__(self, texture, width, height): class ImageTextureCache: def __init__(self): - self.cache = {} + self.cache = {} def get_texture(self, texture_id) -> CachedImageTexture: return self.cache.get(texture_id) @@ -37,7 +37,7 @@ def get_texture(self, texture_id) -> CachedImageTexture: def add_texture(self, texture_id, surface, texture): self.cache[texture_id] = CachedImageTexture(texture, surface.contents.w, surface.contents.h) return True - + def clear_cache(self): for entry in self.cache.values(): sdl2.SDL_DestroyTexture(entry.texture) @@ -61,7 +61,7 @@ def __init__(self, texture, width, height): class TextTextureCache: def __init__(self): - self.cache = {} + self.cache = {} def get_texture(self, texture_id, font, color) -> CachedTextTexture: return self.cache.get(TextTextureKey(texture_id, font, color)) @@ -69,12 +69,12 @@ def get_texture(self, texture_id, font, color) -> CachedTextTexture: def add_texture(self, texture_id, font, color, surface, texture): self.cache[TextTextureKey(texture_id, font, color)] = CachedTextTexture(texture, surface.contents.w, surface.contents.h) return True - + def clear_cache(self): for entry in self.cache.values(): sdl2.SDL_DestroyTexture(entry.texture) self.cache.clear() - + class Display: debug = False renderer = None @@ -105,13 +105,13 @@ class Display: def init(cls): cls._init_display() #Outside init_fonts as it should only ever be called once - with log_timing("sdl2.sdlttf.TTF_Init()", PyUiLogger.get_logger()): + with log_timing("sdl2.sdlttf.TTF_Init()", PyUiLogger.get_logger()): if sdl2.sdlttf.TTF_Init() == -1: raise RuntimeError("Failed to initialize SDL_ttf") cls.init_fonts() - with log_timing("sdl2 create render canvas", PyUiLogger.get_logger()): + with log_timing("sdl2 create render canvas", PyUiLogger.get_logger()): cls.render_canvas = sdl2.SDL_CreateTexture( cls.renderer.renderer, sdl2.SDL_PIXELFORMAT_ARGB1555, @@ -121,10 +121,10 @@ def init(cls): ) cls.log_sdl_error_if_any() - with log_timing("sdl2.SDL_SetRenderTarget", PyUiLogger.get_logger()): + with log_timing("sdl2.SDL_SetRenderTarget", PyUiLogger.get_logger()): sdl2.SDL_SetRenderTarget(cls.renderer.renderer, cls.render_canvas) cls.log_sdl_error_if_any() - with log_timing("sdl2.SDL_SetRenderDrawBlendMode", PyUiLogger.get_logger()): + with log_timing("sdl2.SDL_SetRenderDrawBlendMode", PyUiLogger.get_logger()): sdl2.SDL_SetRenderDrawBlendMode(cls.renderer.renderer, sdl2.SDL_BLENDMODE_BLEND) cls.log_sdl_error_if_any() @@ -138,11 +138,11 @@ def init(cls): cls.supported_formats = set(info.texture_formats[:info.num_texture_formats]) - with log_timing("restore_bg", PyUiLogger.get_logger()): + with log_timing("restore_bg", PyUiLogger.get_logger()): cls.restore_bg() - with log_timing("clear", PyUiLogger.get_logger()): - cls.clear("", force_top_and_bottom_bar=True) + with log_timing("clear", PyUiLogger.get_logger()): + cls.clear("", force_top_and_bottom_bar=True) if(False): Display.log_sdl_render_drivers() @@ -220,10 +220,10 @@ def init_fonts(cls): @classmethod def _init_display(cls): - with log_timing("sdl2.ext.init", PyUiLogger.get_logger()): + with log_timing("sdl2.ext.init", PyUiLogger.get_logger()): sdl2.ext.init(controller=False) - with log_timing("sdl2.SDL_DisplayMode", PyUiLogger.get_logger()): + with log_timing("sdl2.SDL_DisplayMode", PyUiLogger.get_logger()): display_mode = sdl2.SDL_DisplayMode() if sdl2.SDL_GetCurrentDisplayMode(0, display_mode) != 0: PyUiLogger.get_logger().error("Failed to get display mode, using fallback 640x480") @@ -232,12 +232,12 @@ def _init_display(cls): width, height = display_mode.w, display_mode.h #PyUiLogger.get_logger().info(f"Display size: {width}x{height}") - with log_timing("sdl2.ext.Window", PyUiLogger.get_logger()): + with log_timing("sdl2.ext.Window", PyUiLogger.get_logger()): cls.window = sdl2.ext.Window("Minimal SDL2 GUI", size=(width, height), flags=sdl2.SDL_WINDOW_FULLSCREEN) cls.window.show() - with log_timing("sdl2.ext.Renderer", PyUiLogger.get_logger()): + with log_timing("sdl2.ext.Renderer", PyUiLogger.get_logger()): sdl2.SDL_SetHint(sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"2") cls.renderer = sdl2.ext.Renderer(cls.window, flags=sdl2.SDL_RENDERER_ACCELERATED) @@ -412,7 +412,7 @@ def lock_current_image(cls): if cls.bg_canvas: sdl2.SDL_DestroyTexture(cls.bg_canvas) cls.bg_canvas = None - + cls.bg_canvas = cls.render_canvas cls.render_canvas = sdl2.SDL_CreateTexture( cls.renderer.renderer, @@ -432,14 +432,14 @@ def unlock_current_image(cls): cls.bg_canvas = None @classmethod - def clear(cls, - top_bar_text, + def clear(cls, + top_bar_text, hide_top_bar_icons = False, bottom_bar_text = None, render_bottom_bar_icons_and_images = True, force_top_and_bottom_bar = False): cls.top_bar_text = top_bar_text - + if cls.is_custom_theme_background: #cls.render_image(cls.bg_path, Device.get_device().screen_width()//2, Device.get_device().screen_height()//2, RenderMode.MIDDLE_CENTER_ALIGNED, Device.get_device().screen_width(), Device.get_device().screen_height(), ResizeType.ZOOM) cls.render_image(cls.bg_path, 0, 0, RenderMode.TOP_LEFT_ALIGNED, Device.get_device().screen_width(), Device.get_device().screen_height(), ResizeType.ZOOM) @@ -449,7 +449,7 @@ def clear(cls, sdl2.SDL_RenderCopy(cls.renderer.sdlrenderer, cls.background_texture, None, None) else: PyUiLogger.get_logger().warning("No background texture to render") - + if not Theme.render_top_and_bottom_bar_last() or force_top_and_bottom_bar: cls.top_bar.render_top_bar(cls.top_bar_text,hide_top_bar_icons) @@ -498,7 +498,7 @@ def _render_surface_texture(cls, x, y, texture, w, h, render_mode: RenderMode, t #Need to push this further up stream though if(resize_type is None): resize_type=ResizeType.FIT - + orig_w = w orig_h = h render_w, render_h = cls._calculate_scaled_width_and_height(orig_w, orig_h, scale_width, scale_height, resize_type) @@ -506,7 +506,7 @@ def _render_surface_texture(cls, x, y, texture, w, h, render_mode: RenderMode, t # Adjust position based on render mode adj_x = x adj_y = y - + if resize_type == ResizeType.ZOOM and scale_width and scale_height: src_w = int(scale_width * (orig_w / render_w)) src_h = int(scale_height * (orig_h / render_h)) @@ -543,7 +543,7 @@ def _render_surface_texture(cls, x, y, texture, w, h, render_mode: RenderMode, t return scale_width, scale_height else: - + if XRenderOption.CENTER == render_mode.x_mode: adj_x = x - (render_w) // 2 elif XRenderOption.RIGHT == render_mode.x_mode: @@ -578,12 +578,12 @@ def log_sdl_error_and_clear_cache_image(cls, image_path=None): err = sdl2.sdlttf.TTF_GetError() err_msg = err.decode('utf-8') if err else "Unknown error" PyUiLogger.get_logger().warning(f"SDL Error received on loading {image_path} : {err_msg}") - + if any(keyword in err_msg for keyword in cls._problematic_image_keywords): if(image_path is not None): cls._problematic_images.add(image_path) PyUiLogger.get_logger().warning(f"Marking as image to permanently stop trying to load: {image_path}") - + return False else: PyUiLogger.get_logger().warning(f"Clearing cache : {err_msg}") @@ -596,14 +596,14 @@ def log_sdl_error_and_clear_cache_image(cls, image_path=None): def log_sdl_error_and_clear_cache_text(cls, text,purpose): err = sdl2.sdlttf.TTF_GetError() err_msg = err.decode('utf-8') if err else "Unknown error" - + if not (any(keyword in err_msg for keyword in cls._problematic_image_keywords)): PyUiLogger.get_logger().warning(f"SDL Error received on loading {text} w/ purpose {purpose}") PyUiLogger.get_logger().warning(f"Clearing cache : {err_msg}") cls._text_texture_cache.clear_cache() cls._image_texture_cache.clear_cache() return True - + return False @@ -652,13 +652,13 @@ def render_text(cls, text, x, y, color, purpose: FontPurpose, render_mode=Render sdl2.SDL_SetTextureAlphaMod(texture, alpha) w,h = cls._render_surface_texture( x=x, - y=y, - texture=texture, - w=surface.contents.w, + y=y, + texture=texture, + w=surface.contents.w, h=surface.contents.h, - render_mode=render_mode, - texture_id=text, - crop_w=crop_w, + render_mode=render_mode, + texture_id=text, + crop_w=crop_w, crop_h=crop_h) sdl2.SDL_DestroyTexture(texture) sdl2.SDL_FreeSurface(surface) @@ -672,18 +672,18 @@ def render_text(cls, text, x, y, color, purpose: FontPurpose, render_mode=Render w,h = cls._render_surface_texture( x=x, - y=y, - texture=texture, - w=texture_w, + y=y, + texture=texture, + w=texture_w, h=texture_h, - render_mode=render_mode, - texture_id=text, - crop_w=crop_w, + render_mode=render_mode, + texture_id=text, + crop_w=crop_w, crop_h=crop_h) - + if not cached: sdl2.SDL_DestroyTexture(texture) - + return w,h @@ -695,7 +695,7 @@ def render_text_centered(cls, text, x, y, color, purpose: FontPurpose): def image_load(cls, image_path): #PyUiLogger.get_logger().info(f"Loading {image_path}") return sdl2.sdlimage.IMG_Load(image_path.encode("utf-8")) - + @classmethod def convert_surface_to_safe_format(cls, surface): if(Device.get_device().might_require_surface_format_conversion() and surface): @@ -709,9 +709,9 @@ def convert_surface_to_safe_format(cls, surface): surface = converted_surface return surface - + @classmethod - def render_image(cls, image_path: str, x: int, y: int, render_mode=RenderMode.TOP_LEFT_ALIGNED, target_width=None, target_height=None, resize_type=None, crop_w=None, crop_h=None): + def render_image(cls, image_path: str, x: int, y: int, render_mode=RenderMode.TOP_LEFT_ALIGNED, target_width=None, target_height=None, resize_type=None, crop_w=None, crop_h=None): if(image_path is None or image_path in cls._problematic_images): return 0, 0 @@ -746,7 +746,7 @@ def render_image(cls, image_path: str, x: int, y: int, render_mode=RenderMode.TO sdl2.SDL_FreeSurface(surface) PyUiLogger.get_logger().warning( f"Image is too large to render ({surface_width} x {surface_height} with max of {Device.get_device().max_texture_width()} x {Device.get_device().max_texture_height()}). Skipping {image_path}\n" - ) + ) cls._problematic_images.add(image_path) return 0, 0 @@ -771,18 +771,18 @@ def render_image(cls, image_path: str, x: int, y: int, render_mode=RenderMode.TO texture_h = surface.contents.h sdl2.SDL_FreeSurface(surface) - final_w,final_h = cls._render_surface_texture(x=x, - y=y, - texture=texture, - w=texture_w, + final_w,final_h = cls._render_surface_texture(x=x, + y=y, + texture=texture, + w=texture_w, h=texture_h, - render_mode=render_mode, - scale_width=target_width, + render_mode=render_mode, + scale_width=target_width, scale_height=target_height, - resize_type=resize_type, + resize_type=resize_type, texture_id=image_path, crop_w=crop_w, crop_h=crop_h) - + if(not cached): sdl2.SDL_DestroyTexture(texture) PyUiLogger.get_logger().info(f"Destroyed {image_path}. Image cache size is {cls._image_texture_cache.size()}"); @@ -923,7 +923,7 @@ def rotate_canvas(cls) -> sdl2.SDL_Texture: w = sdl2.c_int() h = sdl2.c_int() query_texture_result = sdl2.SDL_QueryTexture(cls.render_canvas, None, None, w, h) - + if query_texture_result != 0: # Destroy the old texture if it exists if cls.render_canvas: @@ -942,8 +942,8 @@ def rotate_canvas(cls) -> sdl2.SDL_Texture: ) if not cls.render_canvas: PyUiLogger.get_logger().error("Failed to recreate render_canvas: " + sdl2.SDL_GetError().decode()) - return None - + return None + src_w, src_h = w.value, h.value # Determine new target size after rotation @@ -968,7 +968,7 @@ def rotate_canvas(cls) -> sdl2.SDL_Texture: # Set render target sdl2.SDL_SetRenderTarget(cls.renderer.sdlrenderer, rotated_texture) - + # Clear it sdl2.SDL_SetRenderDrawColor(cls.renderer.sdlrenderer, 0, 0, 0, 0) sdl2.SDL_RenderClear(cls.renderer.sdlrenderer) @@ -999,7 +999,7 @@ def rotate_canvas(cls) -> sdl2.SDL_Texture: sdl2.SDL_SetRenderTarget(cls.renderer.sdlrenderer, None) return rotated_texture - + @classmethod def present(cls, fade=False): if Theme.render_top_and_bottom_bar_last(): @@ -1021,7 +1021,7 @@ def present(cls, fade=False): cls.fade_transition(cls.bg_canvas, cls.render_canvas) else: sdl2.SDL_RenderCopy(cls.renderer.sdlrenderer, cls.render_canvas, None, None) - else: + else: rotated_texture = cls.rotate_canvas() if(rotated_texture is not None): sdl2.SDL_RenderCopy(cls.renderer.sdlrenderer, rotated_texture, None, None) @@ -1052,7 +1052,7 @@ def get_center_of_usable_screen_height(cls, force_include_top_bar = False): def get_image_dimensions(cls, img): if(img is None): return 0, 0 - + surface = Display.image_load(img) if not surface: return 0, 0 @@ -1071,14 +1071,14 @@ def get_space_dimensions(cls, font_purpose=FontPurpose.LIST): if cls._cached_space_dimensions is None: cls._cached_space_dimensions = cls.get_text_dimensions(font_purpose, " ") return cls._cached_space_dimensions - + @classmethod def get_text_dimensions(cls, purpose, text="A"): w = sdl2.Sint32() h = sdl2.Sint32() sdl2.sdlttf.TTF_SizeUTF8(cls.fonts[purpose].font, text.encode('utf-8'), w, h) return int(w.value * Device.get_device().get_text_width_measurement_multiplier()), h.value - + @classmethod def add_index_text(cls, index, total, force_include_index = False, letter = None): @@ -1151,7 +1151,7 @@ def split_message(cls, message: str, font_purpose, clip_to_device_width) -> list # Convert anything that's not a string to string if not isinstance(message, str): message = str(message) - + # First split on explicit newlines raw_lines = message.split("\n") final_lines = [] @@ -1190,7 +1190,7 @@ def split_message(cls, message: str, font_purpose, clip_to_device_width) -> list @classmethod def display_message_multiline(cls,split_message, duration_ms=0): - Display.clear("") + Display.clear("") cls.write_message_multiline(split_message, Device.get_device().screen_height()//2) Display.present() # Sleep for the specified duration in milliseconds @@ -1223,7 +1223,7 @@ def write_message_multiline_starting_height_specified(cls,split_message, startin def display_message(cls,message, duration_ms=0): split_message = Display.split_message(message, FontPurpose.LIST,clip_to_device_width=True) cls.display_message_multiline(split_message,duration_ms) - + @classmethod def display_image(cls,image_path, duration_ms=0): Display.clear("") diff --git a/main-ui/display/screensaver.py b/main-ui/display/screensaver.py new file mode 100644 index 00000000..b49a88df --- /dev/null +++ b/main-ui/display/screensaver.py @@ -0,0 +1,444 @@ +import time +import datetime +import os +import sdl2 +import sdl2.sdlimage +import sdl2.sdlttf + +from devices.device import Device +from display.font_purpose import FontPurpose +from display.render_mode import RenderMode +from themes.theme import Theme +from utils.logger import PyUiLogger + + +class ScreenSaver: + _animation_path = None + _animation = None + _animation_frame = 0 + _animation_next_time = 0 + + @classmethod + def render(cls): + try: + renderer = Device.get_device() + screen_w = renderer.screen_width() + screen_h = renderer.screen_height() + from display.display import Display + + cls._render_background(screen_w, screen_h, Display) + + widgets = cls._get_enabled_widgets() + for widget in widgets: + cls._render_widget(widget, screen_w, screen_h, Display) + + cls._present_without_bars(Display) + except Exception as e: + PyUiLogger.get_logger().error(f"ScreenSaver render error: {e}") + + @classmethod + def render_if_needed(cls): + if cls._animation_path and time.time() >= cls._animation_next_time: + cls.render() + + @classmethod + def clear_cache(cls): + cls._clear_animation() + + @classmethod + def _present_without_bars(cls, Display): + sdl2.SDL_SetRenderTarget(Display.renderer.renderer, None) + + if Device.get_device().should_scale_screen(): + scaled_canvas = Display.scale_texture_to_fit( + Display.render_canvas, + Device.get_device().output_screen_width(), + Device.get_device().output_screen_height() + ) + sdl2.SDL_RenderCopy(Display.renderer.sdlrenderer, scaled_canvas, None, None) + sdl2.SDL_DestroyTexture(scaled_canvas) + elif Device.get_device().screen_rotation() == 0: + sdl2.SDL_RenderCopy(Display.renderer.sdlrenderer, Display.render_canvas, None, None) + else: + rotated_texture = Display.rotate_canvas() + if rotated_texture is not None: + sdl2.SDL_RenderCopy(Display.renderer.sdlrenderer, rotated_texture, None, None) + sdl2.SDL_DestroyTexture(rotated_texture) + + sdl2.SDL_SetRenderTarget(Display.renderer.renderer, Display.render_canvas) + Display.renderer.present() + Device.get_device().post_present_operations() + + @classmethod + def _render_background(cls, screen_w, screen_h, Display): + ss = Theme._data.get("screensaver", {}) + bg_image = cls._get_bg_image(ss.get("bgImage", "")) + overlay_opacity = ss.get("overlayOpacity", 0.3) + overlay_color = Theme.hex_to_color(ss.get("overlayColor", "#000000")) + blur = ss.get("blur", 0) + bg_color = Theme.hex_to_color(ss.get("bgColor", "#000000")) + + if bg_image and os.path.exists(bg_image): + if bg_image.lower().endswith(".gif"): + cls._render_gif_background(bg_image, screen_w, screen_h, Display, bg_color) + else: + cls._clear_animation() + cls._render_static_background(bg_image, screen_w, screen_h, Display, bg_color, blur) + else: + cls._clear_animation() + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, + bg_color[0], bg_color[1], bg_color[2], 255) + sdl2.SDL_RenderClear(Display.renderer.sdlrenderer) + + if 0 < overlay_opacity <= 1.0: + alpha = int(overlay_opacity * 255) + sdl2.SDL_SetRenderDrawBlendMode(Display.renderer.sdlrenderer, sdl2.SDL_BLENDMODE_BLEND) + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, + overlay_color[0], overlay_color[1], overlay_color[2], alpha) + rect = sdl2.SDL_Rect(0, 0, screen_w, screen_h) + sdl2.SDL_RenderFillRect(Display.renderer.sdlrenderer, rect) + + @classmethod + def _render_static_background(cls, bg_image, screen_w, screen_h, Display, bg_color, blur): + surface = sdl2.sdlimage.IMG_Load(bg_image.encode("utf-8")) + if surface: + if blur > 0: + surface = cls._apply_blur(surface, blur) + texture = sdl2.SDL_CreateTextureFromSurface(Display.renderer.renderer, surface) + if texture: + sdl2.SDL_SetTextureBlendMode(texture, sdl2.SDL_BLENDMODE_BLEND) + src_w = surface.contents.w + src_h = surface.contents.h + src = sdl2.SDL_Rect(0, 0, src_w, src_h) + dst = sdl2.SDL_Rect(0, 0, screen_w, screen_h) + sdl2.SDL_RenderCopy(Display.renderer.renderer, texture, src, dst) + sdl2.SDL_DestroyTexture(texture) + sdl2.SDL_FreeSurface(surface) + else: + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, + bg_color[0], bg_color[1], bg_color[2], 255) + sdl2.SDL_RenderClear(Display.renderer.sdlrenderer) + + @classmethod + def _render_gif_background(cls, bg_image, screen_w, screen_h, Display, bg_color): + if cls._animation_path != bg_image: + cls._load_animation(bg_image, Display) + + if not cls._animation: + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, + bg_color[0], bg_color[1], bg_color[2], 255) + sdl2.SDL_RenderClear(Display.renderer.sdlrenderer) + return + + anim = cls._animation.contents + frame = cls._animation_frame % anim.count + surface = anim.frames[frame] + texture = sdl2.SDL_CreateTextureFromSurface(Display.renderer.renderer, surface) + if not texture: + return + sdl2.SDL_SetTextureBlendMode(texture, sdl2.SDL_BLENDMODE_BLEND) + src = sdl2.SDL_Rect(0, 0, anim.w, anim.h) + dst = sdl2.SDL_Rect(0, 0, screen_w, screen_h) + sdl2.SDL_RenderCopy(Display.renderer.renderer, texture, src, dst) + sdl2.SDL_DestroyTexture(texture) + + delay = anim.delays[frame] if anim.delays else 100 + if delay < 100: + delay = 100 + cls._animation_frame = (frame + 1) % anim.count + cls._animation_next_time = time.time() + delay / 1000.0 + + @classmethod + def _load_animation(cls, bg_image, Display): + cls._clear_animation() + animation = sdl2.sdlimage.IMG_LoadAnimation(bg_image.encode("utf-8")) + if not animation: + return + + cls._animation_path = bg_image + cls._animation = animation + cls._animation_frame = 0 + cls._animation_next_time = 0 + anim = animation.contents + + @classmethod + def _clear_animation(cls): + if cls._animation: + sdl2.sdlimage.IMG_FreeAnimation(cls._animation) + cls._animation = None + cls._animation_path = None + cls._animation_frame = 0 + cls._animation_next_time = 0 + + @classmethod + def _get_bg_image(cls, configured_path): + candidates = [] + if configured_path: + candidates.append(configured_path) + if not os.path.isabs(configured_path): + candidates.append(os.path.join(Theme.get_theme_path(), configured_path)) + + candidates.append(os.path.join(Theme.get_theme_path(), "screensaver.gif")) + candidates.append(os.path.join(Theme.get_theme_path(), "screensaver.png")) + candidates.append("/mnt/SDCARD/App/PyUI/main-ui/themes/screensaver.gif") + candidates.append("/mnt/SDCARD/App/PyUI/main-ui/themes/screensaver.png") + + for path in candidates: + if path and os.path.exists(path): + return path + return "" + + @classmethod + def _apply_blur(cls, surface, radius): + w = surface.contents.w + h = surface.contents.h + fmt = surface.contents.format.contents + src_pixels = sdl2.SDL_LockSurface(surface) + if src_pixels == 0: + return surface + + try: + pixels = surface.contents.pixels + pitch = surface.contents.pitch + bpp = fmt.BytesPerPixel + temp = sdl2.SDL_CreateRGBSurfaceWithFormat(0, w, h, 32, fmt.format) + if not temp: + return surface + + sdl2.SDL_LockSurface(temp) + try: + temp_pixels = temp.contents.pixels + temp_pitch = temp.contents.pitch + + r = min(radius, min(w, h) // 2) + if r < 1: + r = 1 + + kernel_size = 2 * r + 1 + for y in range(h): + for x in range(w): + sum_r, sum_g, sum_b, count = 0, 0, 0, 0 + for ky in range(-r, r + 1): + sy = y + ky + if sy < 0 or sy >= h: + continue + for kx in range(-r, r + 1): + sx = x + kx + if sx < 0 or sx >= w: + continue + offset = sy * pitch + sx * bpp + if bpp == 4: + b = pixels[offset] + g = pixels[offset + 1] + rd = pixels[offset + 2] + a = pixels[offset + 3] + elif bpp == 3: + b = pixels[offset] + g = pixels[offset + 1] + rd = pixels[offset + 2] + a = 255 + else: + continue + sum_r += rd + sum_g += g + sum_b += b + count += 1 + if count > 0: + avg_r = sum_r // count + avg_g = sum_g // count + avg_b = sum_b // count + else: + avg_r, avg_g, avg_b = 0, 0, 0 + dst_offset = y * temp_pitch + x * bpp + if bpp == 4: + temp_pixels[dst_offset] = avg_b + temp_pixels[dst_offset + 1] = avg_g + temp_pixels[dst_offset + 2] = avg_r + temp_pixels[dst_offset + 3] = 255 + elif bpp == 3: + temp_pixels[dst_offset] = avg_b + temp_pixels[dst_offset + 1] = avg_g + temp_pixels[dst_offset + 2] = avg_r + finally: + sdl2.SDL_UnlockSurface(temp) + finally: + sdl2.SDL_UnlockSurface(surface) + + sdl2.SDL_BlitSurface(temp, None, surface, None) + sdl2.SDL_FreeSurface(temp) + return surface + + @classmethod + def _get_enabled_widgets(cls): + ss = Theme._data.get("screensaver", {}) + show_clock = ss.get("showClock", True) + show_date = ss.get("showDate", True) + show_battery = ss.get("showBattery", True) + + all_widgets = Theme.get_screensaver_widgets() + enabled = [] + for w in all_widgets: + if not w.get("enabled", True): + continue + wtype = w.get("type", "") + if wtype == "clock" and not show_clock: + continue + if wtype == "date" and not show_date: + continue + if wtype == "battery" and not show_battery: + continue + enabled.append(w) + return enabled + + @classmethod + def _render_widget(cls, widget, screen_w, screen_h, Display): + wtype = widget.get("type", "") + x = int(widget.get("x", 0.5) * screen_w) + y = int(widget.get("y", 0.5) * screen_h) + font_size = int(widget.get("fontSize", 24)) + color = Theme.hex_to_color(widget.get("color", "#FFFFFF")) + multiplier = Theme._default_multiplier + scaled_size = max(12, int(font_size * multiplier)) + + if wtype == "clock": + now = datetime.datetime.now() + text = now.strftime("%H:%M") + cls._draw_text(text, x, y, color, scaled_size, Display, center=True, widget=widget) + + elif wtype == "date": + now = datetime.datetime.now() + text = now.strftime("%A, %B %d") + cls._draw_text(text, x, y, color, scaled_size, Display, center=True, widget=widget) + + elif wtype == "battery": + try: + percent = Device.get_device().get_battery_percent() + charging = Device.get_device().get_charge_status() + from devices.charge.charge_status import ChargeStatus + symbol = "+" if charging == ChargeStatus.CHARGING else "" + text = f"{symbol}{percent}%" + if percent <= 10: + color = (255, 80, 80) + elif percent <= 30: + color = (255, 200, 80) + style = widget.get("style", "percent") + if style == "bar": + cls._draw_battery_bar(percent, x, y, color, screen_w, screen_h, Display) + elif style == "pill": + cls._draw_battery_pill(percent, x, y, color, screen_w, screen_h, Display) + elif style == "segments": + cls._draw_battery_segments(percent, x, y, color, screen_w, screen_h, Display) + elif style == "blocks": + filled = max(0, min(5, int(round(percent / 20)))) + text = "[" + ("|" * filled).ljust(5, " ") + f"] {percent}%" + cls._draw_text(text, x, y, color, scaled_size, Display, center=True, widget=widget) + else: + cls._draw_text(text, x, y, color, scaled_size, Display, center=True, widget=widget) + except Exception: + pass + + elif wtype == "text": + text = widget.get("value", "") + if text: + cls._draw_text(text, x, y, color, scaled_size, Display, center=True, widget=widget) + + @classmethod + def _draw_battery_bar(cls, percent, x, y, color, screen_w, screen_h, Display): + width = max(80, int(screen_w * 0.11)) + height = max(24, int(screen_h * 0.035)) + left = x - width // 2 + top = y - height // 2 + sdl2.SDL_SetRenderDrawBlendMode(Display.renderer.sdlrenderer, sdl2.SDL_BLENDMODE_BLEND) + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, color[0], color[1], color[2], 255) + sdl2.SDL_RenderDrawRect(Display.renderer.sdlrenderer, sdl2.SDL_Rect(left, top, width, height)) + sdl2.SDL_RenderDrawRect(Display.renderer.sdlrenderer, sdl2.SDL_Rect(left + width, top + height // 4, 6, height // 2)) + fill_width = max(0, int((width - 6) * percent / 100)) + if fill_width > 0: + sdl2.SDL_RenderFillRect(Display.renderer.sdlrenderer, sdl2.SDL_Rect(left + 3, top + 3, fill_width, height - 6)) + + @classmethod + def _draw_battery_pill(cls, percent, x, y, color, screen_w, screen_h, Display): + width = max(92, int(screen_w * 0.13)) + height = max(26, int(screen_h * 0.04)) + left = x - width // 2 + top = y - height // 2 + sdl2.SDL_SetRenderDrawBlendMode(Display.renderer.sdlrenderer, sdl2.SDL_BLENDMODE_BLEND) + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, color[0], color[1], color[2], 255) + sdl2.SDL_RenderDrawRect(Display.renderer.sdlrenderer, sdl2.SDL_Rect(left, top, width, height)) + sdl2.SDL_RenderDrawRect(Display.renderer.sdlrenderer, sdl2.SDL_Rect(left + 2, top + 2, width - 4, height - 4)) + fill_width = max(0, int((width - 10) * percent / 100)) + if fill_width > 0: + sdl2.SDL_RenderFillRect(Display.renderer.sdlrenderer, sdl2.SDL_Rect(left + 5, top + 5, fill_width, height - 10)) + + @classmethod + def _draw_battery_segments(cls, percent, x, y, color, screen_w, screen_h, Display): + segments = 5 + width = max(100, int(screen_w * 0.14)) + height = max(24, int(screen_h * 0.035)) + gap = 4 + segment_w = (width - gap * (segments - 1)) // segments + left = x - width // 2 + top = y - height // 2 + filled = max(0, min(segments, int(round(percent / 20)))) + sdl2.SDL_SetRenderDrawBlendMode(Display.renderer.sdlrenderer, sdl2.SDL_BLENDMODE_BLEND) + for index in range(segments): + alpha = 255 if index < filled else 70 + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, color[0], color[1], color[2], alpha) + rect = sdl2.SDL_Rect(left + index * (segment_w + gap), top, segment_w, height) + if index < filled: + sdl2.SDL_RenderFillRect(Display.renderer.sdlrenderer, rect) + else: + sdl2.SDL_RenderDrawRect(Display.renderer.sdlrenderer, rect) + + @classmethod + def _draw_text(cls, text, x, y, color, font_size, Display, center=True, widget=None): + font_path = cls._get_widget_font(widget) + try: + font = sdl2.sdlttf.TTF_OpenFont(font_path.encode("utf-8"), font_size) if font_path else None + except Exception: + font = None + + if font is None: + return + + try: + sdl_color = sdl2.SDL_Color(color[0], color[1], color[2], 255) + surface = sdl2.sdlttf.TTF_RenderUTF8_Blended(font, text.encode("utf-8"), sdl_color) + if not surface: + return + + texture = sdl2.SDL_CreateTextureFromSurface(Display.renderer.renderer, surface) + if not texture: + sdl2.SDL_FreeSurface(surface) + return + + w = surface.contents.w + h = surface.contents.h + if center: + draw_x = x - w // 2 + else: + draw_x = x + draw_y = y - h // 2 + + dst = sdl2.SDL_Rect(draw_x, draw_y, w, h) + sdl2.SDL_RenderCopy(Display.renderer.renderer, texture, None, dst) + sdl2.SDL_DestroyTexture(texture) + sdl2.SDL_FreeSurface(surface) + except Exception as e: + PyUiLogger.get_logger().error(f"ScreenSaver text render error: {e}") + finally: + sdl2.sdlttf.TTF_CloseFont(font) + + @classmethod + def _get_widget_font(cls, widget): + font = widget.get("font") if widget else None + candidates = [] + if font: + candidates.append(font) + candidates.append(os.path.join("/mnt/SDCARD/App/PyUI/fonts", font)) + candidates.append(os.path.join(Theme.get_theme_path(), font)) + candidates.append(Theme.get_font(FontPurpose.LIST)) + for path in candidates: + if path and os.path.exists(path): + return path + return None diff --git a/main-ui/menus/games/in_game_menu_listener.py b/main-ui/menus/games/in_game_menu_listener.py index e04eab8f..1fd8e84c 100644 --- a/main-ui/menus/games/in_game_menu_listener.py +++ b/main-ui/menus/games/in_game_menu_listener.py @@ -17,7 +17,7 @@ class InGameMenuListener: def __init__(self): self.popup_menu = InGameMenuPopup() self.ra_popup_menu = RetroarchInGameMenuPopup() - + def send_signal(self, proc: subprocess.Popen, sig, timeout: float = 3.0): try: import psutil @@ -38,7 +38,7 @@ def send_signal(self, proc: subprocess.Popen, sig, timeout: float = 3.0): if not ps_proc.is_running() and all(not child.is_running() for child in children): return # All terminated gracefully time.sleep(0.1) - + # If still running, force kill for child in children: if child.is_running(): @@ -50,7 +50,7 @@ def send_signal(self, proc: subprocess.Popen, sig, timeout: float = 3.0): except Exception as e: PyUiLogger.get_logger().error(f"Error in send_signal: {e}") - + def game_launched(self, game_process: subprocess.Popen, game: RomInfo): support_menu_button_in_game = game.game_system.game_system_config.run_in_game_menu() uses_retroarch = game.game_system.game_system_config.uses_retroarch() @@ -59,7 +59,7 @@ def game_launched(self, game_process: subprocess.Popen, game: RomInfo): if (ControllerInput.MENU == Controller.last_input() and support_menu_button_in_game): held_down = True hold_start = time.time() - while(held_down and time.time() - hold_start < 0.3): + while(held_down and time.time() - hold_start < 0.3): held_down = Controller.still_held_down() and Controller.last_input() == ControllerInput.MENU time.sleep(0.05) @@ -76,7 +76,7 @@ def game_launched(self, game_process: subprocess.Popen, game: RomInfo): PyUiLogger.get_logger().info(f"Finished Taking snapshot before in-game menu") Device.get_device().capture_framebuffer() Display.reinitialize(snapshot) - + PyUiLogger.get_logger().debug(f"In game menu opened") if(uses_retroarch): continue_running = self.ra_popup_menu.run_in_game_menu() @@ -93,5 +93,5 @@ def game_launched(self, game_process: subprocess.Popen, game: RomInfo): time.sleep(0.1) self.send_signal(game_process, signal.SIGTERM) - + PyUiLogger.get_logger().debug(f"Game exit code was {game_process.poll()}") diff --git a/main-ui/menus/settings/extra_settings_menu.py b/main-ui/menus/settings/extra_settings_menu.py index 4273baf1..b18d90a2 100644 --- a/main-ui/menus/settings/extra_settings_menu.py +++ b/main-ui/menus/settings/extra_settings_menu.py @@ -58,7 +58,7 @@ def launch_controller_settings(self,input): def launch_animation_settings(self,input): if(ControllerInput.A == input): AnimationSettingsMenu().show_menu() - + def launch_gammeswitcher_settings(self,input): if(ControllerInput.A == input): GameSwitcherSettingsMenu().show_menu() @@ -77,7 +77,7 @@ def launch_game_art_display_settings(self,input): def resize_boxart(self, input): if (ControllerInput.A == input): BoxArtResizer.patch_boxart() - + def launch_settings_for_category(self,input, category): if(ControllerInput.A == input): @@ -93,7 +93,7 @@ def is_excluded_setting(self,category): def build_options_list(self): option_list = [] - + option_list.append( GridOrListEntry( primary_text=Language.display_settings(), @@ -128,7 +128,7 @@ def build_options_list(self): value=self.launch_time_settings ) ) - + option_list.append( GridOrListEntry( primary_text=Language.game_system_select_settings(), @@ -140,7 +140,7 @@ def build_options_list(self): value=self.launch_game_system_select_settings ) ) - + option_list.append( GridOrListEntry( primary_text=Language.game_select_settings(), @@ -152,7 +152,7 @@ def build_options_list(self): value=self.launch_game_select_settings ) ) - + if(PyUiConfig.allow_pyui_game_switcher()): option_list.append( GridOrListEntry( @@ -198,9 +198,9 @@ def build_options_list(self): icon=None, value=self.change_language_setting ) - ) + ) + - if(PyUiConfig.include_stock_os_launch_option()): option_list.append( GridOrListEntry( @@ -214,7 +214,7 @@ def build_options_list(self): ) ) - + for category in CfwSystemConfig.get_categories(): menu_options = CfwSystemConfig.get_menu_options(category=category) contains_entry_for_device = False @@ -234,12 +234,12 @@ def build_options_list(self): image_path_selected=None, description=None, icon=None, - value=lambda - input_value, + value=lambda + input_value, category=category: self.launch_settings_for_category(input_value, category) ) ) - + return option_list diff --git a/main-ui/menus/settings/screensaver_settings_menu.py b/main-ui/menus/settings/screensaver_settings_menu.py new file mode 100644 index 00000000..a0b6baed --- /dev/null +++ b/main-ui/menus/settings/screensaver_settings_menu.py @@ -0,0 +1,450 @@ +import os +import copy + +from controller.controller_inputs import ControllerInput +from menus.language.language import Language +from menus.settings import settings_menu +from themes.theme import Theme +from utils.py_ui_config import PyUiConfig +from views.grid_or_list_entry import GridOrListEntry +from views.view_creator import ViewCreator +from views.view_type import ViewType + + +COLOR_PALETTE = [ + "#FFFFFF", "#66F7FF", "#FF4FD8", "#FFE66D", "#80FF72", + "#FF9F1C", "#FF4040", "#AAAAAA", "#000000", +] + + +class ScreenSaverSettingsMenu(settings_menu.SettingsMenu): + def __init__(self): + super().__init__() + + def launch_screen_saver(self, input): + pass + + def toggle_show_clock(self, input): + if ControllerInput.A == input: + current = Theme._data.get("screensaver", {}).get("showClock", True) + self._set_screensaver_prop("showClock", not current) + + def toggle_show_date(self, input): + if ControllerInput.A == input: + current = Theme._data.get("screensaver", {}).get("showDate", True) + self._set_screensaver_prop("showDate", not current) + + def toggle_show_battery(self, input): + if ControllerInput.A == input: + current = Theme._data.get("screensaver", {}).get("showBattery", True) + self._set_screensaver_prop("showBattery", not current) + + def change_timeout(self, input): + current = PyUiConfig.get_screensaver_timeout_sec() + if ControllerInput.DPAD_RIGHT == input or ControllerInput.R1 == input: + new_val = min(300, current + 30) + elif ControllerInput.DPAD_LEFT == input or ControllerInput.L1 == input: + new_val = max(0, current - 30) + elif ControllerInput.A == input: + new_val = 0 if current > 0 else 120 + else: + return + PyUiConfig.set("screensaverTimeoutSec", new_val) + PyUiConfig.save() + + def change_overlay_opacity(self, input): + current = Theme._data.get("screensaver", {}).get("overlayOpacity", 0.3) + if ControllerInput.DPAD_RIGHT == input or ControllerInput.R1 == input: + new_val = min(1.0, round(current + 0.1, 1)) + elif ControllerInput.DPAD_LEFT == input or ControllerInput.L1 == input: + new_val = max(0.0, round(current - 0.1, 1)) + elif ControllerInput.A == input: + new_val = 0.0 if current > 0 else 0.5 + else: + return + self._set_screensaver_prop("overlayOpacity", new_val) + + def change_blur(self, input): + current = Theme._data.get("screensaver", {}).get("blur", 0) + if ControllerInput.DPAD_RIGHT == input or ControllerInput.R1 == input: + new_val = min(30, current + 2) + elif ControllerInput.DPAD_LEFT == input or ControllerInput.L1 == input: + new_val = max(0, current - 2) + elif ControllerInput.A == input: + new_val = 0 if current > 0 else 10 + else: + return + self._set_screensaver_prop("blur", new_val) + + def edit_layout(self, input): + if ControllerInput.A != input: + return + + original = copy.deepcopy(Theme._data.get("screensaver", {})) + ss = Theme._data.get("screensaver", {}) + widgets = ss.get("widgets") or Theme.get_screensaver_widgets() + ss["widgets"] = widgets + Theme._data["screensaver"] = ss + + selected = 0 + from controller.controller import Controller + from display.screensaver import ScreenSaver + from display.display import Display + + dirty = True + while True: + selected = max(0, min(selected, len(widgets) - 1)) + if dirty: + self._render_layout_preview(ScreenSaver, widgets, selected) + dirty = False + + if not Controller.get_input(timeout=0.2): + continue + + pressed = Controller.last_input() + widget = widgets[selected] + + if pressed == ControllerInput.A: + Theme.save_changes() + return + if pressed == ControllerInput.B: + Theme._data["screensaver"] = original + Display.clear_cache() + return + if pressed == ControllerInput.SELECT: + selected = (selected + 1) % len(widgets) + dirty = True + continue + if pressed == ControllerInput.X: + widget["enabled"] = not widget.get("enabled", True) + dirty = True + continue + if pressed == ControllerInput.DPAD_LEFT: + widget["x"] = max(0.0, round(widget.get("x", 0.5) - 0.02, 2)) + dirty = True + elif pressed == ControllerInput.DPAD_RIGHT: + widget["x"] = min(1.0, round(widget.get("x", 0.5) + 0.02, 2)) + dirty = True + elif pressed == ControllerInput.DPAD_UP: + widget["y"] = max(0.0, round(widget.get("y", 0.5) - 0.02, 2)) + dirty = True + elif pressed == ControllerInput.DPAD_DOWN: + widget["y"] = min(1.0, round(widget.get("y", 0.5) + 0.02, 2)) + dirty = True + elif pressed == ControllerInput.L1: + widget["fontSize"] = max(10, int(widget.get("fontSize", 24)) - 2) + dirty = True + elif pressed == ControllerInput.R1: + widget["fontSize"] = min(120, int(widget.get("fontSize", 24)) + 2) + dirty = True + elif pressed == ControllerInput.L2: + widget["color"] = self._next_palette_color(widget.get("color", "#FFFFFF"), -1) + dirty = True + elif pressed == ControllerInput.R2: + widget["color"] = self._next_palette_color(widget.get("color", "#FFFFFF"), 1) + dirty = True + elif pressed == ControllerInput.Y: + if widget.get("type") == "battery": + widget["style"] = self._next_battery_style(widget.get("style", "percent")) + else: + widget["font"] = self._next_font(widget.get("font")) + dirty = True + elif pressed == ControllerInput.START: + new_color = self._show_rgb_picker(widget.get("color", "#FFFFFF")) + if new_color is not None: + widget["color"] = new_color + dirty = True + + def _render_layout_preview(self, ScreenSaver, widgets, selected): + original_color = widgets[selected].get("color", "#FFFFFF") + original_value = widgets[selected].get("value") + widgets[selected]["color"] = "#FFE66D" + if widgets[selected].get("type") == "text": + widgets[selected]["value"] = original_value or "TEXT" + ScreenSaver.render() + widgets[selected]["color"] = original_color + if original_value is not None: + widgets[selected]["value"] = original_value + elif "value" in widgets[selected]: + del widgets[selected]["value"] + + def _next_palette_color(self, color, direction): + color = color.upper() + try: + index = COLOR_PALETTE.index(color) + except ValueError: + index = 0 + return COLOR_PALETTE[(index + direction) % len(COLOR_PALETTE)] + + def _next_battery_style(self, current): + styles = ["percent", "blocks", "bar", "pill", "segments"] + try: + index = styles.index(current) + except ValueError: + index = 0 + return styles[(index + 1) % len(styles)] + + def _next_font(self, current): + fonts = self._find_fonts() + if not fonts: + return current + try: + index = fonts.index(current) + except ValueError: + index = -1 + return fonts[(index + 1) % len(fonts)] + + def _find_fonts(self): + font_dirs = ["/mnt/SDCARD/App/PyUI/fonts", Theme.get_theme_path()] + fonts = [] + for font_dir in font_dirs: + if not os.path.isdir(font_dir): + continue + for name in os.listdir(font_dir): + if name.lower().endswith((".ttf", ".otf")) and name not in fonts: + fonts.append(name) + fonts.sort() + return fonts + + def change_bg_color(self, input): + if ControllerInput.A != input: + return + + current = Theme._data.get("screensaver", {}).get("bgColor", "#000000") + new_color = self._show_rgb_picker(current) + if new_color is not None: + self._set_screensaver_prop("bgColor", new_color) + + def _show_rgb_picker(self, current): + color = self._normalize_hex_color(current) or "#000000" + values = [int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)] + channel = 0 + labels = ["R", "G", "B"] + from controller.controller import Controller + from display.display import Display + from display.font_purpose import FontPurpose + from display.render_mode import RenderMode + from devices.device import Device + + while True: + hex_color = "#" + "".join(f"{v:02X}" for v in values) + Display.clear(Language.get("screensaverBgColor", "Background color")) + w = Device.get_device().screen_width() + h = Device.get_device().screen_height() + swatch_w = int(w * 0.55) + swatch_h = int(h * 0.28) + x = (w - swatch_w) // 2 + y = int(h * 0.18) + import sdl2 + rgb = tuple(values) + sdl2.SDL_SetRenderDrawColor(Display.renderer.sdlrenderer, rgb[0], rgb[1], rgb[2], 255) + sdl2.SDL_RenderFillRect(Display.renderer.sdlrenderer, sdl2.SDL_Rect(x, y, swatch_w, swatch_h)) + + start_y = y + swatch_h + 35 + for index, label in enumerate(labels): + prefix = "> " if index == channel else " " + bar_len = 18 + filled = int(values[index] / 255 * bar_len) + bar = "[" + ("#" * filled).ljust(bar_len, "-") + "]" + text = f"{prefix}{label} {values[index]:03d} {bar}" + color_tuple = (255, 230, 109) if index == channel else (255, 255, 255) + Display.render_text(text, int(w * 0.22), start_y + index * 42, color_tuple, FontPurpose.LIST, RenderMode.TOP_LEFT_ALIGNED) + + Display.render_text(hex_color, int(w * 0.5), start_y + 145, (102, 247, 255), FontPurpose.LIST, RenderMode.TOP_CENTER_ALIGNED) + Display.render_text("Up/Down channel Left/Right +/-1 L1/R1 +/-10 X reset A save B cancel", + int(w * 0.5), h - 55, (170, 170, 170), FontPurpose.LIST, RenderMode.TOP_CENTER_ALIGNED) + Display.present() + + if not Controller.get_input(timeout=0.1): + continue + + pressed = Controller.last_input() + if pressed == ControllerInput.A: + return hex_color + if pressed == ControllerInput.B: + return None + if pressed == ControllerInput.X: + values = [0, 0, 0] + elif pressed == ControllerInput.DPAD_UP: + channel = (channel - 1) % 3 + elif pressed == ControllerInput.DPAD_DOWN: + channel = (channel + 1) % 3 + elif pressed == ControllerInput.DPAD_LEFT: + values[channel] = max(0, values[channel] - 1) + elif pressed == ControllerInput.DPAD_RIGHT: + values[channel] = min(255, values[channel] + 1) + elif pressed == ControllerInput.L1: + values[channel] = max(0, values[channel] - 10) + elif pressed == ControllerInput.R1: + values[channel] = min(255, values[channel] + 10) + + def cycle_bg_image(self, input): + if ControllerInput.A != input: + return + + ss = Theme._data.get("screensaver", {}) + current = ss.get("bgImage", "") + images = [""] + self._find_bg_images() + selected = self._show_image_selection(images, current) + if selected is not None: + self._set_screensaver_prop("bgImage", selected) + + def _show_image_selection(self, images, current): + selected_index = 0 + option_list = [] + for index, path in enumerate(images): + if path == current: + selected_index = index + image_path = path or "/mnt/SDCARD/App/PyUI/main-ui/themes/screensaver.png" + option_list.append( + GridOrListEntry( + primary_text=self._format_image_label(path), + value=path, + image_path=image_path if os.path.exists(image_path) else None, + image_path_selected=image_path if os.path.exists(image_path) else None, + description=path or Language.get("screensaverBgDefault", "Default"), + ) + ) + + view = ViewCreator.create_view( + view_type=ViewType.TEXT_AND_IMAGE, + top_bar_text=Language.get("screensaverBgImage", "Background image"), + options=option_list, + selected_index=selected_index) + + while True: + selected = view.get_selection([ControllerInput.A, ControllerInput.B]) + if ControllerInput.A == selected.get_input(): + return selected.get_selection().get_value() + if ControllerInput.B == selected.get_input(): + return None + + def _normalize_hex_color(self, color): + color = color.strip() + if not color.startswith("#"): + color = "#" + color + if len(color) != 7: + return None + hex_part = color[1:] + if any(c not in "0123456789abcdefABCDEF" for c in hex_part): + return None + return "#" + hex_part.upper() + + def _find_bg_images(self): + user_dir = "/mnt/SDCARD/App/PyUI/screensavers" + os.makedirs(user_dir, exist_ok=True) + paths = [user_dir, Theme.get_theme_path(), "/mnt/SDCARD/App/PyUI/main-ui/themes"] + images = [] + exts = (".png", ".jpg", ".jpeg", ".bmp", ".gif") + for base in paths: + if not os.path.exists(base): + continue + for f in os.listdir(base): + path = os.path.join(base, f) + lower = f.lower() + is_user_screensaver = base == user_dir + is_screensaver = is_user_screensaver or (lower.startswith("screensaver") and not lower.startswith("screensaver_color_")) + if os.path.isfile(path) and lower.endswith(exts) and is_screensaver and path not in images: + images.append(path) + images.sort() + return images + + def _set_screensaver_prop(self, key, value): + ss = Theme._data.get("screensaver", {}) + ss[key] = value + Theme._data["screensaver"] = ss + Theme.save_changes() + + def _get_screensaver_prop(self, key, default=True): + return Theme._data.get("screensaver", {}).get(key, default) + + def _format_image_label(self, path): + if not path: + return Language.get("screensaverBgDefault", "Default") + return os.path.basename(path) + + def _format_color_label(self, color): + labels = { + "#000000": "Black", + "#1A1A1A": "Dark gray", + "#102030": "Deep blue", + "#201020": "Deep purple", + "#102010": "Deep green", + "#301010": "Deep red", + "#302010": "Deep orange", + "#303010": "Deep yellow", + "#103030": "Deep teal", + "#303030": "Gray", + "#FFFFFF": "White", + } + return labels.get(color, color) + + def build_options_list(self): + option_list = [] + + timeout = PyUiConfig.get_screensaver_timeout_sec() + timeout_text = f"{timeout // 60}m {timeout % 60}s" if timeout > 0 else Language.get("off", "Off") + option_list.append( + GridOrListEntry( + primary_text=Language.get("screensaverTimeout", "Timeout"), + value_text="< " + timeout_text + " >", + image_path=None, + image_path_selected=None, + description=Language.get("screensaverTimeoutDesc", "Minutes before screensaver activates (0=off)"), + icon=None, + value=self.change_timeout + ) + ) + + current_image = self._get_screensaver_prop("bgImage", "") + option_list.append( + GridOrListEntry( + primary_text=Language.get("screensaverBgImage", "Background image"), + value_text="< " + self._format_image_label(current_image) + " >", + image_path=None, + image_path_selected=None, + description=Language.get("screensaverBgImageDesc", "Choose a screensaver background image"), + icon=None, + value=self.cycle_bg_image + ) + ) + + current_color = self._get_screensaver_prop("bgColor", "#000000") + option_list.append( + GridOrListEntry( + primary_text=Language.get("screensaverBgColor", "Background color"), + value_text="< " + self._format_color_label(current_color) + " >", + image_path=None, + image_path_selected=None, + description=Language.get("screensaverBgColorDesc", "Solid background color when no image is selected"), + icon=None, + value=self.change_bg_color + ) + ) + + overlay_opacity = self._get_screensaver_prop("overlayOpacity", 0.3) + option_list.append( + GridOrListEntry( + primary_text=Language.get("screensaverOverlayOpacity", "Overlay opacity"), + value_text="< " + f"{int(overlay_opacity * 100)}%" + " >", + image_path=None, + image_path_selected=None, + description=Language.get("screensaverOverlayOpacityDesc", "Dark overlay over background (0=off, 100=full black)"), + icon=None, + value=self.change_overlay_opacity + ) + ) + + option_list.append( + GridOrListEntry( + primary_text=Language.get("screensaverEditLayout", "Edit layout"), + value_text=None, + image_path=None, + image_path_selected=None, + description=Language.get("screensaverEditLayoutDesc", "D-pad move, L1/R1 size, L2/R2 color, START RGB color, Y font/battery style, X show/hide, A save, B cancel"), + icon=None, + value=self.edit_layout + ) + ) + + return option_list diff --git a/main-ui/themes/screensaver.png b/main-ui/themes/screensaver.png new file mode 100644 index 00000000..3e29618c Binary files /dev/null and b/main-ui/themes/screensaver.png differ diff --git a/main-ui/themes/theme.py b/main-ui/themes/theme.py index baae864e..a24da5a8 100644 --- a/main-ui/themes/theme.py +++ b/main-ui/themes/theme.py @@ -182,8 +182,8 @@ def save_changes(cls): PyUiLogger.get_logger().info(f"Wrote Theme : {cls._data.get('description', 'UNKNOWN')}") from display.display import Display Display.clear_cache() - - + + @classmethod def _resolve_png_path(cls, base_folder, parts): path = os.path.join(cls._path, base_folder, *parts) @@ -193,13 +193,13 @@ def _resolve_png_path(cls, base_folder, parts): return png_path return path - + @classmethod def _resolve_file(cls, base_folder, parts, cache_missing=True): """ Shared resolver: - Checks full path - - If missing and ends in .qoi, tries .png + - If missing and ends in .qoi, tries .png - Caches results """ key = (base_folder, parts) @@ -236,7 +236,7 @@ def _bg(cls, *parts, cache_missing=True): @classmethod def _icon(cls, *parts): return cls._resolve_file(cls._icon_folder, parts) - + @classmethod def background(cls, page = None): if(page is None): @@ -247,125 +247,125 @@ def background(cls, page = None): @classmethod def favorite(cls): return cls._asset("ic-favorite-n.qoi") - + @classmethod def favorite_selected(cls): return cls._asset("ic-favorite-f.qoi") - + @classmethod def recent(cls): return cls._asset("ic-recent-n.qoi") - + @classmethod def recent_selected(cls): return cls._asset("ic-recent-f.qoi") @classmethod def collection(cls): return cls._asset("ic-collection-n.qoi") - + @classmethod def collection_selected(cls): return cls._asset("ic-collection-f.qoi") - + @classmethod def game(cls): return cls._asset("ic-game-n.qoi") - + @classmethod def game_selected(cls): return cls._asset("ic-game-f.qoi") - + @classmethod def app(cls): return cls._asset("ic-app-n.qoi") - + @classmethod def app_selected(cls): return cls._asset("ic-app-f.qoi") - + @classmethod def settings(cls): return cls._asset("ic-setting-n.qoi") - + @classmethod def settings_selected(cls): return cls._asset("ic-setting-f.qoi") - + @classmethod def get_title_bar_bg(cls): return cls._asset("bg-title.qoi") - + @classmethod def bottom_bar_bg(cls): return cls._asset("tips-bar-bg.qoi") - + @classmethod def confirm_icon(cls): return cls._asset("icon-A-54.qoi") - + @classmethod def back_icon(cls): return cls._asset("icon-B-54.qoi") - + @classmethod def start_icon(cls): return cls._asset("icon-START.qoi") - + @classmethod def show_bottom_bar(cls): return cls._data.get("showBottomBar", True) is not False - + @classmethod def ignore_top_and_bottom_bar_for_layout(cls): return cls._data.get("ignoreTopAndBottomBarForLayout", False) - + @classmethod def show_top_bar_text(cls): return cls._data.get("showTopBarText", True) - + @classmethod - def render_top_and_bottom_bar_last(cls): + def render_top_and_bottom_bar_last(cls): return cls._data.get("renderTopAndBottomBarLast", False) - + @classmethod - def set_render_top_and_bottom_bar_last(cls, value): + def set_render_top_and_bottom_bar_last(cls, value): cls._data["renderTopAndBottomBarLast"] = value cls.save_changes() - + @classmethod def confirm_text(cls): return cls._data.get("confirmText", "Okay") - + @classmethod def back_text(cls): return cls._data.get("backText", "Back") - + @classmethod def favorite_icon(cls): return cls._asset("ic-favorite-mark.qoi") - + @classmethod def get_list_large_selected_bg(cls): return cls._asset("bg-list-l.qoi") - + @classmethod - def menu_popup_bg_large(cls): + def menu_popup_bg_large(cls): menu_selected_bg = cls._asset("bg-pop-menu-4.qoi", cache_missing=False) if(menu_selected_bg is None): cls.create_bg_pop_menu_4() return cls._asset("bg-pop-menu-4.qoi") @classmethod - def create_bg_pop_menu_4(cls): + def create_bg_pop_menu_4(cls): #Background isn't the best but its the only one that often doesnt have transparency on the bottom input_image = cls._resolve_png_path(cls._skin_folder,["background.png"]) output_image = cls._resolve_png_path(cls._skin_folder,["bg-pop-menu-4.png"]) - PyUiLogger.get_logger().info(f"Creating resized {output_image} from {input_image}") + PyUiLogger.get_logger().info(f"Creating resized {output_image} from {input_image}") Device.get_device().get_image_utils().resize_image(input_image, output_image, 320, 240, preserve_aspect_ratio=False) - + @classmethod - def keyboard_bg(cls): + def keyboard_bg(cls): return cls._asset("bg-grid-s.qoi") - + @classmethod def keyboard_entry_bg(cls): return cls._asset("bg-list-l.qoi") - + @classmethod def key_bg(cls): return cls._asset("bg-btn-01-n.qoi") - + @classmethod def key_selected_bg(cls): return cls._asset("bg-btn-01-f.qoi") - + @classmethod def get_list_small_selected_bg(cls): return cls._asset("bg-list-s.qoi") - + @classmethod - def create_bg_list_s2(cls): + def create_bg_list_s2(cls): input_image = cls._resolve_png_path(cls._skin_folder,["bg-list-s.png"]) output_image = cls._resolve_png_path(cls._skin_folder,["bg-list-s2.png"]) - PyUiLogger.get_logger().info(f"Creating resized {output_image} from {input_image}") + PyUiLogger.get_logger().info(f"Creating resized {output_image} from {input_image}") Device.get_device().get_image_utils().resize_image(input_image, output_image, 320, @@ -373,15 +373,15 @@ def create_bg_list_s2(cls): preserve_aspect_ratio=False) @classmethod - def get_popup_menu_selected_bg(cls): + def get_popup_menu_selected_bg(cls): menu_selected_bg = cls._asset("bg-list-s2.qoi", cache_missing=False) if(menu_selected_bg is None): cls.create_bg_list_s2() return cls._asset("bg-list-s2.qoi") - + @classmethod def get_missing_image_path(cls): return cls._asset("missing_image.qoi") - + @classmethod def get_battery_icon(cls, charging, battery_percent): if ChargeStatus.CHARGING == charging: @@ -406,7 +406,7 @@ def get_battery_icon(cls, charging, battery_percent): return cls._asset("power-20%-icon.qoi") else: return cls._asset("power-0%-icon.qoi") - + @classmethod def get_wifi_icon(cls, status): if status == WifiStatus.OFF: @@ -430,7 +430,7 @@ def get_volume_indicator(cls, volume): @classmethod def _grid_multi_row_selected_bg(cls): return cls._asset("bg-game-item-f.qoi") - + @classmethod def _grid_multi_row_unselected_bg(cls): return cls._asset("bg-game-item-n.qoi") @@ -459,7 +459,7 @@ def get_default_system_icon(cls): if(cls._daijisho_theme_index is not None): return cls._daijisho_theme_index.get_default_filename() else: - return None + return None @classmethod def get_system_icon_selected(cls, system): @@ -477,36 +477,36 @@ def get_font(cls, font_purpose : FontPurpose): try: match font_purpose: case FontPurpose.TOP_BAR_TEXT: - font = os.path.join(cls._path,cls._data["list"]["font"]) + font = os.path.join(cls._path,cls._data["list"]["font"]) case FontPurpose.BATTERY_PERCENT: - font = os.path.join(cls._path,cls._data["list"]["font"]) + font = os.path.join(cls._path,cls._data["list"]["font"]) case FontPurpose.ON_SCREEN_KEYBOARD: - font = os.path.join(cls._path,cls._data["list"]["font"]) + font = os.path.join(cls._path,cls._data["list"]["font"]) case FontPurpose.GRID_ONE_ROW: - font = os.path.join(cls._path,cls._data["grid"]["font"]) + font = os.path.join(cls._path,cls._data["grid"]["font"]) case FontPurpose.GRID_MULTI_ROW: - font = os.path.join(cls._path,cls._data["grid"]["font"]) + font = os.path.join(cls._path,cls._data["grid"]["font"]) case FontPurpose.LIST: - font = os.path.join(cls._path,cls._data["list"]["font"]) + font = os.path.join(cls._path,cls._data["list"]["font"]) case FontPurpose.DESCRIPTIVE_LIST_TITLE: - font = os.path.join(cls._path,cls._data["list"]["font"]) + font = os.path.join(cls._path,cls._data["list"]["font"]) case FontPurpose.MESSAGE: - font = os.path.join(cls._path,cls._data["list"]["font"]) + font = os.path.join(cls._path,cls._data["list"]["font"]) case FontPurpose.DESCRIPTIVE_LIST_DESCRIPTION: - font = os.path.join(cls._path,cls._data["list"]["font"]) + font = os.path.join(cls._path,cls._data["list"]["font"]) case FontPurpose.SHADOWED: - font = os.path.join(cls._path,cls._data["shadowed"]["font"]) + font = os.path.join(cls._path,cls._data["shadowed"]["font"]) case FontPurpose.SHADOWED_BACKDROP: - font = os.path.join(cls._path,cls._data["shadowed"]["font"]) + font = os.path.join(cls._path,cls._data["shadowed"]["font"]) case FontPurpose.SHADOWED_SMALL: - font = os.path.join(cls._path,cls._data["shadowed"]["font"]) + font = os.path.join(cls._path,cls._data["shadowed"]["font"]) case FontPurpose.SHADOWED_BACKDROP_SMALL: - font = os.path.join(cls._path,cls._data["shadowed"]["font"]) + font = os.path.join(cls._path,cls._data["shadowed"]["font"]) case _: - font = os.path.join(cls._path,cls._data["list"]["font"]) - + font = os.path.join(cls._path,cls._data["list"]["font"]) + if os.path.exists(font): - return font + return font else: return Theme.get_fallback_font() except Exception as e: @@ -549,7 +549,7 @@ def get_font_size(cls, font_purpose : FontPurpose): return cls._data.get("indexTotalSize",cls._data["list"].get("size", int(20*cls._default_multiplier))) case FontPurpose.SHADOWED: try: - return cls._data["shadowed"]["shadowedFontSize"] + return cls._data["shadowed"]["shadowedFontSize"] except Exception as e: #PyUiLogger.get_logger().warning(f"Theme is missing [\"shadowed\"][\"shadowedFontSize\"] ") return int(40 * cls._default_multiplier) @@ -561,7 +561,7 @@ def get_font_size(cls, font_purpose : FontPurpose): return int(40 * cls._default_multiplier) case FontPurpose.SHADOWED_SMALL: try: - return cls._data["shadowed"]["shadowedFontSmallSize"] + return cls._data["shadowed"]["shadowedFontSmallSize"] except Exception as e: #PyUiLogger.get_logger().warning(f"Theme is missing [\"shadowed\"][\"shadowedFontSmallSize\"] ") return int(26 * cls._default_multiplier) @@ -620,7 +620,7 @@ def set_font_size(cls, font_purpose: FontPurpose, size): case _: PyUiLogger.get_logger().error( f"set_font_size: Unknown font purpose {font_purpose}") - + cls.save_changes() except Exception as e: PyUiLogger.get_logger().error(f"set_font_size error occurred: {e}") @@ -686,7 +686,7 @@ def text_color(cls, font_purpose : FontPurpose): except Exception as e: PyUiLogger.get_logger().error(f"text_color error occurred: {e}") return cls.hex_to_color("#808080") - + @classmethod def text_color_selected(cls, font_purpose : FontPurpose): try: @@ -698,7 +698,7 @@ def text_color_selected(cls, font_purpose : FontPurpose): case FontPurpose.LIST | FontPurpose.DESCRIPTIVE_LIST_TITLE | FontPurpose.DESCRIPTIVE_LIST_DESCRIPTION: if(cls._data.get("list") and cls._data.get("list").get("selectedcolor")): color = cls.hex_to_color(cls._data.get("list").get("selectedcolor")) - #PyUiLogger.get_logger().error(f"list selected color is {color}") + #PyUiLogger.get_logger().error(f"list selected color is {color}") return color else: #PyUiLogger.get_logger().error(f"list selectedcolor not found, using grid") @@ -758,7 +758,7 @@ def get_grid_multirow_text_offset_y_percent(cls): @classmethod def get_system_select_show_sel_bg_grid_mode(cls): return cls._data.get("systemSelectShowSelectedBgGridMode", True) - + @classmethod def set_system_select_show_sel_bg_grid_mode(cls, value): cls._data["systemSelectShowSelectedBgGridMode"] = value @@ -767,7 +767,7 @@ def set_system_select_show_sel_bg_grid_mode(cls, value): @classmethod def get_system_selection_set_top_bar_text(cls): return cls._data.get("systemSelectionSetTopBarText", False) - + @classmethod def set_system_selection_set_top_bar_text(cls, value): cls._data["systemSelectionSetTopBarText"] = value @@ -776,7 +776,7 @@ def set_system_selection_set_top_bar_text(cls, value): @classmethod def get_system_selection_set_bottom_bar_text(cls): return cls._data.get("systemSelectionSetBottomBarText", False) - + @classmethod def set_system_selection_set_bottom_bar_text(cls, value): cls._data["systemSelectionSetBottomBarText"] = value @@ -785,7 +785,7 @@ def set_system_selection_set_bottom_bar_text(cls, value): @classmethod def get_system_select_show_text_grid_mode(cls): return cls._data.get("systemSelectShowTextGridMode", True) - + @classmethod def set_system_select_show_text_grid_mode(cls, value): cls._data["systemSelectShowTextGridMode"] = value @@ -794,7 +794,7 @@ def set_system_select_show_text_grid_mode(cls, value): @classmethod def get_system_select_render_full_screen_grid_text_overlay(cls): return cls._data.get("systemSelectFullScreenGridTextOverlay", True) - + @classmethod def set_system_select_render_full_screen_grid_text_overlay(cls, value): cls._data["systemSelectFullScreenGridTextOverlay"] = value @@ -812,7 +812,7 @@ def set_game_select_show_text_grid_mode(cls, value): @classmethod def get_game_select_show_sel_bg_grid_mode(cls): return cls._data.get("gameSelectShowSelectedBgGridMode", True) - + @classmethod def set_game_select_show_sel_bg_grid_mode(cls, value): cls._data["gameSelectShowSelectedBgGridMode"] = value @@ -821,7 +821,7 @@ def set_game_select_show_sel_bg_grid_mode(cls, value): @classmethod def get_main_menu_show_text_grid_mode(cls): return cls._data.get("mainMenuShowTextGridMode", True) - + @classmethod def set_main_menu_show_text_grid_mode(cls, value): cls._data["mainMenuShowTextGridMode"] = value @@ -840,7 +840,7 @@ def get_grid_bg(cls, rows, cols, use_multi_row_select_as_backup = False): return cls._grid_multi_row_selected_bg() else: return None - + @classmethod def get_grid_bg_unselected(cls, rows, cols, use_multi_row_select_as_backup = False): # TODO better handle this dynamically @@ -934,7 +934,7 @@ def get_carousel_system_additional_y_offset(cls): def set_carousel_system_additional_y_offset(cls, value): cls._data["carouselSystemAdditionalYOffset"] = value cls.save_changes() - + @classmethod def get_carousel_system_resize_type(cls): view_type_str = cls._data.get("carouselSystemResizeType", "FIT") @@ -953,7 +953,7 @@ def get_carousel_system_selected_offset(cls): def set_carousel_system_selected_offset(cls, value): cls._data["carouselSystemSelectedOffset"] = value cls.save_changes() - + @classmethod def get_carousel_use_selected_image_in_animation(cls): return cls._data.get("carouselSystemUseSelectedImageInAnimation", True) @@ -962,7 +962,7 @@ def get_carousel_use_selected_image_in_animation(cls): def set_carousel_use_selected_image_in_animation(cls, value): cls._data["carouselSystemUseSelectedImageInAnimation"] = value cls.save_changes() - + @classmethod def get_carousel_system_external_x_offset(cls): return cls._data.get("carouselSystemExternalXPad", 0) @@ -1019,8 +1019,8 @@ def get_game_system_select_col_count(cls): @classmethod def get_game_system_select_row_count(cls): - return cls._data.get("gameSystemSelectRowCount", int(2 * cls.height_multiplier)) - + return cls._data.get("gameSystemSelectRowCount", int(2 * cls.height_multiplier)) + @classmethod def set_game_system_select_col_count(cls, count): cls._data["gameSystemSelectColCount"] = count @@ -1030,7 +1030,7 @@ def set_game_system_select_col_count(cls, count): @classmethod def get_game_system_select_carousel_col_count(cls): return cls._data.get("gameSystemSelectCarouselColCount", cls.get_game_system_select_col_count()) - + @classmethod def set_game_system_select_carousel_col_count(cls, count): cls._data["gameSystemSelectCarouselColCount"] = count @@ -1040,7 +1040,7 @@ def set_game_system_select_carousel_col_count(cls, count): def set_game_system_select_row_count(cls, count): cls._data["gameSystemSelectRowCount"] = count cls.save_changes() - + @classmethod def pop_menu_x_offset(cls): return cls._data.get("popupMenuXOffsetPercent", 0) / 100 @@ -1131,7 +1131,7 @@ def get_favorites_enabled(cls): def set_favorites_enabled(cls, value): cls._data["favoritesEnabled"] = value cls.save_changes() - + @classmethod def get_apps_enabled(cls): return cls._data.get("appsEnabled", True) @@ -1156,7 +1156,7 @@ def get_main_menu_option_ordering(cls): @classmethod def get_game_select_row_count(cls): - return cls._data.get("gameSelectRowCount", int(2 * cls.height_multiplier)) + return cls._data.get("gameSelectRowCount", int(2 * cls.height_multiplier)) @classmethod def set_game_select_row_count(cls, value): @@ -1167,7 +1167,7 @@ def set_game_select_row_count(cls, value): def get_game_select_col_count(cls, default_value=None): if default_value is not None: return cls._data.get("gameSelectColCount", default_value) - return cls._data.get("gameSelectColCount", int(4*cls.width_multiplier)) + return cls._data.get("gameSelectColCount", int(4*cls.width_multiplier)) @classmethod def set_game_select_col_count(cls, value): @@ -1176,7 +1176,7 @@ def set_game_select_col_count(cls, value): @classmethod def get_game_select_carousel_col_count(cls): - return cls._data.get("gameSelectCarouselColCount", cls.get_game_select_col_count(int(3*cls.width_multiplier))) + return cls._data.get("gameSelectCarouselColCount", cls.get_game_select_col_count(int(3*cls.width_multiplier))) @classmethod def set_game_select_carousel_col_count(cls, value): @@ -1186,7 +1186,7 @@ def set_game_select_carousel_col_count(cls, value): @classmethod def get_game_select_img_width(cls): return cls._data.get("gameSelectImgWidth", int(320 * cls._default_multiplier)) - + @classmethod def set_game_select_img_width(cls, value): cls._data["gameSelectImgWidth"] = value @@ -1195,7 +1195,7 @@ def set_game_select_img_width(cls, value): @classmethod def get_grid_game_select_img_width(cls): return cls._data.get("gridGameSelectImgWidth", int(cls._grid_game_default_size * cls._default_multiplier)) - + @classmethod def set_grid_game_select_img_width(cls, value): cls._data["gridGameSelectImgWidth"] = value @@ -1204,7 +1204,7 @@ def set_grid_game_select_img_width(cls, value): @classmethod def get_list_game_select_img_width(cls): return cls._data.get("listGameSelectImgWidth", cls.get_game_select_img_width()) - + @classmethod def set_list_game_select_img_width(cls, value): cls._data["listGameSelectImgWidth"] = value @@ -1213,7 +1213,7 @@ def set_list_game_select_img_width(cls, value): @classmethod def get_grid_system_select_img_width(cls): return cls._data.get("gridSystemSelectImgWidth", int(cls._grid_game_default_size * cls._default_multiplier)) - + @classmethod def set_grid_system_select_img_width(cls, value): cls._data["gridSystemSelectImgWidth"] = value @@ -1222,7 +1222,7 @@ def set_grid_system_select_img_width(cls, value): @classmethod def get_list_system_select_img_width(cls): return cls._data.get("listSystemSelectImgWidth", cls.get_game_select_img_width()) - + @classmethod def set_list_system_select_img_width(cls, value): cls._data["listSystemSelectImgWidth"] = value @@ -1231,7 +1231,7 @@ def set_list_system_select_img_width(cls, value): @classmethod def get_grid_main_menu_img_width(cls): return cls._data.get("gridMainMenuImgWidth", 0) - + @classmethod def set_grid_main_menu_img_width(cls, value): cls._data["gridMainMenuImgWidth"] = value @@ -1240,7 +1240,7 @@ def set_grid_main_menu_img_width(cls, value): @classmethod def get_grid_main_menu_img_height(cls): return cls._data.get("gridMainMenuImgHeight", 0) - + @classmethod def set_grid_main_menu_img_height(cls, value): cls._data["gridMainMenuImgHeight"] = value @@ -1249,7 +1249,7 @@ def set_grid_main_menu_img_height(cls, value): @classmethod def get_carousel_game_select_primary_img_width(cls): return cls._data.get("carouselGameSelectPrimaryImgWidth", 50) - + @classmethod def set_carousel_game_select_primary_img_width(cls, value): cls._data["carouselGameSelectPrimaryImgWidth"] = value @@ -1258,7 +1258,7 @@ def set_carousel_game_select_primary_img_width(cls, value): @classmethod def get_carousel_system_select_primary_img_width(cls): return cls._data.get("carouselSystemSelectPrimaryImgWidth", 40) - + @classmethod def set_carousel_system_select_primary_img_width(cls, value): cls._data["carouselSystemSelectPrimaryImgWidth"] = value @@ -1267,7 +1267,7 @@ def set_carousel_system_select_primary_img_width(cls, value): @classmethod def get_carousel_game_select_shrink_further_away(cls): return cls._data.get("carouselGameSelectShrinkFurtherAway", False) - + @classmethod def set_carousel_game_select_shrink_further_away(cls, value): cls._data["carouselGameSelectShrinkFurtherAway"] = value @@ -1276,7 +1276,7 @@ def set_carousel_game_select_shrink_further_away(cls, value): @classmethod def get_carousel_system_select_shrink_further_away(cls): return cls._data.get("carouselSystemSelectShrinkFurtherAway", False) - + @classmethod def set_carousel_system_select_shrink_further_away(cls, value): cls._data["carouselSystemSelectShrinkFurtherAway"] = value @@ -1289,7 +1289,7 @@ def get_carousel_game_select_sides_hang_off(cls): @classmethod def set_carousel_game_select_sides_hang_off(cls, value): cls._data["carouselGameSelectSidesHangOff"] = value - cls.save_changes() + cls.save_changes() @classmethod def get_carousel_system_select_sides_hang_off(cls): @@ -1298,12 +1298,12 @@ def get_carousel_system_select_sides_hang_off(cls): @classmethod def set_carousel_system_select_sides_hang_off(cls, value): cls._data["carouselSystemSelectSidesHangOff"] = value - cls.save_changes() + cls.save_changes() @classmethod def get_game_select_img_height(cls): return cls._data.get("gameSelectImgHeight", int(300 * cls._default_multiplier)) - + @classmethod def set_game_select_img_height(cls, value): cls._data["gameSelectImgHeight"] = value @@ -1312,7 +1312,7 @@ def set_game_select_img_height(cls, value): @classmethod def get_grid_game_select_img_height(cls): return cls._data.get("gridGameSelectImgHeight", int(cls._grid_game_default_size * cls._default_multiplier)) - + @classmethod def set_grid_game_select_img_height(cls, value): cls._data["grid_gameSelectImgHeight"] = value @@ -1321,7 +1321,7 @@ def set_grid_game_select_img_height(cls, value): @classmethod def get_list_game_select_img_height(cls): return cls._data.get("listGameSelectImgHeight", cls.get_game_select_img_height()) - + @classmethod def set_list_game_select_img_height(cls, value): cls._data["listGameSelectImgHeight"] = value @@ -1330,7 +1330,7 @@ def set_list_game_select_img_height(cls, value): @classmethod def get_grid_system_select_img_height(cls): return cls._data.get("gridSystemSelectImgHeight", int(cls._grid_game_default_size * cls._default_multiplier)) - + @classmethod def set_grid_system_select_img_height(cls, value): cls._data["gridSystemSelectImgHeight"] = value @@ -1339,7 +1339,7 @@ def set_grid_system_select_img_height(cls, value): @classmethod def get_list_system_select_img_height(cls): return cls._data.get("listSystemSelectImgHeight", cls.get_game_select_img_height()) - + @classmethod def set_list_system_select_img_height(cls, value): cls._data["listSystemSelectImgHeight"] = value @@ -1348,7 +1348,7 @@ def set_list_system_select_img_height(cls, value): @classmethod def get_set_top_bar_text_to_game_selection(cls): return cls._data.get("setTopBarTextToGameSelection", False) - + @classmethod def set_set_top_bar_text_to_game_selection(cls, value): cls._data["setTopBarTextToGameSelection"] = value @@ -1357,7 +1357,7 @@ def set_set_top_bar_text_to_game_selection(cls, value): @classmethod def get_game_switcher_use_vertical_carousel(cls): return cls._data.get("gameSwitcherUseVerticalCarousel", False) - + @classmethod def set_game_switcher_use_vertical_carousel(cls, value): cls._data["gameSwitcherUseVerticalCarousel"] = value @@ -1366,7 +1366,7 @@ def set_game_switcher_use_vertical_carousel(cls, value): @classmethod def get_game_select_use_vertical_carousel(cls): return cls._data.get("gameSelectUseVerticalCarousel", False) - + @classmethod def set_game_select_use_vertical_carousel(cls, value): cls._data["gameSelectUseVerticalCarousel"] = value @@ -1375,12 +1375,12 @@ def set_game_select_use_vertical_carousel(cls, value): @classmethod def get_system_select_grid_wrap_around_single_row(cls): return cls._data.get("systemSelectGridWrapAroundSingleRow", True) - + @classmethod def set_system_select_grid_wrap_around_single_row(cls, value): cls._data["systemSelectGridWrapAroundSingleRow"] = value cls.save_changes() - + @classmethod def set_set_top_bar_text_to_game_selection(cls, value): cls._data["setTopBarTextToGameSelection"] = value @@ -1389,7 +1389,7 @@ def set_set_top_bar_text_to_game_selection(cls, value): @classmethod def get_main_menu_grid_wrap_around_single_row(cls): return cls._data.get("mainMenuGridWrapAroundSingleRow", False) - + @classmethod def set_main_menu_grid_wrap_around_single_row(cls, value): cls._data["mainMenuGridWrapAroundSingleRow"] = value @@ -1425,7 +1425,7 @@ def set_show_extras_in_system_select_menu(cls, value): @classmethod def get_grid_multi_row_sel_bg_resize_pad_width(cls): return cls._data.get("gridMultiRowSelBgResizePadWidth", int(20*cls._default_multiplier)) - + @classmethod def set_grid_multi_row_sel_bg_resize_pad_width(cls, value): cls._data["gridMultiRowSelBgResizePadWidth"] = value @@ -1434,7 +1434,7 @@ def set_grid_multi_row_sel_bg_resize_pad_width(cls, value): @classmethod def get_grid_multi_row_sel_bg_resize_pad_height(cls): return cls._data.get("gridMultiRowSelBgResizePadHeight", int(20*cls._default_multiplier)) - + @classmethod def set_grid_multi_row_sel_bg_resize_pad_height(cls, value): cls._data["gridMultiRowSelBgResizePadHeight"] = value @@ -1448,12 +1448,12 @@ def get_top_bar_initial_x_offset(cls): def set_top_bar_initial_x_offset(cls, value): cls._data["topBarInitialXOffset"] = value cls.save_changes() - + @classmethod def get_grid_multi_row_img_y_offset(cls, text_height): default_height = -25 if(0 != text_height): - default_height = -1 * text_height + default_height = -1 * text_height return default_height + cls._data.get("gridMultiRowImageYOffset", 0) @@ -1498,7 +1498,7 @@ def set_resize_type_for_game_switcher(cls, resize_type): @classmethod def get_full_screen_grid_game_menu_resize_type(cls): view_type_str = cls._data.get("fullScreenGridGameMenuResizeType", "ZOOM") - return getattr(ResizeType, view_type_str, ResizeType.ZOOM) + return getattr(ResizeType, view_type_str, ResizeType.ZOOM) @classmethod def set_full_screen_grid_game_menu_resize_type(cls, resize_type): @@ -1520,7 +1520,7 @@ def set_full_screen_grid_system_select_menu_resize_type(cls, resize_type): @classmethod def get_set_top_bar_text_to_game_selection_for_game_switcher(cls): return cls._data.get("gameSwitcherSetTopBarTextToGameSelection", True) - + @classmethod def set_set_top_bar_text_to_game_selection_for_game_switcher(cls, value): cls._data["gameSwitcherSetTopBarTextToGameSelection"] = value @@ -1529,7 +1529,7 @@ def set_set_top_bar_text_to_game_selection_for_game_switcher(cls, value): @classmethod def true_full_screen_game_switcher(cls): return cls._data.get("gameSwitcherTrueFullScreen", True) - + @classmethod def set_true_full_screen_game_switcher(cls, value): cls._data["gameSwitcherTrueFullScreen"] = value @@ -1538,7 +1538,7 @@ def set_true_full_screen_game_switcher(cls, value): @classmethod def display_battery_percent(cls): return cls._data.get("displayBatteryPercent", True) - + @classmethod def set_display_battery_percent(cls, value): cls._data["displayBatteryPercent"] = value @@ -1547,7 +1547,7 @@ def set_display_battery_percent(cls, value): @classmethod def display_battery_icon(cls): return cls._data.get("displayBatteryIcon", True) - + @classmethod def set_display_battery_icon(cls, value): cls._data["displayBatteryIcon"] = value @@ -1556,7 +1556,7 @@ def set_display_battery_icon(cls, value): @classmethod def display_volume_numbers(cls): return cls._data.get("displayVolumeNumbers", False) - + @classmethod def set_display_volume_numbers(cls, value): cls._data["displayVolumeNumbers"] = value @@ -1565,7 +1565,7 @@ def set_display_volume_numbers(cls, value): @classmethod def show_bottom_bar_buttons(cls): return cls._data.get("showBottomBarButtons", True) - + @classmethod def set_show_bottom_bar_buttons(cls, value): cls._data["showBottomBarButtons"] = value @@ -1574,7 +1574,7 @@ def set_show_bottom_bar_buttons(cls, value): @classmethod def get_main_menu_title(cls): return cls._data.get("mainMenuTitle", PyUiConfig.get_main_menu_title()) - + @classmethod def set_main_menu_title(cls, value): cls._data["mainMenuTitle"] = value @@ -1619,7 +1619,7 @@ def set_multi_row_grid_text_y_offset(cls, value): @classmethod def check_and_create_asset(cls, output_image, input_image, target_width, target_height, target_alpha_channel): if(not os.path.exists(output_image)): - PyUiLogger.get_logger().info(f"Creating resized {output_image} from {input_image}") + PyUiLogger.get_logger().info(f"Creating resized {output_image} from {input_image}") Device.get_device().get_image_utils().resize_image(input_image, output_image, target_width, @@ -1628,7 +1628,7 @@ def check_and_create_asset(cls, output_image, input_image, target_width, target_ target_alpha_channel=target_alpha_channel) @classmethod - def check_and_create_ra_assets(cls): + def check_and_create_ra_assets(cls): cls.check_and_create_asset( cls._resolve_png_path(cls._skin_folder,["menu-6line-bg.png"]), cls._resolve_png_path(cls._skin_folder,["background.png"]), 320, @@ -1651,10 +1651,10 @@ def get_cfw_default_icon(cls, icon_name): return None else: cfw_theme_path = os.path.join(PyUiConfig.get("themeDir"),cfw_theme) - path = os.path.join(cfw_theme_path, - cls._get_asset_folder(cfw_theme_path, "icons", - Device.get_device().screen_width(), - Device.get_device().screen_height()), + path = os.path.join(cfw_theme_path, + cls._get_asset_folder(cfw_theme_path, "icons", + Device.get_device().screen_width(), + Device.get_device().screen_height()), "app",icon_name) if os.path.exists(path): @@ -1667,10 +1667,10 @@ def get_cfw_default_icon(cls, icon_name): return png_path return None - + @classmethod - def get_relative_img(cls, img_path, folder): + def get_relative_img(cls, img_path, folder): if not img_path: return None p = Path(img_path) @@ -1688,7 +1688,7 @@ def get_relative_img(cls, img_path, folder): @classmethod def get_bg_for_img(cls, img_path): return cls.get_relative_img(img_path, "bg") - + @classmethod def get_overlay_for_img(cls, img_path): return cls.get_relative_img(img_path, "overlay") diff --git a/main-ui/utils/py_ui_config.py b/main-ui/utils/py_ui_config.py index 9d5cc93a..6ba0562c 100644 --- a/main-ui/utils/py_ui_config.py +++ b/main-ui/utils/py_ui_config.py @@ -154,7 +154,7 @@ def set_game_system_sort_brand_priority(cls, value): @classmethod def game_system_sort_year_priority(cls): return cls.get("gameSystemSortYearPrio",3) - + @classmethod def set_game_system_sort_year_priority(cls, value): cls._data["gameSystemSortYearPrio"] = value @@ -163,7 +163,7 @@ def set_game_system_sort_year_priority(cls, value): @classmethod def game_system_sort_name_priority(cls): return cls.get("gameSystemSortNamePrio",4) - + @classmethod def set_game_system_sort_name_priority(cls, value): cls._data["gameSystemSortNamePrio"] = value @@ -209,15 +209,15 @@ def get_video_root_path(cls): @classmethod def get_activity_log_path(cls): return cls.get("activityLogPath",None) - + @classmethod def get_about_entries(cls): return cls.get("aboutEntries",[]) - + @classmethod def get_poweroff_cmd(cls): return cls.get("poweroffCmd",None) - + @classmethod def get_reboot_cmd(cls): return cls.get("rebootCmd",None) diff --git a/main-ui/views/carousel_view.py b/main-ui/views/carousel_view.py index f1b335e1..0db7bcbf 100644 --- a/main-ui/views/carousel_view.py +++ b/main-ui/views/carousel_view.py @@ -13,12 +13,12 @@ from views.view import View class CarouselView(View): - def __init__(self,top_bar_text, options: List[GridOrListEntry], cols : int, - selected_index=0, show_grid_text=False, - set_top_bar_text_to_selection=False, - set_bottom_bar_text_to_selection=None, + def __init__(self,top_bar_text, options: List[GridOrListEntry], cols : int, + selected_index=0, show_grid_text=False, + set_top_bar_text_to_selection=False, + set_bottom_bar_text_to_selection=None, resize_type=None, - selected_entry_width_percent=None, + selected_entry_width_percent=None, shrink_further_away = None, sides_hang_off_edge = None, missing_image_path = None, @@ -49,7 +49,7 @@ def __init__(self,top_bar_text, options: List[GridOrListEntry], cols : int, self.options_length = len(options) if(self.selected_entry_width_percent is None): self.selected_entry_width_percent = 40 - self.fixed_width = fixed_width + self.fixed_width = fixed_width if(x_pad is None): x_pad = 10 @@ -74,13 +74,13 @@ def __init__(self,top_bar_text, options: List[GridOrListEntry], cols : int, while(len(self.options) <= cols*2): self.options += self.options - + cols = min(cols, len(self.options)) if(cols %2 == 0): - cols +=1 - + cols +=1 + cols = min(cols, len(self.options)) - + self.cols = cols self.current_left = len(self.options)-(cols-1)//2 self.current_right = (cols-1)//2 @@ -183,7 +183,7 @@ def get_width_percentages(self) -> List[float]: right = [secondary_width_percent for i in range(k)] image_widths = left + mid + right - + if(self.sides_hang_off_edge): n = len(image_widths) @@ -198,7 +198,7 @@ def get_width_percentages(self) -> List[float]: new_widths = [w + x for w in image_widths] else: total = sum(image_widths) - target = 100 + target = 100 extra_per_item = (target - total) / len(image_widths) new_widths = [w + extra_per_item for w in image_widths] @@ -207,7 +207,7 @@ def get_width_percentages(self) -> List[float]: def _clear(self): - + if self.selected < len(self.options): selected = self.options[self.selected] bg = Theme.get_bg_for_img(selected.get_image_path()) @@ -224,34 +224,34 @@ def _clear(self): Display.clear("", bottom_bar_text="") def _render_image(self, - image_path: str, - x: int, - y: int, - render_mode, - target_width, + image_path: str, + x: int, + y: int, + render_mode, + target_width, target_height, resize_type): - width, height = Display.render_image(image_path=image_path, - x=x, + width, height = Display.render_image(image_path=image_path, + x=x, y=y, render_mode=render_mode, target_width=target_width, target_height=target_height, resize_type=resize_type) if(0 == width and 0 == height): - Display.render_image(image_path=self.missing_image_path, - x=x, + Display.render_image(image_path=self.missing_image_path, + x=x, y=y, render_mode=render_mode, target_width=target_width, target_height=target_height, resize_type=resize_type) - + def _render_horizontal(self): self.correct_selected_for_off_list() self._clear() - + #TODO Get hard coded values for padding from + usable_width = Device.get_device().screen_width() visible_options, selected_visible_index = self.get_visible_options() @@ -269,7 +269,7 @@ def _render_horizontal(self): else: widths = [self.fixed_width for _ in range(self.cols)] - widths[selected_visible_index] = self.fixed_selected_width + widths[selected_visible_index] = self.fixed_selected_width # Step 1: cumulative offsets x_offsets = [0] + [sum(widths[:i]) for i in range(1, len(widths))] @@ -308,7 +308,7 @@ def _render_horizontal(self): self.animate_transition_horizontal() else: self.animated_count = 0 - + iterable = list(enumerate(visible_options)) self.render_images_horizontal(iterable, x_offsets, widths, render_selected_only=False) @@ -331,7 +331,7 @@ def _render_horizontal(self): letter = '' if(self.options_are_sorted): letter = self.options[self.selected].get_primary_text()[0] - Display.add_index_text(self.selected%self.options_length + 1, self.options_length, + Display.add_index_text(self.selected%self.options_length + 1, self.options_length, letter=letter) Display.present() @@ -340,7 +340,7 @@ def _render_vertical(self): self.correct_selected_for_off_list() self._clear() - + #TODO Get hard coded values for padding from + usable_height = Device.get_device().screen_height() visible_options, selected_visible_index = self.get_visible_options() @@ -358,7 +358,7 @@ def _render_vertical(self): else: heights = [self.fixed_width for _ in range(self.cols)] - heights[selected_visible_index] = self.fixed_selected_width + heights[selected_visible_index] = self.fixed_selected_width # Step 1: cumulative offsets y_offsets = [0] + [sum(heights[:i]) for i in range(1, len(heights))] @@ -397,7 +397,7 @@ def _render_vertical(self): self.animate_transition_vertical() else: self.animated_count = 0 - + iterable = list(enumerate(visible_options)) self.render_images_vertical(iterable, y_offsets, heights, render_selected_only=False) @@ -420,7 +420,7 @@ def _render_vertical(self): letter = '' if(self.options_are_sorted): letter = self.options[self.selected].get_primary_text()[0] - Display.add_index_text(self.selected%self.options_length + 1, self.options_length, + Display.add_index_text(self.selected%self.options_length + 1, self.options_length, letter=letter) Display.present() @@ -447,21 +447,21 @@ def render_images_horizontal(self, iterable, x_offsets, widths, render_selected_ elif(not render_selected_only and is_selected): continue x_offset = x_offsets[visible_index] - + y_image_offset = Display.get_center_of_usable_screen_height() + self.additional_y_offset if(is_selected): image = imageTextPair.get_image_path_selected_ideal(widths[visible_index],Display.get_usable_screen_height()) else: image = imageTextPair.get_image_path_ideal(widths[visible_index],Display.get_usable_screen_height()) - self._render_image(image, - x_offset, + self._render_image(image, + x_offset, y_image_offset, render_mode, target_width=widths[visible_index], target_height=Display.get_usable_screen_height(), resize_type=self.resize_type) - + def render_images_vertical(self, iterable, y_offsets, heights, render_selected_only): render_mode = self.get_img_render_mode() @@ -472,20 +472,20 @@ def render_images_vertical(self, iterable, y_offsets, heights, render_selected_o elif(not render_selected_only and is_selected): continue y_offset = y_offsets[visible_index] - + x_image_offset = Device.get_device().screen_width() //2 + self.additional_y_offset if(is_selected): image = imageTextPair.get_image_path_selected_ideal(Device.get_device().screen_width() ,heights[visible_index]) else: image = imageTextPair.get_image_path_ideal(Device.get_device().screen_width() ,heights[visible_index]) - self._render_image(image, - x_image_offset, + self._render_image(image, + x_image_offset, y_offset, render_mode, target_width=Device.get_device().screen_width() , target_height=heights[visible_index], - resize_type=self.resize_type) + resize_type=self.resize_type) def get_selected_option(self): if 0 <= self.selected < len(self.options): @@ -495,7 +495,7 @@ def get_selected_option(self): def get_selection(self, select_controller_inputs = [ControllerInput.A]): self._render() - + if(Controller.get_input()): if Controller.last_input() == ControllerInput.DPAD_LEFT: self.adjust_selected(-1, skip_by_letter=False) @@ -531,7 +531,7 @@ def get_selection(self, select_controller_inputs = [ControllerInput.A]): elif Controller.last_input() == ControllerInput.B: Display.restore_bg() return Selection(self.get_selected_option(),Controller.last_input(), self.selected) - + return Selection(self.get_selected_option(),None, self.selected) def adjust_selected(self, amount, skip_by_letter): @@ -555,7 +555,7 @@ def animate_transition_horizontal(self): widths_for_animation = list(self.prev_widths) image_list = list(self.prev_visible_options) new_visible_options, selected_visible_index = self.get_visible_options() - + if(not self.sides_hang_off_edge): if rotate_left: image_list.insert(0,new_visible_options[0]) @@ -584,7 +584,7 @@ def animate_transition_horizontal(self): end_width = widths_for_animation[i+1] else: # Last item exits to the right - end_x_offset = (x_offsets_for_animation[-1] - x_offsets_for_animation[-2]) + x_offsets_for_animation[-1] + end_x_offset = (x_offsets_for_animation[-1] - x_offsets_for_animation[-2]) + x_offsets_for_animation[-1] end_width = start_width else: if i > 0: @@ -597,7 +597,7 @@ def animate_transition_horizontal(self): new_x_offset = start_x_offset + (end_x_offset - start_x_offset) * t new_width = start_width + (end_width - start_width) * t - frame_x_offset.append(int(new_x_offset)) + frame_x_offset.append(int(new_x_offset)) frame_widths.append(new_width) if(not self.sides_hang_off_edge): @@ -615,8 +615,8 @@ def animate_transition_horizontal(self): else: image = imageTextPair.get_image_path_ideal(frame_widths[visible_index],Display.get_usable_screen_height()) - self._render_image(image, - x_offset, + self._render_image(image, + x_offset, y_image_offset, render_mode, target_width=frame_widths[visible_index], @@ -632,7 +632,7 @@ def animate_transition_horizontal(self): letter=letter) Display.present() - + self.animated_count += 1 else: self.skip_next_animation = False @@ -651,7 +651,7 @@ def animate_transition_vertical(self): heights_for_animation = list(self.prev_heights) image_list = list(self.prev_visible_options) new_visible_options, selected_visible_index = self.get_visible_options() - + if(not self.sides_hang_off_edge): if rotate_up: image_list.insert(0,new_visible_options[0]) @@ -680,7 +680,7 @@ def animate_transition_vertical(self): end_height = heights_for_animation[i+1] else: # Last item exits to the right - end_y_offset = (y_offsets_for_animation[-1] - y_offsets_for_animation[-2]) + y_offsets_for_animation[-1] + end_y_offset = (y_offsets_for_animation[-1] - y_offsets_for_animation[-2]) + y_offsets_for_animation[-1] end_height = start_height else: if i > 0: @@ -693,7 +693,7 @@ def animate_transition_vertical(self): new_y_offset = start_y_offset + (end_y_offset - start_y_offset) * t new_height = start_height + (end_height - start_height) * t - frame_y_offset.append(int(new_y_offset)) + frame_y_offset.append(int(new_y_offset)) frame_heights.append(new_height) if(not self.sides_hang_off_edge): @@ -711,8 +711,8 @@ def animate_transition_vertical(self): else: image = imageTextPair.get_image_path_ideal(frame_heights[visible_index],Display.get_usable_screen_height()) - self._render_image(image, - x_image_offset, + self._render_image(image, + x_image_offset, y_offset, render_mode, target_width=Device.get_device().screen_width(), @@ -728,7 +728,7 @@ def animate_transition_vertical(self): letter=letter) Display.present() - + self.animated_count += 1 else: self.skip_next_animation = False \ No newline at end of file diff --git a/main-ui/views/full_screen_grid_view.py b/main-ui/views/full_screen_grid_view.py index 4cf4ac34..781eae3a 100644 --- a/main-ui/views/full_screen_grid_view.py +++ b/main-ui/views/full_screen_grid_view.py @@ -18,7 +18,7 @@ class FullScreenGridView(View): def __init__(self, top_bar_text, options: List[GridOrListEntry], selected_bg: str = None, selected_index=0, show_grid_text=True, - set_top_bar_text_to_selection=False, + set_top_bar_text_to_selection=False, unselected_bg = None, missing_image_path=None, resize_type = ResizeType.ZOOM, render_text_overlay = True, @@ -96,7 +96,7 @@ def correct_selected_for_off_list(self): def _render_shadowed_text(self, primary_text, y_offset_base, backdrop_font, front_font, x_offset, alpha=None): if(self.render_text_overlay): - #TODO hardcoded values of 25 + #TODO hardcoded values of 25 shadow_color = Theme.text_color(backdrop_font) # Render black text surfaces at offsets for the "outline" shift_amt = 5 @@ -146,13 +146,13 @@ def _render_shadowed_text(self, primary_text, y_offset_base, backdrop_font, fron def _render_primary_image(self, image_path: str, - x: int, - y: int, - render_mode=RenderMode.TOP_LEFT_ALIGNED, - target_width=None, - target_height=None, + x: int, + y: int, + render_mode=RenderMode.TOP_LEFT_ALIGNED, + target_width=None, + target_height=None, resize_type=None): - + w,h = Display.render_image(image_path=image_path, x=x, y=y, @@ -160,7 +160,7 @@ def _render_primary_image(self, target_width=target_width, target_height=target_height, resize_type=resize_type,) - + if(w == 0): w,h = Display.render_image(image_path=self.missing_image_path, x=x, @@ -180,9 +180,9 @@ def get_top_bar_height(self): def _render_image(self, index=None, x_offset=0, y_add_offset=0, render_text_overlay=True, text_alpha=None): imageTextPair = self.options[index] - image_path = imageTextPair.get_image_path_selected_ideal(self.resized_width, self.resized_height) + image_path = imageTextPair.get_image_path_selected_ideal(self.resized_width, self.resized_height) primary_text = imageTextPair.get_primary_text_long() - secondary_text = imageTextPair.get_description() + secondary_text = imageTextPair.get_description() y_offset = self.get_top_bar_height() if(self.resize_type is ResizeType.FIT): render_mode = RenderMode.TOP_CENTER_ALIGNED @@ -201,7 +201,7 @@ def _render_image(self, index=None, x_offset=0, y_add_offset=0, render_text_over render_mode = RenderMode.MIDDLE_CENTER_ALIGNED x_offset += Device.get_device().screen_width() // 2 y_offset = Display.get_top_bar_height(False) + (Display.get_usable_screen_height(False))//2 - + y_offset += y_add_offset self._render_primary_image( image_path, x_offset, @@ -210,11 +210,11 @@ def _render_image(self, index=None, x_offset=0, y_add_offset=0, render_text_over target_width=self.resized_width, target_height=self.resized_height, resize_type=self.resize_type) - + if(render_text_overlay and (not self.set_top_bar_text_to_selection or self.image_resize_height_multiplier == 1.0)): self._render_shadowed_text(primary_text, Device.get_device().screen_height() * 0.68, FontPurpose.SHADOWED_BACKDROP, FontPurpose.SHADOWED, 25,text_alpha) self._render_shadowed_text(secondary_text, Device.get_device().screen_height() * 0.78, FontPurpose.SHADOWED_BACKDROP_SMALL, FontPurpose.SHADOWED_SMALL, 27,text_alpha) - + def calculate_start_index(self): start_index = self.selected + 1 if self.selected != len(self.options) -1 else self.selected current_width = self.option_text_widths[start_index] + self.x_text_pad @@ -227,7 +227,7 @@ def calculate_start_index(self): start_index = i return start_index - + def _render_bottom_bar_text(self): if(self.render_bottom_bar_text_enabled): start_index = self.calculate_start_index() @@ -318,7 +318,7 @@ def get_selection(self, select_controller_inputs=[ControllerInput.A]): self.adjust_selected(+5, skip_by_letter=True if not Theme.skip_main_menu() else Device.get_device().get_system_config().get_skip_by_letter()) return Selection(self.get_selected_option(), None, self.selected) - + def adjust_selected(self, amount, skip_by_letter): amount = self.calculate_amount_to_move_by(amount, skip_by_letter) self.selected += amount @@ -330,10 +330,10 @@ def animate_transition(self): animation_duration = 0.30 / Device.get_device().animation_divisor() - self.animated_count *0.04 # seconds start_time = time.time() - + if(self.y_rotate_instead): total_shift = Device.get_device().screen_height() - else: + else: total_shift = Device.get_device().screen_width() last_frame_time = 0 @@ -365,13 +365,13 @@ def animate_transition(self): new_y_offset = new_frame_offset else: old_x_offset = old_frame_offset - new_x_offset = new_frame_offset + new_x_offset = new_frame_offset if(t < 1.0): self._render_image(self.last_selected, old_x_offset, old_y_offset,render_text_overlay=False, text_alpha=int(256 * (1.0-t)//1.0)) else: self._render_image(self.last_selected, old_x_offset, old_y_offset,render_text_overlay=False) - + self._render_image(self.selected, new_x_offset, new_y_offset,render_text_overlay=True, text_alpha=256) diff --git a/main-ui/views/grid_view.py b/main-ui/views/grid_view.py index e6727f82..68ab5424 100644 --- a/main-ui/views/grid_view.py +++ b/main-ui/views/grid_view.py @@ -18,7 +18,7 @@ class GridView(View): def __init__(self, top_bar_text, options: List[GridOrListEntry], cols: int, rows: int, selected_bg: str = None, selected_index=0, show_grid_text=True, resized_width=None, resized_height=None, - set_top_bar_text_to_selection=False, set_bottom_bar_text_to_selection=False, resize_type=None, + set_top_bar_text_to_selection=False, set_bottom_bar_text_to_selection=False, resize_type=None, unselected_bg = None, grid_img_y_offset=None, missing_image_path=None, wrap_around_single_row=None): super().__init__() @@ -81,7 +81,7 @@ def single_row_in_window(self): else: # wrapped window case, e.g. left=28 right=2 return self.selected >= self.current_left or self.selected < self.current_right - + def correct_selected_for_off_list(self): while (self.selected < 0): self.selected = len(self.options) + self.selected @@ -127,13 +127,13 @@ def correct_selected_for_off_list(self): def _render_primary_image(self, image_path: str, - x: int, - y: int, - render_mode=RenderMode.TOP_LEFT_ALIGNED, - target_width=None, - target_height=None, + x: int, + y: int, + render_mode=RenderMode.TOP_LEFT_ALIGNED, + target_width=None, + target_height=None, resize_type=None): - + w,h = Display.render_image(image_path=image_path, x=x, y=y, @@ -141,7 +141,7 @@ def _render_primary_image(self, target_width=target_width, target_height=target_height, resize_type=resize_type) - + if(w == 0): w,h = Display.render_image(image_path=self.missing_image_path, x=x, @@ -170,7 +170,7 @@ def _render_cell(self, visible_index, imageTextPair): elif(YRenderOption.BOTTOM == render_mode.y_mode): cell_y = bottom_row_y offset_divisor = 1 - + bg_width = self.resized_width bg_height = self.resized_height @@ -178,12 +178,12 @@ def _render_cell(self, visible_index, imageTextPair): text_height = Display.get_line_height(self.font_purpose) else: text_height = 0 - + if(self.rows == 1): img_offset = self.img_offset if self.img_offset is not None else 0 else: img_offset = Theme.get_grid_multi_row_img_y_offset(text_height) - + bg_offset = 0 if (self.resized_width is not None): # TODO not fixed values @@ -195,7 +195,7 @@ def _render_cell(self, visible_index, imageTextPair): else: bg_offset = 0 elif(YRenderOption.BOTTOM == render_mode.y_mode): - bg_offset = Theme.get_grid_multi_row_sel_bg_resize_pad_height() //2 + bg_offset = Theme.get_grid_multi_row_sel_bg_resize_pad_height() //2 if (actual_index == self.selected): if (self.selected_bg is not None): @@ -224,11 +224,11 @@ def _render_cell(self, visible_index, imageTextPair): self.font_purpose) if actual_index == self.selected else Theme.text_color(self.font_purpose) if (self.show_grid_text): - if(self.rows == 1) : + if(self.rows == 1) : y_text = int(Device.get_device().screen_height() * 310/480) + Theme.single_row_grid_text_y_offset() else: y_text = bottom_row_y - text_height + Theme.multi_row_grid_text_y_offset() - + Display.render_text(imageTextPair.get_primary_text(), x_offset, y_text, @@ -255,7 +255,7 @@ def _render(self): else: # Wrapped window: take end → wrap → beginning visible_options = ( - self.options[self.current_left:] + + self.options[self.current_left:] + self.options[:self.current_right] ) @@ -368,16 +368,16 @@ def animate_transition(self): new_x_offset = start_x_offset + (end_x_offset - start_x_offset) * t new_width = start_width + (end_width - start_width) * t - frame_x_offset.append(new_x_offset) + frame_x_offset.append(new_x_offset) frame_widths.append(new_width) for visible_index, imageTextPair in enumerate(self.prev_visible_options): x_offset = frame_x_offset[visible_index] y_image_offset = Display.get_center_of_usable_screen_height() - - self._render_image(imageTextPair.get_image_path_ideal(frame_widths[visible_index], Display.get_usable_screen_height()), - x_offset, + + self._render_image(imageTextPair.get_image_path_ideal(frame_widths[visible_index], Display.get_usable_screen_height()), + x_offset, y_image_offset, render_mode, target_width=frame_widths[visible_index], @@ -396,5 +396,5 @@ def animate_transition(self): letter) Display.present() last_frame_time = time.time() - + self.animated_count += 1 diff --git a/main-ui/views/list_view.py b/main-ui/views/list_view.py index 8ed58a89..466a201f 100644 --- a/main-ui/views/list_view.py +++ b/main-ui/views/list_view.py @@ -88,13 +88,13 @@ def get_selection(self, select_controller_inputs = [ControllerInput.A]): return Selection(self.get_selected_option(),Controller.last_input(), self.selected) return Selection(self.get_selected_option(), None, self.selected) - + def options_are_alphabetized(self): return False - + def _render_common(self): Display.clear(self.top_bar_text) - + self.adjust_selected_top_bottom_for_overflow() self._render() @@ -102,14 +102,14 @@ def _render_common(self): letter = '' if(self.options_are_alphabetized()): letter=self.options[self.selected].get_primary_text()[0] - Display.add_index_text(self.selected+1, len(self.options), force_include_index = True, + Display.add_index_text(self.selected+1, len(self.options), force_include_index = True, letter=letter) Display.present() def adjust_selected_top_bottom_for_overflow(self): self.selected = max(0, self.selected) self.selected = min(len(self.options)-1, self.selected) - + while(self.selected < self.current_top): self.current_top -= 1 self.current_bottom -=1