diff --git a/npbackup/gui/__main__.py b/npbackup/gui/__main__.py index 5c4961a..4710d13 100644 --- a/npbackup/gui/__main__.py +++ b/npbackup/gui/__main__.py @@ -28,6 +28,7 @@ from ofunctions.threading import threaded from ofunctions.misc import BytesConverter import FreeSimpleGUI as sg import _tkinter +from npbackup.gui.window_utils import fit_window_to_screen import npbackup.configuration import npbackup.common from resources.customization import ( @@ -419,8 +420,11 @@ def ls_window(parent_window: sg.Window, repo_config: dict, snapshot_id: str) -> layout=layout, grab_anywhere=True, keep_on_top=False, + resizable=True, enable_close_attempted_event=True, + finalize=True, ) + fit_window_to_screen(window) # Reclaim memory from thread result # Note from v3 dev: This doesn't actually improve memory usage @@ -493,8 +497,14 @@ def restore_window( layout = [[sg.Column(left_col, element_justification="C")]] window = sg.Window( - _t("main_gui.restoration"), layout=layout, grab_anywhere=True, keep_on_top=False + _t("main_gui.restoration"), + layout=layout, + grab_anywhere=True, + keep_on_top=False, + resizable=True, + finalize=True, ) + fit_window_to_screen(window) result = None while True: event, values = window.read() @@ -1126,11 +1136,14 @@ def _main_gui(viewer_mode: bool): alpha_channel=1.0, default_button_element_size=(16, 1), right_click_menu=right_click_menu, + resizable=True, finalize=True, ) # Auto reisze table to window size window["snapshot-list"].expand(True, True) + # Fit window to screen if too large + fit_window_to_screen(window) window.read(timeout=0.01) if not config_file and not full_config and not viewer_mode: diff --git a/npbackup/gui/config.py b/npbackup/gui/config.py index ae0236b..2084779 100644 --- a/npbackup/gui/config.py +++ b/npbackup/gui/config.py @@ -18,6 +18,7 @@ import FreeSimpleGUI as sg import textwrap from datetime import datetime, timezone from ruamel.yaml.comments import CommentedMap +from npbackup.gui.window_utils import fit_window_to_screen from npbackup import configuration from ofunctions.misc import get_key_from_value, BytesConverter from npbackup.core.i18n_helper import _t @@ -2626,9 +2627,11 @@ Google Cloud storage: GOOGLE_PROJECT_ID GOOGLE_APPLICATION_CREDENTIALS\n\ alpha_channel=1.0, default_button_element_size=(16, 1), right_click_menu=right_click_menu, + resizable=True, finalize=True, enable_close_attempted_event=True, ) + fit_window_to_screen(window) # Init fresh config objects BAD_KEYS_FOUND_IN_CONFIG = set() diff --git a/npbackup/gui/operations.py b/npbackup/gui/operations.py index b79897c..6929e7f 100644 --- a/npbackup/gui/operations.py +++ b/npbackup/gui/operations.py @@ -16,6 +16,7 @@ from logging import getLogger from collections import namedtuple from ofunctions.misc import BytesConverter import FreeSimpleGUI as sg +from npbackup.gui.window_utils import fit_window_to_screen from npbackup.configuration import ( get_repo_config, get_repo_list, @@ -444,6 +445,7 @@ def operations_gui(full_config: dict) -> dict: alpha_channel=1.0, default_button_element_size=(20, 1), right_click_menu=right_click_menu, + resizable=True, finalize=True, ) @@ -451,6 +453,7 @@ def operations_gui(full_config: dict) -> dict: # Auto reisze table to window size window["repo-and-group-list"].expand(True, True) + fit_window_to_screen(window) while True: event, values = window.read() diff --git a/npbackup/gui/window_utils.py b/npbackup/gui/window_utils.py new file mode 100644 index 0000000..d616df3 --- /dev/null +++ b/npbackup/gui/window_utils.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# +# Window size utilities for small screens +# Ensures windows fit within screen bounds with proper margins + +import os +import FreeSimpleGUI as sg +from typing import Tuple, Optional + +# Fallback margin for window decorations (used if system method fails) +FALLBACK_MARGIN = 60 + + +def get_work_area() -> Optional[Tuple[int, int]]: + """ + Get work area size (screen minus taskbar) using OS-specific methods. + Returns None if not available. + """ + if os.name == "nt": + try: + import ctypes + from ctypes import wintypes + + class RECT(ctypes.Structure): + _fields_ = [ + ("left", wintypes.LONG), + ("top", wintypes.LONG), + ("right", wintypes.LONG), + ("bottom", wintypes.LONG), + ] + + rect = RECT() + # SPI_GETWORKAREA = 0x0030 + ctypes.windll.user32.SystemParametersInfoW(0x0030, 0, ctypes.byref(rect), 0) + width = rect.right - rect.left + height = rect.bottom - rect.top + if width > 0 and height > 0: + return width, height + except Exception: + pass + return None + + +def get_screen_size() -> Tuple[int, int]: + """Get screen dimensions.""" + try: + # Create a small temporary window to get screen size + temp_window = sg.Window( + "", [[]], alpha_channel=0, finalize=True, no_titlebar=True, size=(50, 50) + ) + screen_width, screen_height = temp_window.get_screen_size() + temp_window.close() + return screen_width, screen_height + except Exception: + # Fallback to minimum supported resolution + return 1024, 768 + + +def get_available_size(margin: int = 0) -> Tuple[int, int]: + """ + Get available window size accounting for decorations and taskbar. + Uses OS work area if available, otherwise falls back to screen size minus margin. + + Args: + margin: Additional pixels to subtract for window decorations + + Returns: + Tuple of (width, height) in pixels + """ + # Try OS-specific work area first (excludes taskbar) + work_area = get_work_area() + if work_area: + return work_area[0] - margin, work_area[1] - margin + + # Fallback: full screen minus default margin + screen_width, screen_height = get_screen_size() + return screen_width - FALLBACK_MARGIN - margin, screen_height - FALLBACK_MARGIN - margin + + +def fit_window_to_screen( + window: sg.Window, + margin: int = 0, + center: bool = True +) -> None: + """ + Resize window to fit within screen bounds if it's too large. + Should be called after window.finalize() or window.read(timeout=0). + + Args: + window: The window to resize + margin: Pixels margin for decorations + center: Whether to center the window after resizing + """ + try: + available_width, available_height = get_available_size(margin) + current_size = window.size + + if current_size is None: + return + + current_width, current_height = current_size + + new_width = min(current_width, available_width) + new_height = min(current_height, available_height) + + if new_width < current_width or new_height < current_height: + window.set_size((new_width, new_height)) + if center: + center_window(window) + except Exception: + # Silently fail - window sizing is not critical + pass + + +def center_window(window: sg.Window) -> None: + """Center window on screen.""" + try: + screen_width, screen_height = get_screen_size() + window_size = window.size + if window_size: + win_width, win_height = window_size + x = (screen_width - win_width) // 2 + y = (screen_height - win_height) // 2 + window.move(x, y) + except Exception: + pass + + +def make_scrollable_layout( + layout: list, + max_size: Optional[Tuple[int, int]] = None, + margin: int = 0 +) -> list: + """ + Wrap layout in a scrollable column if it would exceed screen size. + + Args: + layout: The original layout + max_size: Maximum size (width, height) or None to use screen size + margin: Margin for decorations + + Returns: + Layout wrapped in scrollable Column if needed, otherwise original layout + """ + if max_size is None: + max_size = get_available_size(margin) + + max_width, max_height = max_size + + # Wrap in scrollable column with size limit + scrollable_layout = [ + [ + sg.Column( + layout, + scrollable=True, + vertical_scroll_only=False, + size=(max_width, max_height), + expand_x=True, + expand_y=True, + ) + ] + ] + + return scrollable_layout