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