mirror of
https://github.com/netinvent/npbackup.git
synced 2026-02-23 18:54:05 +08:00
Add adaptive window sizing for small screens
- Add window_utils.py with functions to fit windows to screen size - Use Windows API (SPI_GETWORKAREA) to get work area without taskbar - Make main windows resizable: main, config, operations, content, restoration - Automatically resize windows that exceed available screen space Fixes issue where windows were too large for 1080p and smaller screens. Related to upstream issue #175. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f70d63a235
commit
f4c22507da
4 changed files with 184 additions and 1 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
164
npbackup/gui/window_utils.py
Normal file
164
npbackup/gui/window_utils.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue