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