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