From c0495d91e4cc7d286d12128eb3c78795122e92ec Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Tue, 5 Dec 2023 23:24:31 +0700 Subject: [PATCH] #4064 opengl configure dialog --- xpra/client/gui/fake_client.py | 1 + xpra/gtk/configure/common.py | 23 +++ xpra/gtk/configure/gstreamer.py | 3 +- xpra/gtk/configure/main.py | 47 +++--- xpra/gtk/configure/opengl.py | 242 +++++++++++++++++++++++++--- xpra/gtk/dialogs/base_gui_window.py | 6 +- xpra/scripts/main.py | 37 ++--- xpra/util/parsing.py | 116 +++++++------ 8 files changed, 349 insertions(+), 126 deletions(-) diff --git a/xpra/client/gui/fake_client.py b/xpra/client/gui/fake_client.py index b929cec5b4..2bf8d40b6d 100644 --- a/xpra/client/gui/fake_client.py +++ b/xpra/client/gui/fake_client.py @@ -21,6 +21,7 @@ def __init__(self): self.mmap = None self.readonly = False self.encoding_defaults = {} + self.modal_windows = [] self._focused = None self._remote_server_mode = "seamless" self.wheel_smooth = False diff --git a/xpra/gtk/configure/common.py b/xpra/gtk/configure/common.py index 59f665fe1e..c920c95abe 100644 --- a/xpra/gtk/configure/common.py +++ b/xpra/gtk/configure/common.py @@ -3,9 +3,12 @@ # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. +import os.path from subprocess import check_call from xpra.os_util import POSIX +from xpra.util.env import osexpand +from xpra.util.parsing import parse_simple_dict DISCLAIMER = """ IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, @@ -21,3 +24,23 @@ def sync() -> None: if POSIX: check_call("sync") + + +def get_user_config_file() -> str: + from xpra.platform.paths import get_user_conf_dirs + return osexpand(os.path.join(get_user_conf_dirs()[0], "99_configure_tool.conf")) + + +def parse_user_config_file() -> dict: + filename = get_user_config_file() + if not os.path.exists(filename): + return {} + with open(filename, "r", encoding="utf8") as f: + return parse_simple_dict(f.read()) + + +def save_user_config_file(options: dict) -> None: + filename = get_user_config_file() + with open(filename, "w", encoding="utf8") as f: + for k, v in options.items(): + f.write(f"{k} = {v}\n") diff --git a/xpra/gtk/configure/gstreamer.py b/xpra/gtk/configure/gstreamer.py index 97b8d3a941..da25fb3ed9 100644 --- a/xpra/gtk/configure/gstreamer.py +++ b/xpra/gtk/configure/gstreamer.py @@ -41,8 +41,7 @@ def __init__(self, parent: Gtk.Window | None = None): self.set_resizable(False) def populate(self): - for x in self.vbox.get_children(): - self.vbox.remove(x) + self.clear_vbox() if not self.warning_shown: self.populate_with_warning() else: diff --git a/xpra/gtk/configure/main.py b/xpra/gtk/configure/main.py index 1316a67550..be18fe17a9 100644 --- a/xpra/gtk/configure/main.py +++ b/xpra/gtk/configure/main.py @@ -6,10 +6,10 @@ import os.path from importlib import import_module +from xpra.gtk.configure.common import get_user_config_file from xpra.scripts.config import InitExit from xpra.exit_codes import ExitCode from xpra.os_util import gi_import -from xpra.util.parsing import parse_simple_dict from xpra.gtk.dialogs.base_gui_window import BaseGUIWindow from xpra.gtk.widget import label from xpra.log import Logger @@ -29,18 +29,19 @@ def __init__(self): default_size=(480, 300), header_bar=(True, False), ) - self.dialogs : dict[str,BaseGUIWindow] = {} + self.dialogs : dict[str, BaseGUIWindow] = {} def populate(self): self.vbox.add(label("Configure Xpra", font="sans 20")) self.vbox.add(label("Tune your xpra configuration:", font="sans 14")) - self.sub("Features", "features.png","Enable or disable feature groups", "features") - self.sub("Picture compression", "encoding.png","Encodings, speed and quality", "encodings") - self.sub("GStreamer", "gstreamer.png","Configure the GStreamer codecs", "gstreamer") - self.sub("OpenGL acceleration", "opengl.png","Test and validate OpenGL renderer", "opengl") + self.sub("Features", "features.png", "Enable or disable feature groups", "features") + self.sub("Picture compression", "encoding.png", "Encodings, speed and quality", "encodings") + self.sub("GStreamer", "gstreamer.png", "Configure the GStreamer codecs", "gstreamer") + self.sub("OpenGL acceleration", "opengl.png", "Test and validate OpenGL renderer", "opengl") - def sub(self, title="", icon_name="browse.png", tooltip="", configure:str="") -> None: - def callback(btn): + def sub(self, title="", icon_name="browse.png", tooltip="", configure: str = "") -> None: + + def callback(_btn): dialog = self.dialogs.get(configure) if dialog is None: mod = import_module(f"xpra.gtk.configure.{configure}") @@ -68,29 +69,11 @@ def run_gui(gui_class=ConfigureGUI) -> int: return 0 -def get_user_config_file() -> str: - from xpra.platform.paths import get_user_conf_dirs - return os.path.join(get_user_conf_dirs()[0], "99_configure_tool.conf") - -def parse_user_config_file() -> dict: - filename = get_user_config_file() - if not os.path.exists(filename): - return {} - with open(filename, "r", encoding="utf8") as f: - return parse_simple_dict(f.read()) - -def save_user_config_file(options:dict) -> None: - filename = get_user_config_file() - with open(filename, "w", encoding="utf8") as f: - for k,v in options.items(): - f.write(f"{k} = {v}") - - def main(args) -> ExitCode: if args: conf = get_user_config_file() subcommand = args[0] - if subcommand=="reset": + if subcommand == "reset": import datetime now = datetime.datetime.now() with open(conf, "w", encoding="utf8") as f: @@ -105,7 +88,7 @@ def main(args) -> ExitCode: with open(bak, "w", encoding="utf8") as write: write.write(read.read()) return ExitCode.OK - elif subcommand=="show": + elif subcommand == "show": if not os.path.exists(conf): print(f"# {conf!r} does not exist yet") else: @@ -113,7 +96,13 @@ def main(args) -> ExitCode: print(f.read()) return ExitCode.OK else: - raise InitExit(ExitCode.FILE_NOT_FOUND, f"unknown configure subcommand {subcommand!r}") + if any(not str.isalnum(x) for x in subcommand): + raise ValueError("invalid characters found in subcommand") + from importlib import import_module + mod = import_module(f"xpra.gtk.configure.{subcommand}") + if not mod: + raise InitExit(ExitCode.FILE_NOT_FOUND, f"unknown configure subcommand {subcommand!r}") + return mod.main(args[1:]) return run_gui(ConfigureGUI) diff --git a/xpra/gtk/configure/opengl.py b/xpra/gtk/configure/opengl.py index c2c04bd127..024b6976cb 100644 --- a/xpra/gtk/configure/opengl.py +++ b/xpra/gtk/configure/opengl.py @@ -3,8 +3,12 @@ # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. -from xpra.os_util import gi_import +import struct + +from xpra.os_util import gi_import, WIN32 +from xpra.util.types import typedict from xpra.gtk.dialogs.base_gui_window import BaseGUIWindow +from xpra.gtk.configure.common import sync, parse_user_config_file, save_user_config_file, get_user_config_file from xpra.gtk.widget import label from xpra.log import Logger @@ -12,10 +16,102 @@ log = Logger("opengl", "util") +WW = 640 +WH = 480 + + +def BGRA(b: int, g: int, r: int, a: int): + return struct.pack("@BBBB", b, g, r, a) + + +def BGR(b: int, g: int, r: int): + return struct.pack("@BBB", b, g, r) + + +def draw_data(x: int, y: int, w: int, h: int, pixel_data: bytes): + coding = "rgb32" if len(pixel_data) == 4 else "rgb24" + rgb_format = "BGRX" if len(pixel_data) == 4 else "BGR" + return ( + x, y, w, h, + coding, pixel_data*w*h, len(pixel_data)*w, {"rgb_format" : rgb_format} + ) + + +CLEAR = ( + "white background", + ( + draw_data(0, 0, WW, WH, BGRA(0xff, 0xff, 0xff, 0xff)), + ), + ) + +TEST_STEPS = ( + CLEAR, + ( + "a grey square in the top left corner", + ( + draw_data(0, 0, 128, 128, BGR(0x80, 0x80, 0x80)), + ), + ), + ( + "four squares dividing the screen, red, green, blue and grey", + ( + draw_data(0, 0, WW//2, WH//2, BGR(0, 0, 0xFF)), + draw_data(WW//2, 0, WW//2, WH//2, BGR(0, 0xFF, 0)), + draw_data(WW//2, WH//2, WW//2, WH//2, BGR(0xFF, 0, 0)), + draw_data(0, WH//2, WW//2, WH//2, BGR(0x40, 0x40, 0x40)), + ), + ), +) + + +def create_twin_test_windows(): + from xpra.gtk.window import add_close_accel + from xpra.client.gui.fake_client import FakeClient + from xpra.client.gui.window_border import WindowBorder + from xpra.client.gl.window import get_gl_client_window_module + from xpra.client.gtk3.window import ClientWindow + opengl_props, gl_client_window_module = get_gl_client_window_module() + gl_window_class = gl_client_window_module.GLClientWindow + pixel_depth = 0 # int(opts.pixel_depth) + noclient = FakeClient() + ww, wh = WW, WH + metadata = typedict({ + # prevent resizing: + "maximum-size" : (WW, WH), + "minimum-size" : (WW, WH), + "modal" : True, + "has-alpha" : not WIN32, + }) + border = WindowBorder() + max_window_size = None # (1024, 1024) + default_cursor_data = None + windows = [] + for i, window_class, title in ( + (0, gl_window_class, "OpenGL Window"), + (1, ClientWindow, "Non-OpenGL Window"), + ): + x, y = 100+ww*i, 100 + window = window_class(noclient, None, 0, 2 ** 32 - 1, x, y, ww, wh, ww, wh, + metadata, False, typedict({}), + border, max_window_size, default_cursor_data, pixel_depth, + ) + window.set_title(title) + windows.append(window) + + def window_close_event(*_args): + pass + + add_close_accel(window, window_close_event) + return opengl_props, windows + class ConfigureGUI(BaseGUIWindow): def __init__(self, parent: Gtk.Window | None = None): + self.opengl_props = {} + self.windows = [] + self.test_steps = TEST_STEPS + self.step = 0 super().__init__( "Configure Xpra's OpenGL Renderer", "opengl.png", @@ -24,30 +120,140 @@ def __init__(self, parent: Gtk.Window | None = None): parent=parent, ) + def dismiss(self, *args): + for window in self.windows: + window.close() + super().dismiss() + def populate(self): + self.populate_form( + ( + "This tool can cause your system to crash if your GPU drivers are buggy.", + "Use with caution.", + "", + "When enabled, OpenGL rendering is faster.", + "Your xpra client will be able to skip its OpenGL self-tests and start faster.", + "", + "This test will present two windows which will be painted using various picture encodings.", + "You will be asked to confirm that the rendering was correct and identical in both windows.", + ), + ("Proceed", self.start_test), + ("Exit", self.dismiss), + ) + + def start_test(self, *_args): + sync() + self.opengl_props, self.windows = create_twin_test_windows() + glp = typedict(self.opengl_props) + version = ".".join(str(x) for x in glp.inttupleget("opengl", ())) + renderer = glp.get("renderer", "unknown").split(";")[0] + backend = glp.get("backend", "unknown") + vendor = glp.get("vendor", "unknown vendor") + glinfo = f"OpenGL {version} has been initialized using the {backend!r} backend"+\ + f"and {renderer!r} driver from {vendor}" + self.populate_form( + ( + glinfo, + "", + "This tool will now be showing two windows which will be painted using various picture encodings.", + "Try to arrange them side by side.", + "", + "You will be asked to confirm that the rendering was correct and identical in both windows.", + ), + ("Understood", self.paint_step), + ("Exit", self.dismiss), + ) + + def paint_step(self, *_args): + for window in self.windows: + window.show() + window.present() + step_data = self.test_steps[self.step] + description = step_data[0] + self.paint_twin_windows(*CLEAR) + self.paint_twin_windows(*step_data) + self.populate_form( + ( + "Please compare the two windows.", + "They should be indistinguishable from each other.", + "The colors and content should look 100% identical.", + "", + "The windows should be both showing:", + description, + ), + ("Restart", self.restart), + ("Identical", self.test_passed), + ("Not identical", self.test_failed), + ) + + def paint_twin_windows(self, description: str, paint_data: tuple): + log("paint_step() %r", description) + callbacks = [] + seq = 1 + for x, y, w, h, encoding, img_data, rowstride, options in paint_data: + for window in self.windows: + window.draw_region(x, y, w, h, encoding, img_data, rowstride, seq, typedict(options), callbacks) + seq += 1 + + def restart(self, *args): + for window in self.windows: + window.destroy() + self.start_test() + + def test_passed(self, *_args): + log("test_passed()") + self.step += 1 + if self.step < len(self.test_steps): + self.paint_step() + return + # reached the last step! + self.populate_form( + ( + "OpenGL can be enabled safely using this GPU.", + ), + ("Enable OpenGL", self.enable_opengl), + ("Exit", self.dismiss), + ) + + def enable_opengl(self, *_args): + config = parse_user_config_file() + config["opengl"] = "noprobe" + save_user_config_file(config) + self.populate_form( + ( + "OpenGL is now enabled in your user's xpra configuration file:", + "'%s'" % get_user_config_file(), + "If you experience issues later, you may want to reset your configuration.", + ), + ("Exit", self.dismiss), + ) + + def test_failed(self, *_args): + description = self.test_steps[self.step][0] + self.populate_form( + ( + "Please report this issue at https://github.com/Xpra-org/xpra/issues/new/choose", + "", + f"The test failed on step: {description!r}", + "Please try to include a screenshot covering both windows", + ), + ("Exit", self.dismiss), + ) + + def populate_form(self, lines: tuple[str, ...] = (), *buttons): + self.clear_vbox() self.add_widget(label("Configure Xpra's OpenGL Renderer", font="sans 20")) - text = "".join(( - "This tool can cause your system to crash", - "if your GPU drivers are buggy.", - "Use with caution.", - "\n", - "A window will be painted using various picture encodings.", - "You will be asked to confirm that the rendering was correct", - )) + text = "\n".join(lines) lbl = label(text, font="Sans 14") lbl.set_line_wrap(True) self.add_widget(lbl) hbox = Gtk.HBox() self.add_widget(hbox) - proceed = Gtk.Button.new_with_label("Proceed") - proceed.connect("clicked", self.run_test) - hbox.pack_start(proceed, True, True) - cancel = Gtk.Button.new_with_label("Exit") - cancel.connect("clicked", self.dismiss) - hbox.pack_start(cancel, True, True) - - def run_test(self, *args): - pass + for button_label, callback in buttons: + btn = Gtk.Button.new_with_label(button_label) + btn.connect("clicked", callback) + hbox.pack_start(btn, True, True) + self.show_all() def main(_args) -> int: diff --git a/xpra/gtk/dialogs/base_gui_window.py b/xpra/gtk/dialogs/base_gui_window.py index 96ae9cc1f6..48e2091ac3 100644 --- a/xpra/gtk/dialogs/base_gui_window.py +++ b/xpra/gtk/dialogs/base_gui_window.py @@ -89,6 +89,10 @@ def __init__(self, self.connect("focus-in-event", self.focus_in) self.connect("focus-out-event", self.focus_out) + def clear_vbox(self): + for x in self.vbox.get_children(): + self.vbox.remove(x) + def dismiss(self, *args): log(f"dismiss{args} calling {self.do_dismiss}") self.do_dismiss() @@ -212,7 +216,7 @@ def may_exit(): # if we exit immediately after we spawn the attach command GLib.timeout_add(2000, may_exit) - def may_notify(self, nid: NotificationID, summary:str, body:str): + def may_notify(self, nid: NotificationID, summary: str, body: str): log.info(summary) log.info(body) from xpra.platform.gui import get_native_notifier_classes diff --git a/xpra/scripts/main.py b/xpra/scripts/main.py index e0c5727f1e..b019975b8b 100755 --- a/xpra/scripts/main.py +++ b/xpra/scripts/main.py @@ -30,7 +30,8 @@ from xpra.os_util import ( getuid, getgid, get_username_for_uid, gi_import, - WIN32, OSX, POSIX, ) + WIN32, OSX, POSIX +) from xpra.util.io import is_socket, stderr_print, use_tty from xpra.util.system import is_Wayland, is_Ubuntu, SIGNAMES, set_proc_title, is_systemd_pid1 from xpra.scripts.parsing import ( @@ -40,7 +41,7 @@ fixup_defaults, validated_encodings, validate_encryption, do_parse_cmdline, show_audio_codec_help, MODE_ALIAS, REVERSE_MODE_ALIAS, - ) +) from xpra.scripts.config import ( XpraConfig, OPTION_TYPES, TRUE_OPTIONS, FALSE_OPTIONS, OFF_OPTIONS, ALL_BOOLEAN_OPTIONS, @@ -50,7 +51,7 @@ fixup_options, dict_to_validated_config, get_xpra_defaults_dirs, get_defaults, read_xpra_conf, make_defaults_struct, parse_bool, has_audio_support, name_to_field, - ) +) from xpra.net.common import DEFAULT_PORTS from xpra.log import is_debug_enabled, Logger, get_debug_args assert callable(error), "used by modules importing this function from here" @@ -758,16 +759,17 @@ def do_run_mode(script_file:str, cmdline, error_cb, options, args, mode:str, def script = xpra_runner_shell_script(script_file, os.getcwd()) write_runner_shell_scripts(script, False) return ExitCode.OK - if mode=="auth": + if mode == "auth": return run_auth(options, args) if mode == "configure": - return run_configure(args) + from xpra.gtk.configure.main import main + return main(args) if mode == "showconfig": return run_showconfig(options, args) if mode == "showsetting": return run_showsetting(args) - #unknown subcommand: - if mode!="help": + # unknown subcommand: + if mode != "help": print(f"Invalid subcommand {mode!r}") print("Usage:") if not POSIX or OSX: @@ -1382,7 +1384,7 @@ def connect_to_server(app, display_desc:dict[str,Any], opts) -> None: #before we can call connect() #because connect() may run a subprocess, #and Gdk locks up the system if the main loop is not running by then! - from gi.repository import GLib # @UnresolvedImport + GLib = gi_import("GLib") log = Logger("network") def do_setup_connection(): try: @@ -3176,7 +3178,7 @@ def run_list_mdns(error_cb, extra_args) -> ExitValue: except ImportError: error_cb("sorry, 'list-mdns' requires an mdns module") from xpra.dbus.common import loop_init - from gi.repository import GLib # @UnresolvedImport + GLib = gi_import("GLib") loop_init() found : dict[tuple[str,str,str],list] = {} shown = set() @@ -3718,8 +3720,8 @@ def display_wm_info(args) -> dict[str,Any]: init_gdk_display_source() from xpra.x11.gtk_x11.wm_check import get_wm_info info = get_wm_info() - from gi.repository import Gdk #pylint: disable=import-outside-toplevel - display = Gdk.Display.get_default() + gdk = gi_import("Gdk") + display = gdk.Display.get_default() info["display"] = display.get_name(), return info @@ -3972,19 +3974,6 @@ def run_auth(_options, args) -> ExitValue: return main_fn(argv) -def run_configure(args) -> ExitValue: - mod = "main" - if args: - mod = args[0] - valid = ("main", "gstreamer", "encodings", "features") - if mod not in valid: - raise ValueError(f"unsupported 'configure' argument {mod}, must be one of {csv(valid)}") - args = args[1:] - from importlib import import_module - mod = import_module(f"xpra.gtk.configure.{mod}") - return mod.main(args) - - def run_showconfig(options, args) -> ExitValue: log = get_logger() d = dict_to_validated_config({}) diff --git a/xpra/util/parsing.py b/xpra/util/parsing.py index db824f9485..c9c781d1ec 100644 --- a/xpra/util/parsing.py +++ b/xpra/util/parsing.py @@ -5,60 +5,68 @@ import binascii import os -from xpra.util.io import get_util_logger from xpra.util.env import envfloat -from xpra.log import Logger from xpra.scripts.config import TRUE_OPTIONS from xpra.util.str_fn import bytestostr - -log = Logger("scaling") +from xpra.log import Logger MIN_SCALING = envfloat("XPRA_MIN_SCALING", 0.1) MAX_SCALING = envfloat("XPRA_MAX_SCALING", 8) -SCALING_OPTIONS = [float(x) for x in os.environ.get("XPRA_TRAY_SCALING_OPTIONS", - "0.25,0.5,0.666,1,1.25,1.5,2.0,3.0,4.0,5.0").split(",") if MAX_SCALING>=float(x)>=MIN_SCALING] +SCALING_OPTIONS = [float(x) for x in + os.environ.get("XPRA_TRAY_SCALING_OPTIONS", + "0.25,0.5,0.666,1,1.25,1.5,2.0,3.0,4.0,5.0").split(",") + if MAX_SCALING >= float(x) >= MIN_SCALING] SCALING_EMBARGO_TIME = int(os.environ.get("XPRA_SCALING_EMBARGO_TIME", "1000"))/1000 -def r4cmp(v, rounding=1000.0): #ignore small differences in floats for scale values + +def r4cmp(v, rounding=1000.0): # ignore small differences in floats for scale values return round(v*rounding) -def fequ(v1, v2): - return r4cmp(v1)==r4cmp(v2) -def scaleup_value(scaling): - return tuple(v for v in SCALING_OPTIONS if r4cmp(v, 10)>r4cmp(scaling, 10)) -def scaledown_value(scaling): - return tuple(v for v in SCALING_OPTIONS if r4cmp(v, 10) tuple[float,float]: +def fequ(v1, v2) -> bool: + return r4cmp(v1) == r4cmp(v2) + + +def scaleup_value(scaling) -> tuple[float, ...]: + return tuple(v for v in SCALING_OPTIONS if r4cmp(v, 10) > r4cmp(scaling, 10)) + + +def scaledown_value(scaling) -> tuple[float, ...]: + return tuple(v for v in SCALING_OPTIONS if r4cmp(v, 10) < r4cmp(scaling, 10)) + + +def parse_scaling(desktop_scaling, root_w: int, root_h: int, + min_scaling=MIN_SCALING, max_scaling=MAX_SCALING) -> tuple[float, float]: + log = Logger("util", "scaling") log("parse_scaling(%s)", (desktop_scaling, root_w, root_h, min_scaling, max_scaling)) if desktop_scaling in TRUE_OPTIONS: return 1, 1 if desktop_scaling.startswith("auto"): - #figure out if the command line includes settings to use for auto mode: - #here are our defaults: - limits : list[tuple[int,int,float,float]] = [ - (3960, 2160, 1.0, 1.0), #100% no auto scaling up to 4k - (7680, 4320, 1.25, 1.25), #125% - (8192, 8192, 1.5, 1.5), #150% - (16384, 16384, 5.0/3, 5.0/3), #166% + # figure out if the command line includes settings to use for auto mode: + # here are our defaults: + limits : list[tuple[int, int, float, float]] = [ + (3960, 2160, 1.0, 1.0), # 100% no auto scaling up to 4k + (7680, 4320, 1.25, 1.25), # 125% + (8192, 8192, 1.5, 1.5), # 150% + (16384, 16384, 5.0/3, 5.0/3), # 166% (32768, 32768, 2, 2), (65536, 65536, 4, 4), - ] #200% if higher (who has this anyway?) + ] # 200% if higher (who has this anyway?) if desktop_scaling.startswith("auto:"): - limstr = desktop_scaling[5:] #ie: '1920x1080:1,2560x1600:1.5,... + limstr = desktop_scaling[5:] # ie: '1920x1080:1,2560x1600:1.5,... limp = limstr.split(",") limits = [] for l in limp: try: ldef = l.split(":") - assert len(ldef)==2, "could not find 2 parts separated by ':' in '%s'" % ldef + assert len(ldef) == 2, "could not find 2 parts separated by ':' in '%s'" % ldef dims = ldef[0].split("x") - assert len(dims)==2, "could not find 2 dimensions separated by 'x' in '%s'" % ldef[0] + assert len(dims) == 2, "could not find 2 dimensions separated by 'x' in '%s'" % ldef[0] x, y = int(dims[0]), int(dims[1]) scaleparts = ldef[1].replace("*", "x").replace("/", "x").split("x") - assert len(scaleparts)<=2, "found more than 2 scaling dimensions!" - if len(scaleparts)==1: + assert len(scaleparts) <= 2, "found more than 2 scaling dimensions!" + if len(scaleparts) == 1: sx = sy = float(scaleparts[0]) else: sx = float(scaleparts[0]) @@ -69,17 +77,18 @@ def parse_scaling(desktop_scaling, root_w, root_h, min_scaling=MIN_SCALING, max_ log.warn("Warning: failed to parse limit string '%s':", l) log.warn(" %s", e) log.warn(" should use the format WIDTHxHEIGTH:SCALINGVALUE") - elif desktop_scaling!="auto": + elif desktop_scaling != "auto": log.warn(f"Warning: invalid 'auto' scaling value {desktop_scaling}") sx, sy = 1.0, 1.0 matched = False for mx, my, tsx, tsy in limits: - if root_w*root_h<=mx*my: + if root_w*root_h <= mx*my: sx, sy = tsx, tsy matched = True break log("matched=%s : %sx%s with limits %s: %sx%s", matched, root_w, root_h, limits, sx, sy) - return sx,sy + return sx, sy + def parse_item(v) -> float: div = 1 try: @@ -88,16 +97,16 @@ def parse_item(v) -> float: v = v[:-1] except ValueError: pass - if div==1: + if div == 1: try: - return int(v) #ie: desktop-scaling=2 + return int(v) # ie: desktop-scaling=2 except ValueError: pass try: - return float(v)/div #ie: desktop-scaling=1.5 + return float(v)/div # ie: desktop-scaling=1.5 except (ValueError, ZeroDivisionError): pass - #ie: desktop-scaling=3/2, or desktop-scaling=3:2 + # ie: desktop-scaling=3/2, or desktop-scaling=3:2 pair = v.replace(":", "/").split("/", 1) try: return float(pair[0])/float(pair[1]) @@ -105,44 +114,47 @@ def parse_item(v) -> float: pass log.warn("Warning: failed to parse scaling value '%s'", v) return 0 - if desktop_scaling.find("x")>0 and desktop_scaling.find(":")>0: + if desktop_scaling.find("x") > 0 and desktop_scaling.find(":") > 0: log.warn("Warning: found both 'x' and ':' in desktop-scaling fixed value") log.warn(" maybe the 'auto:' prefix is missing?") return 1, 1 - #split if we have two dimensions: "1600x1200" -> ["1600", "1200"], if not: "2" -> ["2"] + # split if we have two dimensions: "1600x1200" -> ["1600", "1200"], if not: "2" -> ["2"] values = desktop_scaling.replace(",", "x").split("x", 1) sx = parse_item(values[0]) if not sx: return 1, 1 - if len(values)==1: - #just one value: use the same for X and Y + if len(values) == 1: + # just one value: use the same for X and Y sy = sx else: sy = parse_item(values[1]) if sy is None: return 1, 1 log("parse_scaling(%s) parsed items=%s", desktop_scaling, (sx, sy)) - #normalize absolute values into floats: - if sx>max_scaling or sy>max_scaling: + # normalize absolute values into floats: + if sx > max_scaling or sy > max_scaling: log(" normalizing dimensions to a ratio of %ix%i", root_w, root_h) sx /= root_w sy /= root_h - if sxmax_scaling or sy>max_scaling: + if sx < min_scaling or sy < min_scaling or sx > max_scaling or sy > max_scaling: log.warn("Warning: scaling values %sx%s are out of range", sx, sy) return 1, 1 log("parse_scaling(%s)=%s", desktop_scaling, (sx, sy)) return sx, sy -def parse_simple_dict(s:str="", sep:str=",") -> dict[str, str | list[str]]: - #parse the options string and add the pairs: +def parse_simple_dict(s: str = "", sep: str = ",") -> dict[str, str | list[str]]: + # parse the options string and add the pairs: d : dict[str, str | list[str]] = {} for el in s.split(sep): if not el: continue try: k, v = el.split("=", 1) - def may_add() -> str| list[str]: + k = k.strip() + v = v.strip() + + def may_add() -> str | list[str]: cur = d.get(k) if cur is None: return v @@ -152,13 +164,13 @@ def may_add() -> str| list[str]: return cur d[k] = may_add() except Exception as e: - log = get_util_logger() + log = Logger("util") log.warn("Warning: failed to parse dictionary option '%s':", s) log.warn(" %s", e) return d -def parse_scaling_value(v) -> tuple[int,int] | None: +def parse_scaling_value(v) -> tuple[int, int] | None: if not v: return None if v.endswith("%"): @@ -166,11 +178,11 @@ def parse_scaling_value(v) -> tuple[int,int] | None: values = v.replace("/", ":").replace(",", ":").split(":", 1) values = [int(x) for x in values] for x in values: - assert x>0, f"invalid scaling value {x}" - if len(values)==1: + assert x > 0, f"invalid scaling value {x}" + if len(values) == 1: ret = 1, values[0] else: - assert values[0]<=values[1], "cannot upscale" + assert values[0] <= values[1], "cannot upscale" ret = values[0], values[1] return ret @@ -181,14 +193,14 @@ def from0to100(v): def intrangevalidator(v, min_value=None, max_value=None): v = int(v) - if min_value is not None and vmax_value: + if max_value is not None and v > max_value: raise ValueError(f"value must be lower than {max_value}") return v -def parse_encoded_bin_data(data:str) -> bytes: +def parse_encoded_bin_data(data: str) -> bytes: if not data: return b"" header = bytestostr(data).lower()[:10]