diff --git a/xpra/client/gtk_base/gtk_tray_menu_base.py b/xpra/client/gtk_base/gtk_tray_menu_base.py index a8684d942e..7bf7d161c5 100644 --- a/xpra/client/gtk_base/gtk_tray_menu_base.py +++ b/xpra/client/gtk_base/gtk_tray_menu_base.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # This file is part of Xpra. -# Copyright (C) 2011-2021 Antoine Martin +# Copyright (C) 2011-2022 Antoine Martin # 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 import re from gi.repository import GLib, Gtk @@ -56,6 +57,9 @@ MENU_ICONS = envbool("XPRA_MENU_ICONS", True) +NEW_MONITOR_RESOLUTIONS = os.environ.get("XPRA_NEW_MONITOR_RESOLUTIONS", + "640x480,1024x768,1600x1200,FHD,4K").split(",") + CLIPBOARD_LABELS = ["Clipboard", "Primary", "Secondary"] CLIPBOARD_LABEL_TO_NAME = { "Clipboard" : "CLIPBOARD", @@ -1191,7 +1195,7 @@ def populate_monitors(*args): for x in menu.get_children(): menu.remove(x) def monitor_changed(mitem, index): - log("monitor_changed(%s, %s)", monitor, index) + log("monitor_changed(%s, %s)", mitem, index) self.client.send_remove_monitor(index) for i, monitor in self.client.server_monitors.items(): mitem = Gtk.CheckMenuItem(label=monitor.get("name", "VFB-%i" % i)) @@ -1204,11 +1208,11 @@ def monitor_changed(mitem, index): resolutions_menu = Gtk.Menu() add_monitor_item.set_submenu(resolutions_menu) def add_monitor(mitem, resolution): - log("add_monitor(%s)", resolution) + log("add_monitor(%s, %s)", mitem, resolution) #older servers may not have all the aliases: resolution = RESOLUTION_ALIASES.get(resolution, resolution) self.client.send_add_monitor(resolution) - for resolution in ("1024x768", "1080p", "4k"): + for resolution in NEW_MONITOR_RESOLUTIONS: mitem = self.menuitem(resolution) mitem.connect("activate", add_monitor, resolution) resolutions_menu.append(mitem) diff --git a/xpra/client/mixins/display.py b/xpra/client/mixins/display.py index f7b9cd07bc..fbaac850a8 100644 --- a/xpra/client/mixins/display.py +++ b/xpra/client/mixins/display.py @@ -240,10 +240,10 @@ def parse_server_capabilities(self, c : typedict) -> bool: self.server_randr = c.boolget("resize_screen") log("server has randr: %s", self.server_randr) self.server_opengl = c.dictget("opengl") - Logger("opengl")("server opengl=%s", self.server_opengl) + Logger("screen", "opengl")("server opengl=%s", self.server_opengl) self.server_multi_monitors = c.boolget("multi-monitors", False) self.server_monitors = c.dictget("monitors") - Logger("screen").warn("server multi-monitors=%s, monitors=%s", + log("server multi-monitors=%s, monitors=%s", self.server_multi_monitors, self.server_monitors) return True diff --git a/xpra/server/mixins/window_server.py b/xpra/server/mixins/window_server.py index ac06b2de40..4da1300e21 100644 --- a/xpra/server/mixins/window_server.py +++ b/xpra/server/mixins/window_server.py @@ -310,10 +310,12 @@ def update_batch_config(self, proto, wid_windows, batch_props, client_properties self._set_client_properties(proto, wid, window, client_properties) ss.update_batch(wid, window, batch_props) - def _refresh_windows(self, proto, wid_windows, opts): + def _refresh_windows(self, proto, wid_windows, opts=None): ss = self.get_server_source(proto) - if ss is None: - return + if ss: + self.do_refresh_windows(ss, wid_windows, opts) + + def do_refresh_windows(self, ss, wid_windows, opts=None): for wid, window in wid_windows.items(): if window is None or not window.is_managed(): continue @@ -325,6 +327,10 @@ def _refresh_windows(self, proto, wid_windows, opts): def _idle_refresh_all_windows(self, proto): self.idle_add(self._refresh_windows, proto, self._id_to_window, {}) + def refresh_all_windows(self): + for ss in tuple(self._server_sources.values()): + if not isinstance(ss, WindowsMixin): + self.do_refresh_windows(ss, self._id_to_window) def get_window_position(self, _window): #where the window is actually mapped on the server screen: diff --git a/xpra/x11/bindings/randr_bindings.pyx b/xpra/x11/bindings/randr_bindings.pyx index f8d21df15b..9f695964cf 100644 --- a/xpra/x11/bindings/randr_bindings.pyx +++ b/xpra/x11/bindings/randr_bindings.pyx @@ -1026,30 +1026,29 @@ cdef class RandRBindingsInstance(X11CoreBindingsInstance): primary = 0 try: #re-configure all crtcs: - for mi in range(rsc.ncrtc): - m = monitor_defs.get(mi, {}) - crtc = rsc.crtcs[mi] - assert rsc.noutput>mi - output = rsc.outputs[mi] - log("monitor %i is crtc %i and output %i: %s", mi, crtc, output, m) + for i in range(rsc.ncrtc): + m = monitor_defs.get(i, {}) + crtc = rsc.crtcs[i] + assert rsc.noutput>i + output = rsc.outputs[i] + log("%i: crtc %i and output %i: %s", i, crtc, output, m) crtc_info = XRRGetCrtcInfo(self.display, rsc, crtc) if not crtc_info: - log.error("Error: crtc %i not found (%#x)", mi, crtc) + log.error("Error: crtc %i not found (%#x)", i, crtc) continue try: if m.get("primary", False): - primary = mi + primary = i width = m.get("width", 0) height = m.get("height", 0) - output_info = XRRGetOutputInfo(self.display, rsc, output) if not output_info: - log.error("Error: output %i not found (%#x)", mi, output) + log.error("Error: output %i not found (%#x)", i, output) continue if crtc_info.noutput==0 and output_info.connection==RR_Disconnected and not m: #crtc is not enabled and the corresponding output is not connected, #which is exactly what we want, so just leave it alone - log("crtc and output %i are already disabled", mi) + log("crtc and output %i are already disabled", i) continue noutput = 1 mode = 0 @@ -1081,7 +1080,7 @@ cdef class RandRBindingsInstance(X11CoreBindingsInstance): mode = self.do_add_screen_size(mode_name, width, height) assert mode!=0, "mode %ix%i not found" % (width, height) XRRAddOutputMode(self.display, output, mode) - log("mode %r (%#x) added to output %i (%i)", mode_name, mode, mi, output) + log("mode %r (%#x) added to output %i (%i)", mode_name, mode, i, output) else: noutput = 0 @@ -1094,17 +1093,18 @@ cdef class RandRBindingsInstance(X11CoreBindingsInstance): CurrentTime, x, y, mode, RR_Rotate_0, &output, noutput) if r: - raise Exception("failed to set crtc config for monitor %i" % mi) + raise Exception("failed to set crtc config for monitor %i" % i) mmw = m.get("mm-width", dpi96(width)) mmh = m.get("mm-height", dpi96(height)) - self.set_output_int_property(mi, "WIDTH_MM", mmw) - self.set_output_int_property(mi, "HEIGHT_MM", mmh) + self.set_output_int_property(i, "WIDTH_MM", mmw) + self.set_output_int_property(i, "HEIGHT_MM", mmh) #this allows us to disconnect the output of this crtc: - self.set_output_int_property(mi, "SUSPENDED", not bool(m)) + self.set_output_int_property(i, "SUSPENDED", not bool(m)) posinfo = "" if x or y: posinfo = " at %i,%i" % (x, y) - log.info("setting dummy crtc %i to %ix%i (%ix%i mm)%s", mi, width, height, mmw, mmh, posinfo) + log.info("setting dummy crtc and output %i to %ix%i (%ix%i mm)%s", + i, width, height, mmw, mmh, posinfo) finally: if output_info: XRRFreeOutputInfo(output_info) @@ -1118,18 +1118,20 @@ cdef class RandRBindingsInstance(X11CoreBindingsInstance): if not monitors: log.error("Error: failed to retrieve the list of monitors") return False - #start by removing the ones we don't use, - #which makes it easier to prevent name Atom conflicts + log("got %i monitors for %s crtcs", nmonitors, len(monitor_defs)) + #start by renaming the ones we do use and removing the ones we don't, + #which makes it easier to prevent name Atom conflicts: try: + #we only need as many monitors as we have crtcs, + #delete any extra ones: + for mi in range(nmonitors, len(monitor_defs)): + name_atom = monitors[mi].name + log("deleting monitor %i: %s", mi, bytestostr(self.XGetAtomName(name_atom))) + XRRDeleteMonitor(self.display, window, name_atom) + #use a temporary name that won't conflict when we modify the monitors: for mi in range(nmonitors): - if mi not in monitor_defs: - name_atom = monitors[mi].name - log("deleting monitor %i: %s", mi, bytestostr(self.XGetAtomName(name_atom))) - XRRDeleteMonitor(self.display, window, name_atom) - else: - #use a temporary name that won't conflict: - monitors[mi].name = self.xatom("DUMMY%i-%s" % (mi, monotonic())) - XRRSetMonitor(self.display, window, &monitors[mi]) + monitors[mi].name = self.xatom("DUMMY%i-%s" % (mi, monotonic())) + XRRSetMonitor(self.display, window, &monitors[mi]) finally: XRRFreeMonitors(monitors) self.XSync() @@ -1141,17 +1143,16 @@ cdef class RandRBindingsInstance(X11CoreBindingsInstance): for mi in range(nmonitors): names[mi] = bytestostr(self.XGetAtomName(monitors[mi].name)) log("found %i monitors still active: %s", nmonitors, csv(names.values())) + active_names = [] try: - for mi in range(nmonitors): - m = monitor_defs.get(mi, {}) - if not m: - log("no monitor definition for index %i", mi) - continue - log("setting monitor %i: %s", mi, m) - name = (prettify_plug_name(m.get("name", "")) or ("DUMMY%i" % mi)) + mi = 0 + for i, m in monitor_defs.items(): + log("matching monitor index %i to %i: %s", mi, i, m) + name = (prettify_plug_name(m.get("name", "")) or ("VFB-%i" % mi)) while name in names.values() and names.get(mi)!=name: name += "-%i" % mi names[mi] = name + active_names.append(name) monitor.name = self.xatom(name) monitor.primary = m.get("primary", primary==mi) monitor.automatic = m.get("automatic", True) @@ -1161,14 +1162,29 @@ cdef class RandRBindingsInstance(X11CoreBindingsInstance): monitor.height = m.get("height", 128) monitor.mwidth = m.get("mm-width", dpi96(monitor.width)) monitor.mheight = m.get("mm-height", dpi96(monitor.height)) - assert rsc.noutput>mi, "only %i outputs, cannot set %i" % (rsc.noutput, mi) - output = rsc.outputs[mi] + assert rsc.noutput>i, "only %i outputs, cannot set %i" % (rsc.noutput, i) + output = rsc.outputs[i] monitor.outputs = &output monitor.noutput = 1 log("XRRSetMonitor(%#x, %#x, %#x)", self.display, window, &monitor) XRRSetMonitor(self.display, window, &monitor) log.info("monitor %i is %r %ix%i", mi, name, monitor.width, monitor.height) self.XSync() + mi += 1 + finally: + XRRFreeMonitors(monitors) + monitors = XRRGetMonitors(self.display, window, True, &nmonitors) + if not monitors: + log.error("Error: failed to retrieve the list of monitors") + return False + #delete inactive monitors: + try: + for mi in range(nmonitors): + name_atom = monitors[mi].name + name = bytestostr(self.XGetAtomName(name_atom)) + if name not in active_names: + log("deleting monitor %i: %s", mi, name) + XRRDeleteMonitor(self.display, window, name_atom) finally: XRRFreeMonitors(monitors) finally: diff --git a/xpra/x11/desktop_server.py b/xpra/x11/desktop_server.py index 81d35dbc43..62cf766281 100644 --- a/xpra/x11/desktop_server.py +++ b/xpra/x11/desktop_server.py @@ -10,6 +10,7 @@ from gi.repository import GObject, Gdk, Gio, GLib from xpra.os_util import get_generic_os_name, load_binary_file +from xpra.scripts.config import FALSE_OPTIONS from xpra.util import updict, log_screen_sizes, envbool, csv from xpra.platform.paths import get_icon, get_icon_filename from xpra.platform.gui import get_wm_name @@ -340,14 +341,14 @@ class MonitorDesktopModel(DesktopModel): MAX_RECEIVERS = 20 def __repr__(self): - return "MonitorDesktopModel(%s)" % (self.monitor_geometry,) + return "MonitorDesktopModel(%s : %s)" % (self.name, self.monitor_geometry) def __init__(self, monitor): super().__init__() self.init(monitor) def init(self, monitor): - self.monitor = monitor + self.name = monitor.get("name", "") self.resize_delta = 0, 0 x = monitor.get("x", 0) y = monitor.get("y", 0) @@ -361,11 +362,10 @@ def init(self, monitor): def get_title(self): title = get_wm_name() # pylint: disable=assignment-from-none - name = self.monitor.get("name", "") - if name: + if self.name: if not title: - return name - title += " on %s" % name + return self.name + title += " on %s" % self.name return title def get_geometry(self): @@ -375,6 +375,17 @@ def get_dimensions(self): return self.monitor_geometry[2:4] + def get_definition(self): + x, y, width, height = self.monitor_geometry + return { + "x" : x, + "y" : y, + "width" : width, + "height" : height, + "name" : self.name, + } + + def do_xpra_damage_event(self, event): #ie: ) @@ -460,7 +471,7 @@ def server_init(self): from xpra.x11.vfb_util import set_initial_resolution, DEFAULT_DESKTOP_VFB_RESOLUTIONS screenlog("server_init() randr=%s, multi-monitors=%s, initial-resolutions=%s, default-resolutions=%s", self.randr, self.multi_monitors, self.initial_resolutions, DEFAULT_DESKTOP_VFB_RESOLUTIONS) - if not self.randr: + if not self.randr or self.initial_resolutions==(): return res = self.initial_resolutions or DEFAULT_DESKTOP_VFB_RESOLUTIONS if not self.multi_monitors and len(res)>1: @@ -691,31 +702,58 @@ def monitor_resized(self, model): def reconfigure_monitors(self): #now we can do the virtual crtcs, outputs and monitors defs = self.get_monitor_config() + screenlog("reconfigure_monitors() definitions=%s", defs) self.apply_monitor_config(defs) #and tell the client: self.setting_changed("monitors", defs) + def validate_monitors(self): + for model in self._id_to_window.values(): + x, y, width, height = model.get_geometry() + if x+width>=MAX_SIZE[0] or y+height>=MAX_SIZE[1]: + new_x, new_y = 0, 0 + mdef = model.get_definition() + mdef.update({ + "x" : new_x, + "y" : new_y, + }) + model.init(mdef) + def _adjust_monitors(self, after_wid, delta_x, delta_y): - if delta_x==0 or delta_y==0: - return models = dict((wid, model) for wid, model in self._id_to_window.items() if wid>after_wid) - for model in models: - monitor = model.monitor - monitor["x"] = max(0, monitor.get("x", 0)+delta_x) - monitor["y"] = max(0, monitor.get("y", 0)+delta_y) - model.init(monitor) + screenlog("adjust_monitors(%i, %i, %i) models=%s", after_wid, delta_x, delta_y, models) + if (delta_x==0 and delta_y==0) or not models: + return + for wid, model in models.items(): + self._adjust_monitor(model, delta_x, delta_y) + + def _adjust_monitor(self, model, delta_x, delta_y): + screenlog("adjust_monitors(%s, %i, %i)", model, delta_x, delta_y) + if (delta_x==0 and delta_y==0): + return + x, y = model.get_geometry()[:2] + new_x = max(0, x+delta_x) + new_y = max(0, y+delta_y) + if new_x!=x or new_y!=y: + screenlog("adjusting monitor %s from %s to %s", + model, (x, y), (new_x, new_y)) + mdef = model.get_definition() + mdef.update({ + "x" : new_x, + "y" : new_y, + }) + model.init(mdef) def get_monitor_config(self): monitor_defs = {} - for model in self._id_to_window.values(): - monitor = model.monitor - monitor_defs[monitor["index"]] = monitor + for wid, model in self._id_to_window.items(): + monitor = model.get_definition() + i = wid-1 + monitor["index"] = i + monitor_defs[i] = monitor return monitor_defs def apply_monitor_config(self, monitor_defs): - for model in self._id_to_window.values(): - monitor = model.monitor - monitor_defs[monitor["index"]] = monitor with xsync: RandR.set_crtc_config(monitor_defs) @@ -724,27 +762,23 @@ def _process_configure_monitor(self, proto, packet): action = packet[1] if action=="remove": identifier = packet[2] + value = packet[3] if identifier=="wid": - wid = packet[3] + wid = value elif identifier=="index": - index = packet[3] - #find the window with this index: - wid = None - for twid, model in self._id_to_window.items(): - if model.monitor.get("index", 0)==index: - wid = twid - break - assert wid is not None, "monitor not found for index %r" % index + #index is zero-based + wid = value+1 else: raise ValueError("unsupported monitor identifier %r" % identifier) model = self._id_to_window.get(wid) + screenlog("removing %s %i : %s", identifier, value, model) assert model, "monitor %r not found" % wid assert len(self._id_to_window)>1, "cannot remove the last monitor" + delta_x = -model.get_definition().get("width", 0) + delta_y = 0 #model.monitor.get("width", 0) model.unmanage() rwid = self._remove_window(model) #adjust the position of the other monitors: - delta_x = model.monitor.get("width", 0) - delta_y = 0 #model.monitor.get("width", 0) self._adjust_monitors(rwid, delta_x, delta_y) self.reconfigure_monitors() return @@ -760,9 +794,9 @@ def _process_configure_monitor(self, proto, packet): #find the wid to use: #prefer just incrementing the wid, but we cannot go higher than 16 def rightof(wid): - monitor = self._id_to_window[wid].monitor - x = monitor.get("x", 0)+monitor.get("width", 0) - y = monitor.get("y", 0) #+monitor.get("height", 0) + mdef = self._id_to_window[wid].get_definition() + x = mdef.get("x", 0)+mdef.get("width", 0) + y = mdef.get("y", 0) #+monitor.get("height", 0) return x, y wid = self._max_window_id x = y = 0 @@ -782,6 +816,9 @@ def rightof(wid): if prev: x, y = rightof(prev) self._adjust_monitors(wid-1, width, 0) + #ensure no monitors end up too far to the right or bottom: + #(better have them overlap - though we could do something smarter here) + self.validate_monitors() #now we can add our new monitor: xdpi = self.xdpi or self.dpi or 96 ydpi = self.ydpi or self.dpi or 96 @@ -806,6 +843,7 @@ def rightof(wid): if not isinstance(ss, WindowsMixin): continue self.send_new_monitor(model, ss) + self.refresh_all_windows() return raise ValueError("unsupported 'configure-monitor' action %r" % action) diff --git a/xpra/x11/vfb_util.py b/xpra/x11/vfb_util.py index 61171821eb..c54f1fcc7a 100644 --- a/xpra/x11/vfb_util.py +++ b/xpra/x11/vfb_util.py @@ -1,5 +1,5 @@ # This file is part of Xpra. -# Copyright (C) 2010-2021 Antoine Martin +# Copyright (C) 2010-2022 Antoine Martin # Copyright (C) 2008 Nathaniel Smith # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -15,7 +15,7 @@ import os.path from xpra.common import RESOLUTION_ALIASES -from xpra.scripts.config import InitException, get_Xdummy_confdir +from xpra.scripts.config import InitException, get_Xdummy_confdir, FALSE_OPTIONS from xpra.util import envbool, envint from xpra.os_util import ( shellsub, @@ -41,8 +41,10 @@ def parse_resolution(s): assert len(res)==2, "invalid resolution string '%s'" % s return res def parse_resolutions(s): - if not s: + if not s or s.lower() in FALSE_OPTIONS: return None + if s.lower() in ("none", "default"): + return () return (parse_resolution(v) for v in s.split(",")) def parse_env_resolutions(envkey="XPRA_DEFAULT_VFB_RESOLUTIONS", single_envkey="XPRA_DEFAULT_VFB_RESOLUTION", @@ -373,7 +375,7 @@ def set_initial_resolution(resolutions=DEFAULT_VFB_RESOLUTIONS): log("RandR setting new screen size to %s", res) randr.set_screen_size(*res) except Exception as e: - log("set_initial_resolution(%s)", res, exc_info=True) + log("set_initial_resolution(%s)", resolutions, exc_info=True) log.error("Error: failed to set the default screen size:") log.error(" %s", e) diff --git a/xpra/x11/x11_server_core.py b/xpra/x11/x11_server_core.py index 7a6c629873..5221b40916 100644 --- a/xpra/x11/x11_server_core.py +++ b/xpra/x11/x11_server_core.py @@ -110,8 +110,8 @@ def do_init(self, opts): try: self.initial_resolutions = parse_resolutions(opts.resize_display) except ValueError: - pass - self.randr = bool(self.initial_resolutions) or not (opts.resize_display in FALSE_OPTIONS) + self.initial_resolutions = None + self.randr = opts.resize_display not in FALSE_OPTIONS self.randr_exact_size = False self.fake_xinerama = "no" #only enabled in seamless server self.current_xinerama_config = None