Hide keyboard shortcuts

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. 

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 

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 

32 

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") 

41 

42 

43X11Window = X11WindowBindings() 

44 

45IconicState = constants["IconicState"] 

46NormalState = constants["NormalState"] 

47 

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) 

68 

69 

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 

74 

75 

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.""" 

79 

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")] 

85 

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 }) 

139 

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" 

152 

153 def __init__(self, parking_window, client_window, desktop_geometry, size_constraints=None): 

154 """Register a new client window with the WM. 

155 

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.""" 

159 

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 

169 

170 self.call_setup() 

171 

172 ######################################### 

173 # Setup and teardown 

174 ######################################### 

175 

176 def setup(self): 

177 super().setup() 

178 

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) 

194 

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) 

203 

204 log("setup() adding to save set") 

205 X11Window.XAddToSaveSet(self.xid) 

206 self.in_save_set = True 

207 

208 log("setup() reparenting") 

209 X11Window.Reparent(self.xid, cxid, 0, 0) 

210 self.client_reparented = True 

211 

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() 

231 

232 

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 

245 

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) 

255 

256 

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() 

307 

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) 

342 

343 

344 ######################################### 

345 # Actions specific to WindowModel 

346 ######################################### 

347 

348 def raise_window(self): 

349 X11Window.XRaiseWindow(self.corral_window.get_xid()) 

350 X11Window.XRaiseWindow(self.client_window.get_xid()) 

351 

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) 

357 

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) 

363 

364 

365 ######################################### 

366 # X11 Events 

367 ######################################### 

368 

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) 

373 

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) 

383 

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() 

398 

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) 

404 

405 

406 ######################################### 

407 # Hooks for WM 

408 ######################################### 

409 

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 

435 

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() 

439 

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) 

465 

466 

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) 

479 

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) 

511 

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) 

531 

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)) 

561 

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)) 

582 

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)) 

592 

593 if event.value_mask & CWStackMode: 

594 self.emit("restack", event.detail, event.above) 

595 

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.) 

609 

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) 

633 

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 

639 

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() 

651 

652 ######################################### 

653 # X11 properties synced to Python objects 

654 ######################################### 

655 

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)) 

663 

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)) 

676 

677 

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() 

741 

742 

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) 

747 

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 }) 

756 

757 

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 

792 

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 

803 

804 

805 ################################ 

806 # Focus handling: 

807 ################################ 

808 

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() 

815 

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() 

844 

845 

846GObject.type_register(WindowModel)