1# This file is part of Xpra. 2# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com> 3# Copyright (C) 2012-2021 Antoine Martin <antoine@xpra.org> 4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 5# later version. See the file COPYING for details. 6 7import os 8from gi.repository import GObject, Gdk 9 10from xpra.util import envbool 11from xpra.common import MAX_WINDOW_SIZE 12from xpra.gtk_common.error import xsync, xswallow 13from xpra.x11.gtk_x11.prop import prop_set, prop_get, prop_del 14from xpra.x11.window_info import window_name, window_info 15from xpra.gtk_common.gobject_util import no_arg_signal, one_arg_signal 16from xpra.gtk_common.gtk_util import get_default_root_window 17from xpra.x11.common import Unmanageable 18from xpra.x11.gtk_x11 import GDKX11Window 19from xpra.x11.gtk_x11.selection import ManagerSelection 20from xpra.x11.models.window import WindowModel, configure_bits 21from xpra.x11.gtk_x11.world_window import WorldWindow, destroy_world_window 22from xpra.x11.gtk_x11.gdk_bindings import ( 23 add_event_receiver, #@UnresolvedImport 24 add_fallback_receiver, remove_fallback_receiver, #@UnresolvedImport 25 get_children, #@UnresolvedImport 26 ) 27from xpra.x11.bindings.window_bindings import constants, X11WindowBindings #@UnresolvedImport 28from xpra.x11.bindings.keyboard_bindings import X11KeyboardBindings #@UnresolvedImport 29from xpra.log import Logger 30 31log = Logger("x11", "window") 32 33X11Window = X11WindowBindings() 34X11Keyboard = X11KeyboardBindings() 35 36focuslog = Logger("x11", "window", "focus") 37screenlog = Logger("x11", "window", "screen") 38framelog = Logger("x11", "window", "frame") 39 40CWX = constants["CWX"] 41CWY = constants["CWY"] 42CWWidth = constants["CWWidth"] 43CWHeight = constants["CWHeight"] 44 45NotifyPointerRoot = constants["NotifyPointerRoot"] 46NotifyDetailNone = constants["NotifyDetailNone"] 47 48LOG_MANAGE_FAILURES = envbool("XPRA_LOG_MANAGE_FAILURES", False) 49 50NO_NET_SUPPORTED = os.environ.get("XPRA_NO_NET_SUPPORTED", "").split(",") 51 52DEFAULT_NET_SUPPORTED = [ 53 "_NET_SUPPORTED", # a bit redundant, perhaps... 54 "_NET_SUPPORTING_WM_CHECK", 55 "_NET_WM_FULL_PLACEMENT", 56 "_NET_WM_HANDLED_ICONS", 57 "_NET_CLIENT_LIST", 58 "_NET_CLIENT_LIST_STACKING", 59 "_NET_DESKTOP_VIEWPORT", 60 "_NET_DESKTOP_GEOMETRY", 61 "_NET_NUMBER_OF_DESKTOPS", 62 "_NET_DESKTOP_NAMES", 63 "_NET_WORKAREA", 64 "_NET_ACTIVE_WINDOW", 65 "_NET_CURRENT_DESKTOP", 66 67 "WM_NAME", "_NET_WM_NAME", 68 "WM_ICON_NAME", "_NET_WM_ICON_NAME", 69 "WM_CLASS", 70 "WM_PROTOCOLS", 71 "_NET_WM_PID", 72 "WM_CLIENT_MACHINE", 73 "WM_STATE", 74 75 "_NET_WM_FULLSCREEN_MONITORS", 76 77 "_NET_WM_ALLOWED_ACTIONS", 78 "_NET_WM_ACTION_CLOSE", 79 "_NET_WM_ACTION_FULLSCREEN", 80 81 # We don't actually use _NET_WM_USER_TIME at all (yet), but it is 82 # important to say we support the _NET_WM_USER_TIME_WINDOW property, 83 # because this tells applications that they do not need to constantly 84 # ping any pagers etc. that might be running -- see EWMH for details. 85 # (Though it's not clear that any applications actually take advantage 86 # of this yet.) 87 "_NET_WM_USER_TIME", 88 "_NET_WM_USER_TIME_WINDOW", 89 # Not fully: 90 "WM_HINTS", 91 "WM_NORMAL_HINTS", 92 "WM_TRANSIENT_FOR", 93 "_NET_WM_STRUT", 94 "_NET_WM_STRUT_PARTIAL" 95 "_NET_WM_ICON", 96 97 "_NET_CLOSE_WINDOW", 98 99 # These aren't supported in any particularly meaningful way, but hey. 100 "_NET_WM_WINDOW_TYPE", 101 "_NET_WM_WINDOW_TYPE_NORMAL", 102 "_NET_WM_WINDOW_TYPE_DESKTOP", 103 "_NET_WM_WINDOW_TYPE_DOCK", 104 "_NET_WM_WINDOW_TYPE_TOOLBAR", 105 "_NET_WM_WINDOW_TYPE_MENU", 106 "_NET_WM_WINDOW_TYPE_UTILITY", 107 "_NET_WM_WINDOW_TYPE_SPLASH", 108 "_NET_WM_WINDOW_TYPE_DIALOG", 109 "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", 110 "_NET_WM_WINDOW_TYPE_POPUP_MENU", 111 "_NET_WM_WINDOW_TYPE_TOOLTIP", 112 "_NET_WM_WINDOW_TYPE_NOTIFICATION", 113 "_NET_WM_WINDOW_TYPE_COMBO", 114 # "_NET_WM_WINDOW_TYPE_DND", 115 116 "_NET_WM_STATE", 117 "_NET_WM_STATE_DEMANDS_ATTENTION", 118 "_NET_WM_STATE_MODAL", 119 # More states to support: 120 "_NET_WM_STATE_STICKY", 121 "_NET_WM_STATE_MAXIMIZED_VERT", 122 "_NET_WM_STATE_MAXIMIZED_HORZ", 123 "_NET_WM_STATE_SHADED", 124 "_NET_WM_STATE_SKIP_TASKBAR", 125 "_NET_WM_STATE_SKIP_PAGER", 126 "_NET_WM_STATE_HIDDEN", 127 "_NET_WM_STATE_FULLSCREEN", 128 "_NET_WM_STATE_ABOVE", 129 "_NET_WM_STATE_BELOW", 130 "_NET_WM_STATE_FOCUSED", 131 132 "_NET_WM_DESKTOP", 133 134 "_NET_WM_MOVERESIZE", 135 "_NET_MOVERESIZE_WINDOW", 136 137 "_MOTIF_WM_HINTS", 138 "_MOTIF_WM_INFO", 139 140 "_NET_REQUEST_FRAME_EXTENTS", 141 "_NET_RESTACK_WINDOW", 142 ] 143FRAME_EXTENTS = envbool("XPRA_FRAME_EXTENTS", True) 144if FRAME_EXTENTS: 145 DEFAULT_NET_SUPPORTED.append("_NET_FRAME_EXTENTS") 146 147NET_SUPPORTED = [x for x in DEFAULT_NET_SUPPORTED if x not in NO_NET_SUPPORTED] 148 149DEFAULT_SIZE_CONSTRAINTS = (0, 0, MAX_WINDOW_SIZE, MAX_WINDOW_SIZE) 150 151 152class Wm(GObject.GObject): 153 154 __gproperties__ = { 155 "windows": (GObject.TYPE_PYOBJECT, 156 "Set of managed windows (as WindowModels)", "", 157 GObject.ParamFlags.READABLE), 158 "toplevel": (GObject.TYPE_PYOBJECT, 159 "Toplevel container widget for the display", "", 160 GObject.ParamFlags.READABLE), 161 } 162 __gsignals__ = { 163 # Public use: 164 # A new window has shown up: 165 "new-window": one_arg_signal, 166 "show-desktop": one_arg_signal, 167 # You can emit this to cause the WM to quit, or the WM may 168 # spontaneously raise it if another WM takes over the display. By 169 # default, unmanages all windows: 170 "quit": no_arg_signal, 171 172 # Mostly intended for internal use: 173 "child-map-request-event": one_arg_signal, 174 "child-configure-request-event": one_arg_signal, 175 "xpra-focus-in-event": one_arg_signal, 176 "xpra-focus-out-event": one_arg_signal, 177 "xpra-client-message-event": one_arg_signal, 178 "xpra-xkb-event": one_arg_signal, 179 } 180 181 def __init__(self, replace_other_wm, wm_name, display=None): 182 super().__init__() 183 184 if display is None: 185 display = Gdk.Display.get_default() 186 self._display = display 187 self._root = self._display.get_default_screen().get_root_window() 188 self._wm_name = wm_name 189 self._ewmh_window = None 190 191 self._windows = {} 192 # EWMH says we have to know the order of our windows oldest to 193 # youngest... 194 self._windows_in_order = [] 195 196 # Become the Official Window Manager of this year's display: 197 self._wm_selection = ManagerSelection("WM_S0") 198 self._cm_wm_selection = ManagerSelection("_NET_WM_CM_S0") 199 self._wm_selection.connect("selection-lost", self._lost_wm_selection) 200 self._cm_wm_selection.connect("selection-lost", self._lost_wm_selection) 201 # May throw AlreadyOwned: 202 if replace_other_wm: 203 mode = self._wm_selection.FORCE 204 else: 205 mode = self._wm_selection.IF_UNOWNED 206 self._wm_selection.acquire(mode) 207 self._cm_wm_selection.acquire(mode) 208 209 # Set up the necessary EWMH properties on the root window. 210 self._setup_ewmh_window() 211 # Start with just one desktop: 212 self.set_desktop_list((u"Main", )) 213 self.set_current_desktop(0) 214 # Start with the full display as workarea: 215 root_w, root_h = get_default_root_window().get_geometry()[2:4] 216 self.root_set("_NET_SUPPORTED", ["atom"], NET_SUPPORTED) 217 self.set_workarea(0, 0, root_w, root_h) 218 self.set_desktop_geometry(root_w, root_h) 219 self.root_set("_NET_DESKTOP_VIEWPORT", ["u32"], [0, 0]) 220 221 self.size_constraints = DEFAULT_SIZE_CONSTRAINTS 222 223 # Load up our full-screen widget 224 self._world_window = WorldWindow(self._display.get_default_screen()) 225 self.notify("toplevel") 226 self._world_window.show_all() 227 228 # Okay, ready to select for SubstructureRedirect and then load in all 229 # the existing clients. 230 add_event_receiver(self._root, self) 231 add_fallback_receiver("xpra-client-message-event", self) 232 #when reparenting, the events may get sent 233 #to a window that is already destroyed 234 #and we don't want to miss those events, so: 235 add_fallback_receiver("child-map-request-event", self) 236 rxid = self._root.get_xid() 237 X11Window.substructureRedirect(rxid) 238 239 for w in get_children(self._root): 240 # Checking for FOREIGN here filters out anything that we've 241 # created ourselves (like, say, the world window), and checking 242 # for mapped filters out any withdrawn windows. 243 xid = w.get_xid() 244 if (w.get_window_type() == Gdk.WindowType.FOREIGN 245 and not X11Window.is_override_redirect(xid) 246 and X11Window.is_mapped(xid)): 247 log("Wm managing pre-existing child window %#x", xid) 248 self._manage_client(w) 249 250 # Also watch for focus change events on the root window 251 X11Window.selectFocusChange(rxid) 252 X11Keyboard.selectBellNotification(True) 253 254 # FIXME: 255 # Need viewport abstraction for _NET_CURRENT_DESKTOP... 256 # Tray's need to provide info for _NET_ACTIVE_WINDOW and _NET_WORKAREA 257 # (and notifications for both) 258 259 def root_set(self, *args): 260 prop_set(self._root, *args) 261 262 def root_get(self, *args): 263 return prop_get(self._root, *args) 264 265 def set_dpi(self, xdpi, ydpi): 266 #this is used by some newer versions of the dummy driver (xf86-driver-dummy) 267 #(and will not be honoured by anything else..) 268 self.root_set("dummy-constant-xdpi", "u32", xdpi) 269 self.root_set("dummy-constant-ydpi", "u32", ydpi) 270 screenlog("set_dpi(%i, %i)", xdpi, ydpi) 271 272 def set_workarea(self, x, y, width, height): 273 v = [x, y, width, height] 274 screenlog("_NET_WORKAREA=%s", v) 275 self.root_set("_NET_WORKAREA", ["u32"], v) 276 277 def set_desktop_geometry(self, width, height): 278 v = [width, height] 279 screenlog("_NET_DESKTOP_GEOMETRY=%s", v) 280 self.root_set("_NET_DESKTOP_GEOMETRY", ["u32"], v) 281 #update all the windows: 282 for model in self._windows.values(): 283 model.update_desktop_geometry(width, height) 284 285 def set_size_constraints(self, minw=0, minh=0, maxw=MAX_WINDOW_SIZE, maxh=MAX_WINDOW_SIZE): 286 log("set_size_constraints%s", (minw, minh, maxw, maxh)) 287 self.size_constraints = minw, minh, maxw, maxh 288 #update all the windows: 289 for model in self._windows.values(): 290 model.update_size_constraints(minw, minh, maxw, maxh) 291 292 293 def set_default_frame_extents(self, v): 294 framelog("set_default_frame_extents(%s)", v) 295 if not v or len(v)!=4: 296 v = (0, 0, 0, 0) 297 self.root_set("DEFAULT_NET_FRAME_EXTENTS", ["u32"], v) 298 #update the models that are using the global default value: 299 for win in self._windows.values(): 300 if win.is_OR() or win.is_tray(): 301 continue 302 cur = win.get_property("frame") 303 if cur is None: 304 win._handle_frame_changed() 305 306 307 def do_get_property(self, pspec): 308 if pspec.name == "windows": 309 return frozenset(self._windows.values()) 310 if pspec.name == "toplevel": 311 return self._world_window 312 assert False 313 314 # This is in some sense the key entry point to the entire WM program. We 315 # have detected a new client window, and start managing it: 316 def _manage_client(self, gdkwindow): 317 if not gdkwindow: 318 return 319 if gdkwindow in self._windows: 320 #already managed 321 return 322 try: 323 with xsync: 324 log("_manage_client(%s)", gdkwindow) 325 desktop_geometry = self.root_get("_NET_DESKTOP_GEOMETRY", ["u32"], True, False) 326 win = WindowModel(self._root, gdkwindow, desktop_geometry, self.size_constraints) 327 except Exception as e: 328 if LOG_MANAGE_FAILURES or not isinstance(e, Unmanageable): 329 l = log.warn 330 else: 331 l = log 332 l("Warning: failed to manage client window %#x:", gdkwindow.get_xid()) 333 l(" %s", e) 334 l("", exc_info=True) 335 with xswallow: 336 l(" window name: %s", window_name(gdkwindow)) 337 l(" window info: %s", window_info(gdkwindow)) 338 else: 339 win.managed_connect("unmanaged", self._handle_client_unmanaged) 340 self._windows[gdkwindow] = win 341 self._windows_in_order.append(gdkwindow) 342 self.notify("windows") 343 self._update_window_list() 344 self.emit("new-window", win) 345 346 def _handle_client_unmanaged(self, window, _wm_exiting): 347 gdkwindow = window.get_property("client-window") 348 assert gdkwindow in self._windows 349 del self._windows[gdkwindow] 350 self._windows_in_order.remove(gdkwindow) 351 self._update_window_list() 352 self.notify("windows") 353 354 def _update_window_list(self, *_args): 355 # Ignore errors because not all the windows may still exist; if so, 356 # then it's okay to leave the lists out of date for a moment, because 357 # in a moment we'll get a signal telling us about the window that 358 # doesn't exist anymore, will remove it from the list, and then call 359 # _update_window_list again. 360 with xswallow: 361 self.root_set("_NET_CLIENT_LIST", ["window"], self._windows_in_order) 362 # This is a lie, but we don't maintain a stacking order, so... 363 self.root_set("_NET_CLIENT_LIST_STACKING", ["window"], self._windows_in_order) 364 365 def do_xpra_client_message_event(self, event): 366 # FIXME 367 # Need to listen for: 368 # _NET_ACTIVE_WINDOW 369 # _NET_CURRENT_DESKTOP 370 # _NET_WM_PING responses 371 # and maybe: 372 # _NET_WM_STATE 373 log("do_xpra_client_message_event(%s)", event) 374 if event.message_type=="_NET_SHOWING_DESKTOP": 375 show = bool(event.data[0]) 376 self.emit("show-desktop", show) 377 elif event.message_type=="_NET_REQUEST_FRAME_EXTENTS" and FRAME_EXTENTS: 378 #if we're here, that means the window model does not exist 379 #(or it would have processed the event) 380 #so this must be a an unmapped window 381 frame = (0, 0, 0, 0) 382 with xswallow: 383 if not X11Window.is_override_redirect(event.window.get_xid()): 384 #use the global default: 385 frame = prop_get(self._root, "DEFAULT_NET_FRAME_EXTENTS", ["u32"], ignore_errors=True) 386 if not frame: 387 #fallback: 388 frame = (0, 0, 0, 0) 389 framelog("_NET_REQUEST_FRAME_EXTENTS: setting _NET_FRAME_EXTENTS=%s on %#x", 390 frame, event.window.get_xid()) 391 prop_set(event.window, "_NET_FRAME_EXTENTS", ["u32"], frame) 392 393 def _lost_wm_selection(self, selection): 394 log.info("Lost WM selection %s, exiting", selection) 395 self.emit("quit") 396 397 def do_quit(self): 398 self.cleanup() 399 400 def cleanup(self): 401 remove_fallback_receiver("xpra-client-message-event", self) 402 remove_fallback_receiver("child-map-request-event", self) 403 for win in tuple(self._windows.values()): 404 win.unmanage(True) 405 with xswallow: 406 prop_del(self._ewmh_window, "_NET_SUPPORTING_WM_CHECK") 407 prop_del(self._ewmh_window, "_NET_WM_NAME") 408 destroy_world_window() 409 410 411 def do_child_map_request_event(self, event): 412 log("Found a potential client") 413 self._manage_client(event.window) 414 415 def do_child_configure_request_event(self, event): 416 # The point of this method is to handle configure requests on 417 # withdrawn windows. We simply allow them to move/resize any way they 418 # want. This is harmless because the window isn't visible anyway (and 419 # apps can create unmapped windows with whatever coordinates they want 420 # anyway, no harm in letting them move existing ones around), and it 421 # means that when the window actually gets mapped, we have more 422 # accurate info on what the app is actually requesting. 423 model = self._windows.get(event.window) 424 if model: 425 #the window has been reparented already, 426 #but we're getting the configure request event on the root window 427 #forward it to the model 428 log("do_child_configure_request_event(%s) value_mask=%s, forwarding to %s", 429 event, configure_bits(event.value_mask), model) 430 model.do_child_configure_request_event(event) 431 return 432 log("do_child_configure_request_event(%s) value_mask=%s, reconfigure on withdrawn window", 433 event, configure_bits(event.value_mask)) 434 with xswallow: 435 xid = event.window.get_xid() 436 x, y, w, h = X11Window.getGeometry(xid)[:4] 437 if event.value_mask & CWX: 438 x = event.x 439 if event.value_mask & CWY: 440 y = event.y 441 if event.value_mask & CWWidth: 442 w = event.width 443 if event.value_mask & CWHeight: 444 h = event.height 445 if event.value_mask & (CWX | CWY | CWWidth | CWHeight): 446 log("updated window geometry for window %#x from %s to %s", 447 xid, X11Window.getGeometry(xid)[:4], (x, y, w, h)) 448 X11Window.configureAndNotify(xid, x, y, w, h, event.value_mask) 449 450 def do_xpra_focus_in_event(self, event): 451 # The purpose of this function is to detect when the focus mode has 452 # gone to PointerRoot or None, so that it can be given back to 453 # something real. This is easy to detect -- a FocusIn event with 454 # detail PointerRoot or None is generated on the root window. 455 focuslog("wm.do_xpra_focus_in_event(%s)", event) 456 if event.detail in (NotifyPointerRoot, NotifyDetailNone) and self._world_window: 457 self._world_window.reset_x_focus() 458 459 def do_xpra_focus_out_event(self, event): 460 focuslog("wm.do_xpra_focus_out_event(%s) XGetInputFocus=%s", event, X11Window.XGetInputFocus()) 461 462 def set_desktop_list(self, desktops): 463 log("set_desktop_list(%s)", desktops) 464 self.root_set("_NET_NUMBER_OF_DESKTOPS", "u32", len(desktops)) 465 self.root_set("_NET_DESKTOP_NAMES", ["utf8"], desktops) 466 467 def set_current_desktop(self, index): 468 self.root_set("_NET_CURRENT_DESKTOP", "u32", index) 469 470 def _setup_ewmh_window(self): 471 # Set up a 1x1 invisible unmapped window, with which to participate in 472 # EWMH's _NET_SUPPORTING_WM_CHECK protocol. The only important things 473 # about this window are the _NET_SUPPORTING_WM_CHECK property, and 474 # its title (which is supposed to be the name of the window manager). 475 476 # NB, GDK will do strange things to this window. We don't want to use 477 # it for anything. (In particular, it will call XSelectInput on it, 478 # which is fine normally when GDK is running in a client, but since it 479 # happens to be using the same connection as we the WM, it will 480 # clobber any XSelectInput calls that *we* might have wanted to make 481 # on this window.) Also, GDK might silently swallow all events that 482 # are detected on it, anyway. 483 self._ewmh_window = GDKX11Window(self._root, wclass=Gdk.WindowWindowClass.INPUT_ONLY, title=self._wm_name) 484 prop_set(self._ewmh_window, "_NET_SUPPORTING_WM_CHECK", 485 "window", self._ewmh_window) 486 self.root_set("_NET_SUPPORTING_WM_CHECK", "window", self._ewmh_window) 487 self.root_set("_NET_WM_NAME", "utf8", self._wm_name) 488 489 def get_net_wm_name(self): 490 try: 491 return prop_get(self._ewmh_window, "_NET_WM_NAME", "utf8", ignore_errors=False, raise_xerrors=False) 492 except Exception as e: 493 log.error("error querying _NET_WM_NAME: %s", e) 494 495GObject.type_register(Wm) 496