1# This file is part of Xpra.
2# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
3# Copyright (C) 2011-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
7from gi.repository import GObject, Gtk, Gdk
8
9from xpra.util import envint, envbool, typedict
10from xpra.common import MAX_WINDOW_SIZE
11from xpra.gtk_common.gobject_util import one_arg_signal, non_none_list_accumulator, SIGNAL_RUN_LAST
12from xpra.gtk_common.error import XError, XSwallowContext, xlog
13from xpra.x11.gtk_x11 import GDKX11Window
14from xpra.x11.gtk_x11.send_wm import send_wm_take_focus
15from xpra.x11.gtk_x11.prop import prop_set, prop_get
16from xpra.x11.prop_conv import MotifWMHints
17from xpra.x11.bindings.window_bindings import X11WindowBindings #@UnresolvedImport
18from xpra.x11.common import Unmanageable
19from xpra.x11.models.size_hints_util import sanitize_size_hints
20from xpra.x11.models.base import BaseWindowModel, constants
21from xpra.x11.models.core import sanestr, xswallow, xsync
22from xpra.x11.gtk_x11.gdk_bindings import (
23    add_event_receiver, remove_event_receiver,
24    get_children,
25    calc_constrained_size,
26    x11_get_server_time,
27    )
28
29from xpra.gtk_common.gtk_util import get_default_root_window
30from xpra.log import Logger
31
32log = Logger("x11", "window")
33workspacelog = Logger("x11", "window", "workspace")
34shapelog = Logger("x11", "window", "shape")
35grablog = Logger("x11", "window", "grab")
36metalog = Logger("x11", "window", "metadata")
37iconlog = Logger("x11", "window", "icon")
38focuslog = Logger("x11", "window", "focus")
39geomlog = Logger("x11", "window", "geometry")
40
41
42X11Window = X11WindowBindings()
43
44IconicState = constants["IconicState"]
45NormalState = constants["NormalState"]
46
47CWX             = constants["CWX"]
48CWY             = constants["CWY"]
49CWWidth         = constants["CWWidth"]
50CWHeight        = constants["CWHeight"]
51CWBorderWidth   = constants["CWBorderWidth"]
52CWSibling       = constants["CWSibling"]
53CWStackMode     = constants["CWStackMode"]
54CONFIGURE_GEOMETRY_MASK = CWX | CWY | CWWidth | CWHeight
55CW_MASK_TO_NAME = {
56                   CWX              : "X",
57                   CWY              : "Y",
58                   CWWidth          : "Width",
59                   CWHeight         : "Height",
60                   CWBorderWidth    : "BorderWidth",
61                   CWSibling        : "Sibling",
62                   CWStackMode      : "StackMode",
63                   CWBorderWidth    : "BorderWidth",
64                   }
65def configure_bits(value_mask):
66    return "|".join(v for k,v in CW_MASK_TO_NAME.items() if k&value_mask)
67
68
69FORCE_XSETINPUTFOCUS = envbool("XPRA_FORCE_XSETINPUTFOCUS", True)
70VALIDATE_CONFIGURE_REQUEST = envbool("XPRA_VALIDATE_CONFIGURE_REQUEST", False)
71CLAMP_OVERLAP = envint("XPRA_WINDOW_CLAMP_OVERLAP", 20)
72assert CLAMP_OVERLAP>=0
73
74
75class WindowModel(BaseWindowModel):
76    """This represents a managed client window.  It allows one to produce
77    widgets that view that client window in various ways."""
78
79    _NET_WM_ALLOWED_ACTIONS = ["_NET_WM_ACTION_%s" % x for x in (
80        "CLOSE", "MOVE", "RESIZE", "FULLSCREEN",
81        "MINIMIZE", "SHADE", "STICK",
82        "MAXIMIZE_HORZ", "MAXIMIZE_VERT",
83        "CHANGE_DESKTOP", "ABOVE", "BELOW")]
84
85    __gproperties__ = dict(BaseWindowModel.__common_properties__)
86    __gproperties__.update({
87        "owner": (GObject.TYPE_PYOBJECT,
88                  "Owner", "",
89                  GObject.ParamFlags.READABLE),
90        # Interesting properties of the client window, that will be
91        # automatically kept up to date:
92        "requested-position": (GObject.TYPE_PYOBJECT,
93                               "Client-requested position on screen", "",
94                               GObject.ParamFlags.READABLE),
95        "requested-size": (GObject.TYPE_PYOBJECT,
96                           "Client-requested size on screen", "",
97                           GObject.ParamFlags.READABLE),
98        "set-initial-position": (GObject.TYPE_BOOLEAN,
99                                 "Should the requested position be honoured?", "",
100                                 False,
101                                 GObject.ParamFlags.READWRITE),
102        # Toggling this property does not actually make the window iconified,
103        # i.e. make it appear or disappear from the screen -- it merely
104        # updates the various window manager properties that inform the world
105        # whether or not the window is iconified.
106        "iconic": (GObject.TYPE_BOOLEAN,
107                   "ICCCM 'iconic' state -- any sort of 'not on desktop'.", "",
108                   False,
109                   GObject.ParamFlags.READWRITE),
110        #from WM_NORMAL_HINTS
111        "size-hints": (GObject.TYPE_PYOBJECT,
112                       "Client hints on constraining its size", "",
113                       GObject.ParamFlags.READABLE),
114        #from _NET_WM_ICON_NAME or WM_ICON_NAME
115        "icon-title": (GObject.TYPE_PYOBJECT,
116                       "Icon title (unicode or None)", "",
117                       GObject.ParamFlags.READABLE),
118        #from _NET_WM_ICON
119        "icons": (GObject.TYPE_PYOBJECT,
120                 "Icons in raw RGBA format, by size", "",
121                 GObject.ParamFlags.READABLE),
122        #from _MOTIF_WM_HINTS.decorations
123        "decorations": (GObject.TYPE_INT,
124                       "Should the window decorations be shown", "",
125                       -1, 65535, -1,
126                       GObject.ParamFlags.READABLE),
127        "children" : (GObject.TYPE_PYOBJECT,
128                        "Sub-windows", None,
129                        GObject.ParamFlags.READABLE),
130        })
131    __gsignals__ = dict(BaseWindowModel.__common_signals__)
132    __gsignals__.update({
133        "ownership-election"            : (SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (), non_none_list_accumulator),
134        "child-map-request-event"       : one_arg_signal,
135        "child-configure-request-event" : one_arg_signal,
136        "xpra-destroy-event"            : one_arg_signal,
137        })
138
139    _property_names         = BaseWindowModel._property_names + [
140                              "size-hints", "icon-title", "icons", "decorations",
141                              "modal", "set-initial-position", "iconic",
142                              ]
143    _dynamic_property_names = BaseWindowModel._dynamic_property_names + [
144                              "size-hints", "icon-title", "icons", "decorations", "modal", "iconic"]
145    _initial_x11_properties = BaseWindowModel._initial_x11_properties + [
146                              "WM_HINTS", "WM_NORMAL_HINTS", "_MOTIF_WM_HINTS",
147                              "WM_ICON_NAME", "_NET_WM_ICON_NAME", "_NET_WM_ICON",
148                              "_NET_WM_STRUT", "_NET_WM_STRUT_PARTIAL"]
149    _internal_property_names = BaseWindowModel._internal_property_names+["children"]
150    _MODELTYPE = "Window"
151
152    def __init__(self, parking_window, client_window, desktop_geometry, size_constraints=None):
153        """Register a new client window with the WM.
154
155        Raises an Unmanageable exception if this window should not be
156        managed, for whatever reason.  ATM, this mostly means that the window
157        died somehow before we could do anything with it."""
158
159        super().__init__(client_window)
160        self.parking_window = parking_window
161        self.corral_window = None
162        self.desktop_geometry = desktop_geometry
163        self.size_constraints = size_constraints or (0, 0, MAX_WINDOW_SIZE, MAX_WINDOW_SIZE)
164        #extra state attributes so we can unmanage() the window cleanly:
165        self.in_save_set = False
166        self.client_reparented = False
167        self.kill_count = 0
168
169        self.call_setup()
170
171    #########################################
172    # Setup and teardown
173    #########################################
174
175    def setup(self):
176        super().setup()
177
178        ogeom = self.client_window.get_geometry()
179        ox, oy, ow, oh = ogeom[:4]
180        # We enable PROPERTY_CHANGE_MASK so that we can call
181        # x11_get_server_time on this window.
182        # clamp this window to the desktop size:
183        x, y = self._clamp_to_desktop(ox, oy, ow, oh)
184        geomlog("setup() clamp_to_desktop(%s)=%s", ogeom, (x, y))
185        self.corral_window = GDKX11Window(self.parking_window,
186                                        x=x, y=y, width=ow, height=oh,
187                                        window_type=Gdk.WindowType.CHILD,
188                                        event_mask=Gdk.EventMask.PROPERTY_CHANGE_MASK,
189                                        title = "CorralWindow-%#x" % self.xid)
190        cxid = self.corral_window.get_xid()
191        log("setup() corral_window=%#x", cxid)
192        prop_set(self.corral_window, "_NET_WM_NAME", "utf8", "Xpra-CorralWindow-%#x" % self.xid)
193        X11Window.substructureRedirect(cxid)
194        add_event_receiver(self.corral_window, self)
195
196        # The child might already be mapped, in case we inherited it from
197        # a previous window manager.  If so, we unmap it now, and save the
198        # serial number of the request -- this way, when we get an
199        # UnmapNotify later, we'll know that it's just from us unmapping
200        # the window, not from the client withdrawing the window.
201        if X11Window.is_mapped(self.xid):
202            log("hiding inherited window")
203            self.last_unmap_serial = X11Window.Unmap(self.xid)
204
205        log("setup() adding to save set")
206        X11Window.XAddToSaveSet(self.xid)
207        self.in_save_set = True
208
209        log("setup() reparenting")
210        X11Window.Reparent(self.xid, cxid, 0, 0)
211        self.client_reparented = True
212
213        geom = X11Window.geometry_with_border(self.xid)
214        if geom is None:
215            raise Unmanageable("window %#x disappeared already" % self.xid)
216        geomlog("setup() geometry=%s, ogeom=%s", geom, ogeom)
217        nx, ny, w, h = geom[:4]
218        #after reparenting, the coordinates of the client window should be 0,0
219        #use the coordinates of the corral window:
220        if nx==ny==0:
221            nx, ny = x, y
222        hints = self.get_property("size-hints")
223        geomlog("setup() hints=%s size=%ix%i", hints, w, h)
224        nw, nh = self.calc_constrained_size(w, h, hints)
225        pos = hints.get("position")
226        if pos==(0, 0) and (nx!=0 or ny!=0):
227            #never override with 0,0
228            hints.pop("position")
229            pos = None
230        if ox==0 and oy==0 and pos:
231            nx, ny = pos
232        self._updateprop("geometry", (nx, ny, nw, nh))
233        geomlog("setup() resizing windows to %sx%s, moving to %i,%i", nw, nh, nx, ny)
234        #don't trigger a move or resize unless we have to:
235        if (ox,oy)!=(nx,ny) and (ow,oh)!=(nw,nh):
236            self.corral_window.move_resize(nx, ny, nw, nh)
237        elif (ox,oy)!=(nx,ny):
238            self.corral_window.move(nx, ny)
239        elif (ow,oh)!=(nw,nh):
240            self.corral_window.resize(nw, nh)
241        if (ow,oh)!=(nw,nh):
242            self.client_window.resize(nw, nh)
243        self.client_window.show_unraised()
244        #this is here to trigger X11 errors if any are pending
245        #or if the window is deleted already:
246        self.client_window.get_geometry()
247
248
249    def _clamp_to_desktop(self, x, y, w, h):
250        if self.desktop_geometry:
251            dw, dh = self.desktop_geometry
252            if x+w<0:
253                x = min(0, CLAMP_OVERLAP-w)
254            elif x>=dw:
255                x = max(0, dw-CLAMP_OVERLAP)
256            if y+h<0:
257                y = min(0, CLAMP_OVERLAP-h)
258            elif y>dh:
259                y = max(0, dh-CLAMP_OVERLAP)
260        return x, y
261
262    def update_desktop_geometry(self, width, height):
263        if self.desktop_geometry==(width, height):
264            return  #no need to do anything
265        self.desktop_geometry = (width, height)
266        x, y, w, h = self.corral_window.get_geometry()[:4]
267        nx, ny = self._clamp_to_desktop(x, y, w, h)
268        if nx!=x or ny!=y:
269            log("update_desktop_geometry(%i, %i) adjusting corral window to new location: %i,%i", width, height, nx, ny)
270            self.corral_window.move(nx, ny)
271
272
273    def _read_initial_X11_properties(self):
274        metalog("read_initial_X11_properties() window")
275        # WARNING: have to handle _NET_WM_STATE before we look at WM_HINTS;
276        # WM_HINTS assumes that our "state" property is already set.  This is
277        # because there are four ways a window can get its urgency
278        # ("attention-requested") bit set:
279        #   1) _NET_WM_STATE_DEMANDS_ATTENTION in the _initial_ state hints
280        #   2) setting the bit WM_HINTS, at _any_ time
281        #   3) sending a request to the root window to add
282        #      _NET_WM_STATE_DEMANDS_ATTENTION to their state hints
283        #   4) if we (the wm) decide they should be and set it
284        # To implement this, we generally track the urgency bit via
285        # _NET_WM_STATE (since that is under our sole control during normal
286        # operation).  Then (1) is accomplished through the normal rule that
287        # initial states are read off from the client, and (2) is accomplished
288        # by having WM_HINTS affect _NET_WM_STATE.  But this means that
289        # WM_HINTS and _NET_WM_STATE handling become intertangled.
290        def set_if_unset(propname, value):
291            #the property may not be initialized yet,
292            #if that's the case then calling get_property throws an exception:
293            try:
294                if self.get_property(propname) not in (None, ""):
295                    return
296            except TypeError:
297                pass
298            self._internal_set_property(propname, value)
299        #"decorations" needs to be set before reading the X11 properties
300        #because handle_wm_normal_hints_change reads it:
301        set_if_unset("decorations", -1)
302        super()._read_initial_X11_properties()
303        net_wm_state = self.get_property("state")
304        assert net_wm_state is not None, "_NET_WM_STATE should have been read already"
305        geom = X11Window.getGeometry(self.xid)
306        if not geom:
307            raise Unmanageable("failed to get geometry for %#x" % self.xid)
308        #initial position and size, from the Window object,
309        #but allow size hints to override it if specified
310        x, y, w, h = geom[:4]
311        size_hints = self.get_property("size-hints")
312        ax, ay = size_hints.get("position", (0, 0))
313        if ax==ay==0 and (x!=0 or y!=0):
314            #don't override with 0,0
315            size_hints.pop("position", None)
316            ax, ay = x, y
317        aw, ah = size_hints.get("size", (w, h))
318        geomlog("initial X11 position and size: requested(%s, %s, %s)=%s",
319                (x, y, w, h), size_hints, geom, (ax, ay, aw, ah))
320        set_if_unset("modal", "_NET_WM_STATE_MODAL" in net_wm_state)
321        set_if_unset("requested-position", (ax, ay))
322        set_if_unset("requested-size", (aw, ah))
323        #it may have been set already:
324        sip = self.get_property("set-initial-position")
325        if not sip and ("position" in size_hints):
326            self._internal_set_property("set-initial-position", True)
327        elif sip is None:
328            self._internal_set_property("set-initial-position", False)
329        self.update_children()
330
331    def do_unmanaged(self, wm_exiting):
332        log("unmanaging window: %s (%s - %s)", self, self.corral_window, self.client_window)
333        self._internal_set_property("owner", None)
334        cwin = self.corral_window
335        if cwin:
336            self.corral_window = None
337            remove_event_receiver(cwin, self)
338            geom = None
339            #use a new context so we will XSync right here
340            #and detect if the window is already gone:
341            with XSwallowContext():
342                geom = X11Window.getGeometry(self.xid)
343            if geom is not None:
344                if self.client_reparented:
345                    self.client_window.reparent(get_default_root_window(), 0, 0)
346                self.client_window.set_events(self.client_window_saved_events)
347            self.client_reparented = False
348            # It is important to remove from our save set, even after
349            # reparenting, because according to the X spec, windows that are
350            # in our save set are always Mapped when we exit, *even if those
351            # windows are no longer inferior to any of our windows!* (see
352            # section 10. Connection Close).  This causes "ghost windows", see
353            # bug #27:
354            if self.in_save_set:
355                with xswallow:
356                    X11Window.XRemoveFromSaveSet(self.xid)
357                self.in_save_set = False
358            with xswallow:
359                X11Window.sendConfigureNotify(self.xid)
360            if wm_exiting:
361                self.client_window.show_unraised()
362            #it is now safe to destroy the corral window:
363            cwin.destroy()
364        super().do_unmanaged(wm_exiting)
365
366
367    #########################################
368    # Actions specific to WindowModel
369    #########################################
370
371    def raise_window(self):
372        X11Window.XRaiseWindow(self.corral_window.get_xid())
373        X11Window.XRaiseWindow(self.client_window.get_xid())
374
375    def unmap(self):
376        with xsync:
377            if X11Window.is_mapped(self.xid):
378                self.last_unmap_serial = X11Window.Unmap(self.xid)
379                log("client window %#x unmapped, serial=%#x", self.xid, self.last_unmap_serial)
380
381    def map(self):
382        with xsync:
383            if not X11Window.is_mapped(self.xid):
384                X11Window.MapWindow(self.xid)
385                log("client window %#x mapped", self.xid)
386
387
388    #########################################
389    # X11 Events
390    #########################################
391
392    def do_xpra_property_notify_event(self, event):
393        if event.delivered_to is self.corral_window:
394            return
395        super().do_xpra_property_notify_event(event)
396
397    def do_child_map_request_event(self, event):
398        # If we get a MapRequest then it might mean that someone tried to map
399        # this window multiple times in quick succession, before we actually
400        # mapped it (so that several MapRequests ended up queued up; FSF Emacs
401        # 22.1.50.1 does this, at least).  It alternatively might mean that
402        # the client is naughty and tried to map their window which is
403        # currently not displayed.  In either case, we should just ignore the
404        # request.
405        log("do_child_map_request_event(%s)", event)
406
407    def do_xpra_unmap_event(self, event):
408        if event.delivered_to is self.corral_window or self.corral_window is None:
409            return
410        assert event.window is self.client_window
411        # The client window got unmapped.  The question is, though, was that
412        # because it was withdrawn/destroyed, or was it because we unmapped it
413        # going into IconicState?
414        #
415        # Also, if we receive a *synthetic* UnmapNotify event, that always
416        # means that the client has withdrawn the window (even if it was not
417        # mapped in the first place) -- ICCCM section 4.1.4.
418        log("do_xpra_unmap_event(%s) client window unmapped, last_unmap_serial=%#x", event, self.last_unmap_serial)
419        if event.send_event or self.serial_after_last_unmap(event.serial):
420            self.unmanage()
421
422    def do_xpra_destroy_event(self, event):
423        if event.delivered_to is self.corral_window or self.corral_window is None:
424            return
425        assert event.window is self.client_window
426        super().do_xpra_destroy_event(event)
427
428
429    #########################################
430    # Hooks for WM
431    #########################################
432
433    def ownership_election(self):
434        #returns True if we have updated the geometry
435        candidates = self.emit("ownership-election")
436        if candidates:
437            rating, winner = sorted(candidates)[-1]
438            if rating < 0:
439                winner = None
440        else:
441            winner = None
442        old_owner = self.get_property("owner")
443        log("ownership_election() winner=%s, old owner=%s, candidates=%s", winner, old_owner, candidates)
444        if old_owner is winner:
445            return False
446        if old_owner is not None:
447            self.corral_window.hide()
448            self.corral_window.reparent(self.parking_window, 0, 0)
449        self._internal_set_property("owner", winner)
450        if winner is not None:
451            winner.take_window(self, self.corral_window)
452            self._update_client_geometry()
453            self.corral_window.show_unraised()
454            return True
455        with xswallow:
456            X11Window.sendConfigureNotify(self.xid)
457        return False
458
459    def maybe_recalculate_geometry_for(self, maybe_owner):
460        if maybe_owner and self.get_property("owner") is maybe_owner:
461            self._update_client_geometry()
462
463    def _update_client_geometry(self):
464        """ figure out where we're supposed to get the window geometry from,
465            and call do_update_client_geometry which will send a Configure and Notify
466        """
467        owner = self.get_property("owner")
468        if owner is not None:
469            geomlog("_update_client_geometry: using owner=%s (setup_done=%s)", owner, self._setup_done)
470            def window_size():
471                return  owner.window_size(self)
472            def window_position(w, h):
473                return  owner.window_position(self, w, h)
474        elif not self._setup_done:
475            #try to honour initial size and position requests during setup:
476            def window_size():
477                return self.get_property("requested-size")
478            def window_position(_w, _h):
479                return self.get_property("requested-position")
480            geomlog("_update_client_geometry: using initial size=%s and position=%s", window_size, window_position)
481        else:
482            geomlog("_update_client_geometry: ignored, owner=%s, setup_done=%s", owner, self._setup_done)
483            def window_size():
484                return self.get_property("geometry")[2:4]
485            def window_position(_w, _h):
486                return self.get_property("geometry")[:2]
487        self._do_update_client_geometry(window_size, window_position)
488
489
490    def _do_update_client_geometry(self, window_size_cb, window_position_cb):
491        allocated_w, allocated_h = window_size_cb()
492        geomlog("_do_update_client_geometry: allocated %ix%i (from %s)", allocated_w, allocated_h, window_size_cb)
493        hints = self.get_property("size-hints")
494        w, h = self.calc_constrained_size(allocated_w, allocated_h, hints)
495        geomlog("_do_update_client_geometry: size(%s)=%ix%i", hints, w, h)
496        x, y = window_position_cb(w, h)
497        geomlog("_do_update_client_geometry: position=%ix%i (from %s)", x, y, window_position_cb)
498        self.corral_window.move_resize(x, y, w, h)
499        self._updateprop("geometry", (x, y, w, h))
500        with xswallow:
501            X11Window.configureAndNotify(self.xid, 0, 0, w, h)
502
503    def do_xpra_configure_event(self, event):
504        cxid = self.corral_window.get_xid()
505        geomlog("WindowModel.do_xpra_configure_event(%s) corral=%#x, client=%#x, managed=%s",
506                event, cxid, self.xid, self._managed)
507        if not self._managed:
508            return
509        if event.window==self.corral_window:
510            #we only care about events on the client window
511            geomlog("WindowModel.do_xpra_configure_event: event is on the corral window %#x, ignored", cxid)
512            return
513        if event.window!=self.client_window:
514            #we only care about events on the client window
515            geomlog("WindowModel.do_xpra_configure_event: event is not on the client window but on %#x, ignored",
516                    event.window.get_xid())
517            return
518        if self.corral_window is None or not self.corral_window.is_visible():
519            geomlog("WindowModel.do_xpra_configure_event: corral window is not visible")
520            return
521        if self.client_window is None or not self.client_window.is_visible():
522            geomlog("WindowModel.do_xpra_configure_event: client window is not visible")
523            return
524        try:
525            #workaround applications whose windows disappear from underneath us:
526            with xsync:
527                #event.border_width unused
528                self.resize_corral_window(event.x, event.y, event.width, event.height)
529                self.update_children()
530        except XError as e:
531            geomlog("do_xpra_configure_event(%s)", event, exc_info=True)
532            geomlog.warn("Warning: failed to resize corral window %#x", cxid)
533            geomlog.warn(" %s", e)
534
535    def update_children(self):
536        ww, wh = self.client_window.get_geometry()[2:4]
537        children = []
538        for w in get_children(self.client_window):
539            xid = w.get_xid()
540            if X11Window.is_inputonly(xid):
541                continue
542            geom = X11Window.getGeometry(xid)
543            if not geom:
544                continue
545            if geom[2]==geom[3]==1:
546                #skip 1x1 windows, as those are usually just event windows
547                continue
548            if geom[0]==geom[1]==0 and geom[2]==ww and geom[3]==wh:
549                #exact same geometry as the window itself
550                continue
551            #record xid and geometry:
552            children.append([xid]+list(geom))
553        self._internal_set_property("children", children)
554
555    def resize_corral_window(self, x : int, y : int, w : int, h : int):
556        #the client window may have been resized or moved (generally programmatically)
557        #so we may need to update the corral_window to match
558        cox, coy, cow, coh = self.corral_window.get_geometry()[:4]
559        #size changes (and position if any):
560        hints = self.get_property("size-hints")
561        w, h = self.calc_constrained_size(w, h, hints)
562        cx, cy, cw, ch = self.get_property("geometry")
563        resized = cow!=w or coh!=h
564        moved = x!=0 or y!=0
565        geomlog("resize_corral_window%s hints=%s, constrained size=%s, geometry=%s, resized=%s, moved=%s",
566                (x, y, w, h), hints, (w, h), (cx, cy, cw, ch), resized, moved)
567        if resized:
568            if moved:
569                self._internal_set_property("set-initial-position", True)
570                geomlog("resize_corral_window() move and resize from %s to %s", (cox, coy, cow, coh), (x, y, w, h))
571                self.corral_window.move_resize(x, y, w, h)
572                self.client_window.move(0, 0)
573                self._updateprop("geometry", (x, y, w, h))
574            else:
575                geomlog("resize_corral_window() resize from %s to %s", (cow, coh), (w, h))
576                self.corral_window.resize(w, h)
577                self._updateprop("geometry", (cx, cy, w, h))
578        elif moved:
579            self._internal_set_property("set-initial-position", True)
580            geomlog("resize_corral_window() moving corral window from %s to %s", (cox, coy), (x, y))
581            self.corral_window.move(x, y)
582            self.client_window.move(0, 0)
583            self._updateprop("geometry", (x, y, cw, ch))
584
585    def do_child_configure_request_event(self, event):
586        cxid = self.corral_window.get_xid()
587        hints = self.get_property("size-hints")
588        geomlog("do_child_configure_request_event(%s) client=%#x, corral=%#x, value_mask=%s, size-hints=%s",
589                event, self.xid, cxid, configure_bits(event.value_mask), hints)
590        if event.value_mask & CWStackMode:
591            geomlog(" restack above=%s, detail=%s", event.above, event.detail)
592        # Also potentially update our record of what the app has requested:
593        ogeom = self.get_property("geometry")
594        x, y, w, h = ogeom[:4]
595        rx, ry = self.get_property("requested-position")
596        if event.value_mask & CWX:
597            x = event.x
598            rx = x
599        if event.value_mask & CWY:
600            y = event.y
601            ry = y
602        if event.value_mask & CWX or event.value_mask & CWY:
603            self._internal_set_property("set-initial-position", True)
604            self._updateprop("requested-position", (rx, ry))
605
606        rw, rh = self.get_property("requested-size")
607        if event.value_mask & CWWidth:
608            w = event.width
609            rw = w
610        if event.value_mask & CWHeight:
611            h = event.height
612            rh = h
613        if event.value_mask & CWWidth or event.value_mask & CWHeight:
614            self._updateprop("requested-size", (rw, rh))
615
616        if event.value_mask & CWStackMode:
617            self.emit("restack", event.detail, event.above)
618
619        if VALIDATE_CONFIGURE_REQUEST:
620            w, h = self.calc_constrained_size(w, h, hints)
621        #update the geometry now, as another request may come in
622        #before we've had a chance to process the ConfigureNotify that the code below will generate
623        self._updateprop("geometry", (x, y, w, h))
624        geomlog("do_child_configure_request_event updated requested geometry from %s to  %s", ogeom, (x, y, w, h))
625        # As per ICCCM 4.1.5, even if we ignore the request
626        # send back a synthetic ConfigureNotify telling the client that nothing has happened.
627        with xswallow:
628            X11Window.configureAndNotify(self.xid, x, y, w, h, event.value_mask)
629        # FIXME: consider handling attempts to change stacking order here.
630        # (In particular, I believe that a request to jump to the top is
631        # meaningful and should perhaps even be respected.)
632
633    def process_client_message_event(self, event):
634        if event.message_type=="_NET_MOVERESIZE_WINDOW":
635            #TODO: honour gravity, show source indication
636            geom = self.corral_window.get_geometry()
637            x, y, w, h, _ = geom
638            if event.data[0] & 0x100:
639                x = event.data[1]
640            if event.data[0] & 0x200:
641                y = event.data[2]
642            if event.data[0] & 0x400:
643                w = event.data[3]
644            if event.data[0] & 0x800:
645                h = event.data[4]
646            self._internal_set_property("set-initial-position", (event.data[0] & 0x100) or (event.data[0] & 0x200))
647            #honour hints:
648            hints = self.get_property("size-hints")
649            w, h = self.calc_constrained_size(w, h, hints)
650            geomlog("_NET_MOVERESIZE_WINDOW on %s (data=%s, current geometry=%s, new geometry=%s)",
651                    self, event.data, geom, (x,y,w,h))
652            with xswallow:
653                X11Window.configureAndNotify(self.xid, x, y, w, h)
654            return True
655        return super().process_client_message_event(event)
656
657    def calc_constrained_size(self, w, h, hints):
658        mhints = typedict(hints)
659        cw, ch = calc_constrained_size(w, h, mhints)
660        geomlog("calc_constrained_size%s=%s (size_constraints=%s)", (w, h, mhints), (cw, ch), self.size_constraints)
661        return cw, ch
662
663    def update_size_constraints(self, minw=0, minh=0, maxw=MAX_WINDOW_SIZE, maxh=MAX_WINDOW_SIZE):
664        if self.size_constraints==(minw, minh, maxw, maxh):
665            geomlog("update_size_constraints%s unchanged", (minw, minh, maxw, maxh))
666            return  #no need to do anything
667        ominw, ominh, omaxw, omaxh = self.size_constraints
668        self.size_constraints = minw, minh, maxw, maxh
669        if minw<=ominw and minh<=ominh and maxw>=omaxw and maxh>=omaxh:
670            geomlog("update_size_constraints%s less restrictive, no need to recalculate", (minw, minh, maxw, maxh))
671            return
672        geomlog("update_size_constraints%s recalculating client geometry", (minw, minh, maxw, maxh))
673        self._update_client_geometry()
674
675    #########################################
676    # X11 properties synced to Python objects
677    #########################################
678
679    def _handle_icon_title_change(self):
680        icon_name = self.prop_get("_NET_WM_ICON_NAME", "utf8", True)
681        iconlog("_NET_WM_ICON_NAME=%s", icon_name)
682        if icon_name is None:
683            icon_name = self.prop_get("WM_ICON_NAME", "latin1", True)
684            iconlog("WM_ICON_NAME=%s", icon_name)
685        self._updateprop("icon-title", sanestr(icon_name))
686
687    def _handle_motif_wm_hints_change(self):
688        #motif_hints = self.prop_get("_MOTIF_WM_HINTS", "motif-hints")
689        motif_hints = prop_get(self.client_window, "_MOTIF_WM_HINTS", "motif-hints",
690                               ignore_errors=False, raise_xerrors=True)
691        metalog("_MOTIF_WM_HINTS=%s", motif_hints)
692        if motif_hints:
693            if motif_hints.flags & (2**MotifWMHints.DECORATIONS_BIT):
694                if self._updateprop("decorations", motif_hints.decorations):
695                    #we may need to clamp the window size:
696                    self._handle_wm_normal_hints_change()
697            if motif_hints.flags & (2**MotifWMHints.INPUT_MODE_BIT):
698                self._updateprop("modal", int(motif_hints.input_mode))
699
700
701    def _handle_wm_normal_hints_change(self):
702        with xswallow:
703            size_hints = X11Window.getSizeHints(self.xid)
704        metalog("WM_NORMAL_HINTS=%s", size_hints)
705        #getSizeHints exports fields using their X11 names as defined in the "XSizeHints" structure,
706        #but we use a different naming (for historical reason and backwards compatibility)
707        #so rename the fields:
708        hints = {}
709        if size_hints:
710            TRANSLATED_NAMES = {
711                "position"          : "position",
712                "size"              : "size",
713                "base_size"         : "base-size",
714                "resize_inc"        : "increment",
715                "win_gravity"       : "gravity",
716                "min_aspect_ratio"  : "minimum-aspect-ratio",
717                "max_aspect_ratio"  : "maximum-aspect-ratio",
718                }
719            for k,v in size_hints.items():
720                trans_name = TRANSLATED_NAMES.get(k)
721                if trans_name:
722                    hints[trans_name] = v
723        #handle min-size and max-size,
724        #applying our size constraints if we have any:
725        mhints = typedict(size_hints or {})
726        hminw, hminh = mhints.inttupleget("min_size", (0, 0), 2, 2)
727        hmaxw, hmaxh = mhints.inttupleget("max_size", (MAX_WINDOW_SIZE, MAX_WINDOW_SIZE), 2, 2)
728        d = self.get("decorations", -1)
729        decorated = d==-1 or any((d & 2**b) for b in (
730            MotifWMHints.ALL_BIT,
731            MotifWMHints.TITLE_BIT,
732            MotifWMHints.MINIMIZE_BIT,
733            MotifWMHints.MAXIMIZE_BIT,
734            ))
735        cminw, cminh, cmaxw, cmaxh = self.size_constraints
736        if decorated:
737            #min-size only applies to decorated windows
738            if cminw>0 and cminw>hminw:
739                hminw = cminw
740            if cminh>0 and cminh>hminh:
741                hminh = cminh
742        #max-size applies to all windows:
743        if 0<cmaxw<hmaxw:
744            hmaxw = cmaxw
745        if 0<cmaxh<hmaxh:
746            hmaxh = cmaxh
747        #if the values mean something, expose them:
748        if hminw>0 or hminh>0:
749            hints["minimum-size"] = hminw, hminh
750        if hmaxw<MAX_WINDOW_SIZE or hmaxh<MAX_WINDOW_SIZE:
751            hints["maximum-size"] = hmaxw, hmaxh
752        sanitize_size_hints(hints)
753        #we don't use the "size" attribute for anything yet,
754        #and changes to this property could send us into a loop
755        hints.pop("size", None)
756        # Don't send out notify and ConfigureNotify events when this property
757        # gets no-op updated -- some apps like FSF Emacs 21 like to update
758        # their properties every time they see a ConfigureNotify, and this
759        # reduces the chance for us to get caught in loops:
760        if self._updateprop("size-hints", hints):
761            metalog("updated: size-hints=%s", hints)
762            if self._setup_done:
763                self._update_client_geometry()
764
765
766    def _handle_net_wm_icon_change(self):
767        iconlog("_NET_WM_ICON changed on %#x, re-reading", self.xid)
768        icons = self.prop_get("_NET_WM_ICON", "icons")
769        self._internal_set_property("icons", icons)
770
771    _x11_property_handlers = dict(BaseWindowModel._x11_property_handlers)
772    _x11_property_handlers.update({
773        "WM_ICON_NAME"                  : _handle_icon_title_change,
774        "_NET_WM_ICON_NAME"             : _handle_icon_title_change,
775        "_MOTIF_WM_HINTS"               : _handle_motif_wm_hints_change,
776        "WM_NORMAL_HINTS"               : _handle_wm_normal_hints_change,
777        "_NET_WM_ICON"                  : _handle_net_wm_icon_change,
778       })
779
780
781    def get_default_window_icon(self, size=48):
782        #return the icon which would be used from the wmclass
783        c_i = self.get_property("class-instance")
784        iconlog("get_default_window_icon(%i) class-instance=%s", size, c_i)
785        if not c_i or len(c_i)!=2:
786            return None
787        wmclass_name = c_i[0]
788        if not wmclass_name:
789            return None
790        it = Gtk.IconTheme.get_default()
791        pixbuf = None
792        iconlog("get_default_window_icon(%i) icon theme=%s, wmclass_name=%s", size, it, wmclass_name)
793        for icon_name in (
794            "%s-color" % wmclass_name,
795            wmclass_name,
796            "%s_%ix%i" % (wmclass_name, size, size),
797            "application-x-%s" % wmclass_name,
798            "%s-symbolic" % wmclass_name,
799            "%s.symbolic" % wmclass_name,
800            ):
801            i = it.lookup_icon(icon_name, size, 0)
802            iconlog("lookup_icon(%s)=%s", icon_name, i)
803            if not i:
804                continue
805            try:
806                pixbuf = i.load_icon()
807                iconlog("load_icon()=%s", pixbuf)
808                if pixbuf:
809                    w, h = pixbuf.props.width, pixbuf.props.height
810                    iconlog("using '%s' pixbuf %ix%i", icon_name, w, h)
811                    return w, h, "RGBA", pixbuf.get_pixels()
812            except Exception:
813                iconlog("%s.load_icon()", i, exc_info=True)
814        return None
815
816    def get_wm_state(self, prop):
817        state_names = self._state_properties.get(prop)
818        assert state_names, "invalid window state %s" % prop
819        log("get_wm_state(%s) state_names=%s", prop, state_names)
820        #this is a virtual property for _NET_WM_STATE:
821        #return True if any is set (only relevant for maximized)
822        for x in state_names:
823            if self._state_isset(x):
824                return True
825        return False
826
827
828    ################################
829    # Focus handling:
830    ################################
831
832    def give_client_focus(self):
833        """The focus manager has decided that our client should receive X
834        focus.  See world_window.py for details."""
835        log("give_client_focus() corral_window=%s", self.corral_window)
836        if self.corral_window:
837            with xlog:
838                self.do_give_client_focus()
839
840    def do_give_client_focus(self):
841        protocols = self.get_property("protocols")
842        focuslog("Giving focus to %#x, input_field=%s, FORCE_XSETINPUTFOCUS=%s, protocols=%s",
843                 self.xid, self._input_field, FORCE_XSETINPUTFOCUS, protocols)
844        # Have to fetch the time, not just use CurrentTime, both because ICCCM
845        # says that WM_TAKE_FOCUS must use a real time and because there are
846        # genuine race conditions here (e.g. suppose the client does not
847        # actually get around to requesting the focus until after we have
848        # already changed our mind and decided to give it to someone else).
849        now = x11_get_server_time(self.corral_window)
850        # ICCCM 4.1.7 *claims* to describe how we are supposed to give focus
851        # to a window, but it is completely opaque.  From reading the
852        # metacity, kwin, gtk+, and qt code, it appears that the actual rules
853        # for giving focus are:
854        #   -- the WM_HINTS input field determines whether the WM should call
855        #      XSetInputFocus
856        #   -- independently, the WM_TAKE_FOCUS protocol determines whether
857        #      the WM should send a WM_TAKE_FOCUS ClientMessage.
858        # If both are set, both methods MUST be used together. For example,
859        # GTK+ apps respect WM_TAKE_FOCUS alone but I'm not sure they handle
860        # XSetInputFocus well, while Qt apps ignore (!!!) WM_TAKE_FOCUS
861        # (unless they have a modal window), and just expect to get focus from
862        # the WM's XSetInputFocus.
863        if bool(self._input_field) or FORCE_XSETINPUTFOCUS:
864            focuslog("... using XSetInputFocus")
865            X11Window.XSetInputFocus(self.xid, now)
866        if "WM_TAKE_FOCUS" in protocols:
867            focuslog("... using WM_TAKE_FOCUS")
868            send_wm_take_focus(self.client_window, now)
869        self.set_active()
870
871
872GObject.type_register(WindowModel)
873