Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/mixins/window_manager.py : 46%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2010-2019 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
8#pylint: disable-msg=E1101
10import os
11import errno
12import signal
13import datetime
14from collections import deque
15from time import sleep, time
16from queue import Queue
17from gi.repository import GLib
19from xpra.platform.gui import (
20 get_vrefresh, get_window_min_size, get_window_max_size,
21 get_double_click_time, get_double_click_distance, get_native_system_tray_classes,
22 )
23from xpra.platform.features import SYSTEM_TRAY_SUPPORTED
24from xpra.platform.paths import get_icon_filename
25from xpra.scripts.config import FALSE_OPTIONS
26from xpra.make_thread import make_thread
27from xpra.os_util import (
28 bytestostr, monotonic_time, memoryview_to_bytes,
29 OSX, POSIX, is_Ubuntu,
30 )
31from xpra.util import (
32 iround, envint, envbool, typedict,
33 make_instance, updict, repr_ellipsized, csv,
34 )
35from xpra.client.mixins.stub_client_mixin import StubClientMixin
36from xpra.log import Logger
38log = Logger("window")
39geomlog = Logger("geometry")
40paintlog = Logger("paint")
41drawlog = Logger("draw")
42focuslog = Logger("focus")
43grablog = Logger("grab")
44iconlog = Logger("icon")
45mouselog = Logger("mouse")
46cursorlog = Logger("cursor")
47metalog = Logger("metadata")
48traylog = Logger("client", "tray")
50MOUSE_SHOW = envbool("XPRA_MOUSE_SHOW", True)
52PAINT_FAULT_RATE = envint("XPRA_PAINT_FAULT_INJECTION_RATE")
53PAINT_FAULT_TELL = envbool("XPRA_PAINT_FAULT_INJECTION_TELL", True)
54PAINT_DELAY = envint("XPRA_PAINT_DELAY", 0)
56WM_CLASS_CLOSEEXIT = os.environ.get("XPRA_WM_CLASS_CLOSEEXIT", "Xephyr").split(",")
57TITLE_CLOSEEXIT = os.environ.get("XPRA_TITLE_CLOSEEXIT", "Xnest").split(",")
59OR_FORCE_GRAB_STR = os.environ.get("XPRA_OR_FORCE_GRAB", "DIALOG:sun-awt-X11")
60OR_FORCE_GRAB = {}
61for s in OR_FORCE_GRAB_STR.split(","):
62 parts = s.split(":")
63 if len(parts)==1:
64 OR_FORCE_GRAB.setdefault("*", []).append(s)
65 else:
66 OR_FORCE_GRAB.setdefault(parts[0], []).append(parts[1])
68SKIP_DUPLICATE_BUTTON_EVENTS = envbool("XPRA_SKIP_DUPLICATE_BUTTON_EVENTS", True)
69REVERSE_HORIZONTAL_SCROLLING = envbool("XPRA_REVERSE_HORIZONTAL_SCROLLING", OSX)
71DYNAMIC_TRAY_ICON = envbool("XPRA_DYNAMIC_TRAY_ICON", not OSX and not is_Ubuntu())
72ICON_OVERLAY = envint("XPRA_ICON_OVERLAY", 50)
73ICON_SHRINKAGE = envint("XPRA_ICON_SHRINKAGE", 75)
74SAVE_WINDOW_ICONS = envbool("XPRA_SAVE_WINDOW_ICONS", False)
75SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False)
76SIGNAL_WATCHER = envbool("XPRA_SIGNAL_WATCHER", True)
78FAKE_SUSPEND_RESUME = envint("XPRA_FAKE_SUSPEND_RESUME", 0)
81DRAW_TYPES = {bytes : "bytes", str : "bytes", tuple : "arrays", list : "arrays"}
84"""
85Utility superclass for clients that handle windows:
86create, resize, paint, grabs, cursors, etc
87"""
88class WindowClient(StubClientMixin):
90 def __init__(self):
91 StubClientMixin.__init__(self)
92 self._window_to_id = {}
93 self._id_to_window = {}
95 self.auto_refresh_delay = -1
96 self.min_window_size = 0, 0
97 self.max_window_size = 0, 0
99 #draw thread:
100 self._draw_queue = None
101 self._draw_thread = None
102 self._draw_counter = 0
104 #statistics and server info:
105 self.pixel_counter = deque(maxlen=1000)
107 self.readonly = False
108 self.windows_enabled = True
109 self.pixel_depth = 0
111 self.server_window_frame_extents = False
112 self.server_is_desktop = False
113 self.server_window_states = []
114 self.server_window_signals = ()
116 self.server_input_devices = None
117 self.server_precise_wheel = False
118 self.server_pointer_relative = False
120 self.input_devices = "auto"
122 self.overlay_image = None
124 self.server_cursors = False
125 self.client_supports_system_tray = False
126 self.client_supports_cursors = False
127 self.client_supports_bell = False
128 self.cursors_enabled = False
129 self.default_cursor_data = None
130 self.server_bell = False
131 self.bell_enabled = False
133 self.border = None
134 self.window_close_action = "forward"
135 self.modal_windows = True
137 self._pid_to_signalwatcher = {}
138 self._signalwatcher_to_wids = {}
140 self.wheel_map = {}
141 self.wheel_deltax = 0
142 self.wheel_deltay = 0
144 #state:
145 self.lost_focus_timer = None
146 self._focused = None
147 self._window_with_grab = None
148 self._suspended_at = 0
149 self._button_state = {}
151 def init(self, opts):
152 if opts.system_tray and SYSTEM_TRAY_SUPPORTED:
153 try:
154 from xpra.client import client_tray
155 assert client_tray
156 except ImportError:
157 log.warn("Warning: the tray forwarding module is missing")
158 else:
159 self.client_supports_system_tray = True
160 self.client_supports_cursors = opts.cursors
161 self.client_supports_bell = opts.bell
162 self.input_devices = opts.input_devices
163 self.auto_refresh_delay = opts.auto_refresh_delay
164 def parse_window_size(v, attribute="max-size"):
165 if v:
166 try:
167 pv = tuple(int(x.strip()) for x in v.split("x", 1))
168 assert len(pv)==2
169 return pv
170 except:
171 #the main script does some checking, but we could be called from a config file launch
172 log.warn("Warning: invalid window %s specified: %s", attribute, v)
173 return None
174 self.min_window_size = parse_window_size(opts.min_size) or get_window_min_size()
175 self.max_window_size = parse_window_size(opts.max_size) or get_window_max_size()
176 self.pixel_depth = int(opts.pixel_depth)
177 if self.pixel_depth not in (0, 16, 24, 30) and self.pixel_depth<32:
178 log.warn("Warning: invalid pixel depth %i", self.pixel_depth)
179 self.pixel_depth = 0
181 self.windows_enabled = opts.windows
182 if self.windows_enabled:
183 if opts.window_close not in ("forward", "ignore", "disconnect", "shutdown", "auto"):
184 self.window_close_action = "forward"
185 log.warn("Warning: invalid 'window-close' option: '%s'", opts.window_close)
186 log.warn(" using '%s'", self.window_close_action)
187 else:
188 self.window_close_action = opts.window_close
189 self.modal_windows = self.windows_enabled and opts.modal_windows
191 self.border_str = opts.border
192 if opts.border:
193 self.parse_border()
195 #mouse wheel:
196 mw = (opts.mousewheel or "").lower().replace("-", "")
197 if mw not in FALSE_OPTIONS:
198 UP = 4
199 LEFT = 6
200 Z1 = 8
201 for i in range(20):
202 btn = 4+i*2
203 invert = (
204 mw=="invert" or
205 mw=="invertall" or
206 (btn==UP and mw=="inverty") or
207 (btn==LEFT and mw=="invertx") or
208 (btn==Z1 and mw=="invertz")
209 )
210 if not invert:
211 self.wheel_map[btn] = btn
212 self.wheel_map[btn+1] = btn+1
213 else:
214 self.wheel_map[btn+1] = btn
215 self.wheel_map[btn] = btn+1
216 mouselog("wheel_map(%s)=%s", mw, self.wheel_map)
218 if 0<ICON_OVERLAY<=100:
219 icon_filename = get_icon_filename("xpra")
220 if icon_filename:
221 try:
222 #make sure Pillow's PNG image loader doesn't spam the output with debug messages:
223 import logging
224 logging.getLogger("PIL.PngImagePlugin").setLevel(logging.INFO)
225 from PIL import Image #@UnresolvedImport
226 self.overlay_image = Image.open(icon_filename)
227 except Exception as e:
228 log.error("Error: failed to load overlay icon '%s':", icon_filename, exc_info=True)
229 log.error(" %s", e)
230 traylog("overlay_image=%s", self.overlay_image)
231 self._draw_queue = Queue()
232 self._draw_thread = make_thread(self._draw_thread_loop, "draw")
235 def parse_border(self):
236 #not implemented here (see gtk3 client)
237 pass
240 def run(self):
241 #we decode pixel data in this thread
242 self._draw_thread.start()
243 if FAKE_SUSPEND_RESUME:
244 self.timeout_add(FAKE_SUSPEND_RESUME*1000, self.suspend)
245 self.timeout_add(FAKE_SUSPEND_RESUME*1000*2, self.resume)
248 def cleanup(self):
249 log("WindowClient.cleanup()")
250 #tell the draw thread to exit:
251 dq = self._draw_queue
252 if dq:
253 dq.put(None)
254 #the protocol has been closed, it is now safe to close all the windows:
255 #(cleaner and needed when we run embedded in the client launcher)
256 self.destroy_all_windows()
257 self.cancel_lost_focus_timer()
258 if dq:
259 dq.put(None)
260 log("WindowClient.cleanup() done")
263 def set_modal_windows(self, modal_windows):
264 self.modal_windows = modal_windows
265 #re-set flag on all the windows:
266 for w in self._id_to_window.values():
267 modal = w._metadata.boolget("modal", False)
268 w.set_modal(modal)
270 def set_windows_cursor(self, client_windows, new_cursor):
271 raise NotImplementedError()
273 def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name):
274 raise NotImplementedError()
277 def get_info(self):
278 info = {
279 "count" : len(self._window_to_id),
280 "min-size" : self.min_window_size,
281 "max-size" : self.max_window_size,
282 "draw-counter" : self._draw_counter,
283 "read-only" : self.readonly,
284 "wheel" : {
285 "delta-x" : self.wheel_deltax,
286 "delta-y" : self.wheel_deltay,
287 },
288 "focused" : self._focused or 0,
289 "grabbed" : self._window_with_grab or 0,
290 "buttons" : self._button_state,
291 }
292 for wid, window in tuple(self._id_to_window.items()):
293 info[wid] = window.get_info()
294 return {"windows" : info}
297 ######################################################################
298 # hello:
299 def get_caps(self) -> dict:
300 #FIXME: the messy bits without proper namespace:
301 caps = {
302 #generic server flags:
303 #mouse and cursors:
304 "mouse.show" : MOUSE_SHOW,
305 "mouse.initial-position" : self.get_mouse_position(),
306 "named_cursors" : False,
307 "cursors" : self.client_supports_cursors,
308 "double_click.time" : get_double_click_time(),
309 "double_click.distance" : get_double_click_distance(),
310 #features:
311 "bell" : self.client_supports_bell,
312 "vrefresh" : get_vrefresh(),
313 "windows" : self.windows_enabled,
314 "auto_refresh_delay" : int(self.auto_refresh_delay*1000),
315 #system tray forwarding:
316 "system_tray" : self.client_supports_system_tray,
317 "wants_default_cursor" : True,
318 }
319 updict(caps, "window", self.get_window_caps())
320 updict(caps, "encoding", {
321 "eos" : True,
322 })
323 return caps
325 def get_window_caps(self) -> dict:
326 return {
327 #implemented in the gtk client:
328 "min-size" : self.min_window_size,
329 "max-size" : self.max_window_size,
330 "restack" : True,
331 }
334 def parse_server_capabilities(self, c : typedict) -> bool:
335 self.server_window_frame_extents = c.boolget("window.frame-extents")
336 self.server_cursors = c.boolget("cursors", True) #added in 0.5, default to True!
337 self.cursors_enabled = self.server_cursors and self.client_supports_cursors
338 self.default_cursor_data = c.tupleget("cursor.default", None)
339 self.server_bell = c.boolget("bell") #added in 0.5, default to True!
340 self.bell_enabled = self.server_bell and self.client_supports_bell
341 if c.boolget("windows", True):
342 if self.windows_enabled:
343 server_auto_refresh_delay = c.intget("auto_refresh_delay", 0)/1000.0
344 if server_auto_refresh_delay==0 and self.auto_refresh_delay>0:
345 log.warn("Warning: server does not support auto-refresh!")
346 else:
347 log.warn("Warning: window forwarding is not enabled on this server")
348 self.server_window_signals = c.strtupleget("window.signals")
349 self.server_window_states = c.strtupleget("window.states", (
350 "iconified", "fullscreen",
351 "above", "below",
352 "sticky", "iconified", "maximized",
353 ))
354 self.server_is_desktop = c.boolget("shadow") or c.boolget("desktop")
355 #input devices:
356 self.server_input_devices = c.strget("input-devices")
357 self.server_precise_wheel = c.boolget("wheel.precise", False)
358 self.server_pointer_relative = c.boolget("pointer.relative", False)
359 return True
362 ######################################################################
363 # pointer:
364 def _process_pointer_position(self, packet):
365 wid, x, y = packet[1:4]
366 if len(packet)>=6:
367 rx, ry = packet[4:6]
368 else:
369 rx, ry = -1, -1
370 cx, cy = self.get_mouse_position()
371 start_time = monotonic_time()
372 mouselog("process_pointer_position: %i,%i (%i,%i relative to wid %i) - current position is %i,%i",
373 x, y, rx, ry, wid, cx, cy)
374 size = 10
375 for i,w in self._id_to_window.items():
376 #not all window implementations have this method:
377 #(but GLClientWindow does)
378 show_pointer_overlay = getattr(w, "show_pointer_overlay", None)
379 if show_pointer_overlay:
380 if i==wid:
381 value = rx, ry, size, start_time
382 else:
383 value = None
384 show_pointer_overlay(value)
386 def send_wheel_delta(self, wid, button, distance, *args):
387 modifiers = self.get_current_modifiers()
388 pointer = self.get_mouse_position()
389 buttons = []
390 mouselog("send_wheel_delta(%i, %i, %.4f, %s) precise wheel=%s, modifiers=%s, pointer=%s",
391 wid, button, distance, args, self.server_precise_wheel, modifiers, pointer)
392 if self.server_precise_wheel:
393 #send the exact value multiplied by 1000 (as an int)
394 idist = int(distance*1000)
395 if abs(idist)>0:
396 packet = ["wheel-motion", wid,
397 button, idist,
398 pointer, modifiers, buttons] + list(args)
399 mouselog("send_wheel_delta(..) %s", packet)
400 self.send_positional(packet)
401 return 0
402 else:
403 #server cannot handle precise wheel,
404 #so we have to use discrete events,
405 #and send a click for each step:
406 steps = abs(int(distance))
407 for _ in range(steps):
408 self.send_button(wid, button, True, pointer, modifiers, buttons)
409 self.send_button(wid, button, False, pointer, modifiers, buttons)
410 #return remainder:
411 return float(distance) - int(distance)
413 def wheel_event(self, wid, deltax=0, deltay=0, deviceid=0):
414 #this is a different entry point for mouse wheel events,
415 #which provides finer grained deltas (if supported by the server)
416 #accumulate deltas:
417 if REVERSE_HORIZONTAL_SCROLLING:
418 deltax = -deltax
419 self.wheel_deltax += deltax
420 self.wheel_deltay += deltay
421 button = self.wheel_map.get(6+int(self.wheel_deltax>0)) #RIGHT=7, LEFT=6
422 if button>0:
423 self.wheel_deltax = self.send_wheel_delta(wid, button, self.wheel_deltax, deviceid)
424 button = self.wheel_map.get(5-int(self.wheel_deltay>0)) #UP=4, DOWN=5
425 if button>0:
426 self.wheel_deltay = self.send_wheel_delta(wid, button, self.wheel_deltay, deviceid)
427 mouselog("wheel_event%s new deltas=%s,%s",
428 (wid, deltax, deltay, deviceid), self.wheel_deltax, self.wheel_deltay)
430 def send_button(self, wid, button, pressed, pointer, modifiers, buttons, *args):
431 pressed_state = self._button_state.get(button, False)
432 if SKIP_DUPLICATE_BUTTON_EVENTS and pressed_state==pressed:
433 mouselog("button action: unchanged state, ignoring event")
434 return
435 self._button_state[button] = pressed
436 packet = ["button-action", wid,
437 button, pressed,
438 pointer, modifiers, buttons] + list(args)
439 mouselog("button packet: %s", packet)
440 self.send_positional(packet)
442 def scale_pointer(self, pointer):
443 #subclass may scale this:
444 #return int(pointer[0]/self.xscale), int(pointer[1]/self.yscale)
445 return int(pointer[0]), int(pointer[1])
447 def send_input_devices(self, fmt, input_devices):
448 assert self.server_input_devices
449 self.send("input-devices", fmt, input_devices)
452 ######################################################################
453 # cursor:
454 def _process_cursor(self, packet):
455 if not self.cursors_enabled:
456 return
457 if len(packet)==2:
458 #marker telling us to use the default cursor:
459 new_cursor = packet[1]
460 else:
461 if len(packet)<9:
462 raise Exception("invalid cursor packet: %s items" % len(packet))
463 encoding = packet[1]
464 if not isinstance(encoding, bytes):
465 log.warn("Warning: received an invalid cursor packet:")
466 tmp_packet = list(packet)
467 try:
468 tmp_packet[9] = ".."
469 except IndexError:
470 pass
471 log.warn(" %s", repr_ellipsized(tmp_packet))
472 log.warn(" data types:")
473 log.warn(" %s", csv(type(x) for x in packet))
474 raise Exception("invalid cursor packet format: cursor type is a %s" % type(encoding))
475 #trim packet-type:
476 new_cursor = packet[1:]
477 pixels = new_cursor[8]
478 if encoding==b"png":
479 if SAVE_CURSORS:
480 serial = new_cursor[7]
481 with open("raw-cursor-%#x.png" % serial, 'wb') as f:
482 f.write(pixels)
483 from xpra.codecs.pillow.decoder import open_only
484 img = open_only(pixels, ("png",))
485 new_cursor[8] = img.tobytes("raw", "BGRA")
486 cursorlog("used PIL to convert png cursor to raw")
487 new_cursor[0] = b"raw"
488 elif encoding!=b"raw":
489 cursorlog.warn("Warning: invalid cursor encoding: %s", encoding)
490 return
491 self.set_windows_cursor(self._id_to_window.values(), new_cursor)
493 def reset_cursor(self):
494 self.set_windows_cursor(self._id_to_window.values(), [])
497 def cook_metadata(self, _new_window, metadata):
498 #subclasses can apply tweaks here:
499 return typedict(metadata)
502 ######################################################################
503 # system tray
504 def _process_new_tray(self, packet):
505 assert self.client_supports_system_tray
506 self._ui_event()
507 wid, w, h = packet[1:4]
508 w = max(1, self.sx(w))
509 h = max(1, self.sy(h))
510 metadata = typedict()
511 if len(packet)>=5:
512 metadata = typedict(packet[4])
513 traylog("tray %i metadata=%s", wid, metadata)
514 assert wid not in self._id_to_window, "we already have a window %s: %s" % (wid, self._id_to_window.get(wid))
515 app_id = wid
516 tray = self.setup_system_tray(self, app_id, wid, w, h, metadata)
517 traylog("process_new_tray(%s) tray=%s", packet, tray)
518 self._id_to_window[wid] = tray
519 self._window_to_id[tray] = wid
522 def make_system_tray(self, *args):
523 """ tray used for application systray forwarding """
524 tc = self.get_system_tray_classes()
525 traylog("make_system_tray%s system tray classes=%s", args, tc)
526 return make_instance(tc, self, *args)
528 def get_system_tray_classes(self):
529 #subclasses may add their toolkit specific variants, if any
530 #by overriding this method
531 #use the native ones first:
532 return get_native_system_tray_classes()
534 def setup_system_tray(self, client, app_id, wid, w, h, metadata):
535 tray_widget = None
536 #this is a tray forwarded for a remote application
537 def tray_click(button, pressed, event_time=0):
538 tray = self._id_to_window.get(wid)
539 traylog("tray_click(%s, %s, %s) tray=%s", button, pressed, event_time, tray)
540 if tray:
541 x, y = self.get_mouse_position()
542 modifiers = self.get_current_modifiers()
543 button_packet = ["button-action", wid, button, pressed, (x, y), modifiers]
544 traylog("button_packet=%s", button_packet)
545 self.send_positional(button_packet)
546 tray.reconfigure()
547 def tray_mouseover(x, y):
548 tray = self._id_to_window.get(wid)
549 traylog("tray_mouseover(%s, %s) tray=%s", x, y, tray)
550 if tray:
551 modifiers = self.get_current_modifiers()
552 buttons = []
553 pointer_packet = ["pointer-position", wid, self.cp(x, y), modifiers, buttons]
554 traylog("pointer_packet=%s", pointer_packet)
555 self.send_mouse_position(pointer_packet)
556 def do_tray_geometry(*args):
557 #tell the "ClientTray" where it now lives
558 #which should also update the location on the server if it has changed
559 tray = self._id_to_window.get(wid)
560 if tray_widget:
561 geom = tray_widget.get_geometry()
562 else:
563 geom = None
564 traylog("tray_geometry(%s) widget=%s, geometry=%s tray=%s", args, tray_widget, geom, tray)
565 if tray and geom:
566 tray.move_resize(*geom)
567 def tray_geometry(*args):
568 #the tray widget may still be None if we haven't returned from make_system_tray yet,
569 #in which case we will check the geometry a little bit later:
570 if tray_widget:
571 do_tray_geometry(*args)
572 else:
573 self.idle_add(do_tray_geometry, *args)
574 def tray_exit(*args):
575 traylog("tray_exit(%s)", args)
576 title = metadata.strget("title", "")
577 tray_widget = self.make_system_tray(app_id, None, title, None, tray_geometry, tray_click, tray_mouseover, tray_exit)
578 traylog("setup_system_tray%s tray_widget=%s", (client, app_id, wid, w, h, title), tray_widget)
579 assert tray_widget, "could not instantiate a system tray for tray id %s" % wid
580 tray_widget.show()
581 from xpra.client.client_tray import ClientTray
582 mmap = getattr(self, "mmap", None)
583 return ClientTray(client, wid, w, h, metadata, tray_widget, self.mmap_enabled, mmap)
586 def get_tray_window(self, app_name, hints):
587 #try to identify the application tray that generated this notification,
588 #so we can show it as coming from the correct systray icon
589 #on platforms that support it (ie: win32)
590 trays = tuple(w for w in self._id_to_window.values() if w.is_tray())
591 if trays:
592 try:
593 pid = int(hints.get("pid") or 0)
594 except (TypeError, ValueError):
595 pass
596 else:
597 if pid:
598 for tray in trays:
599 metadata = getattr(tray, "_metadata", typedict())
600 if metadata.intget("pid")==pid:
601 traylog("tray window: matched pid=%i", pid)
602 return tray.tray_widget
603 if app_name and app_name.lower()!="xpra":
604 #exact match:
605 for tray in trays:
606 #traylog("window %s: is_tray=%s, title=%s", window,
607 # window.is_tray(), getattr(window, "title", None))
608 if tray.title==app_name:
609 return tray.tray_widget
610 for tray in trays:
611 if tray.title.find(app_name)>=0:
612 return tray.tray_widget
613 return self.tray
616 def set_tray_icon(self):
617 #find all the window icons,
618 #and if they are all using the same one, then use it as tray icon
619 #otherwise use the default icon
620 traylog("set_tray_icon() DYNAMIC_TRAY_ICON=%s, tray=%s", DYNAMIC_TRAY_ICON, self.tray)
621 if not self.tray:
622 return
623 if not DYNAMIC_TRAY_ICON:
624 #the icon ends up looking garbled on win32,
625 #and we somehow also lose the settings that can keep us in the visible systray list
626 #so don't bother
627 return
628 windows = tuple(w for w in self._window_to_id if not w.is_tray())
629 #get all the icons:
630 icons = tuple(getattr(w, "_current_icon", None) for w in windows)
631 missing = sum(1 for icon in icons if icon is None)
632 traylog("set_tray_icon() %i windows, %i icons, %i missing", len(windows), len(icons), missing)
633 if icons and not missing:
634 icon = icons[0]
635 for i in icons[1:]:
636 if i!=icon:
637 #found a different icon
638 icon = None
639 break
640 if icon:
641 has_alpha = icon.mode=="RGBA"
642 width, height = icon.size
643 traylog("set_tray_icon() using unique %s icon: %ix%i (has-alpha=%s)",
644 icon.mode, width, height, has_alpha)
645 rowstride = width * (3+int(has_alpha))
646 rgb_data = icon.tobytes("raw", icon.mode)
647 self.tray.set_icon_from_data(rgb_data, has_alpha, width, height, rowstride)
648 return
649 #this sets the default icon (badly named function!)
650 traylog("set_tray_icon() using default icon")
651 self.tray.set_icon()
654 ######################################################################
655 # combine the window icon with our own icon
656 def _window_icon_image(self, wid, width, height, coding, data):
657 #convert the data into a pillow image,
658 #adding the icon overlay (if enabled)
659 from PIL import Image
660 coding = bytestostr(coding)
661 iconlog("%s.update_icon(%s, %s, %s, %s bytes) ICON_SHRINKAGE=%s, ICON_OVERLAY=%s",
662 self, width, height, coding, len(data), ICON_SHRINKAGE, ICON_OVERLAY)
663 if coding=="default":
664 img = self.overlay_image
665 elif coding == "BGRA":
666 rowstride = width*4
667 img = Image.frombytes("RGBA", (width,height), memoryview_to_bytes(data), "raw", "BGRA", rowstride, 1)
668 has_alpha = True
669 elif coding in ("BGRA", "premult_argb32"):
670 if coding == "premult_argb32":
671 #we usually cannot do in-place and this is not performance critical
672 from xpra.codecs.argb.argb import unpremultiply_argb #@UnresolvedImport
673 data = unpremultiply_argb(data)
674 rowstride = width*4
675 img = Image.frombytes("RGBA", (width,height), memoryview_to_bytes(data), "raw", "BGRA", rowstride, 1)
676 has_alpha = True
677 else:
678 from xpra.codecs.pillow.decoder import open_only
679 img = open_only(data, ("png", ))
680 if img.mode not in ("RGB", "RGBA"):
681 img = img.convert("RGBA")
682 has_alpha = img.mode=="RGBA"
683 rowstride = width * (3+int(has_alpha))
684 icon = img
685 save_time = int(time())
686 if SAVE_WINDOW_ICONS:
687 filename = "client-window-%i-icon-%i.png" % (wid, save_time)
688 icon.save(filename, "png")
689 iconlog("client window icon saved to %s", filename)
690 if self.overlay_image and self.overlay_image!=img:
691 if 0<ICON_SHRINKAGE<100:
692 #paste the application icon in the top-left corner,
693 #shrunk by ICON_SHRINKAGE pct
694 shrunk_width = max(1, width*ICON_SHRINKAGE//100)
695 shrunk_height = max(1, height*ICON_SHRINKAGE//100)
696 icon_resized = icon.resize((shrunk_width, shrunk_height), Image.ANTIALIAS)
697 icon = Image.new("RGBA", (width, height))
698 icon.paste(icon_resized, (0, 0, shrunk_width, shrunk_height))
699 if SAVE_WINDOW_ICONS:
700 filename = "client-window-%i-icon-shrunk-%i.png" % (wid, save_time)
701 icon.save(filename, "png")
702 iconlog("client shrunk window icon saved to %s", filename)
703 assert 0<ICON_OVERLAY<=100
704 overlay_width = max(1, width*ICON_OVERLAY//100)
705 overlay_height = max(1, height*ICON_OVERLAY//100)
706 xpra_resized = self.overlay_image.resize((overlay_width, overlay_height), Image.ANTIALIAS)
707 xpra_corner = Image.new("RGBA", (width, height))
708 xpra_corner.paste(xpra_resized, (width-overlay_width, height-overlay_height, width, height))
709 if SAVE_WINDOW_ICONS:
710 filename = "client-window-%i-icon-xpracorner-%i.png" % (wid, save_time)
711 xpra_corner.save(filename, "png")
712 iconlog("client xpracorner window icon saved to %s", filename)
713 composite = Image.alpha_composite(icon, xpra_corner)
714 icon = composite
715 if SAVE_WINDOW_ICONS:
716 filename = "client-window-%i-icon-composited-%i.png" % (wid, save_time)
717 icon.save(filename, "png")
718 iconlog("client composited window icon saved to %s", filename)
719 return icon
722 ######################################################################
723 # regular windows:
724 def _process_new_common(self, packet, override_redirect):
725 self._ui_event()
726 wid, x, y, w, h = packet[1:6]
727 assert 0<=w<32768 and 0<=h<32768
728 metadata = self.cook_metadata(True, packet[6])
729 metalog("process_new_common: %s, metadata=%s, OR=%s", packet[1:7], metadata, override_redirect)
730 assert wid not in self._id_to_window, "we already have a window %s: %s" % (wid, self._id_to_window.get(wid))
731 if w<1 or h<1:
732 log.error("Error: window %i dimensions %ix%i are invalid", wid, w, h)
733 w, h = 1, 1
734 #scaled dimensions of window:
735 wx = self.sx(x)
736 wy = self.sy(y)
737 ww = max(1, self.sx(w))
738 wh = max(1, self.sy(h))
739 #backing size, same as original (server-side):
740 bw, bh = w, h
741 client_properties = {}
742 if len(packet)>=8:
743 client_properties = packet[7]
744 geomlog("process_new_common: wid=%i, OR=%s, geometry(%s)=%s / %s",
745 wid, override_redirect, packet[2:6], (wx, wy, ww, wh), (bw, bh))
746 self.make_new_window(wid, wx, wy, ww, wh, bw, bh, metadata, override_redirect, client_properties)
748 def make_new_window(self, wid, wx, wy, ww, wh, bw, bh, metadata, override_redirect, client_properties):
749 client_window_classes = self.get_client_window_classes(ww, wh, metadata, override_redirect)
750 group_leader_window = self.get_group_leader(wid, metadata, override_redirect)
751 #workaround for "popup" OR windows without a transient-for (like: google chrome popups):
752 #prevents them from being pushed under other windows on OSX
753 #find a "transient-for" value using the pid to find a suitable window
754 #if possible, choosing the currently focused window (if there is one..)
755 pid = metadata.intget("pid", 0)
756 watcher_pid = self.assign_signal_watcher_pid(wid, pid)
757 if override_redirect and pid>0 and metadata.intget("transient-for", 0)==0 and metadata.strget("role")=="popup":
758 tfor = None
759 for twid, twin in self._id_to_window.items():
760 if not twin._override_redirect and twin._metadata.intget("pid", -1)==pid:
761 tfor = twin
762 if twid==self._focused:
763 break
764 if tfor:
765 log("forcing transient for=%s for new window %s", twid, wid)
766 metadata["transient-for"] = twid
767 border = None
768 if self.border:
769 border = self.border.clone()
770 window = None
771 log("make_new_window(..) client_window_classes=%s, group_leader_window=%s",
772 client_window_classes, group_leader_window)
773 for cwc in client_window_classes:
774 try:
775 window = cwc(self, group_leader_window, watcher_pid, wid,
776 wx, wy, ww, wh, bw, bh,
777 metadata, override_redirect, client_properties,
778 border, self.max_window_size, self.default_cursor_data, self.pixel_depth,
779 self.headerbar)
780 break
781 except Exception:
782 log.warn("failed to instantiate %s", cwc, exc_info=True)
783 if window is None:
784 log.warn("no more options.. this window will not be shown, sorry")
785 return None
786 log("make_new_window(..) window(%i)=%s", wid, window)
787 self._id_to_window[wid] = window
788 self._window_to_id[window] = wid
789 window.show_all()
790 if override_redirect:
791 if self.should_force_grab(metadata):
792 grablog.warn("forcing grab for OR window %i, matches %s", wid, OR_FORCE_GRAB)
793 self.window_grab(window)
794 return window
796 def should_force_grab(self, metadata):
797 if not OR_FORCE_GRAB:
798 return False
799 window_types = metadata.get("window-type", [])
800 wm_class = metadata.strtupleget("class-instance", (None, None), 2, 2)
801 c = None
802 if wm_class:
803 c = wm_class[0]
804 if c:
805 for window_type, force_wm_classes in OR_FORCE_GRAB.items():
806 #ie: DIALOG : ["sun-awt-X11"]
807 if window_type=="*" or window_type in window_types:
808 for wmc in force_wm_classes:
809 if wmc=="*" or c and c.startswith(wmc):
810 return True
811 return False
813 ######################################################################
814 # listen for process signals using a watcher process:
815 def assign_signal_watcher_pid(self, wid, pid):
816 if not SIGNAL_WATCHER:
817 return 0
818 if not POSIX or OSX or not pid:
819 return 0
820 proc = self._pid_to_signalwatcher.get(pid)
821 if proc is None or proc.poll():
822 from xpra.child_reaper import getChildReaper
823 from subprocess import Popen, PIPE, STDOUT
824 try:
825 proc = Popen("xpra_signal_listener",
826 stdin=PIPE, stdout=PIPE, stderr=STDOUT,
827 start_new_session=True)
828 except OSError as e:
829 log("assign_signal_watcher_pid(%s, %s)", wid, pid, exc_info=True)
830 log.error("Error: cannot execute signal listener")
831 log.error(" %s", e)
832 proc = None
833 if proc and proc.poll() is None:
834 #def add_process(self, process, name, command, ignore=False, forget=False, callback=None):
835 proc.stdout_io_watch = None
836 def watcher_terminated(*args):
837 #watcher process terminated, remove io watch:
838 #this may be redundant since we also return False from signal_watcher_event
839 log("watcher_terminated%s", args)
840 source = proc.stdout_io_watch
841 if source:
842 proc.stdout_io_watch = None
843 self.source_remove(source)
844 getChildReaper().add_process(proc, "signal listener for remote process %s" % pid,
845 command="xpra_signal_listener", ignore=True, forget=True,
846 callback=watcher_terminated)
847 log("using watcher pid=%i for server pid=%i", proc.pid, pid)
848 self._pid_to_signalwatcher[pid] = proc
849 proc.stdout_io_watch = GLib.io_add_watch(proc.stdout,
850 GLib.PRIORITY_DEFAULT, GLib.IO_IN,
851 self.signal_watcher_event, proc, pid, wid)
852 if proc:
853 self._signalwatcher_to_wids.setdefault(proc, []).append(wid)
854 return proc.pid
855 return 0
857 def signal_watcher_event(self, fd, cb_condition, proc, pid, wid):
858 log("signal_watcher_event%s", (fd, cb_condition, proc, pid, wid))
859 if cb_condition==GLib.IO_HUP:
860 proc.stdout_io_watch = None
861 return False
862 if proc.stdout_io_watch is None:
863 #no longer watched
864 return False
865 if cb_condition==GLib.IO_IN:
866 try:
867 signame = bytestostr(proc.stdout.readline()).strip("\n\r")
868 log("signal_watcher_event: %s", signame)
869 if not signame:
870 pass
871 elif signame not in self.server_window_signals:
872 log("Warning: signal %s cannot be forwarded to this server", signame)
873 else:
874 self.send("window-signal", wid, signame)
875 except Exception as e:
876 log.error("signal_watcher_event%s", (fd, cb_condition, proc, pid, wid), exc_info=True)
877 log.error("Error: processing signal watcher output for pid %i of window %i", pid, wid)
878 log.error(" %s", e)
879 if proc.poll():
880 #watcher ended, stop watching its stdout
881 proc.stdout_io_watch = None
882 return False
883 return True
886 def freeze(self):
887 log("freeze()")
888 for window in self._id_to_window.values():
889 window.freeze()
891 def unfreeze(self):
892 log("unfreeze()")
893 for window in self._id_to_window.values():
894 window.unfreeze()
897 def deiconify_windows(self):
898 log("deiconify_windows()")
899 for window in self._id_to_window.values():
900 deiconify = getattr(window, "deiconify", None)
901 if deiconify:
902 deiconify()
905 def resize_windows(self, new_size_fn):
906 for window in self._id_to_window.values():
907 if window:
908 ww, wh = window._size
909 nw, nh = new_size_fn(ww, wh)
910 #this will apply the new scaling value to the size constraints:
911 window.reset_size_constraints()
912 window.resize(nw, nh)
913 self.send_refresh_all()
916 def reinit_window_icons(self):
917 #make sure the window icons are the ones we want:
918 iconlog("reinit_window_icons()")
919 for wid in tuple(self._id_to_window.keys()):
920 window = self._id_to_window.get(wid)
921 if window:
922 reset_icon = getattr(window, "reset_icon", None)
923 if reset_icon:
924 reset_icon()
926 def reinit_windows(self, new_size_fn=None):
927 #now replace all the windows with new ones:
928 for wid in tuple(self._id_to_window.keys()):
929 window = self._id_to_window.get(wid)
930 if window:
931 self.reinit_window(wid, window, new_size_fn)
932 self.send_refresh_all()
934 def reinit_window(self, wid, window, new_size_fn=None):
935 geomlog("reinit_window%s", (wid, window, new_size_fn))
936 def fake_send(*args):
937 log("fake_send%s", args)
938 if window.is_tray():
939 #trays are never GL enabled, so don't bother re-creating them
940 #might cause problems anyway if we did
941 #just send a configure event in case they are moved / scaled
942 window.send_configure()
943 return
944 #ignore packets from old window:
945 window.send = fake_send
946 #copy attributes:
947 x, y = window._pos
948 ww, wh = window._size
949 if new_size_fn:
950 ww, wh = new_size_fn(ww, wh)
951 try:
952 bw, bh = window._backing.size
953 except:
954 bw, bh = ww, wh
955 client_properties = window._client_properties
956 resize_counter = window._resize_counter
957 metadata = window._metadata
958 override_redirect = window._override_redirect
959 backing = window._backing
960 current_icon = window._current_icon
961 video_decoder, csc_decoder, decoder_lock = None, None, None
962 try:
963 if backing:
964 video_decoder = backing._video_decoder
965 csc_decoder = backing._csc_decoder
966 decoder_lock = backing._decoder_lock
967 if decoder_lock:
968 decoder_lock.acquire()
969 log("reinit_windows() will preserve video=%s and csc=%s for %s", video_decoder, csc_decoder, wid)
970 backing._video_decoder = None
971 backing._csc_decoder = None
972 backing._decoder_lock = None
973 backing.close()
975 #now we can unmap it:
976 self.destroy_window(wid, window)
977 #explicitly tell the server we have unmapped it:
978 #(so it will reset the video encoders, etc)
979 if not window.is_OR():
980 self.send("unmap-window", wid)
981 self._id_to_window.pop(wid, None)
982 self._window_to_id.pop(window, None)
983 #create the new window,
984 #which should honour the new state of the opengl_enabled flag if that's what we changed,
985 #or the new dimensions, etc
986 window = self.make_new_window(wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties)
987 window._resize_counter = resize_counter
988 #if we had a backing already,
989 #restore the attributes we had saved from it
990 if backing:
991 backing = window._backing
992 backing._video_decoder = video_decoder
993 backing._csc_decoder = csc_decoder
994 backing._decoder_lock = decoder_lock
995 if current_icon:
996 window.update_icon(current_icon)
997 finally:
998 if decoder_lock:
999 decoder_lock.release()
1002 def get_group_leader(self, _wid, _metadata, _override_redirect):
1003 #subclasses that wish to implement the feature may override this method
1004 return None
1007 def get_client_window_classes(self, _w, _h, _metadata, _override_redirect):
1008 return (self.ClientWindowClass,)
1011 def _process_new_window(self, packet):
1012 self._process_new_common(packet, False)
1014 def _process_new_override_redirect(self, packet):
1015 if self.modal_windows:
1016 #find any modal windows and remove the flag
1017 #so that the OR window can get the focus
1018 #(it will be re-enabled when the OR window disappears)
1019 for wid, window in self._id_to_window.items():
1020 if window.is_OR() or window.is_tray():
1021 continue
1022 if window.get_modal():
1023 metalog("temporarily removing modal flag from %s", wid)
1024 window.set_modal(False)
1025 self._process_new_common(packet, True)
1028 def _process_initiate_moveresize(self, packet):
1029 wid = packet[1]
1030 window = self._id_to_window.get(wid)
1031 if window:
1032 x_root, y_root, direction, button, source_indication = packet[2:7]
1033 window.initiate_moveresize(self.sx(x_root), self.sy(y_root), direction, button, source_indication)
1035 def _process_window_metadata(self, packet):
1036 wid, metadata = packet[1:3]
1037 metalog("metadata update for window %i: %s", wid, metadata)
1038 window = self._id_to_window.get(wid)
1039 if window:
1040 metadata = self.cook_metadata(False, metadata)
1041 window.update_metadata(metadata)
1043 def _process_window_icon(self, packet):
1044 wid, w, h, coding, data = packet[1:6]
1045 img = self._window_icon_image(wid, w, h, coding, data)
1046 window = self._id_to_window.get(wid)
1047 iconlog("_process_window_icon(%s, %s, %s, %s, %s bytes) image=%s, window=%s",
1048 wid, w, h, coding, len(data), img, window)
1049 if window and img:
1050 window.update_icon(img)
1051 self.set_tray_icon()
1053 def _process_window_move_resize(self, packet):
1054 wid, x, y, w, h = packet[1:6]
1055 ax = self.sx(x)
1056 ay = self.sy(y)
1057 aw = max(1, self.sx(w))
1058 ah = max(1, self.sy(h))
1059 resize_counter = -1
1060 if len(packet)>4:
1061 resize_counter = packet[4]
1062 window = self._id_to_window.get(wid)
1063 geomlog("_process_window_move_resize%s moving / resizing window %s (id=%s) to %s",
1064 packet[1:], window, wid, (ax, ay, aw, ah))
1065 if window:
1066 window.move_resize(ax, ay, aw, ah, resize_counter)
1068 def _process_window_resized(self, packet):
1069 wid, w, h = packet[1:4]
1070 aw = max(1, self.sx(w))
1071 ah = max(1, self.sy(h))
1072 resize_counter = -1
1073 if len(packet)>4:
1074 resize_counter = packet[4]
1075 window = self._id_to_window.get(wid)
1076 geomlog("_process_window_resized%s resizing window %s (id=%s) to %s", packet[1:], window, wid, (aw,ah))
1077 if window:
1078 window.resize(aw, ah, resize_counter)
1080 def _process_raise_window(self, packet):
1081 #implemented in gtk subclass
1082 pass
1084 def _process_restack_window(self, packet):
1085 #implemented in gtk subclass
1086 pass
1089 def _process_configure_override_redirect(self, packet):
1090 wid, x, y, w, h = packet[1:6]
1091 window = self._id_to_window[wid]
1092 ax = self.sx(x)
1093 ay = self.sy(y)
1094 aw = max(1, self.sx(w))
1095 ah = max(1, self.sy(h))
1096 geomlog("_process_configure_override_redirect%s move resize window %s (id=%s) to %s",
1097 packet[1:], window, wid, (ax,ay,aw,ah))
1098 window.move_resize(ax, ay, aw, ah, -1)
1101 def window_close_event(self, wid):
1102 log("window_close_event(%s) close window action=%s", wid, self.window_close_action)
1103 if self.window_close_action=="forward":
1104 self.send("close-window", wid)
1105 elif self.window_close_action=="ignore":
1106 log("close event for window %i ignored", wid)
1107 elif self.window_close_action=="disconnect":
1108 log.info("window-close set to disconnect, exiting (window %i)", wid)
1109 self.quit(0)
1110 elif self.window_close_action=="shutdown":
1111 self.send("shutdown-server", "shutdown on window close")
1112 elif self.window_close_action=="auto":
1113 #forward unless this looks like a desktop
1114 #this allows us behave more like VNC:
1115 window = self._id_to_window.get(wid)
1116 log("window_close_event(%i) window=%s", wid, window)
1117 if self.server_is_desktop:
1118 log.info("window-close event on desktop or shadow window, disconnecting")
1119 self.quit(0)
1120 return True
1121 if window:
1122 metadata = getattr(window, "_metadata", {})
1123 log("window_close_event(%i) metadata=%s", wid, metadata)
1124 class_instance = metadata.strtupleget("class-instance", (None, None), 2, 2)
1125 title = metadata.get("title", "")
1126 log("window_close_event(%i) title=%s, class-instance=%s", wid, title, class_instance)
1127 matching_title_close = [x for x in TITLE_CLOSEEXIT if x and title.startswith(x)]
1128 close = None
1129 if matching_title_close:
1130 close = "window-close event on %s window" % title
1131 elif class_instance and class_instance[1] in WM_CLASS_CLOSEEXIT:
1132 close = "window-close event on %s window" % class_instance[0]
1133 if close:
1134 #honour this close request if there are no other windows:
1135 if len(self._id_to_window)==1:
1136 log.info("%s, disconnecting", close)
1137 self.quit(0)
1138 return True
1139 log("there are %i windows, so forwarding %s", len(self._id_to_window), close)
1140 #default to forward:
1141 self.send("close-window", wid)
1142 else:
1143 log.warn("unknown close-window action: %s", self.window_close_action)
1144 return True
1147 def _process_lost_window(self, packet):
1148 wid = packet[1]
1149 window = self._id_to_window.get(wid)
1150 if window:
1151 if window.is_OR() and self.modal_windows:
1152 self.may_reenable_modal_windows(window)
1153 del self._id_to_window[wid]
1154 del self._window_to_id[window]
1155 self.destroy_window(wid, window)
1156 self.set_tray_icon()
1158 def may_reenable_modal_windows(self, window):
1159 orwids = tuple(wid for wid, w in self._id_to_window.items() if w.is_OR() and w!=window)
1160 if orwids:
1161 #there are other OR windows left, don't do anything
1162 return
1163 for wid, w in self._id_to_window.items():
1164 if w.is_OR() or w.is_tray():
1165 #trays and OR windows cannot be made modal
1166 continue
1167 if w._metadata.boolget("modal") and not w.get_modal():
1168 metalog("re-enabling modal flag on %s", wid)
1169 window.set_modal(True)
1172 def destroy_window(self, wid, window):
1173 log("destroy_window(%s, %s)", wid, window)
1174 window.destroy()
1175 if self._window_with_grab==wid:
1176 log("destroying window %s which has grab, ungrabbing!", wid)
1177 self.window_ungrab()
1178 self._window_with_grab = None
1179 #deal with signal watchers:
1180 log("looking for window %i in %s", wid, self._signalwatcher_to_wids)
1181 for signalwatcher, wids in tuple(self._signalwatcher_to_wids.items()):
1182 if wid in wids:
1183 log("removing %i from %s for signalwatcher %s", wid, wids, signalwatcher)
1184 wids.remove(wid)
1185 if not wids:
1186 log("last window, removing watcher %s", signalwatcher)
1187 self._signalwatcher_to_wids.pop(signalwatcher, None)
1188 self.kill_signalwatcher(signalwatcher)
1189 #now remove any pids that use this watcher:
1190 for pid, w in tuple(self._pid_to_signalwatcher.items()):
1191 if w==signalwatcher:
1192 del self._pid_to_signalwatcher[pid]
1194 def kill_signalwatcher(self, proc):
1195 log("kill_signalwatcher(%s)", proc)
1196 if proc.poll() is None:
1197 stdout_io_watch = proc.stdout_io_watch
1198 if stdout_io_watch:
1199 proc.stdout_io_watch = None
1200 self.source_remove(stdout_io_watch)
1201 try:
1202 proc.stdin.write(b"exit\n")
1203 proc.stdin.flush()
1204 proc.stdin.close()
1205 except IOError:
1206 log.warn("Warning: failed to tell the signal watcher to exit", exc_info=True)
1207 try:
1208 os.kill(proc.pid, signal.SIGKILL)
1209 except OSError as e:
1210 if e.errno!=errno.ESRCH:
1211 log.warn("Warning: failed to tell the signal watcher to exit", exc_info=True)
1213 def destroy_all_windows(self):
1214 for wid, window in self._id_to_window.items():
1215 try:
1216 log("destroy_all_windows() destroying %s / %s", wid, window)
1217 self.destroy_window(wid, window)
1218 except Exception:
1219 pass
1220 self._id_to_window = {}
1221 self._window_to_id = {}
1222 #signal watchers should have been killed in destroy_window(),
1223 #make sure we don't leave any behind:
1224 for signalwatcher in tuple(self._signalwatcher_to_wids.keys()):
1225 try:
1226 self.kill_signalwatcher(signalwatcher)
1227 except Exception:
1228 log("destroy_all_windows() error killing signal watcher %s", signalwatcher, exc_info=True)
1231 ######################################################################
1232 # bell
1233 def _process_bell(self, packet):
1234 if not self.bell_enabled:
1235 return
1236 (wid, device, percent, pitch, duration, bell_class, bell_id, bell_name) = packet[1:9]
1237 window = self._id_to_window.get(wid)
1238 self.window_bell(window, device, percent, pitch, duration, bell_class, bell_id, bell_name)
1241 ######################################################################
1242 # focus:
1243 def send_focus(self, wid):
1244 focuslog("send_focus(%s)", wid)
1245 self.send("focus", wid, self.get_current_modifiers())
1247 def update_focus(self, wid, gotit):
1248 focuslog("update_focus(%s, %s) focused=%s, grabbed=%s", wid, gotit, self._focused, self._window_with_grab)
1249 if gotit:
1250 if self._focused is not wid:
1251 self.send_focus(wid)
1252 self._focused = wid
1253 self.cancel_lost_focus_timer()
1254 else:
1255 if self._window_with_grab:
1256 self.window_ungrab()
1257 self.do_force_ungrab(self._window_with_grab)
1258 self._window_with_grab = None
1259 if wid and self._focused and self._focused!=wid:
1260 #if this window lost focus, it must have had it!
1261 #(catch up - makes things like OR windows work:
1262 # their parent receives the focus-out event)
1263 focuslog("window %s lost a focus it did not have!? (simulating focus before losing it)", wid)
1264 self.send_focus(wid)
1265 if self._focused and not self.lost_focus_timer:
1266 #send the lost-focus via a timer and re-check it
1267 #(this allows a new window to gain focus without having to do a reset_focus)
1268 self.lost_focus_timer = self.timeout_add(20, self.send_lost_focus)
1269 self._focused = None
1271 def send_lost_focus(self):
1272 focuslog("send_lost_focus() focused=%s", self._focused)
1273 self.lost_focus_timer = None
1274 #check that a new window has not gained focus since:
1275 if self._focused is None:
1276 self.send_focus(0)
1278 def cancel_lost_focus_timer(self):
1279 lft = self.lost_focus_timer
1280 if lft:
1281 self.lost_focus_timer = None
1282 self.source_remove(lft)
1285 ######################################################################
1286 # grabs:
1287 def window_grab(self, _window):
1288 grablog.warn("Warning: window grab not implemented in %s", self.client_type())
1290 def window_ungrab(self):
1291 grablog.warn("Warning: window ungrab not implemented in %s", self.client_type())
1293 def do_force_ungrab(self, wid):
1294 grablog("do_force_ungrab(%s)", wid)
1295 #ungrab via dedicated server packet:
1296 self.send_force_ungrab(wid)
1298 def _process_pointer_grab(self, packet):
1299 wid = packet[1]
1300 window = self._id_to_window.get(wid)
1301 grablog("grabbing %s: %s", wid, window)
1302 if window:
1303 self.window_grab(window)
1304 self._window_with_grab = wid
1306 def _process_pointer_ungrab(self, packet):
1307 wid = packet[1]
1308 window = self._id_to_window.get(wid)
1309 grablog("ungrabbing %s: %s", wid, window)
1310 self.window_ungrab()
1311 self._window_with_grab = None
1314 ######################################################################
1315 # window refresh:
1316 def suspend(self):
1317 log.info("system is suspending")
1318 self._suspended_at = time()
1319 #tell the server to slow down refresh for all the windows:
1320 self.control_refresh(-1, True, False)
1322 def resume(self):
1323 elapsed = 0
1324 if self._suspended_at>0:
1325 elapsed = max(0, time()-self._suspended_at)
1326 self._suspended_at = 0
1327 self.send_refresh_all()
1328 if elapsed<1:
1329 #not really suspended
1330 #happens on macos when switching workspace!
1331 return
1332 delta = datetime.timedelta(seconds=int(elapsed))
1333 log.info("system resumed, was suspended for %s", str(delta).lstrip("0:"))
1334 #this will reset the refresh rate too:
1335 if self.opengl_enabled:
1336 #with opengl, the buffers sometimes contain garbage after resuming,
1337 #this should create new backing buffers:
1338 self.reinit_windows()
1339 self.reinit_window_icons()
1341 def control_refresh(self, wid, suspend_resume, refresh, quality=100, options=None, client_properties=None):
1342 packet = ["buffer-refresh", wid, 0, quality]
1343 options = options or {}
1344 client_properties = client_properties or {}
1345 options["refresh-now"] = bool(refresh)
1346 if suspend_resume is True:
1347 options["batch"] = {
1348 "reset" : True,
1349 "delay" : 1000,
1350 "locked" : True,
1351 "always" : True,
1352 }
1353 elif suspend_resume is False:
1354 options["batch"] = {"reset" : True}
1355 else:
1356 pass #batch unchanged
1357 log("sending buffer refresh: options=%s, client_properties=%s", options, client_properties)
1358 packet.append(options)
1359 packet.append(client_properties)
1360 self.send(*packet)
1362 def send_refresh(self, wid):
1363 packet = ["buffer-refresh", wid, 0, 100,
1364 #explicit refresh (should be assumed True anyway),
1365 #also force a reset of batch configs:
1366 {
1367 "refresh-now" : True,
1368 "batch" : {"reset" : True}
1369 },
1370 {} #no client_properties
1371 ]
1372 self.send(*packet)
1374 def send_refresh_all(self):
1375 log("Automatic refresh for all windows ")
1376 self.send_refresh(-1)
1379 ######################################################################
1380 # painting windows:
1381 def _process_draw(self, packet):
1382 if PAINT_DELAY>0:
1383 self.timeout_add(PAINT_DELAY, self._draw_queue.put, packet)
1384 else:
1385 self._draw_queue.put(packet)
1387 def _process_eos(self, packet):
1388 self._draw_queue.put(packet)
1390 def send_damage_sequence(self, wid, packet_sequence, width, height, decode_time, message=""):
1391 packet = "damage-sequence", packet_sequence, wid, width, height, decode_time, message
1392 drawlog("sending ack: %s", packet)
1393 self.send_now(*packet)
1395 def _draw_thread_loop(self):
1396 while self.exit_code is None:
1397 packet = self._draw_queue.get()
1398 if packet is None:
1399 break
1400 try:
1401 self._do_draw(packet)
1402 sleep(0)
1403 except Exception as e:
1404 log.error("Error '%s' processing %s packet", e, packet[0], exc_info=True)
1405 log("draw thread ended")
1407 def _do_draw(self, packet):
1408 """ this runs from the draw thread above """
1409 wid = packet[1]
1410 window = self._id_to_window.get(wid)
1411 if bytestostr(packet[0])=="eos":
1412 if window:
1413 window.eos()
1414 return
1415 x, y, width, height, coding, data, packet_sequence, rowstride = packet[2:10]
1416 coding = bytestostr(coding)
1417 if not window:
1418 #window is gone
1419 def draw_cleanup():
1420 if coding=="mmap":
1421 assert self.mmap_enabled
1422 from xpra.net.mmap_pipe import int_from_buffer
1423 #we need to ack the data to free the space!
1424 data_start = int_from_buffer(self.mmap, 0)
1425 offset, length = data[-1]
1426 data_start.value = offset+length
1427 #clear the mmap area via idle_add so any pending draw requests
1428 #will get a chance to run first (preserving the order)
1429 self.send_damage_sequence(wid, packet_sequence, width, height, -1)
1430 self.idle_add(draw_cleanup)
1431 return
1432 #rename old encoding aliases early:
1433 options = {}
1434 if len(packet)>10:
1435 options = packet[10]
1436 options = typedict(options)
1437 dtype = DRAW_TYPES.get(type(data), type(data))
1438 drawlog("process_draw: %7i %8s for window %3i, sequence %8i, %4ix%-4i at %4i,%-4i using %6s encoding with options=%s",
1439 len(data), dtype, wid, packet_sequence, width, height, x, y, coding, options)
1440 start = monotonic_time()
1441 def record_decode_time(success, message=""):
1442 if success>0:
1443 end = monotonic_time()
1444 decode_time = int(end*1000*1000-start*1000*1000)
1445 self.pixel_counter.append((start, end, width*height))
1446 dms = "%sms" % (int(decode_time/100)/10.0)
1447 paintlog("record_decode_time(%s, %s) wid=%s, %s: %sx%s, %s",
1448 success, message, wid, coding, width, height, dms)
1449 elif success==0:
1450 decode_time = -1
1451 paintlog("record_decode_time(%s, %s) decoding error on wid=%s, %s: %sx%s",
1452 success, message, wid, coding, width, height)
1453 else:
1454 assert success<0
1455 decode_time = 0
1456 paintlog("record_decode_time(%s, %s) decoding or painting skipped on wid=%s, %s: %sx%s",
1457 success, message, wid, coding, width, height)
1458 self.send_damage_sequence(wid, packet_sequence, width, height, decode_time, repr_ellipsized(message, 512))
1459 self._draw_counter += 1
1460 if PAINT_FAULT_RATE>0 and (self._draw_counter % PAINT_FAULT_RATE)==0:
1461 drawlog.warn("injecting paint fault for %s draw packet %i, sequence number=%i",
1462 coding, self._draw_counter, packet_sequence)
1463 if PAINT_FAULT_TELL:
1464 self.idle_add(record_decode_time, False, "fault injection for %s draw packet %i, sequence number=%i" % (coding, self._draw_counter, packet_sequence))
1465 return
1466 #we could expose this to the csc step? (not sure how this could be used)
1467 #if self.xscale!=1 or self.yscale!=1:
1468 # options["client-scaling"] = self.xscale, self.yscale
1469 try:
1470 window.draw_region(x, y, width, height, coding, data, rowstride,
1471 packet_sequence, options, [record_decode_time])
1472 except KeyboardInterrupt:
1473 raise
1474 except Exception as e:
1475 drawlog.error("Error drawing on window %i", wid, exc_info=True)
1476 self.idle_add(record_decode_time, False, str(e))
1477 raise
1480 ######################################################################
1481 # screen scaling:
1482 def fsx(self, v):
1483 """ convert X coordinate from server to client """
1484 return v
1485 def fsy(self, v):
1486 """ convert Y coordinate from server to client """
1487 return v
1488 def sx(self, v) -> int:
1489 """ convert X coordinate from server to client """
1490 return iround(v)
1491 def sy(self, v) -> int:
1492 """ convert Y coordinate from server to client """
1493 return iround(v)
1494 def srect(self, x, y, w, h):
1495 """ convert rectangle coordinates from server to client """
1496 return self.sx(x), self.sy(y), self.sx(w), self.sy(h)
1497 def sp(self, x, y):
1498 """ convert X,Y coordinates from server to client """
1499 return self.sx(x), self.sy(y)
1501 def cx(self, v) -> int:
1502 """ convert X coordinate from client to server """
1503 return iround(v)
1504 def cy(self, v) -> int:
1505 """ convert Y coordinate from client to server """
1506 return iround(v)
1507 def crect(self, x, y, w, h):
1508 """ convert rectangle coordinates from client to server """
1509 return self.cx(x), self.cy(y), self.cx(w), self.cy(h)
1510 def cp(self, x, y):
1511 """ convert X,Y coordinates from client to server """
1512 return self.cx(x), self.cy(y)
1515 def redraw_spinners(self):
1516 #draws spinner on top of the window, or not (plain repaint)
1517 #depending on whether the server is ok or not
1518 ok = self.server_ok()
1519 log("redraw_spinners() ok=%s", ok)
1520 for w in self._id_to_window.values():
1521 if not w.is_tray():
1522 w.spinner(ok)
1524 ######################################################################
1525 # packets:
1526 def init_authenticated_packet_handlers(self):
1527 for packet_type, handler in {
1528 "new-window": self._process_new_window,
1529 "new-override-redirect":self._process_new_override_redirect,
1530 "new-tray": self._process_new_tray,
1531 "raise-window": self._process_raise_window,
1532 "restack-window": self._process_restack_window,
1533 "initiate-moveresize": self._process_initiate_moveresize,
1534 "window-move-resize": self._process_window_move_resize,
1535 "window-resized": self._process_window_resized,
1536 "window-metadata": self._process_window_metadata,
1537 "configure-override-redirect": self._process_configure_override_redirect,
1538 "lost-window": self._process_lost_window,
1539 "window-icon": self._process_window_icon,
1540 "draw": self._process_draw,
1541 "eos": self._process_eos,
1542 "cursor": self._process_cursor,
1543 "bell": self._process_bell,
1544 "pointer-position": self._process_pointer_position,
1545 "pointer-grab": self._process_pointer_grab,
1546 "pointer-ungrab": self._process_pointer_ungrab,
1547 }.items():
1548 self.add_packet_handler(packet_type, handler)