Ticket #2539: strip-client.patch
File strip-client.patch, 128.4 KB (added by , 11 months ago) |
---|
-
xpra/client/client_window_base.py
5 5 # Xpra is released under the terms of the GNU GPL v2, or, at your option, any 6 6 # later version. See the file COPYING for details. 7 7 8 import os9 import re10 11 8 from xpra.client.client_widget_base import ClientWidgetBase 12 from xpra.os_util import bytestostr, OSX, WIN32, is_Wayland 13 from xpra.util import typedict, envbool, envint, WORKSPACE_UNSET, WORKSPACE_NAMES 9 from xpra.util import typedict, envint 14 10 from xpra.log import Logger 15 11 16 12 log = Logger("window") 17 13 plog = Logger("paint") 18 focuslog = Logger("focus")19 mouselog = Logger("mouse")20 workspacelog = Logger("workspace")21 keylog = Logger("keyboard")22 metalog = Logger("metadata")23 geomlog = Logger("geometry")24 iconlog = Logger("icon")25 alphalog = Logger("alpha")26 14 27 15 28 SIMULATE_MOUSE_DOWN = envbool("XPRA_SIMULATE_MOUSE_DOWN", True)29 PROPERTIES_DEBUG = [x.strip() for x in os.environ.get("XPRA_WINDOW_PROPERTIES_DEBUG", "").split(",")]30 AWT_DIALOG_WORKAROUND = envbool("XPRA_AWT_DIALOG_WORKAROUND", WIN32)31 SET_SIZE_CONSTRAINTS = envbool("XPRA_SET_SIZE_CONSTRAINTS", True)32 16 DEFAULT_GRAVITY = envint("XPRA_DEFAULT_GRAVITY", 0) 33 17 OVERRIDE_GRAVITY = envint("XPRA_OVERRIDE_GRAVITY", 0) 34 18 … … 42 26 wx, wy, ww, wh, bw, bh, 43 27 metadata, override_redirect, client_properties, 44 28 border, max_window_size, default_cursor_data, pixel_depth): 45 log("%s%s", type(self),46 (client, group_leader, watcher_pid, wid,47 wx, wy, ww, wh, bw, bh,48 metadata, override_redirect, client_properties,49 border, max_window_size, default_cursor_data, pixel_depth))50 29 super().__init__(client, watcher_pid, wid, metadata.boolget("has-alpha")) 51 30 self._override_redirect = override_redirect 52 self.group_leader = group_leader53 31 self._pos = (wx, wy) 54 32 self._size = (ww, wh) 55 self._client_properties = client_properties56 33 self._set_initial_position = metadata.boolget("set-initial-position", False) 57 34 self.size_constraints = typedict() 58 35 self.geometry_hints = {} 59 self.content_type = ""60 self._fullscreen = None61 self._maximized = False62 self._above = False63 self._below = False64 self._shaded = False65 self._sticky = False66 self._skip_pager = False67 self._skip_taskbar = False68 self._sticky = False69 self._iconified = False70 self._focused = False71 36 self.window_gravity = OVERRIDE_GRAVITY or DEFAULT_GRAVITY 72 37 self.border = border 73 self.cursor_data = None74 self.default_cursor_data = default_cursor_data75 38 self.max_window_size = max_window_size 76 39 self.button_state = {} 77 40 self.pixel_depth = pixel_depth #0 for default 78 self.window_offset = None #actual vs reported coordinates79 41 self.pending_refresh = [] 80 42 81 43 self.init_window(metadata) … … 90 52 self._metadata = typedict() 91 53 # used for only sending focus events *after* the window is mapped: 92 54 self._been_mapped = False 93 self._override_redirect_windows = [] 94 def wn(w): 95 return WORKSPACE_NAMES.get(w, w) 96 workspace = typedict(self._client_properties).intget("workspace", None) 97 workspacelog("init_window(..) workspace from client properties %s: %s", self._client_properties, wn(workspace)) 98 if workspace is not None: 99 #client properties override application specified workspace value on init only: 100 metadata[b"workspace"] = workspace 101 self._window_workspace = WORKSPACE_UNSET #will get set in set_metadata if present 102 self._desktop_workspace = self.get_desktop_workspace() #pylint: disable=assignment-from-none 103 workspacelog("init_window(..) workspace=%s, current workspace=%s", 104 wn(self._window_workspace), wn(self._desktop_workspace)) 105 if self.max_window_size and b"size-constraints" not in metadata: 106 #this ensures that we will set size-constraints and honour max_window_size: 107 metadata[b"size-constraints"] = {} 108 #initialize gravity early: 109 sc = typedict(metadata.dictget("size-constraints", {})) 110 self.window_gravity = OVERRIDE_GRAVITY or sc.intget("gravity", DEFAULT_GRAVITY) 111 self.set_decorated(metadata.boolget("decorations", True)) 55 self.window_gravity = OVERRIDE_GRAVITY 112 56 113 57 114 def get_desktop_workspace(self):115 return None116 117 def get_window_workspace(self):118 return None119 120 121 58 def new_backing(self, bw, bh): 122 59 backing_class = self.get_backing_class() 123 60 log("new_backing(%s, %s) backing_class=%s", bw, bh, backing_class) … … 125 62 w, h = self._size 126 63 self._backing = self.make_new_backing(backing_class, w, h, bw, bh) 127 64 self._backing.border = self.border 128 self._backing.default_cursor_data = self.default_cursor_data129 65 self._backing.gravity = self.window_gravity 130 66 return self._backing._backing 131 67 … … 133 69 def destroy(self): 134 70 #ensure we clear reference to other windows: 135 71 self.group_leader = None 136 self._override_redirect_windows = []137 72 self._metadata = {} 138 73 if self._backing: 139 74 self._backing.close() … … 142 77 143 78 def setup_window(self, bw, bh): 144 79 self.new_backing(bw, bh) 145 #tell the server about the encoding capabilities of this backing instance:146 #but don't bother if they're the same as what we sent as defaults147 #(with a bit of magic to collapse the missing namespace from encoding_defaults)148 backing_props = self._backing.get_encoding_properties()149 encoding_defaults = self._client.encoding_defaults150 for k in tuple(backing_props.keys()):151 v = backing_props[k]152 try:153 #ie: "encodings.rgb_formats" -> "rgb_formats"154 #ie: "encoding.full_csc_modes" -> "full_csc_modes"155 ek = k.split(".", 1)[1]156 except IndexError:157 ek = k158 dv = encoding_defaults.get(ek)159 if dv is not None and dv==v:160 del backing_props[k]161 self._client_properties.update(backing_props)162 80 163 81 164 82 def send(self, *args): 165 83 self._client.send(*args) 166 84 167 def reset_icon(self):168 current_icon = self._current_icon169 iconlog("reset_icon() current icon=%s", current_icon)170 if current_icon:171 self.update_icon(current_icon)172 173 85 def update_icon(self, img): 174 raise NotImplementedError("override me!")86 pass 175 87 176 def apply_transient_for(self, wid):177 raise NotImplementedError("override me!")178 88 179 def paint_spinner(self, context, area):180 raise NotImplementedError("override me!")181 182 def _pointer_modifiers(self, event):183 raise NotImplementedError("override me!")184 185 186 def xget_u32_property(self, target, name):187 raise NotImplementedError("override me!")188 189 190 89 def is_OR(self): 191 90 return self._override_redirect 192 91 193 92 194 93 def update_metadata(self, metadata): 195 metalog("update_metadata(%s)", metadata)196 if self._client.readonly:197 metadata.update(self._force_size_constraint(*self._size))198 self._metadata.update(metadata)199 try:200 self.set_metadata(metadata)201 except Exception:202 metalog.warn("failed to set window metadata to '%s'", metadata, exc_info=True)203 204 def _force_size_constraint(self, *size):205 return {206 b"size-constraints" : {207 b"maximum-size" : size,208 b"minimum-size" : size,209 b"base-size" : size,210 }211 }212 213 def set_metadata(self, metadata):214 metalog("set_metadata(%s)", metadata)215 debug_props = [x for x in PROPERTIES_DEBUG if x in metadata.keys()]216 for x in debug_props:217 metalog.info("set_metadata: %s=%s", x, metadata.get(x))218 #WARNING: "class-instance" needs to go first because others may realize the window219 #(and GTK doesn't set the "class-instance" once the window is realized)220 if b"class-instance" in metadata:221 self.set_class_instance(*self._metadata.strtupleget("class-instance", ("xpra", "Xpra"), 2, 2))222 self.reset_icon()223 224 if b"title" in metadata:225 try:226 title = bytestostr(self._client.title).replace("\0", "")227 if title.find("@")>=0:228 #perform metadata variable substitutions:229 #full of py3k unicode headaches that don't need to be230 default_values = {231 "title" : "<untitled window>",232 "client-machine" : "<unknown machine>",233 "windowid" : str(self._id),234 "server-machine" : getattr(self._client, "_remote_hostname", None) or "<unknown machine>",235 "server-display" : getattr(self._client, "_remote_display", None) or "<unknown display>",236 }237 def getvar(var):238 #"hostname" is magic:239 #we try harder to find a useful value to show:240 if var=="hostname":241 #try to find the hostname:242 proto = getattr(self._client, "_protocol", None)243 if proto:244 conn = getattr(proto, "_conn", None)245 if conn:246 hostname = conn.info.get("host") or bytestostr(conn.target)247 if hostname:248 return hostname249 for m in ("client-machine", "server-machine"):250 value = getvar(m)251 if value not in (252 "localhost",253 "localhost.localdomain",254 "<unknown machine>",255 "",256 None):257 return value258 return "<unknown machine>"259 value = self._metadata.bytesget(var)260 if value is None:261 return default_values.get(var, "<unknown %s>" % var)262 try:263 return value.decode("utf-8")264 except UnicodeDecodeError:265 return str(value)266 def metadata_replace(match):267 atvar = match.group(0) #ie: '@title@'268 var = atvar[1:len(atvar)-1] #ie: 'title'269 if not var:270 #atvar = "@@"271 return "@"272 return getvar(var)273 title = re.sub(r"@[\w\-]*@", metadata_replace, title)274 except Exception as e:275 log.error("Error parsing window title:")276 log.error(" %s", e)277 title = ""278 self.set_title(title)279 280 if b"icon-title" in metadata:281 icon_title = metadata.strget("icon-title")282 self.set_icon_name(icon_title)283 #the DE may have reset the icon now,284 #force it to use the one we really want:285 self.reset_icon()286 287 if b"size-constraints" in metadata:288 sc = typedict(metadata.dictget("size-constraints", {}))289 self.size_constraints = sc290 self._set_initial_position = sc.boolget("set-initial-position", self._set_initial_position)291 self.set_size_constraints(sc, self.max_window_size)292 293 if b"set-initial-position" in metadata:294 #this should be redundant - but we keep it here for consistency295 self._set_initial_position = metadata.boolget("set-initial-position")296 297 if b"transient-for" in metadata:298 wid = metadata.intget("transient-for", -1)299 self.apply_transient_for(wid)300 301 if b"modal" in metadata:302 modal = metadata.boolget("modal")303 self.set_modal(modal)304 305 #apply window-type hint if window has not been mapped yet:306 if b"window-type" in metadata and not self.get_mapped():307 window_types = metadata.strtupleget("window-type")308 self.set_window_type(window_types)309 310 if b"role" in metadata:311 role = metadata.strget("role")312 self.set_role(role)313 314 if b"xid" in metadata:315 xid = metadata.strget("xid")316 self.set_xid(xid)317 318 if b"opacity" in metadata:319 opacity = metadata.intget("opacity", -1)320 if opacity<0:321 opacity = 1322 else:323 opacity = min(1, opacity/0xffffffff)324 #requires gtk>=2.12!325 if hasattr(self, "set_opacity"):326 self.set_opacity(opacity)327 328 if b"has-alpha" in metadata:329 new_alpha = metadata.boolget("has-alpha")330 if new_alpha!=self._has_alpha:331 l = alphalog332 if not WIN32:333 #win32 without opengl can't do transparency,334 #so it triggers too many warnings335 l = log.warn336 l("Warning: window %s changed its transparency attribute", self._id)337 l(" from %s to %s, behaviour is undefined", self._has_alpha, new_alpha)338 self._has_alpha = new_alpha339 340 if b"maximized" in metadata:341 maximized = metadata.boolget("maximized")342 if maximized!=self._maximized:343 self._maximized = maximized344 if maximized:345 self.maximize()346 else:347 self.unmaximize()348 349 if b"fullscreen" in metadata:350 fullscreen = metadata.boolget("fullscreen")351 if self._fullscreen is None or self._fullscreen!=fullscreen:352 self._fullscreen = fullscreen353 self.set_fullscreen(fullscreen)354 355 if b"iconic" in metadata:356 iconified = metadata.boolget("iconic")357 if self._iconified!=iconified:358 self._iconified = iconified359 if iconified:360 self.iconify()361 else:362 self.deiconify()363 364 if b"decorations" in metadata:365 decorated = metadata.boolget("decorations", True)366 was_decorated = self.get_decorated()367 if WIN32 and decorated!=was_decorated:368 log.info("decorations flag toggled, now %s, re-initializing window", decorated)369 self.idle_add(self._client.reinit_window, self._id, self)370 else:371 self.set_decorated(metadata.boolget("decorations"))372 self.apply_geometry_hints(self.geometry_hints)373 374 if b"above" in metadata:375 above = metadata.boolget("above")376 if self._above!=above:377 self._above = above378 self.set_keep_above(above)379 380 if b"below" in metadata:381 below = metadata.boolget("below")382 if self._below!=below:383 self._below = below384 self.set_keep_below(below)385 386 if b"shaded" in metadata:387 shaded = metadata.boolget("shaded")388 if self._shaded!=shaded:389 self._shaded = shaded390 self.set_shaded(shaded)391 392 if b"sticky" in metadata:393 sticky = metadata.boolget("sticky")394 if self._sticky!=sticky:395 self._sticky = sticky396 if sticky:397 self.stick()398 else:399 self.unstick()400 401 if b"skip-taskbar" in metadata:402 skip_taskbar = metadata.boolget("skip-taskbar")403 if self._skip_taskbar!=skip_taskbar:404 self._skip_taskbar = skip_taskbar405 self.set_skip_taskbar_hint(skip_taskbar)406 407 if b"skip-pager" in metadata:408 skip_pager = metadata.boolget("skip-pager")409 if self._skip_pager!=skip_pager:410 self._skip_pager = skip_pager411 self.set_skip_pager_hint(skip_pager)412 413 if b"workspace" in metadata:414 self.set_workspace(metadata.intget("workspace"))415 416 if b"bypass-compositor" in metadata:417 self.set_bypass_compositor(metadata.intget("bypass-compositor"))418 419 if b"strut" in metadata:420 self.set_strut(metadata.dictget("strut", {}))421 422 if b"fullscreen-monitors" in metadata:423 self.set_fullscreen_monitors(metadata.inttupleget("fullscreen-monitors"))424 425 if b"shape" in metadata:426 self.set_shape(metadata.dictget("shape", {}))427 428 if b"command" in metadata:429 self.set_command(metadata.strget("command"))430 431 if b"x11-property" in metadata:432 self.set_x11_property(*metadata.tupleget("x11-property"))433 434 if b"content-type" in metadata:435 self.content_type = metadata.strget("content-type")436 437 438 def set_x11_property(self, *x11_property):439 94 pass 440 95 441 def set_command(self, command):442 pass443 444 def set_class_instance(self, wmclass_name, wmclass_class):445 pass446 447 def set_shape(self, shape):448 log("set_shape(%s) not implemented", shape)449 450 def set_bypass_compositor(self, v):451 pass #see gtk client window base452 453 def set_strut(self, strut):454 pass #see gtk client window base455 456 def set_fullscreen_monitors(self, fsm):457 pass #see gtk client window base458 459 def set_shaded(self, shaded):460 pass #see gtk client window base461 462 463 def reset_size_constraints(self):464 self.set_size_constraints(self.size_constraints, self.max_window_size)465 466 96 def set_size_constraints(self, size_constraints, max_window_size): 467 if not SET_SIZE_CONSTRAINTS: 468 return 469 geomlog("set_size_constraints(%s, %s)", size_constraints, max_window_size) 470 hints = typedict() 471 client = self._client 472 for (a, h1, h2) in ( 473 (b"maximum-size", b"max_width", b"max_height"), 474 (b"minimum-size", b"min_width", b"min_height"), 475 (b"base-size", b"base_width", b"base_height"), 476 (b"increment", b"width_inc", b"height_inc"), 477 ): 478 v = size_constraints.intpair(a) 479 geomlog("intpair(%s)=%s", a, v) 480 if v: 481 v1, v2 = v 482 if a==b"maximum-size" and v1>=32000 and v2>=32000 and WIN32: 483 #causes problems, see #2714 484 continue 485 sv1 = client.sx(v1) 486 sv2 = client.sy(v2) 487 if a in (b"base-size", b"increment"): 488 #rounding is not allowed for these values 489 fsv1 = client.fsx(v1) 490 fsv2 = client.fsy(v2) 491 def closetoint(v): 492 #tolerate some rounding error: 493 #(ie: 2:3 scaling may not give an integer without a tiny bit of rounding) 494 return abs(int(v)-v)<0.00001 495 if not closetoint(fsv1) or not closetoint(fsv2): 496 #the scaled value is not close to an int, 497 #so we can't honour it: 498 geomlog("cannot honour '%s' due to scaling, scaled values are not both integers: %s, %s", 499 a, fsv1, fsv2) 500 continue 501 hints[h1], hints[h2] = sv1, sv2 502 if not OSX: 503 for (a, h) in ( 504 (b"minimum-aspect-ratio", b"min_aspect"), 505 (b"maximum-aspect-ratio", b"max_aspect"), 506 ): 507 v = size_constraints.intpair(a) 508 if v: 509 v1, v2 = v 510 hints[h] = (v1*self._client.xscale)/(v2*self._client.yscale) 511 #apply max-size override if needed: 512 w,h = max_window_size 513 if w>0 and h>0 and not self._fullscreen: 514 #get the min size, if there is one: 515 minw = max(1, hints.intget(b"min_width", 1)) 516 minh = max(1, hints.intget(b"min_height", 1)) 517 #the actual max size is: 518 # * greater than the min-size 519 # * the lowest of the max-size set by the application and the one we have 520 # * ensure we honour the other hints, and round the max-size down if needed: 521 #according to the GTK docs: 522 #allowed window widths are base_width + width_inc * N where N is any integer 523 #allowed window heights are base_height + width_inc * N where N is any integer 524 maxw = hints.intget(b"max_width", 32768) 525 maxh = hints.intget(b"max_height", 32768) 526 maxw = max(minw, min(w, maxw)) 527 maxh = max(minh, min(h, maxh)) 528 rw = (maxw - hints.intget(b"base_width", 0)) % max(hints.intget(b"width_inc", 1), 1) 529 rh = (maxh - hints.intget(b"base_height", 0)) % max(hints.intget(b"height_inc", 1), 1) 530 maxw -= rw 531 maxh -= rh 532 #if the hints combination is invalid, it's possible that we'll end up 533 #not honouring "base" + "inc", but honouring just "min" instead: 534 maxw = max(minw, maxw) 535 maxh = max(minh, maxh) 536 geomlog("modified hints for max window size %s: %s (rw=%s, rh=%s) -> max=%sx%s", 537 max_window_size, hints, rw, rh, maxw, maxh) 538 #ensure we don't have duplicates with bytes / strings, 539 #and that keys are always "bytes": 540 #(in practice this code should never fire, just here as a reminder) 541 for x in ("max_width", "max_height"): 542 try: 543 del hints[x] 544 except KeyError: 545 pass 546 #bug 2214: GTK3 on win32 gets confused if we specify a large max-size 547 # and it will mess up maximizing the window 548 if not WIN32 or (maxw<32000 or maxh<32000): 549 hints[b"max_width"] = maxw 550 hints[b"max_height"] = maxh 551 try: 552 geomlog("calling: %s(%s)", self.apply_geometry_hints, hints) 553 #save them so the window hooks can use the last value used: 554 self.geometry_hints = hints 555 self.apply_geometry_hints(hints) 556 except Exception: 557 geomlog("set_size_constraints%s", (size_constraints, max_window_size), exc_info=True) 558 geomlog.error("Error setting window hints:") 559 for k,v in hints.items(): 560 geomlog.error(" %s=%s", bytestostr(k), v) 561 geomlog.error(" from size constraints:") 562 for k,v in size_constraints.items(): 563 geomlog.error(" %s=%s", k, v) 564 self.window_gravity = OVERRIDE_GRAVITY or size_constraints.intget("gravity", DEFAULT_GRAVITY) 565 b = self._backing 566 if b: 567 b.gravity = self.window_gravity 97 return 568 98 569 99 570 def set_window_type(self, window_types):571 hints = 0572 for window_type in window_types:573 #win32 workaround:574 if AWT_DIALOG_WORKAROUND and window_type=="DIALOG" and self._metadata.boolget("skip-taskbar"):575 wm_class = self._metadata.strtupleget("class-instance", (None, None), 2, 2)576 if wm_class and len(wm_class)==2 and wm_class[0] and wm_class[0].startswith("sun-awt-X11"):577 #replace "DIALOG" with "NORMAL":578 if "NORMAL" in window_types:579 continue580 window_type = "NORMAL"581 hint = self.NAME_TO_HINT.get(window_type, None)582 if hint is not None:583 hints |= hint584 else:585 log("ignoring unknown window type hint: %s", window_type)586 log("set_window_type(%s) hints=%s", window_types, hints)587 if hints:588 self.set_type_hint(hints)589 100 590 def set_workspace(self, workspace):591 pass592 593 def set_fullscreen(self, fullscreen):594 pass595 596 def set_xid(self, xid):597 pass598 599 600 def toggle_debug(self, *_args):601 b = self._backing602 log.info("toggling debug on backing %s for window %i", b, self._id)603 if not b:604 return605 if b.paint_box_line_width>0:606 b.paint_box_line_width = 0607 else:608 b.paint_box_line_width = b.default_paint_box_line_width609 610 def increase_quality(self, *_args):611 if self._client.quality>0:612 #change fixed quality:613 self._client.quality = min(100, self._client.quality + 10)614 self._client.send_quality()615 log("new quality=%s", self._client.quality)616 else:617 self._client.min_quality = min(100, self._client.min_quality + 10)618 self._client.send_min_quality()619 log("new min-quality=%s", self._client.min_quality)620 621 def decrease_quality(self, *_args):622 if self._client.quality>0:623 #change fixed quality:624 self._client.quality = max(1, self._client.quality - 10)625 self._client.send_quality()626 log("new quality=%s", self._client.quality)627 else:628 self._client.min_quality = max(0, self._client.min_quality - 10)629 self._client.send_min_quality()630 log("new min-quality=%s", self._client.min_quality)631 632 def increase_speed(self, *_args):633 if self._client.speed>0:634 #change fixed speed:635 self._client.speed = min(100, self._client.speed + 10)636 self._client.send_speed()637 log("new speed=%s", self._client.speed)638 else:639 self._client.min_speed = min(100, self._client.min_speed + 10)640 self._client.send_min_speed()641 log("new min-speed=%s", self._client.min_speed)642 643 def decrease_speed(self, *_args):644 if self._client.speed>0:645 #change fixed speed:646 self._client.speed = max(1, self._client.speed - 10)647 self._client.send_speed()648 log("new speed=%s", self._client.speed)649 else:650 self._client.min_speed = max(0, self._client.min_speed - 10)651 self._client.send_min_speed()652 log("new min-speed=%s", self._client.min_speed)653 654 def scaleup(self, *_args):655 self._client.scaleup()656 657 def scaledown(self, *_args):658 self._client.scaledown()659 660 def scalingoff(self):661 self._client.scalingoff()662 663 def scalereset(self, *_args):664 self._client.scalereset()665 666 def magic_key(self, *args):667 b = self.border668 if b:669 b.toggle()670 log("magic_key%s border=%s", args, b)671 self.repaint(0, 0, *self._size)672 673 101 def repaint(self, x, y, w, h): 674 102 #self.queue_draw_area(0, 0, *self._size) 675 103 raise NotImplementedError("no repaint on %s", type(self)) 676 104 677 def refresh_window(self, *args):678 log("refresh_window(%s) wid=%s", args, self._id)679 self._client.send_refresh(self._id)680 105 681 def refresh_all_windows(self, *_args):682 #this method is only here because we may want to fire it683 #from a --key-shortcut action and the event is delivered to684 #the "ClientWindow"685 self._client.send_refresh_all()686 687 106 def draw_region(self, x, y, width, height, coding, img_data, rowstride, _packet_sequence, options, callbacks): 688 107 """ Note: this runs from the draw thread (not UI thread) """ 689 108 backing = self._backing … … 706 125 backing = self._backing 707 126 if not backing: 708 127 return 709 if backing.repaint_all or self._client.xscale!=1 or self._client.yscale!=1 or is_Wayland(): 710 #easy: just repaint the whole window: 711 rw, rh = self.get_size() 712 self.idle_add(self.repaint, 0, 0, rw, rh) 713 return 714 pr = self.pending_refresh 715 self.pending_refresh = [] 716 for x, y, w, h in pr: 717 rx, ry, rw, rh = self._client.srect(x, y, w, h) 718 #if self.window_offset: 719 # rx -= self.window_offset[0] 720 # ry -= self.window_offset[1] 721 self.idle_add(self.repaint, rx, ry, rw, rh) 128 rw, rh = self.get_size() 129 self.idle_add(self.repaint, 0, 0, rw, rh) 722 130 723 def eos(self):724 """ Note: this runs from the draw thread (not UI thread) """725 backing = self._backing726 if backing:727 backing.eos()728 729 def spinner(self, _ok):730 if not self.can_have_spinner():731 return732 log("spinner(%s) queueing redraw")733 #with normal windows, we just queue a draw request734 #and let the expose event paint the spinner735 w, h = self.get_size()736 self.repaint(0, 0, w, h)737 738 def can_have_spinner(self):739 if self._backing is None:740 return False741 window_types = self._metadata.strtupleget("window-type")742 if not window_types:743 return False744 return ("NORMAL" in window_types) or \745 ("DIALOG" in window_types) or \746 ("SPLASH" in window_types)747 748 749 def _unfocus(self):750 focuslog("_unfocus() wid=%s, focused=%s", self._id, self._client._focused)751 if self._client._focused==self._id:752 self._client.update_focus(self._id, False)753 754 131 def quit(self): 755 132 self._client.quit(0) 756 757 def void(self):758 pass759 760 def show_session_info(self, *args):761 self._client.show_session_info(*args)762 763 def show_menu(self, *args):764 self._client.show_menu(*args)765 766 def show_start_new_command(self, *args):767 self._client.show_start_new_command(*args)768 769 def show_bug_report(self, *args):770 self._client.show_bug_report(*args)771 772 def show_file_upload(self, *args):773 self._client.show_file_upload(*args)774 775 776 def log(self, message=""):777 log.info(message)778 779 780 def keyboard_layout_changed(self, *args):781 #used by win32 hooks to tell us about keyboard layout changes for this window782 keylog("keyboard_layout_changed%s", args)783 self._client.window_keyboard_layout_changed(self)784 785 786 def dbus_call(self, *args, **kwargs):787 #alias for rpc_call using dbus as rpc_type, see UIXpraClient.dbus_call788 if not self._client.server_dbus_proxy:789 log.error("Error: cannot send remote dbus call:")790 log.error(" this server does not support dbus-proxying")791 return792 rpc_args = [self._id]+args793 self._client.rpc_call("dbus", rpc_args, **kwargs)794 795 796 def get_mouse_event_wid(self, _x, _y):797 #overriden in GTKClientWindowBase798 return self._id799 800 def _do_motion_notify_event(self, event):801 if self._client.readonly or self._client.server_readonly or not self._client.server_pointer:802 return803 pointer, relative_pointer, modifiers, buttons = self._pointer_modifiers(event)804 wid = self.get_mouse_event_wid(*pointer)805 mouselog("do_motion_notify_event(%s) wid=%s / focus=%s / window wid=%i, device=%s, pointer=%s, relative pointer=%s, modifiers=%s, buttons=%s", event, wid, self._client._focused, self._id, self._device_info(event), pointer, relative_pointer, modifiers, buttons)806 pdata = pointer807 if self._client.server_pointer_relative:808 pdata = list(pointer)+list(relative_pointer)809 packet = ["pointer-position", wid, pdata, modifiers, buttons]810 self._client.send_mouse_position(packet)811 812 def _device_info(self, event):813 try:814 return event.device.get_name()815 except AttributeError:816 return ""817 818 def _button_action(self, button, event, depressed, *args):819 if self._client.readonly or self._client.server_readonly or not self._client.server_pointer:820 return821 pointer, relative_pointer, modifiers, buttons = self._pointer_modifiers(event)822 wid = self.get_mouse_event_wid(*pointer)823 mouselog("_button_action(%s, %s, %s) wid=%s / focus=%s / window wid=%i, device=%s, pointer=%s, modifiers=%s, buttons=%s",824 button, event, depressed, wid, self._client._focused, self._id, self._device_info(event), pointer, modifiers, buttons)825 #map wheel buttons via translation table to support inverted axes:826 server_button = button827 if button>3:828 server_button = self._client.wheel_map.get(button)829 if not server_button:830 return831 server_buttons = []832 for b in buttons:833 if b>3:834 sb = self._client.wheel_map.get(button)835 if not sb:836 continue837 b = sb838 server_buttons.append(b)839 pdata = pointer840 if self._client.server_pointer_relative:841 pdata = list(pointer)+list(relative_pointer)842 def send_button(pressed):843 self._client.send_button(wid, server_button, pressed, pdata, modifiers, server_buttons, *args)844 pressed_state = self.button_state.get(button, False)845 if SIMULATE_MOUSE_DOWN and pressed_state is False and depressed is False:846 mouselog("button action: simulating a missing mouse-down event for window %s before sending the mouse-up event", wid)847 #(needed for some dialogs on win32):848 send_button(True)849 self.button_state[button] = depressed850 send_button(depressed)851 852 def do_button_press_event(self, event):853 self._button_action(event.button, event, True)854 855 def do_button_release_event(self, event):856 self._button_action(event.button, event, False) -
xpra/client/gtk3/client_window.py
19 19 def get_backing_class(self): 20 20 return CairoBacking 21 21 22 GObject.type_register(ClientWindow)22 #GObject.type_register(ClientWindow) -
xpra/client/gtk3/gtk3_client_window.py
5 5 # Xpra is released under the terms of the GNU GPL v2, or, at your option, any 6 6 # later version. See the file COPYING for details. 7 7 8 from gi.repository import Gdk 8 from gi.repository import Gdk, Gtk, Gio 9 9 10 from xpra.client.gtk_base.gtk_client_window_base import GTKClientWindowBase, HAS_X11_BINDINGS 11 from xpra.gtk_common.gtk_util import WINDOW_NAME_TO_HINT 12 from xpra.os_util import bytestostr 10 from xpra.client.gtk_base.gtk_client_window_base import GTKClientWindowBase 13 11 from xpra.log import Logger 14 12 15 13 log = Logger("gtk", "window") … … 17 15 metalog = Logger("metadata") 18 16 geomlog = Logger("geometry") 19 17 20 GTK3_OR_TYPE_HINTS = (Gdk.WindowTypeHint.DIALOG,21 Gdk.WindowTypeHint.MENU,22 Gdk.WindowTypeHint.TOOLBAR,23 #Gdk.WindowTypeHint.SPLASHSCREEN,24 #Gdk.WindowTypeHint.UTILITY,25 #Gdk.WindowTypeHint.DOCK,26 #Gdk.WindowTypeHint.DESKTOP,27 Gdk.WindowTypeHint.DROPDOWN_MENU,28 Gdk.WindowTypeHint.POPUP_MENU,29 Gdk.WindowTypeHint.TOOLTIP,30 #Gdk.WindowTypeHint.NOTIFICATION,31 Gdk.WindowTypeHint.COMBO,32 Gdk.WindowTypeHint.DND)33 18 34 35 19 """ 36 20 GTK3 version of the ClientWindow class 37 21 """ 38 22 class GTK3ClientWindow(GTKClientWindowBase): 39 23 40 OR_TYPE_HINTS = GTK3_OR_TYPE_HINTS41 NAME_TO_HINT = WINDOW_NAME_TO_HINT24 OR_TYPE_HINTS = {} 25 NAME_TO_HINT = {} 42 26 43 27 def get_backing_class(self): 44 28 raise NotImplementedError() 45 29 30 def init_window(self, metadata): 31 super().init_window(metadata) 32 if self.get_decorated(): 33 self.add_header_bar() 46 34 47 def xget_u32_property(self, target, name): 48 if HAS_X11_BINDINGS: 49 return GTKClientWindowBase.xget_u32_property(self, target, name) 50 #pure Gdk lookup: 51 try: 52 name_atom = Gdk.Atom.intern(name, False) 53 type_atom = Gdk.Atom.intern("CARDINAL", False) 54 prop = Gdk.property_get(target, name_atom, type_atom, 0, 9999, False) 55 if not prop or len(prop)!=3 or len(prop[2])!=1: 56 return None 57 metalog("xget_u32_property(%s, %s)=%s", target, name, prop[2][0]) 58 return prop[2][0] 59 except Exception as e: 60 metalog.error("xget_u32_property error on %s / %s: %s", target, name, e) 35 def add_header_bar(self): 36 hb = Gtk.HeaderBar() 37 hb.set_show_close_button(True) 38 hb.props.title = "HeaderBar example" 39 self.set_titlebar(hb) 61 40 41 button = Gtk.Button() 42 icon = Gio.ThemedIcon(name="mail-send-receive-symbolic") 43 image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) 44 button.add(image) 45 hb.pack_end(button) 46 47 box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 48 Gtk.StyleContext.add_class(box.get_style_context(), "linked") 49 50 button = Gtk.Button() 51 button.add(Gtk.Arrow(arrow_type=Gtk.ArrowType.LEFT, shadow_type=Gtk.ShadowType.NONE)) 52 box.add(button) 53 54 button = Gtk.Button() 55 button.add(Gtk.Arrow(arrow_type=Gtk.ArrowType.RIGHT, shadow_type=Gtk.ShadowType.NONE)) 56 box.add(button) 57 58 hb.pack_start(box) 59 60 62 61 def get_drawing_area_geometry(self): 63 62 gdkwindow = self.drawing_area.get_window() 64 63 x, y = gdkwindow.get_origin()[1:] … … 65 64 w, h = self.get_size() 66 65 return (x, y, w, h) 67 66 68 def apply_geometry_hints(self, hints):69 """ we convert the hints as a dict into a gdk.Geometry + gdk.WindowHints """70 wh = Gdk.WindowHints71 name_to_hint = {"maximum-size" : wh.MAX_SIZE,72 "max_width" : wh.MAX_SIZE,73 "max_height" : wh.MAX_SIZE,74 "minimum-size" : wh.MIN_SIZE,75 "min_width" : wh.MIN_SIZE,76 "min_height" : wh.MIN_SIZE,77 "base-size" : wh.BASE_SIZE,78 "base_width" : wh.BASE_SIZE,79 "base_height" : wh.BASE_SIZE,80 "increment" : wh.RESIZE_INC,81 "width_inc" : wh.RESIZE_INC,82 "height_inc" : wh.RESIZE_INC,83 "min_aspect_ratio" : wh.ASPECT,84 "max_aspect_ratio" : wh.ASPECT,85 }86 #these fields can be copied directly to the gdk.Geometry as ints:87 INT_FIELDS= ["min_width", "min_height",88 "max_width", "max_height",89 "base_width", "base_height",90 "width_inc", "height_inc"]91 ASPECT_FIELDS = {92 "min_aspect_ratio" : "min_aspect",93 "max_aspect_ratio" : "max_aspect",94 }95 geom = Gdk.Geometry()96 mask = 097 for k,v in hints.items():98 k = bytestostr(k)99 if k in INT_FIELDS:100 setattr(geom, k, v)101 mask |= int(name_to_hint.get(k, 0))102 elif k in ASPECT_FIELDS:103 field = ASPECT_FIELDS.get(k)104 setattr(geom, field, float(v))105 mask |= int(name_to_hint.get(k, 0))106 gdk_hints = Gdk.WindowHints(mask)107 geomlog("apply_geometry_hints(%s) geometry=%s, hints=%s", hints, geom, gdk_hints)108 self.set_geometry_hints(self.drawing_area, geom, gdk_hints)109 110 111 67 def draw_widget(self, widget, context): 112 68 paintlog("draw_widget(%s, %s)", widget, context) 113 69 if not self.get_mapped(): -
xpra/client/gtk_base/gtk_client_window_base.py
5 5 # Xpra is released under the terms of the GNU GPL v2, or, at your option, any 6 6 # later version. See the file COPYING for details. 7 7 8 import math 9 import os.path 10 from urllib.parse import unquote 11 import cairo 12 from gi.repository import Gtk, Gdk, Gio 8 from gi.repository import Gtk 13 9 14 from xpra.os_util import bytestostr, strtobytes, is_X11, monotonic_time, WIN32, OSX, POSIX15 from xpra.util import (16 AdHocStruct, typedict, envint, envbool, nonl, csv, first_time,17 WORKSPACE_UNSET, WORKSPACE_ALL, WORKSPACE_NAMES, MOVERESIZE_DIRECTION_STRING, SOURCE_INDICATION_STRING,18 MOVERESIZE_CANCEL,19 MOVERESIZE_SIZE_TOPLEFT, MOVERESIZE_SIZE_TOP, MOVERESIZE_SIZE_TOPRIGHT,20 MOVERESIZE_SIZE_RIGHT,21 MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_BOTTOM, MOVERESIZE_SIZE_BOTTOMLEFT,22 MOVERESIZE_SIZE_LEFT, MOVERESIZE_MOVE,23 )24 10 from xpra.gtk_common.gobject_util import no_arg_signal, one_arg_signal 25 from xpra.gtk_common.gtk_util import ( 26 get_pixbuf_from_data, get_default_root_window, 27 enable_alpha, 28 BUTTON_MASK, 29 GRAB_STATUS_STRING, 30 WINDOW_EVENT_MASK, 31 ) 32 from xpra.gtk_common.keymap import KEY_TRANSLATIONS 11 from xpra.gtk_common.gtk_util import WINDOW_EVENT_MASK 33 12 from xpra.client.client_window_base import ClientWindowBase 34 from xpra.platform.gui import set_fullscreen_monitors, set_shaded35 from xpra.platform.gui import add_window_hooks, remove_window_hooks36 13 from xpra.log import Logger 37 14 38 15 focuslog = Logger("focus", "grab") 39 workspacelog = Logger("workspace")40 16 log = Logger("window") 41 keylog = Logger("keyboard")42 iconlog = Logger("icon")43 metalog = Logger("metadata")44 statelog = Logger("state")45 eventslog = Logger("events")46 shapelog = Logger("shape")47 mouselog = Logger("mouse")48 17 geomlog = Logger("geometry") 49 grablog = Logger("grab")50 draglog = Logger("dragndrop")51 alphalog = Logger("alpha")52 18 53 CAN_SET_WORKSPACE = False 19 20 XSHAPE = False 54 21 HAS_X11_BINDINGS = False 55 USE_X11_BINDINGS = POSIX and envbool("XPRA_USE_X11_BINDINGS", is_X11())56 prop_get, prop_set, prop_del = None, None, None57 NotifyInferior = None58 if USE_X11_BINDINGS:59 try:60 from xpra.gtk_common.error import xlog, verify_sync61 from xpra.x11.gtk_x11.prop import prop_get, prop_set, prop_del62 from xpra.x11.bindings.window_bindings import constants, X11WindowBindings, SHAPE_KIND #@UnresolvedImport63 from xpra.x11.bindings.core_bindings import X11CoreBindings, set_context_check64 from xpra.x11.gtk_x11.send_wm import send_wm_workspace65 22 66 set_context_check(verify_sync)67 X11Window = X11WindowBindings()68 X11Core = X11CoreBindings()69 NotifyInferior = constants["NotifyInferior"]70 HAS_X11_BINDINGS = True71 23 72 SubstructureNotifyMask = constants["SubstructureNotifyMask"]73 SubstructureRedirectMask = constants["SubstructureRedirectMask"]74 75 def can_set_workspace():76 SET_WORKSPACE = envbool("XPRA_SET_WORKSPACE", True)77 if not SET_WORKSPACE:78 return False79 try:80 #TODO: in theory this is not a proper check, meh - that will do81 root = get_default_root_window()82 supported = prop_get(root, "_NET_SUPPORTED", ["atom"], ignore_errors=True)83 return bool(supported) and "_NET_WM_DESKTOP" in supported84 except Exception as e:85 workspacelog("x11 workspace bindings error", exc_info=True)86 workspacelog.error("Error: failed to setup workspace hooks:")87 workspacelog.error(" %s", e)88 CAN_SET_WORKSPACE = can_set_workspace()89 except ImportError as e:90 log("x11 bindings", exc_info=True)91 log.error("Error: cannot import X11 bindings:")92 log.error(" %s", e)93 94 95 BREAK_MOVERESIZE = os.environ.get("XPRA_BREAK_MOVERESIZE", "Escape").split(",")96 MOVERESIZE_X11 = envbool("XPRA_MOVERESIZE_X11", POSIX)97 CURSOR_IDLE_TIMEOUT = envint("XPRA_CURSOR_IDLE_TIMEOUT", 6)98 DISPLAY_HAS_SCREEN_INDEX = POSIX and os.environ.get("DISPLAY", "").split(":")[-1].find(".")>=099 DRAGNDROP = envbool("XPRA_DRAGNDROP", True)100 CLAMP_WINDOW_TO_SCREEN = envbool("XPRA_CLAMP_WINDOW_TO_SCREEN", True)101 FOCUS_RECHECK_DELAY = envint("XPRA_FOCUS_RECHECK_DELAY", 0)102 103 WINDOW_OVERFLOW_TOP = envbool("XPRA_WINDOW_OVERFLOW_TOP", False)104 AWT_RECENTER = envbool("XPRA_AWT_RECENTER", True)105 SAVE_WINDOW_ICONS = envbool("XPRA_SAVE_WINDOW_ICONS", False)106 UNDECORATED_TRANSIENT_IS_OR = envint("XPRA_UNDECORATED_TRANSIENT_IS_OR", 1)107 XSHAPE = envbool("XPRA_XSHAPE", True)108 LAZY_SHAPE = envbool("XPRA_LAZY_SHAPE", True)109 def parse_padding_colors(colors_str):110 padding_colors = 0, 0, 0111 if colors_str:112 try:113 padding_colors = tuple(float(x.strip()) for x in colors_str.split(","))114 assert len(padding_colors)==3, "you must specify 3 components"115 except Exception as e:116 log.warn("Warning: invalid padding colors specified,")117 log.warn(" %s", e)118 log.warn(" using black")119 padding_colors = 0, 0, 0120 log("parse_padding_colors(%s)=%s", colors_str, padding_colors)121 return padding_colors122 PADDING_COLORS = parse_padding_colors(os.environ.get("XPRA_PADDING_COLORS"))123 124 #window types we map to POPUP rather than TOPLEVEL125 POPUP_TYPE_HINTS = set((126 #"DIALOG",127 #"MENU",128 #"TOOLBAR",129 #"SPLASH",130 #"UTILITY",131 #"DOCK",132 #"DESKTOP",133 "DROPDOWN_MENU",134 "POPUP_MENU",135 #"TOOLTIP",136 #"NOTIFICATION",137 #"COMBO",138 #"DND"139 ))140 #window types for which we skip window decorations (title bar)141 UNDECORATED_TYPE_HINTS = set((142 #"DIALOG",143 "MENU",144 #"TOOLBAR",145 "SPLASH",146 "SPLASHSCREEN",147 "UTILITY",148 "DOCK",149 "DESKTOP",150 "DROPDOWN_MENU",151 "POPUP_MENU",152 "TOOLTIP",153 "NOTIFICATION",154 "COMBO",155 "DND"))156 157 GDK_SCROLL_MAP = {158 Gdk.ScrollDirection.UP : 4,159 Gdk.ScrollDirection.DOWN : 5,160 Gdk.ScrollDirection.LEFT : 6,161 Gdk.ScrollDirection.RIGHT : 7,162 }163 164 165 def wn(w):166 return WORKSPACE_NAMES.get(w, w)167 168 169 class GTKKeyEvent(AdHocStruct):170 pass171 172 173 24 class GTKClientWindowBase(ClientWindowBase, Gtk.Window): 174 25 175 26 __common_gsignals__ = { … … 184 35 MAX_BACKING_DIMS = 16*1024, 16*1024 185 36 186 37 def init_window(self, metadata): 187 self.init_max_window_size() 188 if self._is_popup(metadata): 189 window_type = Gtk.WindowType.POPUP 190 else: 191 window_type = Gtk.WindowType.TOPLEVEL 192 self.on_realize_cb = {} 193 Gtk.Window.__init__(self, type = window_type) 38 #self.init_max_window_size() 39 Gtk.Window.__init__(self, type = Gtk.WindowType.TOPLEVEL) 194 40 self.init_drawing_area() 195 self.set_decorated(self._is_decorated(metadata)) 196 self._window_state = {} 197 self._resize_counter = 0 198 self._can_set_workspace = HAS_X11_BINDINGS and CAN_SET_WORKSPACE 199 self._current_frame_extents = None 200 self._screen = -1 201 self._frozen = False 202 self.window_state_timer = None 203 self.send_iconify_timer = None 204 self.remove_pointer_overlay_timer = None 205 self.show_pointer_overlay_timer = None 206 self.moveresize_timer = None 207 self.moveresize_event = None 208 #add platform hooks 209 self.connect_after("realize", self.on_realize) 210 self.connect('unrealize', self.on_unrealize) 41 self.set_decorated(True) 211 42 self.add_events(WINDOW_EVENT_MASK) 212 if DRAGNDROP and not self._client.readonly:213 self.init_dragndrop()214 self.init_focus()215 43 ClientWindowBase.init_window(self, metadata) 216 44 217 45 def init_drawing_area(self): … … 231 59 232 60 def init_widget_events(self, widget): 233 61 widget.add_events(WINDOW_EVENT_MASK) 234 def motion(_w, event):235 self._do_motion_notify_event(event)236 return True237 widget.connect("motion-notify-event", motion)238 def press(_w, event):239 self._do_button_press_event(event)240 return True241 widget.connect("button-press-event", press)242 def release(_w, event):243 self._do_button_release_event(event)244 return True245 widget.connect("button-release-event", release)246 def scroll(_w, event):247 self._do_scroll_event(event)248 return True249 widget.connect("scroll-event", scroll)250 62 widget.connect("draw", self.draw_widget) 251 63 252 64 def draw_widget(self, widget, context): … … 253 65 raise NotImplementedError() 254 66 255 67 256 ######################################################################257 # drag and drop:258 def init_dragndrop(self):259 targets = [260 Gtk.TargetEntry.new("text/uri-list", 0, 80),261 ]262 flags = Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT263 actions = Gdk.DragAction.COPY # | Gdk.ACTION_LINK264 self.drag_dest_set(flags, targets, actions)265 self.connect('drag_drop', self.drag_drop_cb)266 self.connect('drag_motion', self.drag_motion_cb)267 self.connect('drag_data_received', self.drag_got_data_cb)268 68 269 def drag_drop_cb(self, widget, context, x, y, time):270 targets = list(x.name() for x in context.list_targets())271 draglog("drag_drop_cb%s targets=%s", (widget, context, x, y, time), targets)272 if not targets:273 #this happens on macos, but we can still get the data..274 draglog("Warning: no targets provided, continuing anyway")275 elif "text/uri-list" not in targets:276 draglog("Warning: cannot handle targets:")277 draglog(" %s", csv(targets))278 return279 atom = Gdk.Atom.intern("text/uri-list", False)280 widget.drag_get_data(context, atom, time)281 282 def drag_motion_cb(self, wid, context, x, y, time):283 draglog("drag_motion_cb%s", (wid, context, x, y, time))284 Gdk.drag_status(context, Gdk.DragAction.COPY, time)285 return True #accept this data286 287 def drag_got_data_cb(self, wid, context, x, y, selection, info, time):288 draglog("drag_got_data_cb%s", (wid, context, x, y, selection, info, time))289 #draglog("%s: %s", type(selection), dir(selection))290 #draglog("%s: %s", type(context), dir(context))291 targets = list(x.name() for x in context.list_targets())292 actions = context.get_actions()293 def xid(w):294 #TODO: use a generic window handle function295 #this only used for debugging for now296 if w and POSIX:297 return w.get_xid()298 return 0299 dest_window = xid(context.get_dest_window())300 source_window = xid(context.get_source_window())301 suggested_action = context.get_suggested_action()302 draglog("drag_got_data_cb context: source_window=%#x, dest_window=%#x",303 source_window, dest_window)304 draglog("drag_got_data_cb context: suggested_action=%s, actions=%s, targets=%s",305 suggested_action, actions, targets)306 dtype = selection.get_data_type()307 fmt = selection.get_format()308 l = selection.get_length()309 target = selection.get_target()310 text = selection.get_text()311 uris = selection.get_uris()312 draglog("drag_got_data_cb selection: data type=%s, format=%s, length=%s, target=%s, text=%s, uris=%s",313 dtype, fmt, l, target, text, uris)314 if not uris:315 return316 filelist = []317 for uri in uris:318 if not uri:319 continue320 if not uri.startswith("file://"):321 draglog.warn("Warning: cannot handle drag-n-drop URI '%s'", uri)322 continue323 filename = unquote(uri[len("file://"):].rstrip("\n\r"))324 if WIN32:325 filename = filename.lstrip("/")326 abspath = os.path.abspath(filename)327 if not os.path.isfile(abspath):328 draglog.warn("Warning: '%s' is not a file", abspath)329 continue330 filelist.append(abspath)331 draglog("drag_got_data_cb: will try to upload: %s", csv(filelist))332 pending = set(filelist)333 #when all the files have been loaded / failed,334 #finish the drag and drop context so the source knows we're done with them:335 def file_done(filename):336 if not pending:337 return338 try:339 pending.remove(filename)340 except KeyError:341 pass342 if not pending:343 context.finish(True, False, time)344 for filename in filelist:345 def got_file_info(gfile, result, arg=None):346 draglog("got_file_info(%s, %s, %s)", gfile, result, arg)347 file_info = gfile.query_info_finish(result)348 basename = gfile.get_basename()349 ctype = file_info.get_content_type()350 size = file_info.get_size()351 draglog("file_info(%s)=%s ctype=%s, size=%s", filename, file_info, ctype, size)352 def got_file_data(gfile, result, user_data=None):353 _, data, entity = gfile.load_contents_finish(result)354 filesize = len(data)355 draglog("got_file_data(%s, %s, %s) entity=%s", gfile, result, user_data, entity)356 file_done(filename)357 openit = self._client.remote_open_files358 draglog.info("sending file %s (%i bytes)", basename, filesize)359 self._client.send_file(filename, "", data, filesize=filesize, openit=openit)360 cancellable = None361 user_data = (filename, True)362 gfile.load_contents_async(cancellable, got_file_data, user_data)363 try:364 gfile = Gio.File.new_for_path(filename)365 #basename = gf.get_basename()366 FILE_QUERY_INFO_NONE = 0367 G_PRIORITY_DEFAULT = 0368 cancellable = None369 gfile.query_info_async("standard::*", FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable, got_file_info, None)370 except Exception as e:371 draglog("file upload for %s:", filename, exc_info=True)372 draglog.error("Error: cannot upload '%s':", filename)373 draglog.error(" %s", e)374 del e375 file_done(filename)376 377 ######################################################################378 # focus:379 def init_focus(self):380 self.recheck_focus_timer = 0381 self.when_realized("init-focus", self.do_init_focus)382 383 def do_init_focus(self):384 #hook up the X11 gdk event notifications so we can get focus-out when grabs are active:385 if POSIX and not OSX:386 try:387 from xpra.x11.gtk_x11.gdk_bindings import add_event_receiver388 except ImportError as e:389 log("do_init_focus()", exc_info=True)390 log.warn("Warning: missing gdk bindings:")391 log.warn(" %s", e)392 else:393 self._focus_latest = None394 grablog("adding event receiver so we can get FocusIn and FocusOut events whilst grabbing the keyboard")395 add_event_receiver(self.get_window(), self)396 #other platforms should bet getting regular focus events instead:397 def focus_in(_window, event):398 focuslog("focus-in-event for wid=%s", self._id)399 self.do_xpra_focus_in_event(event)400 def focus_out(_window, event):401 focuslog("focus-out-event for wid=%s", self._id)402 self.do_xpra_focus_out_event(event)403 self.connect("focus-in-event", focus_in)404 self.connect("focus-out-event", focus_out)405 if not self._override_redirect:406 self.connect("notify::has-toplevel-focus", self._focus_change)407 408 def _focus_change(self, *args):409 assert not self._override_redirect410 htf = self.has_toplevel_focus()411 focuslog("%s focus_change%s has-toplevel-focus=%s, _been_mapped=%s", self, args, htf, self._been_mapped)412 if self._been_mapped:413 self._client.update_focus(self._id, htf)414 415 def recheck_focus(self):416 self.recheck_focus_timer = 0417 #we receive pairs of FocusOut + FocusIn following a keyboard grab,418 #so we recheck the focus status via this timer to skip unnecessary churn419 focused = self._client._focused420 focuslog("recheck_focus() wid=%i, focused=%s, latest=%s", self._id, focused, self._focus_latest)421 hasfocus = focused==self._id422 if hasfocus==self._focus_latest:423 #we're already up to date424 return425 if not self._focus_latest:426 self._client.window_ungrab()427 self._client.update_focus(self._id, False)428 else:429 self._client.update_focus(self._id, True)430 431 def cancel_focus_timer(self):432 rft = self.recheck_focus_timer433 if rft:434 self.recheck_focus_timer = 0435 self.source_remove(rft)436 437 def schedule_recheck_focus(self):438 if FOCUS_RECHECK_DELAY<0:439 self.recheck_focus()440 return441 if self.recheck_focus_timer==0:442 self.recheck_focus_timer = self.timeout_add(FOCUS_RECHECK_DELAY, self.recheck_focus)443 return True444 445 def do_xpra_focus_out_event(self, event):446 focuslog("do_xpra_focus_out_event(%s)", event)447 if NotifyInferior is not None:448 detail = getattr(event, "detail", None)449 if detail==NotifyInferior:450 focuslog("dropped NotifyInferior focus event")451 return True452 self._focus_latest = False453 return self.schedule_recheck_focus()454 455 def do_xpra_focus_in_event(self, event):456 focuslog("do_xpra_focus_in_event(%s) been_mapped=%s", event, self._been_mapped)457 if self._been_mapped:458 self._focus_latest = True459 return self.schedule_recheck_focus()460 461 462 def init_max_window_size(self):463 """ used by GL windows to enforce a hard limit on window sizes """464 saved_mws = self.max_window_size465 def clamp_to(maxw, maxh):466 #don't bother if the new limit is greater than 16k:467 if maxw>=16*1024 and maxh>=16*1024:468 return469 #only take into account the current max-window-size if non zero:470 mww, mwh = self.max_window_size471 if mww>0:472 maxw = min(mww, maxw)473 if mwh>0:474 maxh = min(mwh, maxh)475 self.max_window_size = maxw, maxh476 #viewport is easy, measured in window pixels:477 clamp_to(*self.MAX_VIEWPORT_DIMS)478 #backing dimensions are harder,479 #we have to take scaling into account (if any):480 clamp_to(*self._client.sp(*self.MAX_BACKING_DIMS))481 if self.max_window_size!=saved_mws:482 log("init_max_window_size(..) max-window-size changed from %s to %s",483 saved_mws, self.max_window_size)484 log(" because of max viewport dims %s and max backing dims %s",485 self.MAX_VIEWPORT_DIMS, self.MAX_BACKING_DIMS)486 487 488 def is_awt(self, metadata) -> bool:489 wm_class = metadata.get("class-instance")490 return wm_class and len(wm_class)==2 and wm_class[0].startswith("sun-awt-X11")491 492 def _is_popup(self, metadata) -> bool:493 #decide if the window type is POPUP or NORMAL494 if self._override_redirect:495 return True496 if UNDECORATED_TRANSIENT_IS_OR>0:497 transient_for = metadata.get("transient-for", -1)498 decorations = metadata.get("decorations", 0)499 if transient_for>0 and decorations<=0:500 if UNDECORATED_TRANSIENT_IS_OR>1:501 metalog("forcing POPUP type for window transient-for=%s", transient_for)502 return True503 if metadata.get("skip-taskbar") and self.is_awt(metadata):504 metalog("forcing POPUP type for Java AWT skip-taskbar window, transient-for=%s", transient_for)505 return True506 window_types = metadata.strtupleget("window-type")507 popup_types = tuple(POPUP_TYPE_HINTS.intersection(window_types))508 metalog("popup_types(%s)=%s", window_types, popup_types)509 if popup_types:510 metalog("forcing POPUP window type for %s", popup_types)511 return True512 return False513 514 def _is_decorated(self, metadata) -> bool:515 #decide if the window type is POPUP or NORMAL516 #(show window decorations or not)517 if self._override_redirect:518 return False519 return metadata.boolget("decorations", True)520 521 def set_decorated(self, decorated : bool):522 was_decorated = self.get_decorated()523 if self._fullscreen and was_decorated and not decorated:524 #fullscreen windows aren't decorated anyway!525 #calling set_decorated(False) would cause it to get unmapped! (why?)526 pass527 else:528 Gtk.Window.set_decorated(self, decorated)529 if WIN32:530 #workaround for new window offsets:531 #keep the window contents where they were and adjust the frame532 #this generates a configure event which ensures the server has the correct window position533 wfs = self._client.get_window_frame_sizes()534 if wfs and decorated and not was_decorated:535 geomlog("set_decorated(%s) re-adjusting window location using %s", decorated, wfs)536 normal = wfs.get("normal")537 fixed = wfs.get("fixed")538 if normal and fixed:539 nx, ny = normal540 fx, fy = fixed541 x, y = self.get_position()542 Gtk.Window.move(self, max(0, x-nx+fx), max(0, y-ny+fy))543 544 545 69 def setup_window(self, *args): 546 70 log("setup_window%s", args) 547 self.set_alpha()548 549 if self._override_redirect:550 transient_for = self.get_transient_for()551 type_hint = self.get_type_hint()552 if transient_for is not None and type_hint in self.OR_TYPE_HINTS:553 transient_for._override_redirect_windows.append(self)554 555 self.connect("property-notify-event", self.property_changed)556 self.connect("window-state-event", self.window_state_updated)557 558 71 #this will create the backing: 559 72 ClientWindowBase.setup_window(self, *args) 560 561 #try to honour the initial position 562 geomlog("setup_window() position=%s, set_initial_position=%s, OR=%s, decorated=%s", 563 self._pos, self._set_initial_position, self.is_OR(), self.get_decorated()) 564 if self._pos!=(0, 0) or self._set_initial_position or self.is_OR(): 565 x, y = self.adjusted_position(*self._pos) 566 if self.is_OR(): 567 #make sure OR windows are mapped on screen 568 if self._client._current_screen_sizes: 569 w, h = self._size 570 self.window_offset = self.calculate_window_offset(x, y, w, h) 571 geomlog("OR offsets=%s", self.window_offset) 572 if self.window_offset: 573 x += self.window_offset[0] 574 y += self.window_offset[1] 575 elif self.get_decorated(): 576 #try to adjust for window frame size if we can figure it out: 577 #Note: we cannot just call self.get_window_frame_size() here because 578 #the window is not realized yet, and it may take a while for the window manager 579 #to set the frame-extents property anyway 580 wfs = self._client.get_window_frame_sizes() 581 dx, dy = 0, 0 582 if wfs: 583 geomlog("setup_window() window frame sizes=%s", wfs) 584 v = wfs.get("offset") 585 if v: 586 dx, dy = v 587 x = max(0, x-dx) 588 y = max(0, y-dy) 589 self._pos = x, y 590 geomlog("setup_window() adjusted initial position=%s", self._pos) 591 self.move(x, y) 73 self.move(100, 100) 592 74 self.set_default_size(*self._size) 593 75 594 def new_backing(self, bw, bh):595 b = ClientWindowBase.new_backing(self, bw, bh)596 #call via idle_add so that the backing has time to be realized too:597 self.when_realized("cursor", self.idle_add, self._backing.set_cursor_data, self.cursor_data)598 return b599 600 def set_cursor_data(self, cursor_data):601 self.cursor_data = cursor_data602 b = self._backing603 if b:604 self.when_realized("cursor", b.set_cursor_data, cursor_data)605 606 76 def adjusted_position(self, ox, oy): 607 if AWT_RECENTER and self.is_awt(self._metadata):608 ss = self._client._current_screen_sizes609 if ss and len(ss)==1:610 screen0 = ss[0]611 monitors = screen0[5]612 if monitors and len(monitors)>1:613 monitor = monitors[0]614 mw = monitor[3]615 mh = monitor[4]616 w, h = self._size617 #adjust for window centering on monitor instead of screen java618 screen = self.get_screen()619 sw = screen.get_width()620 sh = screen.get_height()621 #re-center on first monitor if the window is within622 #$tolerance of the center of the screen:623 tolerance = 10624 #center of the window:625 cx = ox + w//2626 cy = oy + h//2627 if abs(sw//2 - cx) <= tolerance:628 x = mw//2 - w//2629 else:630 x = ox631 if abs(sh//2 - cy) <= tolerance:632 y = mh//2 - h//2633 else:634 y = oy635 geomlog("adjusted_position(%i, %i)=%i, %i", ox, oy, x, y)636 return x, y637 77 return ox, oy 638 78 639 79 640 def calculate_window_offset(self, wx, wy, ww, wh):641 ss = self._client._current_screen_sizes642 if not ss:643 return None644 if len(ss)!=1:645 geomlog("cannot handle more than one screen for OR offset")646 return None647 screen0 = ss[0]648 monitors = screen0[5]649 if not monitors:650 geomlog("screen %s lacks monitors information: %s", screen0)651 return None652 from xpra.rectangle import rectangle #@UnresolvedImport653 wrect = rectangle(wx, wy, ww, wh)654 rects = [wrect]655 pixels_in_monitor = {}656 for i, monitor in enumerate(monitors):657 plug_name, x, y, w, h = monitor[:5]658 new_rects = []659 for rect in rects:660 new_rects += rect.substract(x, y, w, h)661 geomlog("after removing areas visible on %s from %s: %s", plug_name, rects, new_rects)662 rects = new_rects663 if not rects:664 #the whole window is visible665 return None666 #keep track of how many pixels would be on this monitor:667 inter = wrect.intersection(x, y, w, h)668 if inter:669 pixels_in_monitor[inter.width*inter.height] = i670 #if we're here, then some of the window would land on an area671 #not show on any monitors672 #choose the monitor that had most of the pixels and make it fit:673 geomlog("pixels in monitor=%s", pixels_in_monitor)674 if not pixels_in_monitor:675 i = 0676 else:677 best = max(pixels_in_monitor.keys())678 i = pixels_in_monitor[best]679 monitor = monitors[i]680 plug_name, x, y, w, h = monitor[:5]681 geomlog("calculating OR offset for monitor %i: %s", i, plug_name)682 if ww>w or wh>=h:683 geomlog("window %ix%i is bigger than the monitor %i: %s %ix%i, not adjusting it",684 ww, wh, i, plug_name, w, h)685 return None686 dx = 0687 dy = 0688 if wx<x:689 dx = x-wx690 elif wx+ww>x+w:691 dx = (x+w) - (wx+ww)692 if wy<y:693 dy = y-wy694 elif wy+wh>y+h:695 dy = (y+h) - (wy+wh)696 assert dx!=0 or dy!=0697 geomlog("calculate_window_offset%s=%s", (wx, wy, ww, wh), (dx, dy))698 return dx, dy699 80 700 def when_realized(self, identifier, callback, *args):701 if self.get_realized():702 callback(*args)703 else:704 self.on_realize_cb[identifier] = callback, args705 706 def on_realize(self, widget):707 eventslog("on_realize(%s) gdk window=%s", widget, self.get_window())708 add_window_hooks(self)709 cb = self.on_realize_cb710 self.on_realize_cb = {}711 for x, args in cb.values():712 try:713 x(*args)714 except Exception:715 log.error("Error on realize callback %s for window %i", x, self._id, exc_info=True)716 if HAS_X11_BINDINGS:717 #request frame extents if the window manager supports it718 self._client.request_frame_extents(self)719 if self.watcher_pid:720 log("using watcher pid=%i for wid=%i", self.watcher_pid, self._id)721 prop_set(self.get_window(), "_NET_WM_PID", "u32", self.watcher_pid)722 if self.group_leader:723 self.get_window().set_group(self.group_leader)724 725 def on_unrealize(self, widget):726 eventslog("on_unrealize(%s)", widget)727 remove_window_hooks(self)728 729 730 def set_alpha(self):731 #try to enable alpha on this window if needed,732 #and if the backing class can support it:733 bc = self.get_backing_class()734 alphalog("set_alpha() has_alpha=%s, %s.HAS_ALPHA=%s, realized=%s",735 self._has_alpha, bc, bc.HAS_ALPHA, self.get_realized())736 #by default, only RGB (no transparency):737 #rgb_formats = tuple(BACKING_CLASS.RGB_MODES)738 self._client_properties["encodings.rgb_formats"] = ["RGB", "RGBX"]739 if not self._has_alpha or not bc.HAS_ALPHA:740 self._client_properties["encoding.transparency"] = False741 return742 if self._has_alpha and not self.get_realized():743 if enable_alpha(self):744 self._client_properties["encodings.rgb_formats"] = ["RGBA", "RGB", "RGBX"]745 self._window_alpha = True746 else:747 alphalog("enable_alpha()=False")748 self._has_alpha = False749 self._client_properties["encoding.transparency"] = False750 751 752 def freeze(self):753 #the OpenGL subclasses override this method to also free their GL context754 self._frozen = True755 self.iconify()756 757 def unfreeze(self):758 if not self._frozen or not self._iconified:759 return760 log("unfreeze() wid=%i, frozen=%s, iconified=%s", self._id, self._frozen, self._iconified)761 if not self._frozen or not self._iconified:762 #has been deiconified already763 return764 self._frozen = False765 self.deiconify()766 767 768 def show(self):769 Gtk.Window.show(self)770 771 772 def window_state_updated(self, widget, event):773 statelog("%s.window_state_updated(%s, %s) changed_mask=%s, new_window_state=%s",774 self, widget, repr(event), event.changed_mask, event.new_window_state)775 state_updates = {}776 if event.changed_mask & Gdk.WindowState.FULLSCREEN:777 state_updates["fullscreen"] = bool(event.new_window_state & Gdk.WindowState.FULLSCREEN)778 if event.changed_mask & Gdk.WindowState.ABOVE:779 state_updates["above"] = bool(event.new_window_state & Gdk.WindowState.ABOVE)780 if event.changed_mask & Gdk.WindowState.BELOW:781 state_updates["below"] = bool(event.new_window_state & Gdk.WindowState.BELOW)782 if event.changed_mask & Gdk.WindowState.STICKY:783 state_updates["sticky"] = bool(event.new_window_state & Gdk.WindowState.STICKY)784 if event.changed_mask & Gdk.WindowState.ICONIFIED:785 state_updates["iconified"] = bool(event.new_window_state & Gdk.WindowState.ICONIFIED)786 if event.changed_mask & Gdk.WindowState.MAXIMIZED:787 #this may get sent now as part of map_event code below (and it is irrelevant for the unmap case),788 #or when we get the configure event - which should come straight after789 #if we're changing the maximized state790 state_updates["maximized"] = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)791 if event.changed_mask & Gdk.WindowState.FOCUSED:792 state_updates["focused"] = bool(event.new_window_state & Gdk.WindowState.FOCUSED)793 self.update_window_state(state_updates)794 795 def update_window_state(self, state_updates):796 if self._client.readonly:797 log("update_window_state(%s) ignored in readonly mode", state_updates)798 return799 if state_updates.get("maximized") is False or state_updates.get("fullscreen") is False:800 #if we unfullscreen or unmaximize, re-calculate offsets if we have any:801 w, h = self._backing.render_size802 ww, wh = self.get_size()803 log("update_window_state(%s) unmax or unfullscreen", state_updates)804 log("window_offset=%s, backing render_size=%s, window size=%s",805 self.window_offset, (w, h), (ww, wh))806 if self._backing.offsets!=(0, 0, 0, 0):807 self.center_backing(w, h)808 self.repaint(0, 0, ww, wh)809 #decide if this is really an update by comparing with our local state vars:810 #(could just be a notification of a state change we already know about)811 actual_updates = {}812 for state,value in state_updates.items():813 var = "_" + state.replace("-", "_") #ie: "skip-pager" -> "_skip_pager"814 cur = getattr(self, var) #ie: self._maximized815 if cur!=value:816 setattr(self, var, value) #ie: self._maximized = True817 actual_updates[state] = value818 statelog("%s=%s (was %s)", var, value, cur)819 server_updates = dict((k,v) for k,v in actual_updates.items() if k in self._client.server_window_states)820 #iconification is handled a bit differently...821 try:822 iconified = server_updates.pop("iconified")823 except KeyError:824 iconified = None825 else:826 statelog("iconified=%s", iconified)827 #handle iconification as map events:828 if iconified:829 #usually means it is unmapped830 self._unfocus()831 if not self._override_redirect and not self.send_iconify_timer:832 #tell server, but wait a bit to try to prevent races:833 self.schedule_send_iconify()834 else:835 self.cancel_send_iconifiy_timer()836 self._frozen = False837 self.process_map_event()838 statelog("window_state_updated(..) state updates: %s, actual updates: %s, server updates: %s",839 state_updates, actual_updates, server_updates)840 self._window_state.update(server_updates)841 self.emit("state-updated")842 #if we have state updates, send them back to the server using a configure window packet:843 if self._window_state and not self.window_state_timer:844 self.window_state_timer = self.timeout_add(25, self.send_updated_window_state)845 846 def send_updated_window_state(self):847 self.window_state_timer = None848 if self._window_state and self.get_window():849 self.send_configure_event(True)850 851 def cancel_window_state_timer(self):852 wst = self.window_state_timer853 if wst:854 self.window_state_timer = None855 self.source_remove(wst)856 857 858 def schedule_send_iconify(self):859 #calculate a good delay to prevent races causing minimize/unminimize loops:860 if self._client.readonly:861 return862 delay = 150863 spl = tuple(self._client.server_ping_latency)864 if spl:865 worst = max(x[1] for x in self._client.server_ping_latency)866 delay += int(1000*worst)867 delay = min(1000, delay)868 statelog("telling server about iconification with %sms delay", delay)869 self.send_iconify_timer = self.timeout_add(delay, self.send_iconify)870 871 def send_iconify(self):872 self.send_iconify_timer = None873 if self._iconified:874 self.send("unmap-window", self._id, True, self._window_state)875 #we have sent the window-state already:876 self._window_state = {}877 self.cancel_window_state_timer()878 879 def cancel_send_iconifiy_timer(self):880 sit = self.send_iconify_timer881 if sit:882 self.send_iconify_timer = None883 self.source_remove(sit)884 885 886 def set_command(self, command):887 if not HAS_X11_BINDINGS:888 return889 v = command890 if not isinstance(command, str):891 try:892 v = v.decode("utf8")893 except UnicodeDecodeError:894 v = bytestostr(command)895 def do_set_command():896 metalog("do_set_command() str(%s)='%s' (type=%s)", command, nonl(v), type(command))897 prop_set(self.get_window(), "WM_COMMAND", "latin1", v)898 self.when_realized("command", do_set_command)899 900 901 def set_x11_property(self, prop_name, dtype, dformat, value):902 metalog("set_x11_property%s", (prop_name, dtype, dformat, value))903 dtype = bytestostr(dtype)904 if dtype=="latin1":905 value = bytestostr(value)906 if isinstance(value, (list, tuple)):907 dtype = (dtype, )908 def do_set_prop():909 gdk_window = self.get_window()910 if not dtype and not dformat:911 #remove prop912 prop_del(gdk_window, prop_name)913 else:914 prop_set(gdk_window, prop_name, dtype, value)915 self.when_realized("x11-prop-%s" % prop_name, do_set_prop)916 917 def set_class_instance(self, wmclass_name, wmclass_class):918 if not self.get_realized():919 #Warning: window managers may ignore the icons we try to set920 #if the wm_class value is set and matches something somewhere undocumented921 #(if the default is used, you cannot override the window icon)922 self.set_wmclass(wmclass_name, wmclass_class)923 elif HAS_X11_BINDINGS:924 xid = self.get_window().get_xid()925 with xlog:926 X11Window.setClassHint(xid, strtobytes(wmclass_class), strtobytes(wmclass_name))927 log("XSetClassHint(%s, %s) done", wmclass_class, wmclass_name)928 929 def set_shape(self, shape):930 shapelog("set_shape(%s)", shape)931 if not HAS_X11_BINDINGS or not XSHAPE:932 return933 def do_set_shape():934 xid = self.get_window().get_xid()935 x_off, y_off = shape.get("x", 0), shape.get("y", 0)936 for kind, name in SHAPE_KIND.items(): #@UndefinedVariable937 rectangles = shape.get("%s.rectangles" % name) #ie: Bounding.rectangles = [(0, 0, 150, 100)]938 if rectangles:939 #adjust for scaling:940 if self._client.xscale!=1 or self._client.yscale!=1:941 x_off, y_off = self._client.sp(x_off, y_off)942 rectangles = self.scale_shape_rectangles(name, rectangles)943 #too expensive to log with actual rectangles:944 shapelog("XShapeCombineRectangles(%#x, %s, %i, %i, %i rects)",945 xid, name, x_off, y_off, len(rectangles))946 with xlog:947 X11Window.XShapeCombineRectangles(xid, kind, x_off, y_off, rectangles)948 self.when_realized("shape", do_set_shape)949 950 def scale_shape_rectangles(self, kind_name, rectangles):951 if LAZY_SHAPE or len(rectangles)<2:952 #scale the rectangles without a bitmap...953 #results aren't so good! (but better than nothing?)954 srect = self._client.srect955 return [srect(*x) for x in rectangles]956 from PIL import Image, ImageDraw #@UnresolvedImport957 ww, wh = self._size958 sw, sh = self._client.cp(ww, wh)959 img = Image.new('1', (sw, sh), color=0)960 shapelog("drawing %s on bitmap(%s,%s)=%s", kind_name, sw, sh, img)961 d = ImageDraw.Draw(img)962 for x,y,w,h in rectangles:963 d.rectangle([x, y, x+w, y+h], fill=1)964 img = img.resize((ww, wh))965 shapelog("resized %s bitmap to window size %sx%s: %s", kind_name, ww, wh, img)966 #now convert back to rectangles...967 rectangles = []968 for y in range(wh):969 #for debugging, this is very useful, but costly!970 #shapelog("pixels[%3i]=%s", y, "".join([str(img.getpixel((x, y))) for x in range(ww)]))971 x = 0972 start = None973 while x<ww:974 #find first white pixel:975 while x<ww and img.getpixel((x, y))==0:976 x += 1977 start = x978 #find next black pixel:979 while x<ww and img.getpixel((x, y))!=0:980 x += 1981 end = x982 if start<end:983 rectangles.append((start, y, end-start, 1))984 return rectangles985 986 def set_bypass_compositor(self, v):987 if not HAS_X11_BINDINGS:988 return989 if v not in (0, 1, 2):990 v = 0991 def do_set_bypass_compositor():992 prop_set(self.get_window(), "_NET_WM_BYPASS_COMPOSITOR", "u32", v)993 self.when_realized("bypass-compositor", do_set_bypass_compositor)994 995 996 def set_strut(self, strut):997 if not HAS_X11_BINDINGS:998 return999 log("strut=%s", strut)1000 d = typedict(strut)1001 values = []1002 for x in ("left", "right", "top", "bottom"):1003 v = d.intget(x, 0)1004 #handle scaling:1005 if x in ("left", "right"):1006 v = self._client.sx(v)1007 else:1008 v = self._client.sy(v)1009 values.append(v)1010 has_partial = False1011 for x in ("left_start_y", "left_end_y",1012 "right_start_y", "right_end_y",1013 "top_start_x", "top_end_x",1014 "bottom_start_x", "bottom_end_x"):1015 if x in d:1016 has_partial = True1017 v = d.intget(x, 0)1018 if x.find("_x"):1019 v = self._client.sx(v)1020 elif x.find("_y"):1021 v = self._client.sy(v)1022 values.append(v)1023 log("setting strut=%s, has partial=%s", values, has_partial)1024 def do_set_strut():1025 if has_partial:1026 prop_set(self.get_window(), "_NET_WM_STRUT_PARTIAL", ["u32"], values)1027 prop_set(self.get_window(), "_NET_WM_STRUT", ["u32"], values[:4])1028 self.when_realized("strut", do_set_strut)1029 1030 1031 def set_modal(self, modal):1032 #with gtk2 setting the window as modal would prevent1033 #all other windows we manage from receiving input1034 #including other unrelated applications1035 #what we want is "window-modal"1036 #so we can turn this off using the "modal_windows" feature,1037 #from the command line and the system tray:1038 mw = self._client.modal_windows1039 log("set_modal(%s) modal_windows=%s", modal, mw)1040 Gtk.Window.set_modal(self, modal and mw)1041 1042 1043 def set_fullscreen_monitors(self, fsm):1044 #platform specific code:1045 log("set_fullscreen_monitors(%s)", fsm)1046 def do_set_fullscreen_monitors():1047 set_fullscreen_monitors(self.get_window(), fsm)1048 self.when_realized("fullscreen-monitors", do_set_fullscreen_monitors)1049 1050 1051 def set_shaded(self, shaded):1052 #platform specific code:1053 log("set_shaded(%s)", shaded)1054 def do_set_shaded():1055 set_shaded(self.get_window(), shaded)1056 self.when_realized("shaded", do_set_shaded)1057 1058 1059 def set_fullscreen(self, fullscreen):1060 statelog("%s.set_fullscreen(%s)", self, fullscreen)1061 def do_set_fullscreen():1062 if fullscreen:1063 #we may need to temporarily remove the max-window-size restrictions1064 #to be able to honour the fullscreen request:1065 w, h = self.max_window_size1066 if w>0 and h>0:1067 self.set_size_constraints(self.size_constraints, (0, 0))1068 self.fullscreen()1069 else:1070 self.unfullscreen()1071 #re-apply size restrictions:1072 w, h = self.max_window_size1073 if w>0 and h>0:1074 self.set_size_constraints(self.size_constraints, self.max_window_size)1075 self.when_realized("fullscreen", do_set_fullscreen)1076 1077 def set_xid(self, xid):1078 if not HAS_X11_BINDINGS:1079 return1080 if xid.startswith("0x") and xid.endswith("L"):1081 xid = xid[:-1]1082 try:1083 iid = int(xid, 16)1084 except Exception as e:1085 log("%s.set_xid(%s) error parsing/setting xid: %s", self, xid, e)1086 return1087 def do_set_xid():1088 self.xset_u32_property(self.get_window(), "XID", iid)1089 self.when_realized("xid", do_set_xid)1090 1091 def xget_u32_property(self, target, name):1092 if prop_get:1093 v = prop_get(target, name, "u32", ignore_errors=True)1094 log("%s.xget_u32_property(%s, %s)=%s", self, target, name, v)1095 if isinstance(v, int):1096 return v1097 return None1098 1099 def xset_u32_property(self, target, name, value):1100 prop_set(target, name, "u32", value)1101 1102 1103 def property_changed(self, widget, event):1104 atom = str(event.atom)1105 statelog("property_changed(%s, %s) : %s", widget, event, atom)1106 if atom=="_NET_WM_DESKTOP":1107 if self._been_mapped and not self._override_redirect and self._can_set_workspace:1108 self.do_workspace_changed(event)1109 elif atom=="_NET_FRAME_EXTENTS":1110 if prop_get:1111 v = prop_get(self.get_window(), "_NET_FRAME_EXTENTS", ["u32"], ignore_errors=False)1112 statelog("_NET_FRAME_EXTENTS: %s", v)1113 if v:1114 if v==self._current_frame_extents:1115 #unchanged1116 return1117 if not self._been_mapped:1118 #map event will take care of sending it1119 return1120 if self.is_OR() or self.is_tray():1121 #we can't do it: the server can't handle configure packets for OR windows!1122 return1123 if not self._client.server_window_frame_extents:1124 #can't send cheap "skip-geometry" packets or frame-extents feature not supported:1125 return1126 #tell server about new value:1127 self._current_frame_extents = v1128 statelog("sending configure event to update _NET_FRAME_EXTENTS to %s", v)1129 self._window_state["frame"] = self._client.crect(*v)1130 self.send_configure_event(True)1131 elif atom=="XKLAVIER_STATE":1132 if prop_get:1133 #unused for now, but log it:1134 xklavier_state = prop_get(self.get_window(), "XKLAVIER_STATE", ["integer"], ignore_errors=False)1135 keylog("XKLAVIER_STATE=%s", [hex(x) for x in (xklavier_state or [])])1136 elif atom=="_NET_WM_STATE":1137 if prop_get:1138 wm_state_atoms = prop_get(self.get_window(), "_NET_WM_STATE", ["atom"], ignore_errors=False)1139 #code mostly duplicated from gtk_x11/window.py:1140 WM_STATE_NAME = {1141 "fullscreen" : ("_NET_WM_STATE_FULLSCREEN", ),1142 "maximized" : ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"),1143 "shaded" : ("_NET_WM_STATE_SHADED", ),1144 "sticky" : ("_NET_WM_STATE_STICKY", ),1145 "skip-pager" : ("_NET_WM_STATE_SKIP_PAGER", ),1146 "skip-taskbar" : ("_NET_WM_STATE_SKIP_TASKBAR", ),1147 "above" : ("_NET_WM_STATE_ABOVE", ),1148 "below" : ("_NET_WM_STATE_BELOW", ),1149 "focused" : ("_NET_WM_STATE_FOCUSED", ),1150 }1151 state_atoms = set(wm_state_atoms or [])1152 state_updates = {}1153 for state, atoms in WM_STATE_NAME.items():1154 var = "_" + state.replace("-", "_") #ie: "skip-pager" -> "_skip_pager"1155 cur_state = getattr(self, var)1156 wm_state_is_set = set(atoms).issubset(state_atoms)1157 if wm_state_is_set and not cur_state:1158 state_updates[state] = True1159 elif cur_state and not wm_state_is_set:1160 state_updates[state] = False1161 log("_NET_WM_STATE=%s, state_updates=%s", wm_state_atoms, state_updates)1162 if state_updates:1163 self.update_window_state(state_updates)1164 1165 1166 81 ###################################################################### 1167 # workspace1168 def workspace_changed(self):1169 #on X11 clients, this fires from the root window property watcher1170 ClientWindowBase.workspace_changed(self)1171 if self._can_set_workspace:1172 self.do_workspace_changed("desktop workspace changed")1173 1174 def do_workspace_changed(self, info):1175 #call this method whenever something workspace related may have changed1176 window_workspace = self.get_window_workspace()1177 desktop_workspace = self.get_desktop_workspace()1178 workspacelog("do_workspace_changed(%s) for window %i (window, desktop): from %s to %s",1179 info, self._id,1180 (wn(self._window_workspace), wn(self._desktop_workspace)),1181 (wn(window_workspace), wn(desktop_workspace)))1182 if self._window_workspace==window_workspace and self._desktop_workspace==desktop_workspace:1183 #no change1184 return1185 suspend_resume = None1186 if desktop_workspace<0 or window_workspace is None:1187 #maybe the property has been cleared? maybe the window is being scrubbed?1188 workspacelog("not sure if the window is shown or not: %s vs %s, resuming to be safe",1189 wn(desktop_workspace), wn(window_workspace))1190 suspend_resume = False1191 elif window_workspace==WORKSPACE_UNSET:1192 workspacelog("workspace unset: assume current")1193 suspend_resume = False1194 elif window_workspace==WORKSPACE_ALL:1195 workspacelog("window is on all workspaces")1196 suspend_resume = False1197 elif desktop_workspace!=window_workspace:1198 workspacelog("window is on a different workspace, increasing its batch delay")1199 workspacelog(" desktop: %s, window: %s", wn(desktop_workspace), wn(window_workspace))1200 suspend_resume = True1201 elif self._window_workspace!=self._desktop_workspace:1202 assert desktop_workspace==window_workspace1203 workspacelog("window was on a different workspace, resetting its batch delay")1204 workspacelog(" (was desktop: %s, window: %s, now both on %s)",1205 wn(self._window_workspace), wn(self._desktop_workspace), wn(desktop_workspace))1206 suspend_resume = False1207 self._window_workspace = window_workspace1208 self._desktop_workspace = desktop_workspace1209 client_properties = {}1210 if window_workspace is not None:1211 client_properties["workspace"] = window_workspace1212 self.send_control_refresh(suspend_resume, client_properties)1213 1214 def send_control_refresh(self, suspend_resume, client_properties={}, refresh=False):1215 statelog("send_control_refresh%s", (suspend_resume, client_properties, refresh))1216 #we can tell the server using a "buffer-refresh" packet instead1217 #and also take care of tweaking the batch config1218 options = {"refresh-now" : refresh} #no need to refresh it1219 self._client.control_refresh(self._id, suspend_resume, refresh=refresh, options=options, client_properties=client_properties)1220 1221 def get_workspace_count(self):1222 if not self._can_set_workspace:1223 return None1224 root = get_default_root_window()1225 return self.xget_u32_property(root, "_NET_NUMBER_OF_DESKTOPS")1226 1227 def set_workspace(self, workspace):1228 workspacelog("set_workspace(%s)", workspace)1229 if not self._can_set_workspace:1230 return1231 if not self._been_mapped:1232 #will be dealt with in the map event handler1233 #which will look at the window metadata again1234 workspacelog("workspace=%s will be set when the window is mapped", wn(workspace))1235 return1236 desktop = self.get_desktop_workspace()1237 ndesktops = self.get_workspace_count()1238 current = self.get_window_workspace()1239 workspacelog("set_workspace(%s) realized=%s", wn(workspace), self.get_realized())1240 workspacelog(" current workspace=%s, detected=%s, desktop workspace=%s, ndesktops=%s",1241 wn(self._window_workspace), wn(current), wn(desktop), ndesktops)1242 if not self._can_set_workspace or ndesktops is None:1243 return1244 if workspace==desktop or workspace==WORKSPACE_ALL or desktop is None:1245 #window is back in view1246 self._client.control_refresh(self._id, False, False)1247 if (workspace<0 or workspace>=ndesktops) and workspace not in(WORKSPACE_UNSET, WORKSPACE_ALL):1248 #this should not happen, workspace is unsigned (CARDINAL)1249 #and the server should have the same list of desktops that we have here1250 workspacelog.warn("Warning: invalid workspace number: %s", wn(workspace))1251 workspace = WORKSPACE_UNSET1252 if workspace==WORKSPACE_UNSET:1253 #we cannot unset via send_wm_workspace, so we have to choose one:1254 workspace = self.get_desktop_workspace()1255 if workspace in (None, WORKSPACE_UNSET):1256 workspacelog.warn("workspace=%s (doing nothing)", wn(workspace))1257 return1258 #we will need the gdk window:1259 if current==workspace:1260 workspacelog("window workspace unchanged: %s", wn(workspace))1261 return1262 gdkwin = self.get_window()1263 workspacelog("do_set_workspace: gdkwindow: %#x, mapped=%s, visible=%s",1264 gdkwin.get_xid(), self.get_mapped(), gdkwin.is_visible())1265 root = get_default_root_window()1266 with xlog:1267 send_wm_workspace(root, gdkwin, workspace)1268 1269 def get_desktop_workspace(self):1270 window = self.get_window()1271 if window:1272 root = window.get_screen().get_root_window()1273 else:1274 #if we are called during init.. we don't have a window1275 root = get_default_root_window()1276 return self.do_get_workspace(root, "_NET_CURRENT_DESKTOP")1277 1278 def get_window_workspace(self):1279 return self.do_get_workspace(self.get_window(), "_NET_WM_DESKTOP", WORKSPACE_UNSET)1280 1281 def do_get_workspace(self, target, prop, default_value=None):1282 if not self._can_set_workspace:1283 workspacelog("do_get_workspace: not supported, returning %s", wn(default_value))1284 return default_value #windows and OSX do not have workspaces1285 if target is None:1286 workspacelog("do_get_workspace: target is None, returning %s", wn(default_value))1287 return default_value #window is not realized yet1288 value = self.xget_u32_property(target, prop)1289 if value is not None:1290 workspacelog("do_get_workspace %s=%s on window %i: %#x",1291 prop, wn(value), self._id, target.get_xid())1292 return value1293 workspacelog("do_get_workspace %s unset on window %i: %#x, returning default value=%s",1294 prop, self._id, target.get_xid(), wn(default_value))1295 return default_value1296 1297 1298 def keyboard_ungrab(self, *args):1299 grablog("keyboard_ungrab%s", args)1300 self._client.keyboard_grabbed = False1301 gdkwin = self.get_window()1302 if gdkwin:1303 d = gdkwin.get_display()1304 if d:1305 d.keyboard_ungrab(0)1306 return True1307 1308 def keyboard_grab(self, *args):1309 grablog("keyboard_grab%s", args)1310 r = Gdk.keyboard_grab(self.get_window(), True, 0)1311 self._client.keyboard_grabbed = r==Gdk.GrabStatus.SUCCESS1312 grablog("keyboard_grab%s Gdk.keyboard_grab(%s, True)=%s, keyboard_grabbed=%s",1313 args, self.get_window(), GRAB_STATUS_STRING.get(r), self._client.keyboard_grabbed)1314 1315 def toggle_keyboard_grab(self):1316 grabbed = self._client.keyboard_grabbed1317 grablog("toggle_keyboard_grab() grabbed=%s", grabbed)1318 if grabbed:1319 self.keyboard_ungrab()1320 else:1321 self.keyboard_grab()1322 1323 def pointer_grab(self, *args):1324 gdkwin = self.get_window()1325 em = Gdk.EventMask1326 event_mask = (em.BUTTON_PRESS_MASK |1327 em.BUTTON_RELEASE_MASK |1328 em.POINTER_MOTION_MASK |1329 em.POINTER_MOTION_HINT_MASK |1330 em.ENTER_NOTIFY_MASK |1331 em.LEAVE_NOTIFY_MASK)1332 r = Gdk.pointer_grab(gdkwin, True, event_mask, gdkwin, None, 0)1333 self._client.pointer_grabbed = r==Gdk.GrabStatus.SUCCESS1334 grablog("pointer_grab%s Gdk.pointer_grab(%s, True)=%s, pointer_grabbed=%s",1335 args, self.get_window(), GRAB_STATUS_STRING.get(r), self._client.pointer_grabbed)1336 1337 def pointer_ungrab(self, *args):1338 grablog("pointer_ungrab%s pointer_grabbed=%s",1339 args, self._client.pointer_grabbed)1340 self._client.pointer_grabbed = False1341 gdkwin = self.get_window()1342 if gdkwin:1343 d = gdkwin.get_display()1344 if d:1345 d.pointer_ungrab(0)1346 return True1347 1348 def toggle_pointer_grab(self):1349 pg = self._client.pointer_grabbed1350 grablog("toggle_pointer_grab() pointer_grabbed=%s", pg)1351 if pg:1352 self.pointer_ungrab()1353 else:1354 self.pointer_grab()1355 1356 1357 def toggle_fullscreen(self):1358 geomlog("toggle_fullscreen()")1359 if self._fullscreen:1360 self.unfullscreen()1361 else:1362 self.fullscreen()1363 1364 1365 ######################################################################1366 # pointer overlay handling1367 def cancel_remove_pointer_overlay_timer(self):1368 rpot = self.remove_pointer_overlay_timer1369 if rpot:1370 self.remove_pointer_overlay_timer = None1371 self.source_remove(rpot)1372 1373 def cancel_show_pointer_overlay_timer(self):1374 rsot = self.show_pointer_overlay_timer1375 if rsot:1376 self.show_pointer_overlay_timer = None1377 self.source_remove(rsot)1378 1379 def show_pointer_overlay(self, pos):1380 #schedule do_show_pointer_overlay if needed1381 b = self._backing1382 if not b:1383 return1384 prev = b.pointer_overlay1385 if pos is None:1386 if prev is None:1387 return1388 value = None1389 else:1390 if prev and prev[:2]==pos[:2]:1391 return1392 #store both scaled and unscaled value:1393 #(the opengl client uses the raw value)1394 value = pos[:2]+self._client.sp(*pos[:2])+pos[2:]1395 mouselog("show_pointer_overlay(%s) previous value=%s, new value=%s", pos, prev, value)1396 b.pointer_overlay = value1397 if not self.show_pointer_overlay_timer:1398 self.show_pointer_overlay_timer = self.timeout_add(10, self.do_show_pointer_overlay, prev)1399 1400 def do_show_pointer_overlay(self, prev):1401 #queue a draw event at the previous and current position of the pointer1402 #(so the backend will repaint / overlay the cursor image there)1403 self.show_pointer_overlay_timer = None1404 b = self._backing1405 if not b:1406 return1407 cursor_data = b.cursor_data1408 def abs_coords(x, y, size):1409 if self.window_offset:1410 x += self.window_offset[0]1411 y += self.window_offset[1]1412 w, h = size, size1413 if cursor_data:1414 w = cursor_data[3]1415 h = cursor_data[4]1416 xhot = cursor_data[5]1417 yhot = cursor_data[6]1418 x = x-xhot1419 y = y-yhot1420 return x, y, w, h1421 value = b.pointer_overlay1422 if value:1423 #repaint the scale value (in window coordinates):1424 x, y, w, h = abs_coords(*value[2:5])1425 self.repaint(x, y, w, h)1426 #clear it shortly after:1427 self.cancel_remove_pointer_overlay_timer()1428 def remove_pointer_overlay():1429 self.remove_pointer_overlay_timer = None1430 self.show_pointer_overlay(None)1431 self.remove_pointer_overlay_timer = self.timeout_add(CURSOR_IDLE_TIMEOUT*1000, remove_pointer_overlay)1432 if prev:1433 x, y, w, h = abs_coords(*prev[2:5])1434 self.repaint(x, y, w, h)1435 1436 1437 def _do_button_press_event(self, event):1438 #Gtk.Window.do_button_press_event(self, event)1439 self._button_action(event.button, event, True)1440 1441 def _do_button_release_event(self, event):1442 #Gtk.Window.do_button_release_event(self, event)1443 self._button_action(event.button, event, False)1444 1445 ######################################################################1446 82 # pointer motion 1447 83 1448 def _do_motion_notify_event(self, event):1449 #Gtk.Window.do_motion_notify_event(self, event)1450 if self.moveresize_event:1451 self.motion_moveresize(event)1452 ClientWindowBase._do_motion_notify_event(self, event)1453 1454 def motion_moveresize(self, event):1455 x_root, y_root, direction, button, start_buttons, wx, wy, ww, wh = self.moveresize_event1456 dirstr = MOVERESIZE_DIRECTION_STRING.get(direction, direction)1457 buttons = self._event_buttons(event)1458 if start_buttons is None:1459 #first time around, store the buttons1460 start_buttons = buttons1461 self.moveresize_event[4] = buttons1462 if (button>0 and button not in buttons) or (button==0 and start_buttons!=buttons):1463 geomlog("%s for window button %i is no longer pressed (buttons=%s) cancelling moveresize",1464 dirstr, button, buttons)1465 self.moveresize_event = None1466 else:1467 x = event.x_root1468 y = event.y_root1469 dx = x-x_root1470 dy = y-y_root1471 #clamp resizing using size hints,1472 #or sane defaults: minimum of (1x1) and maximum of (2*15x2*25)1473 minw = self.geometry_hints.get("min_width", 1)1474 minh = self.geometry_hints.get("min_height", 1)1475 maxw = self.geometry_hints.get("max_width", 2**15)1476 maxh = self.geometry_hints.get("max_height", 2**15)1477 geomlog("%s: min=%ix%i, max=%ix%i, window=%ix%i, delta=%ix%i",1478 dirstr, minw, minh, maxw, maxh, ww, wh, dx, dy)1479 if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_BOTTOM, MOVERESIZE_SIZE_BOTTOMLEFT):1480 #height will be set to: wh+dy1481 dy = max(minh-wh, dy)1482 dy = min(maxh-wh, dy)1483 elif direction in (MOVERESIZE_SIZE_TOPRIGHT, MOVERESIZE_SIZE_TOP, MOVERESIZE_SIZE_TOPLEFT):1484 #height will be set to: wh-dy1485 dy = min(wh-minh, dy)1486 dy = max(wh-maxh, dy)1487 if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_RIGHT, MOVERESIZE_SIZE_TOPRIGHT):1488 #width will be set to: ww+dx1489 dx = max(minw-ww, dx)1490 dx = min(maxw-ww, dx)1491 elif direction in (MOVERESIZE_SIZE_BOTTOMLEFT, MOVERESIZE_SIZE_LEFT, MOVERESIZE_SIZE_TOPLEFT):1492 #width will be set to: ww-dx1493 dx = min(ww-minw, dx)1494 dx = max(ww-maxw, dx)1495 #calculate move + resize:1496 if direction==MOVERESIZE_MOVE:1497 data = (wx+dx, wy+dy), None1498 elif direction==MOVERESIZE_SIZE_BOTTOMRIGHT:1499 data = None, (ww+dx, wh+dy)1500 elif direction==MOVERESIZE_SIZE_BOTTOM:1501 data = None, (ww, wh+dy)1502 elif direction==MOVERESIZE_SIZE_BOTTOMLEFT:1503 data = (wx+dx, wy), (ww-dx, wh+dy)1504 elif direction==MOVERESIZE_SIZE_RIGHT:1505 data = None, (ww+dx, wh)1506 elif direction==MOVERESIZE_SIZE_LEFT:1507 data = (wx+dx, wy), (ww-dx, wh)1508 elif direction==MOVERESIZE_SIZE_TOPRIGHT:1509 data = (wx, wy+dy), (ww+dx, wh-dy)1510 elif direction==MOVERESIZE_SIZE_TOP:1511 data = (wx, wy+dy), (ww, wh-dy)1512 elif direction==MOVERESIZE_SIZE_TOPLEFT:1513 data = (wx+dx, wy+dy), (ww-dx, wh-dy)1514 else:1515 #not handled yet!1516 data = None1517 geomlog("%s for window %ix%i: started at %s, now at %s, delta=%s, button=%s, buttons=%s, data=%s",1518 dirstr, ww, wh, (x_root, y_root), (x, y), (dx, dy), button, buttons, data)1519 if data:1520 #modifying the window is slower than moving the pointer,1521 #do it via a timer to batch things together1522 self.moveresize_data = data1523 if self.moveresize_timer is None:1524 self.moveresize_timer = self.timeout_add(20, self.do_moveresize)1525 1526 def do_moveresize(self):1527 self.moveresize_timer = None1528 mrd = self.moveresize_data1529 geomlog("do_moveresize() data=%s", mrd)1530 if not mrd:1531 return1532 move, resize = mrd1533 if move:1534 x, y = int(move[0]), int(move[1])1535 if resize:1536 w, h = int(resize[0]), int(resize[1])1537 if self._client.readonly:1538 #change size-constraints first,1539 #so the resize can be honoured:1540 sc = self._force_size_constraint(w, h)1541 self._metadata.update(sc)1542 self.set_metadata(sc)1543 if move and resize:1544 self.get_window().move_resize(x, y, w, h)1545 elif move:1546 self.get_window().move(x, y)1547 elif resize:1548 self.get_window().resize(w, h)1549 1550 1551 def initiate_moveresize(self, x_root, y_root, direction, button, source_indication):1552 statelog("initiate_moveresize%s",1553 (x_root, y_root, MOVERESIZE_DIRECTION_STRING.get(direction, direction),1554 button, SOURCE_INDICATION_STRING.get(source_indication, source_indication)))1555 if MOVERESIZE_X11 and HAS_X11_BINDINGS:1556 self.initiate_moveresize_X11(x_root, y_root, direction, button, source_indication)1557 return1558 if direction==MOVERESIZE_CANCEL:1559 self.moveresize_event = None1560 self.moveresize_data = None1561 else:1562 #use window coordinates (which include decorations)1563 wx, wy = self.get_window().get_root_origin()1564 ww, wh = self.get_size()1565 self.moveresize_event = [x_root, y_root, direction, button, None, wx, wy, ww, wh]1566 1567 def initiate_moveresize_X11(self, x_root, y_root, direction, button, source_indication):1568 statelog("initiate_moveresize_X11%s",1569 (x_root, y_root, MOVERESIZE_DIRECTION_STRING.get(direction, direction),1570 button, SOURCE_INDICATION_STRING.get(source_indication, source_indication)))1571 event_mask = SubstructureNotifyMask | SubstructureRedirectMask1572 root = self.get_window().get_screen().get_root_window()1573 root_xid = root.get_xid()1574 xwin = self.get_window().get_xid()1575 with xlog:1576 X11Core.UngrabPointer()1577 X11Window.sendClientMessage(root_xid, xwin, False, event_mask, "_NET_WM_MOVERESIZE",1578 x_root, y_root, direction, button, source_indication)1579 1580 1581 def apply_transient_for(self, wid):1582 if wid==-1:1583 def set_root_transient():1584 #root is a gdk window, so we need to ensure we have one1585 #backing our gtk window to be able to call set_transient_for on it1586 log("%s.apply_transient_for(%s) gdkwindow=%s, mapped=%s",1587 self, wid, self.get_window(), self.get_mapped())1588 self.get_window().set_transient_for(get_default_root_window())1589 self.when_realized("transient-for-root", set_root_transient)1590 else:1591 #gtk window is easier:1592 window = self._client._id_to_window.get(wid)1593 log("%s.apply_transient_for(%s) window=%s", self, wid, window)1594 if window:1595 self.set_transient_for(window)1596 1597 84 def cairo_paint_border(self, context, clip_area=None): 1598 log("cairo_paint_border(%s, %s)", context, clip_area) 1599 b = self.border 1600 if b is None or not b.shown: 1601 return 1602 s = b.size 1603 ww, wh = self.get_size() 1604 borders = [] 1605 #window is wide enough, add borders on the side: 1606 borders.append((0, 0, s, wh)) #left 1607 borders.append((ww-s, 0, s, wh)) #right 1608 #window is tall enough, add borders on top and bottom: 1609 borders.append((0, 0, ww, s)) #top 1610 borders.append((0, wh-s, ww, s)) #bottom 1611 for x, y, w, h in borders: 1612 if w<=0 or h<=0: 1613 continue 1614 r = Gdk.Rectangle() 1615 r.x = x 1616 r.y = y 1617 r.width = w 1618 r.height = h 1619 rect = r 1620 if clip_area: 1621 rect = clip_area.intersect(r) 1622 if rect.width==0 or rect.height==0: 1623 continue 1624 context.save() 1625 context.rectangle(x, y, w, h) 1626 context.clip() 1627 context.set_source_rgba(self.border.red, self.border.green, self.border.blue, self.border.alpha) 1628 context.fill() 1629 context.paint() 1630 context.restore() 85 pass 1631 86 1632 1633 def paint_spinner(self, context, area=None):1634 log("%s.paint_spinner(%s, %s)", self, context, area)1635 c = self._client1636 if not c:1637 return1638 ww, wh = self.get_size()1639 w = c.cx(ww)1640 h = c.cy(wh)1641 #add grey semi-opaque layer on top:1642 context.set_operator(cairo.OPERATOR_OVER)1643 context.set_source_rgba(0.2, 0.2, 0.2, 0.4)1644 #we can't use the area as rectangle with:1645 #context.rectangle(area)1646 #because those would be unscaled dimensions1647 #it's easier and safer to repaint the whole window:1648 context.rectangle(0, 0, w, h)1649 context.fill()1650 #add spinner:1651 dim = min(w/3.0, h/3.0, 100.0)1652 context.set_line_width(dim/10.0)1653 context.set_line_cap(cairo.LINE_CAP_ROUND)1654 context.translate(w/2, h/2)1655 from xpra.client.spinner import cv1656 count = int(monotonic_time()*4.0)1657 for i in range(8): #8 lines1658 context.set_source_rgba(0, 0, 0, cv.trs[count%8][i])1659 context.move_to(0.0, -dim/4.0)1660 context.line_to(0.0, -dim)1661 context.rotate(math.pi/4)1662 context.stroke()1663 1664 def spinner(self, _ok):1665 c = self._client1666 if not self.can_have_spinner() or not c:1667 return1668 #with normal windows, we just queue a draw request1669 #and let the expose event paint the spinner1670 w, h = self.get_size()1671 self.repaint(0, 0, w, h)1672 1673 1674 87 def do_map_event(self, event): 1675 88 log("%s.do_map_event(%s) OR=%s", self, event, self._override_redirect) 1676 Gtk.Window.do_map_event(self, event) 1677 if not self._override_redirect: 1678 #we can get a map event for an iconified window on win32: 1679 if self._iconified: 1680 self.deiconify() 1681 self.process_map_event() 89 v = Gtk.Window.do_map_event(self, event) 90 self.process_map_event() 91 return v 1682 92 1683 93 def process_map_event(self): 1684 94 x, y, w, h = self.get_drawing_area_geometry() 1685 95 state = self._window_state 1686 props = self._client_properties1687 96 self._client_properties = {} 1688 97 self._window_state = {} 1689 self.cancel_window_state_timer() 1690 workspace = self.get_window_workspace() 1691 if self._been_mapped: 1692 if workspace is None: 1693 #not set, so assume it is on the current workspace: 1694 workspace = self.get_desktop_workspace() 1695 else: 1696 self._been_mapped = True 1697 workspace = self._metadata.intget("workspace", WORKSPACE_UNSET) 1698 if workspace!=WORKSPACE_UNSET: 1699 log("map event set workspace %s", wn(workspace)) 1700 self.set_workspace(workspace) 1701 if self._window_workspace!=workspace and workspace is not None: 1702 workspacelog("map event: been_mapped=%s, changed workspace from %s to %s", 1703 self._been_mapped, wn(self._window_workspace), wn(workspace)) 1704 self._window_workspace = workspace 1705 if workspace is not None: 1706 props["workspace"] = workspace 1707 if self._client.server_window_frame_extents and "frame" not in state: 1708 wfs = self.get_window_frame_size() 1709 if wfs and len(wfs)==4: 1710 state["frame"] = self._client.crect(*wfs) 1711 self._current_frame_extents = wfs 1712 geomlog("map-window wid=%s, geometry=%s, client props=%s, state=%s", self._id, (x, y, w, h), props, state) 98 geomlog("map-window wid=%s, geometry=%s, client props=%s, state=%s", self._id, (x, y, w, h), {}, state) 1713 99 cx = self._client.cx 1714 100 cy = self._client.cy 1715 101 sx, sy, sw, sh = cx(x), cy(y), cx(w), cy(h) 1716 packet = ["map-window", self._id, sx, sy, sw, sh, props, state]102 packet = ["map-window", self._id, sx, sy, sw, sh, {}, state] 1717 103 self.send(*packet) 1718 104 self._pos = (x, y) 1719 105 self._size = (w, h) … … 1720 106 if self._backing is None: 1721 107 #we may have cleared the backing, so we must re-create one: 1722 108 self._set_backing_size(w, h) 1723 if not self._override_redirect:1724 htf = self.has_toplevel_focus()1725 focuslog("mapped: has-toplevel-focus=%s", htf)1726 if htf:1727 self._client.update_focus(self._id, htf)1728 109 1729 def get_window_frame_size(self):1730 frame = self._client.get_frame_extents(self)1731 if not frame:1732 #default to global value we may have:1733 wfs = self._client.get_window_frame_sizes()1734 if wfs:1735 frame = wfs.get("frame")1736 return frame1737 110 1738 1739 111 def send_configure(self): 1740 112 self.send_configure_event() 1741 113 1742 114 def do_configure_event(self, event): 1743 eventslog("%s.do_configure_event(%s) OR=%s, iconified=%s", 1744 self, event, self._override_redirect, self._iconified) 1745 Gtk.Window.do_configure_event(self, event) 1746 if not self._override_redirect and not self._iconified: 1747 self.process_configure_event() 115 v = Gtk.Window.do_configure_event(self, event) 116 self.process_configure_event() 117 return v 1748 118 1749 119 def process_configure_event(self, skip_geometry=False): 1750 120 assert skip_geometry or not self.is_OR() … … 1760 130 for window in self._override_redirect_windows: 1761 131 x, y = window.get_position() 1762 132 window.move(x+dx, y+dy) 1763 geomlog("configure event: current size=%s, new size=%s, backing=%s, iconified=%s", 1764 self._size, (w, h), self._backing, self._iconified) 1765 if (w, h) != self._size or (self._backing is None and not self._iconified): 133 if (w, h) != self._size or (self._backing is None): 1766 134 self._size = (w, h) 1767 135 self._set_backing_size(w, h) 1768 elif self._backing and not self._iconified:136 elif self._backing: 1769 137 geomlog("configure event: size unchanged, queueing redraw") 1770 138 self.repaint(0, 0, w, h) 1771 139 … … 1774 142 x, y, w, h = self.get_drawing_area_geometry() 1775 143 w = max(1, w) 1776 144 h = max(1, h) 1777 state = self._window_state 1778 props = self._client_properties 145 state = [] 1779 146 self._client_properties = {} 1780 147 self._window_state = {} 1781 self.cancel_window_state_timer()1782 if self._been_mapped:1783 #if the window has been mapped already, the workspace should be set:1784 workspace = self.get_window_workspace()1785 if self._window_workspace!=workspace and workspace is not None:1786 workspacelog("send_configure_event: changed workspace from %s to %s",1787 wn(self._window_workspace), wn(workspace))1788 self._window_workspace = workspace1789 props["workspace"] = workspace1790 148 cx = self._client.cx 1791 149 cy = self._client.cy 1792 150 sx, sy, sw, sh = cx(x), cy(y), cx(w), cy(h) 1793 packet = ["configure-window", self._id, sx, sy, sw, sh, props, self._resize_counter, state, skip_geometry]151 packet = ["configure-window", self._id, sx, sy, sw, sh, {}, 0, state, skip_geometry] 1794 152 pwid = self._id 1795 153 if self.is_OR(): 1796 154 pwid = -1 … … 1807 165 else: 1808 166 self.new_backing(self._client.cx(ww), self._client.cy(wh)) 1809 167 1810 def resize(self, w, h, resize_counter=0):1811 ww, wh = self.get_size()1812 geomlog("resize(%s, %s, %s) current size=%s, fullscreen=%s, maximized=%s",1813 w, h, resize_counter, (ww, wh), self._fullscreen, self._maximized)1814 self._resize_counter = resize_counter1815 if (w, h)==(ww, wh):1816 self._backing.offsets = 0, 0, 0, 01817 self.repaint(0, 0, w, h)1818 return1819 if not self._fullscreen and not self._maximized:1820 Gtk.Window.resize(self, w, h)1821 ww, wh = w, h1822 self._backing.offsets = 0, 0, 0, 01823 else:1824 self.center_backing(w, h)1825 geomlog("backing offsets=%s, window offset=%s", self._backing.offsets, self.window_offset)1826 self._set_backing_size(w, h)1827 self.repaint(0, 0, ww, wh)1828 1829 def center_backing(self, w, h):1830 ww, wh = self.get_size()1831 #align in the middle:1832 dw = max(0, ww-w)1833 dh = max(0, wh-h)1834 ox = dw//21835 oy = dh//21836 geomlog("using window offset values %i,%i", ox, oy)1837 #some backings use top,left values,1838 #(opengl uses left and botton since the viewport starts at the bottom)1839 self._backing.offsets = ox, oy, ox+(dw&0x1), oy+(dh&0x1)1840 geomlog("center_backing(%i, %i) window size=%ix%i, backing offsets=%s", w, h, ww, wh, self._backing.offsets)1841 #adjust pointer coordinates:1842 self.window_offset = ox, oy1843 1844 168 def paint_backing_offset_border(self, backing, context): 1845 w,h = self.get_size() 1846 left, top, right, bottom = backing.offsets 1847 if left!=0 or top!=0 or right!=0 or bottom!=0: 1848 context.save() 1849 context.set_source_rgb(*PADDING_COLORS) 1850 coords = ( 1851 (0, 0, left, h), #left hand side padding 1852 (0, 0, w, top), #top padding 1853 (w-right, 0, right, h), #RHS 1854 (0, h-bottom, w, bottom), #bottom 1855 ) 1856 geomlog("paint_backing_offset_border(%s, %s) offsets=%s, size=%s, rgb=%s, coords=%s", 1857 backing, context, backing.offsets, (w,h), PADDING_COLORS, coords) 1858 for rx, ry, rw, rh in coords: 1859 if rw>0 and rh>0: 1860 context.rectangle(rx, ry, rw, rh) 1861 context.fill() 1862 context.restore() 169 pass 1863 170 1864 171 def clip_to_backing(self, backing, context): 1865 w,h = self.get_size() 1866 left, top, right, bottom = backing.offsets 1867 clip_rect = (left, top, w-left-right, h-top-bottom) 1868 context.rectangle(*clip_rect) 1869 geomlog("clip_to_backing%s rectangle=%s", (backing, context), clip_rect) 1870 context.clip() 172 return 1871 173 1872 def move_resize(self, x, y, w, h, resize_counter=0):1873 geomlog("window %i move_resize%s", self._id, (x, y, w, h, resize_counter))1874 x, y = self.adjusted_position(x, y)1875 w = max(1, w)1876 h = max(1, h)1877 if self.window_offset:1878 x += self.window_offset[0]1879 y += self.window_offset[1]1880 #TODO: check this doesn't move it off-screen!1881 self._resize_counter = resize_counter1882 wx, wy = self.get_drawing_area_geometry()[:2]1883 if (wx, wy)==(x, y):1884 #same location, just resize:1885 if self._size==(w, h):1886 geomlog("window unchanged")1887 else:1888 geomlog("unchanged position %ix%i, using resize(%i, %i)", x, y, w, h)1889 self.resize(w, h)1890 return1891 #we have to move:1892 if not self.get_realized():1893 geomlog("window was not realized yet")1894 self.realize()1895 #adjust for window frame:1896 window = self.get_window()1897 ox, oy = window.get_origin()[-2:]1898 rx, ry = window.get_root_origin()1899 ax = x - (ox - rx)1900 ay = y - (oy - ry)1901 geomlog("window origin=%ix%i, root origin=%ix%i, actual position=%ix%i", ox, oy, rx, ry, ax, ay)1902 #validate against edge of screen (ensure window is shown):1903 if CLAMP_WINDOW_TO_SCREEN:1904 mw, mh = self._client.get_root_size()1905 if (ax + w)<=0:1906 ax = -w + 11907 elif ax >= mw:1908 ax = mw - 11909 if not WINDOW_OVERFLOW_TOP and ay<=0:1910 ay = 01911 elif (ay + h)<=0:1912 ay = -y + 11913 elif ay >= mh:1914 ay = mh -11915 geomlog("validated window position for total screen area %ix%i : %ix%i", mw, mh, ax, ay)1916 if self._size==(w, h):1917 #just move:1918 geomlog("window size unchanged: %ix%i, using move(%i, %i)", w, h, ax, ay)1919 window.move(ax, ay)1920 return1921 #resize:1922 self._size = (w, h)1923 geomlog("%s.move_resize%s", window, (ax, ay, w, h))1924 window.move_resize(ax, ay, w, h)1925 #re-init the backing with the new size1926 self._set_backing_size(w, h)1927 1928 1929 174 def noop_destroy(self): 1930 175 log.warn("Warning: window destroy called twice!") 1931 176 1932 177 def destroy(self): #pylint: disable=method-hidden 1933 self.cancel_window_state_timer()1934 self.cancel_send_iconifiy_timer()1935 self.cancel_show_pointer_overlay_timer()1936 self.cancel_remove_pointer_overlay_timer()1937 self.cancel_focus_timer()1938 mrt = self.moveresize_timer1939 if mrt:1940 self.moveresize_timer = None1941 self.source_remove(mrt)1942 self.on_realize_cb = {}1943 178 ClientWindowBase.destroy(self) 1944 179 Gtk.Window.destroy(self) 1945 self._unfocus()1946 180 self.destroy = self.noop_destroy 1947 181 1948 182 1949 183 def do_unmap_event(self, event): 1950 eventslog("do_unmap_event(%s)", event) 1951 self._unfocus() 1952 if not self._override_redirect: 1953 self.send("unmap-window", self._id, False) 184 self.send("unmap-window", self._id, False) 185 return Gtk.Window.do_unmap_event(event) 1954 186 1955 187 def do_delete_event(self, event): 1956 188 #Gtk.Window.do_delete_event(self, event) 1957 189 eventslog("do_delete_event(%s)", event) 1958 190 self._client.window_close_event(self._id) 1959 return True 1960 1961 1962 def _offset_pointer(self, x, y): 1963 if self.window_offset: 1964 x -= self.window_offset[0] 1965 y -= self.window_offset[1] 1966 return self._client.cp(x, y) 1967 1968 def _get_pointer(self, event): 1969 return event.x_root, event.y_root 1970 1971 def _get_relative_pointer(self, event): 1972 return event.x, event.y 1973 1974 def _pointer_modifiers(self, event): 1975 x, y = self._get_pointer(event) 1976 rx, ry = self._get_relative_pointer(event) 1977 #adjust for window offset: 1978 pointer = self._offset_pointer(x, y) 1979 relative_pointer = self._client.cp(rx, ry) 1980 #FIXME: state is used for both mods and buttons?? 1981 modifiers = self._client.mask_to_names(event.state) 1982 buttons = self._event_buttons(event) 1983 v = pointer, relative_pointer, modifiers, buttons 1984 mouselog("pointer_modifiers(%s)=%s (x_root=%s, y_root=%s, window_offset=%s)", 1985 event, v, event.x_root, event.y_root, self.window_offset) 1986 return v 1987 1988 def _event_buttons(self, event): 1989 return [button for mask, button in BUTTON_MASK.items() if event.state & mask] 1990 1991 def parse_key_event(self, event, pressed): 1992 keyval = event.keyval 1993 keycode = event.hardware_keycode 1994 keyname = Gdk.keyval_name(keyval) 1995 keyname = KEY_TRANSLATIONS.get((keyname, keyval, keycode), keyname) 1996 key_event = GTKKeyEvent() 1997 key_event.modifiers = self._client.mask_to_names(event.state) 1998 key_event.keyname = keyname or "" 1999 key_event.keyval = keyval or 0 2000 key_event.keycode = keycode 2001 key_event.group = event.group 2002 try: 2003 key_event.string = event.string or "" 2004 except UnicodeDecodeError as e: 2005 keylog("parse_key_event(%s, %s)", event, pressed, exc_info=True) 2006 if first_time("key-%s-%s" % (keycode, keyname)): 2007 keylog.warn("Warning: failed to parse string for key") 2008 keylog.warn(" keyname=%s, keycode=%s", keyname, keycode) 2009 keylog.warn(" %s", e) 2010 key_event.string = "" 2011 key_event.pressed = pressed 2012 keylog("parse_key_event(%s, %s)=%s", event, pressed, key_event) 2013 return key_event 2014 2015 def do_key_press_event(self, event): 2016 key_event = self.parse_key_event(event, True) 2017 if self.moveresize_event and key_event.keyname in BREAK_MOVERESIZE: 2018 #cancel move resize if there is one: 2019 self.moveresize_event = None 2020 self._client.handle_key_action(self, key_event) 2021 2022 def do_key_release_event(self, event): 2023 key_event = self.parse_key_event(event, False) 2024 self._client.handle_key_action(self, key_event) 2025 2026 2027 def _do_scroll_event(self, event): 2028 if self._client.readonly: 2029 return 2030 button_mapping = GDK_SCROLL_MAP.get(event.direction, -1) 2031 mouselog("do_scroll_event device=%s, direction=%s, button_mapping=%s", 2032 self._device_info(event), event.direction, button_mapping) 2033 if button_mapping>=0: 2034 self._button_action(button_mapping, event, True) 2035 self._button_action(button_mapping, event, False) 2036 2037 2038 def update_icon(self, img): 2039 self._current_icon = img 2040 has_alpha = img.mode=="RGBA" 2041 width, height = img.size 2042 rowstride = width * (3+int(has_alpha)) 2043 pixbuf = get_pixbuf_from_data(img.tobytes(), has_alpha, width, height, rowstride) 2044 iconlog("%s.set_icon(%s)", self, pixbuf) 2045 self.set_icon(pixbuf) 191 return Gtk.Window.do_delete_event(event)