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