Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/x11/x11_server_core.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# -*- coding: utf-8 -*-
2# This file is part of Xpra.
3# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
4# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
5# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
6# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
7# later version. See the file COPYING for details.
9import os
10import threading
11from gi.repository import Gdk
13from xpra.x11.bindings.core_bindings import set_context_check, X11CoreBindings #@UnresolvedImport
14from xpra.x11.bindings.randr_bindings import RandRBindings #@UnresolvedImport
15from xpra.x11.bindings.keyboard_bindings import X11KeyboardBindings #@UnresolvedImport
16from xpra.x11.bindings.window_bindings import X11WindowBindings #@UnresolvedImport
17from xpra.gtk_common.error import XError, xswallow, xsync, xlog, trap, verify_sync
18from xpra.gtk_common.gtk_util import get_default_root_window
19from xpra.server.server_uuid import save_uuid, get_uuid
20from xpra.x11.vfb_util import parse_resolution
21from xpra.x11.fakeXinerama import find_libfakeXinerama, save_fakeXinerama_config, cleanup_fakeXinerama
22from xpra.x11.gtk_x11.prop import prop_get, prop_set
23from xpra.x11.gtk_x11.gdk_display_source import close_gdk_display_source
24from xpra.x11.gtk_x11.gdk_bindings import init_x11_filter, cleanup_x11_filter, cleanup_all_event_receivers
25from xpra.common import MAX_WINDOW_SIZE
26from xpra.os_util import monotonic_time, strtobytes
27from xpra.util import typedict, iround, envbool, first_time, XPRA_DPI_NOTIFICATION_ID
28from xpra.net.compression import Compressed
29from xpra.server.gtk_server_base import GTKServerBase
30from xpra.x11.xkbhelper import clean_keyboard_state
31from xpra.scripts.config import FALSE_OPTIONS
32from xpra.log import Logger
34set_context_check(verify_sync)
35RandR = RandRBindings()
36X11Keyboard = X11KeyboardBindings()
37X11Core = X11CoreBindings()
38X11Window = X11WindowBindings()
41log = Logger("x11", "server")
42keylog = Logger("x11", "server", "keyboard")
43mouselog = Logger("x11", "server", "mouse")
44grablog = Logger("server", "grab")
45cursorlog = Logger("server", "cursor")
46screenlog = Logger("server", "screen")
47xinputlog = Logger("xinput")
50ALWAYS_NOTIFY_MOTION = envbool("XPRA_ALWAYS_NOTIFY_MOTION", False)
51FAKE_X11_INIT_ERROR = envbool("XPRA_FAKE_X11_INIT_ERROR", False)
54class XTestPointerDevice:
56 def __repr__(self):
57 return "XTestPointerDevice"
59 def move_pointer(self, screen_no, x, y, *_args):
60 mouselog("xtest_fake_motion(%i, %s, %s)", screen_no, x, y)
61 with xsync:
62 X11Keyboard.xtest_fake_motion(screen_no, x, y)
64 def click(self, button, pressed, *_args):
65 mouselog("xtest_fake_button(%i, %s)", button, pressed)
66 with xsync:
67 X11Keyboard.xtest_fake_button(button, pressed)
69 def close(self):
70 pass
72 def has_precise_wheel(self):
73 return False
76class X11ServerCore(GTKServerBase):
77 """
78 Base class for X11 servers,
79 adds X11 specific methods to GTKServerBase.
80 (see XpraServer or XpraX11ShadowServer for actual implementations)
81 """
83 def __init__(self):
84 self.screen_number = Gdk.Screen.get_default().get_number()
85 self.root_window = get_default_root_window()
86 self.pointer_device = XTestPointerDevice()
87 self.touchpad_device = None
88 self.pointer_device_map = {}
89 self.keys_pressed = {}
90 self.last_mouse_user = None
91 self.libfakeXinerama_so = None
92 self.initial_resolution = None
93 self.x11_filter = False
94 self.randr_sizes_added = []
95 GTKServerBase.__init__(self)
96 log("XShape=%s", X11Window.displayHasXShape())
98 def init(self, opts):
99 self.do_init(opts)
100 super().init(opts)
102 def server_init(self):
103 self.x11_init()
104 from xpra.server import server_features
105 if server_features.windows:
106 from xpra.x11.x11_window_filters import init_x11_window_filters
107 init_x11_window_filters()
108 super().server_init()
110 def do_init(self, opts):
111 try:
112 self.initial_resolution = parse_resolution(opts.resize_display)
113 except ValueError:
114 pass
115 self.randr = bool(self.initial_resolution) or not (opts.resize_display in FALSE_OPTIONS)
116 self.randr_exact_size = False
117 self.fake_xinerama = "no" #only enabled in seamless server
118 self.current_xinerama_config = None
119 #x11 keyboard bits:
120 self.current_keyboard_group = None
123 def x11_init(self):
124 if FAKE_X11_INIT_ERROR:
125 raise Exception("fake x11 init error")
126 self.init_fake_xinerama()
127 with xlog:
128 clean_keyboard_state()
129 with xlog:
130 if not X11Keyboard.hasXFixes() and self.cursors:
131 log.error("Error: cursor forwarding support disabled")
132 if not X11Keyboard.hasXTest():
133 log.error("Error: keyboard and mouse disabled")
134 elif not X11Keyboard.hasXkb():
135 log.error("Error: limited keyboard support")
136 with xsync:
137 self.init_x11_atoms()
138 with xlog:
139 if self.randr:
140 self.init_randr()
141 with xlog:
142 self.init_cursor()
143 with xlog:
144 self.x11_filter = init_x11_filter()
145 assert self.x11_filter
147 def init_fake_xinerama(self):
148 if self.fake_xinerama in FALSE_OPTIONS:
149 self.libfakeXinerama_so = None
150 elif os.path.isabs(self.fake_xinerama):
151 self.libfakeXinerama_so = self.fake_xinerama
152 else:
153 self.libfakeXinerama_so = find_libfakeXinerama()
155 def init_randr(self):
156 self.randr = RandR.has_randr()
157 screenlog("randr=%s", self.randr)
158 #check the property first,
159 #because we may be inheriting this display,
160 #in which case the screen sizes list may be longer than 1
161 eprop = prop_get(self.root_window, "_XPRA_RANDR_EXACT_SIZE", "u32", ignore_errors=True, raise_xerrors=False)
162 screenlog("_XPRA_RANDR_EXACT_SIZE=%s", eprop)
163 self.randr_exact_size = eprop==1
164 if not self.randr_exact_size:
165 #ugly hackish way of detecting Xvfb with randr,
166 #assume that it has only one resolution pre-defined:
167 sizes = RandR.get_xrr_screen_sizes()
168 if len(sizes)==1:
169 self.randr_exact_size = True
170 prop_set(self.root_window, "_XPRA_RANDR_EXACT_SIZE", "u32", 1)
171 elif not sizes:
172 #xwayland?
173 self.randr = False
174 self.randr_exact_size = False
175 screenlog("randr=%s, exact size=%s", self.randr, self.randr_exact_size)
176 screenlog("randr enabled: %s", self.randr)
177 if not self.randr:
178 screenlog.warn("Warning: no X11 RandR support on %s", os.environ.get("DISPLAY"))
181 def init_cursor(self):
182 #cursor:
183 self.default_cursor_image = None
184 self.last_cursor_serial = None
185 self.last_cursor_image = None
186 self.send_cursor_pending = False
187 def get_default_cursor():
188 self.default_cursor_image = X11Keyboard.get_cursor_image()
189 cursorlog("get_default_cursor=%s", self.default_cursor_image)
190 trap.swallow_synced(get_default_cursor)
191 X11Keyboard.selectCursorChange(True)
193 def get_display_bit_depth(self):
194 with xlog:
195 return X11Window.get_depth(X11Window.getDefaultRootWindow())
196 return 0
199 def init_x11_atoms(self):
200 #some applications (like openoffice), do not work properly
201 #if some x11 atoms aren't defined, so we define them in advance:
202 atom_names = tuple("_NET_WM_WINDOW_TYPE"+wtype for wtype in (
203 "",
204 "_NORMAL",
205 "_DESKTOP",
206 "_DOCK",
207 "_TOOLBAR",
208 "_MENU",
209 "_UTILITY",
210 "_SPLASH",
211 "_DIALOG",
212 "_DROPDOWN_MENU",
213 "_POPUP_MENU",
214 "_TOOLTIP",
215 "_NOTIFICATION",
216 "_COMBO",
217 "_DND",
218 "_NORMAL"
219 ))
220 X11Core.intern_atoms(atom_names)
223 def set_keyboard_layout_group(self, grp):
224 if not self.keyboard_config:
225 keylog("set_keyboard_layout_group(%i) ignored, no config", grp)
226 return
227 if not self.keyboard_config.xkbmap_layout_groups:
228 keylog("set_keyboard_layout_group(%i) ignored, no layout groups support", grp)
229 #not supported by the client that owns the current keyboard config,
230 #so make sure we stick to the default group:
231 grp = 0
232 if not X11Keyboard.hasXkb():
233 keylog("set_keyboard_layout_group(%i) ignored, no Xkb support", grp)
234 return
235 if grp<0:
236 grp = 0
237 if self.current_keyboard_group!=grp:
238 keylog("set_keyboard_layout_group(%i) ignored, value unchanged", grp)
239 return
240 keylog("set_keyboard_layout_group(%i) config=%s, current keyboard group=%s",
241 grp, self.keyboard_config, self.current_keyboard_group)
242 try:
243 with xsync:
244 self.current_keyboard_group = X11Keyboard.set_layout_group(grp)
245 except XError as e:
246 keylog("set_keyboard_layout_group group=%s", grp, exc_info=True)
247 keylog.error("Error: failed to set keyboard layout group '%s'", grp)
248 keylog.error(" %s", e)
250 def init_packet_handlers(self):
251 GTKServerBase.init_packet_handlers(self)
252 self.add_packet_handler("force-ungrab", self._process_force_ungrab)
253 self.add_packet_handler("wheel-motion", self._process_wheel_motion)
256 def init_virtual_devices(self, _devices):
257 self.input_devices = "xtest"
260 def get_child_env(self) -> dict:
261 #adds fakeXinerama:
262 env = super().get_child_env()
263 if self.fake_xinerama and self.libfakeXinerama_so:
264 env["LD_PRELOAD"] = self.libfakeXinerama_so
265 return env
267 def do_cleanup(self):
268 if self.x11_filter:
269 self.x11_filter = False
270 cleanup_x11_filter()
271 #try a few times:
272 #errors happen because windows are being destroyed
273 #(even more so when we cleanup)
274 #and we don't really care too much about this
275 for l in (log, log, log, log, log.warn):
276 try:
277 with xsync:
278 cleanup_all_event_receivers()
279 #all went well, we're done
280 break
281 except Exception as e:
282 l("failed to remove event receivers: %s", e)
283 if self.fake_xinerama:
284 cleanup_fakeXinerama()
285 with xswallow:
286 clean_keyboard_state()
287 GTKServerBase.do_cleanup(self)
288 log("close_gdk_display_source()")
289 close_gdk_display_source()
292 def get_uuid(self):
293 return get_uuid()
295 def save_uuid(self):
296 save_uuid(str(self.uuid))
298 def set_keyboard_repeat(self, key_repeat):
299 if key_repeat:
300 self.key_repeat_delay, self.key_repeat_interval = key_repeat
301 if self.key_repeat_delay>0 and self.key_repeat_interval>0:
302 X11Keyboard.set_key_repeat_rate(self.key_repeat_delay, self.key_repeat_interval)
303 keylog.info("setting key repeat rate from client: %sms delay / %sms interval",
304 self.key_repeat_delay, self.key_repeat_interval)
305 else:
306 #dont do any jitter compensation:
307 self.key_repeat_delay = -1
308 self.key_repeat_interval = -1
309 #but do set a default repeat rate:
310 X11Keyboard.set_key_repeat_rate(500, 30)
311 keylog("keyboard repeat disabled")
313 def make_hello(self, source):
314 capabilities = super().make_hello(source)
315 capabilities["server_type"] = "Python/gtk/x11"
316 if source.wants_features:
317 capabilities.update({
318 "resize_screen" : self.randr,
319 "resize_exact" : self.randr_exact_size,
320 "force_ungrab" : True,
321 "keyboard.fast-switching" : True,
322 "wheel.precise" : self.pointer_device.has_precise_wheel(),
323 "touchpad-device" : bool(self.touchpad_device),
324 })
325 if self.randr:
326 sizes = self.get_all_screen_sizes()
327 if len(sizes)>1:
328 capabilities["screen-sizes"] = sizes
329 if self.default_cursor_image and source.wants_default_cursor:
330 capabilities["cursor.default"] = self.default_cursor_image
331 return capabilities
333 def do_get_info(self, proto, server_sources) -> dict:
334 start = monotonic_time()
335 info = super().do_get_info(proto, server_sources)
336 sinfo = info.setdefault("server", {})
337 sinfo.update({
338 "type" : "Python/gtk/x11",
339 "fakeXinerama" : bool(self.libfakeXinerama_so),
340 "libfakeXinerama" : self.libfakeXinerama_so or "",
341 })
342 log("X11ServerBase.do_get_info took %ims", (monotonic_time()-start)*1000)
343 return info
345 def get_ui_info(self, proto, wids=None, *args) -> dict:
346 log("do_get_info thread=%s", threading.current_thread())
347 info = super().get_ui_info(proto, wids, *args)
348 #this is added here because the server keyboard config doesn't know about "keys_pressed"..
349 if not self.readonly:
350 with xlog:
351 info.setdefault("keyboard", {}).update({
352 "state" : {
353 "keys_pressed" : tuple(self.keys_pressed.keys()),
354 "keycodes-down" : X11Keyboard.get_keycodes_down(),
355 },
356 "fast-switching" : True,
357 "layout-group" : X11Keyboard.get_layout_group(),
358 })
359 sinfo = info.setdefault("server", {})
360 try:
361 from xpra.x11.gtk_x11.composite import CompositeHelper
362 sinfo["XShm"] = CompositeHelper.XShmEnabled
363 except ImportError:
364 pass
365 #cursor:
366 info.setdefault("cursor", {}).update(self.get_cursor_info())
367 with xswallow:
368 sinfo.update({
369 "Xkb" : X11Keyboard.hasXkb(),
370 "XTest" : X11Keyboard.hasXTest(),
371 })
372 #randr:
373 if self.randr:
374 with xlog:
375 sizes = self.get_all_screen_sizes()
376 if len(sizes)>=0:
377 sinfo["randr"] = {
378 "" : True,
379 "options" : tuple(reversed(sorted(sizes))),
380 "exact" : self.randr_exact_size,
381 }
382 return info
385 def get_cursor_info(self) -> dict:
386 #(NOT from UI thread)
387 #copy to prevent race:
388 cd = self.last_cursor_image
389 if cd is None:
390 return {"" : "None"}
391 dci = self.default_cursor_image
392 cinfo = {
393 "is-default" : bool(dci) and len(dci)>=8 and len(cd)>=8 and cd[7]==dci[7],
394 }
395 #all but pixels:
396 for i, x in enumerate(("x", "y", "width", "height", "xhot", "yhot", "serial", None, "name")):
397 if x:
398 v = cd[i] or ""
399 cinfo[x] = v
400 return cinfo
402 def get_window_info(self, window) -> dict:
403 info = super().get_window_info(window)
404 info["XShm"] = window.uses_XShm()
405 info["geometry"] = window.get_geometry()
406 return info
409 def get_keyboard_config(self, props=typedict()):
410 from xpra.x11.server_keyboard_config import KeyboardConfig
411 keyboard_config = KeyboardConfig()
412 keyboard_config.enabled = props.boolget("keyboard", True)
413 keyboard_config.parse_options(props)
414 keyboard_config.xkbmap_layout = props.strget("xkbmap_layout")
415 keyboard_config.xkbmap_variant = props.strget("xkbmap_variant")
416 keyboard_config.xkbmap_options = props.strget("xkbmap_options")
417 keylog("get_keyboard_config(..)=%s", keyboard_config)
418 return keyboard_config
421 def set_keymap(self, server_source, force=False):
422 if self.readonly:
423 return
424 try:
425 #prevent _keys_changed() from firing:
426 #(using a flag instead of keymap.disconnect(handler) as this did not seem to work!)
427 self.keymap_changing = True
429 #if sharing, don't set the keymap, translate the existing one:
430 other_ui_clients = [s.uuid for s in self._server_sources.values() if s!=server_source and s.ui_client]
431 translate_only = len(other_ui_clients)>0
432 with xsync:
433 server_source.set_keymap(self.keyboard_config, self.keys_pressed, force, translate_only) #pylint: disable=access-member-before-definition
434 self.keyboard_config = server_source.keyboard_config
435 finally:
436 # re-enable via idle_add to give all the pending
437 # events a chance to run first (and get ignored)
438 def reenable_keymap_changes(*args):
439 keylog("reenable_keymap_changes(%s)", args)
440 self.keymap_changing = False
441 self._keys_changed()
442 self.idle_add(reenable_keymap_changes)
445 def clear_keys_pressed(self):
446 if self.readonly:
447 return
448 keylog("clear_keys_pressed()")
449 #make sure the timer doesn't fire and interfere:
450 self.cancel_key_repeat_timer()
451 #clear all the keys we know about:
452 if self.keys_pressed:
453 keylog("clearing keys pressed: %s", self.keys_pressed)
454 with xsync:
455 for keycode in self.keys_pressed:
456 self.fake_key(keycode, False)
457 self.keys_pressed = {}
458 #this will take care of any remaining ones we are not aware of:
459 #(there should not be any - but we want to be certain)
460 clean_keyboard_state()
463 def get_cursor_sizes(self):
464 display = Gdk.Display.get_default()
465 return display.get_default_cursor_size(), display.get_maximal_cursor_size()
467 def get_cursor_image(self):
468 #must be called from the UI thread!
469 with xlog:
470 return X11Keyboard.get_cursor_image()
472 def get_cursor_data(self):
473 #must be called from the UI thread!
474 cursor_image = self.get_cursor_image()
475 if cursor_image is None:
476 cursorlog("get_cursor_data() failed to get cursor image")
477 return None, []
478 self.last_cursor_image = cursor_image
479 pixels = self.last_cursor_image[7]
480 cursorlog("get_cursor_image() cursor=%s", cursor_image[:7]+["%s bytes" % len(pixels)]+cursor_image[8:])
481 if self.default_cursor_image is not None and str(pixels)==str(self.default_cursor_image[7]):
482 cursorlog("get_cursor_data(): default cursor - clearing it")
483 cursor_image = None
484 cursor_sizes = self.get_cursor_sizes()
485 return (cursor_image, cursor_sizes)
488 def get_all_screen_sizes(self):
489 #workaround for #2910: the resolutions we add are not seen by XRRSizes!
490 # so we keep track of the ones we have added ourselves:
491 sizes = list(RandR.get_xrr_screen_sizes())
492 for w, h in self.randr_sizes_added:
493 if (w, h) not in sizes:
494 sizes.append((w, h))
495 return tuple(sizes)
497 def get_max_screen_size(self):
498 max_w, max_h = self.root_window.get_geometry()[2:4]
499 if self.randr:
500 sizes = self.get_all_screen_sizes()
501 if len(sizes)>=1:
502 for w,h in sizes:
503 max_w = max(max_w, w)
504 max_h = max(max_h, h)
505 if max_w>MAX_WINDOW_SIZE or max_h>MAX_WINDOW_SIZE:
506 screenlog.warn("Warning: maximum screen size is very large: %sx%s", max_w, max_h)
507 screenlog.warn(" you may encounter window sizing problems")
508 screenlog("get_max_screen_size()=%s", (max_w, max_h))
509 return max_w, max_h
512 def configure_best_screen_size(self):
513 #return ServerBase.set_best_screen_size(self)
514 """ sets the screen size to use the largest width and height used by any of the clients """
515 root_w, root_h = self.root_window.get_geometry()[2:4]
516 if not self.randr:
517 return root_w, root_h
518 sss = self._server_sources.values()
519 if len(sss)>1:
520 screenlog.info("screen used by %i clients:", len(sss))
521 bigger = True
522 max_w, max_h = 0, 0
523 min_w, min_h = 16384, 16384
524 for ss in sss:
525 client_size = ss.desktop_size
526 if not client_size:
527 size = "unknown"
528 else:
529 w, h = client_size
530 size = "%ix%i" % (w, h)
531 max_w = max(max_w, w)
532 max_h = max(max_h, h)
533 if w>0:
534 min_w = min(min_w, w)
535 if h>0:
536 min_h = min(min_h, h)
537 bigger = bigger and ss.screen_resize_bigger
538 if len(sss)>1:
539 screenlog.info("* %s: %s", ss.uuid, size)
540 if bigger:
541 w, h = max_w, max_h
542 else:
543 w, h = min_w, min_h
544 screenlog("current server resolution is %ix%i", root_w, root_h)
545 screenlog("maximum client resolution is %ix%i", max_w, max_h)
546 screenlog("minimum client resolution is %ix%i", min_w, min_h)
547 screenlog("want: %s, so using %ix%i", "bigger" if bigger else "smaller", w, h)
548 if w<=0 or h<=0:
549 #invalid - use fallback
550 return root_w, root_h
551 return self.set_screen_size(w, h, bigger)
553 def get_best_screen_size(self, desired_w, desired_h, bigger=True):
554 return self.do_get_best_screen_size(desired_w, desired_h, bigger)
556 def do_get_best_screen_size(self, desired_w, desired_h, bigger=True):
557 if not self.randr:
558 return desired_w, desired_h
559 screen_sizes = self.get_all_screen_sizes()
560 if (desired_w, desired_h) in screen_sizes:
561 return desired_w, desired_h
562 if self.randr_exact_size:
563 try:
564 with xsync:
565 v = RandR.add_screen_size(desired_w, desired_h)
566 if v:
567 #we have to wait a little bit
568 #to make sure that everything sees the new resolution
569 #(ideally this method would be split in two and this would be a callback)
570 self.randr_sizes_added.append(v)
571 import time
572 time.sleep(0.5)
573 return v
574 except XError as e:
575 screenlog("add_screen_size(%s, %s)", desired_w, desired_h, exc_info=True)
576 screenlog.warn("Warning: failed to add resolution %ix%i:", desired_w, desired_h)
577 screenlog.warn(" %s", e)
578 #re-query:
579 screen_sizes = self.get_all_screen_sizes()
580 #try to find the best screen size to resize to:
581 new_size = None
582 closest = {}
583 for w,h in screen_sizes:
584 if (w<desired_w)==bigger or (h<desired_h)==bigger:
585 distance = abs(desired_w*desired_h - w*h)
586 closest[distance] = (w, h)
587 continue #size is too small/big for client
588 if new_size:
589 ew,eh = new_size #pylint: disable=unpacking-non-sequence
590 if (ew*eh<w*h)==bigger:
591 continue #we found a better (smaller/bigger) candidate already
592 new_size = w,h
593 if not new_size:
594 screenlog.warn("Warning: no matching resolution found for %sx%s", desired_w, desired_h)
595 if closest:
596 min_dist = sorted(closest.keys())[0]
597 new_size = closest[min_dist]
598 screenlog.warn(" using %sx%s instead", *new_size)
599 else:
600 root_w, root_h = self.root_window.get_size()
601 return root_w, root_h
602 screenlog("best %s resolution for client(%sx%s) is: %s",
603 ["smaller", "bigger"][bigger], desired_w, desired_h, new_size)
604 w, h = new_size
605 return w, h
607 def set_screen_size(self, desired_w, desired_h, bigger=True):
608 screenlog("set_screen_size%s", (desired_w, desired_h, bigger))
609 root_w, root_h = self.root_window.get_geometry()[2:4]
610 if not self.randr:
611 return root_w,root_h
612 if desired_w==root_w and desired_h==root_h and not self.fake_xinerama:
613 return root_w,root_h #unlikely: perfect match already!
614 #clients may supply "xdpi" and "ydpi" (v0.15 onwards), or just "dpi", or nothing...
615 xdpi = self.xdpi or self.dpi
616 ydpi = self.ydpi or self.dpi
617 screenlog("set_screen_size(%s, %s, %s) xdpi=%s, ydpi=%s",
618 desired_w, desired_h, bigger, xdpi, ydpi)
619 if xdpi<=0 or ydpi<=0:
620 #use some sane defaults: either the command line option, or fallback to 96
621 #(96 is better than nothing, because we do want to set the dpi
622 # to avoid Xdummy setting a crazy dpi from the virtual screen dimensions)
623 xdpi = self.default_dpi or 96
624 ydpi = self.default_dpi or 96
625 #find the "physical" screen dimensions, so we can calculate the required dpi
626 #(and do this before changing the resolution)
627 wmm, hmm = 0, 0
628 client_w, client_h = 0, 0
629 sss = self._server_sources.values()
630 for ss in sss:
631 for s in ss.screen_sizes:
632 if len(s)>=10:
633 #(display_name, width, height, width_mm, height_mm, monitors,
634 # work_x, work_y, work_width, work_height)
635 client_w = max(client_w, s[1])
636 client_h = max(client_h, s[2])
637 wmm = max(wmm, s[3])
638 hmm = max(hmm, s[4])
639 if wmm>0 and hmm>0 and client_w>0 and client_h>0:
640 #calculate "real" dpi:
641 xdpi = iround(client_w * 25.4 / wmm)
642 ydpi = iround(client_h * 25.4 / hmm)
643 screenlog("calculated DPI: %s x %s (from w: %s / %s, h: %s / %s)",
644 xdpi, ydpi, client_w, wmm, client_h, hmm)
645 self.set_dpi(xdpi, ydpi)
647 #try to find the best screen size to resize to:
648 w, h = self.get_best_screen_size(desired_w, desired_h, bigger)
650 #fakeXinerama:
651 ui_clients = [s for s in self._server_sources.values() if s.ui_client]
652 source = None
653 screen_sizes = []
654 if len(ui_clients)==1:
655 source = ui_clients[0]
656 screen_sizes = source.screen_sizes
657 else:
658 screenlog("fakeXinerama can only be enabled for a single client (found %s)" % len(ui_clients))
659 xinerama_changed = save_fakeXinerama_config(self.fake_xinerama and len(ui_clients)==1, source, screen_sizes)
660 #we can only keep things unchanged if xinerama was also unchanged
661 #(many apps will only query xinerama again if they get a randr notification)
662 if (w==root_w and h==root_h) and not xinerama_changed:
663 screenlog.info("best resolution matching %sx%s is unchanged: %sx%s", desired_w, desired_h, w, h)
664 return root_w, root_h
665 try:
666 if (w==root_w and h==root_h) and xinerama_changed:
667 #xinerama was changed, but the RandR resolution will not be...
668 #and we need a RandR change to force applications to re-query it
669 #so we temporarily switch to another resolution to force
670 #the change! (ugly! but this works)
671 with xsync:
672 temp = {}
673 for tw,th in self.get_all_screen_sizes():
674 if tw!=w or th!=h:
675 #use the number of extra pixels as key:
676 #(so we can choose the closest resolution)
677 temp[abs((tw*th) - (w*h))] = (tw, th)
678 if not temp:
679 screenlog.warn("cannot find a temporary resolution for Xinerama workaround!")
680 else:
681 k = sorted(temp.keys())[0]
682 tw, th = temp[k]
683 screenlog.info("temporarily switching to %sx%s as a Xinerama workaround", tw, th)
684 with xsync:
685 RandR.set_screen_size(tw, th)
686 with xsync:
687 RandR.get_screen_size()
688 #Xdummy with randr 1.2:
689 screenlog("using XRRSetScreenConfigAndRate with %ix%i", w, h)
690 with xsync:
691 RandR.set_screen_size(w, h)
692 if self.randr_exact_size:
693 #Xvfb with randr > 1.2: the resolution has been added
694 #we can use XRRSetScreenSize:
695 try:
696 with xsync:
697 RandR.xrr_set_screen_size(w, h, self.xdpi or self.dpi or 96, self.ydpi or self.dpi or 96)
698 except XError:
699 screenlog("XRRSetScreenSize failed", exc_info=True)
700 screenlog("calling RandR.get_screen_size()")
701 with xsync:
702 root_w, root_h = RandR.get_screen_size()
703 screenlog("RandR.get_screen_size()=%s,%s", root_w, root_h)
704 screenlog("RandR.get_vrefresh()=%s", RandR.get_vrefresh())
705 if root_w!=w or root_h!=h:
706 screenlog.warn("Warning: tried to set resolution to %ix%i", w, h)
707 screenlog.warn(" and ended up with %ix%i", root_w, root_h)
708 else:
709 msg = "server virtual display now set to %sx%s" % (root_w, root_h)
710 if desired_w!=root_w or desired_h!=root_h:
711 msg += " (best match for %sx%s)" % (desired_w, desired_h)
712 screenlog.info(msg)
713 def show_dpi():
714 wmm, hmm = RandR.get_screen_size_mm() #ie: (1280, 1024)
715 screenlog("RandR.get_screen_size_mm=%s,%s", wmm, hmm)
716 actual_xdpi = iround(root_w * 25.4 / wmm)
717 actual_ydpi = iround(root_h * 25.4 / hmm)
718 if abs(actual_xdpi-xdpi)<=1 and abs(actual_ydpi-ydpi)<=1:
719 screenlog.info("DPI set to %s x %s", actual_xdpi, actual_ydpi)
720 screenlog("wanted: %s x %s", xdpi, ydpi)
721 else:
722 #should this be a warning:
723 l = screenlog.info
724 maxdelta = max(abs(actual_xdpi-xdpi), abs(actual_ydpi-ydpi))
725 if maxdelta>=10:
726 l = log.warn
727 messages = [
728 "DPI set to %s x %s (wanted %s x %s)" % (actual_xdpi, actual_ydpi, xdpi, ydpi),
729 ]
730 if maxdelta>=10:
731 messages.append("you may experience scaling problems, such as huge or small fonts, etc")
732 messages.append("to fix this issue, try the dpi switch, or use a patched Xorg dummy driver")
733 self.notify_dpi_warning("\n".join(messages))
734 for i,message in enumerate(messages):
735 l("%s%s", ["", " "][i>0], message)
736 #show dpi via idle_add so server has time to change the screen size (mm)
737 self.idle_add(show_dpi)
738 except Exception as e:
739 screenlog.error("ouch, failed to set new resolution: %s", e, exc_info=True)
740 return root_w, root_h
742 def notify_dpi_warning(self, body):
743 sources = tuple(self._server_sources.values())
744 if len(sources)==1:
745 ss = sources[0]
746 if first_time("DPI-warning-%s" % ss.uuid):
747 sources[0].may_notify(XPRA_DPI_NOTIFICATION_ID, "DPI Issue", body, icon_name="font")
750 def _process_server_settings(self, _proto, packet):
751 settings = packet[1]
752 log("process_server_settings: %s", settings)
753 self.update_server_settings(settings)
755 def update_server_settings(self, _settings, _reset=False):
756 #implemented in the X11 xpra server only for now
757 #(does not make sense to update a shadow server)
758 log("ignoring server settings update in %s", self)
761 def _process_force_ungrab(self, proto, _packet):
762 #ignore the window id: wid = packet[1]
763 grablog("force ungrab from %s", proto)
764 self.X11_ungrab()
766 def X11_ungrab(self):
767 grablog("X11_ungrab")
768 with xsync:
769 X11Core.UngrabKeyboard()
770 X11Core.UngrabPointer()
773 def fake_key(self, keycode, press):
774 keylog("fake_key(%s, %s)", keycode, press)
775 mink, maxk = X11Keyboard.get_minmax_keycodes()
776 if keycode<mink or keycode>maxk:
777 return
778 with xsync:
779 X11Keyboard.xtest_fake_key(keycode, press)
782 def do_xpra_cursor_event(self, event):
783 if not self.cursors:
784 return
785 if self.last_cursor_serial==event.cursor_serial:
786 cursorlog("ignoring cursor event %s with the same serial number %s", event, self.last_cursor_serial)
787 return
788 cursorlog("cursor_event: %s", event)
789 self.last_cursor_serial = event.cursor_serial
790 from xpra.server.source.windows_mixin import WindowsMixin
791 for ss in self._server_sources.values():
792 if isinstance(ss, WindowsMixin):
793 ss.send_cursor()
796 def _motion_signaled(self, model, event):
797 mouselog("motion_signaled(%s, %s) last mouse user=%s", model, event, self.last_mouse_user)
798 #find the window model for this gdk window:
799 wid = self._window_to_id.get(model)
800 if not wid:
801 return
802 for ss in self._server_sources.values():
803 if ALWAYS_NOTIFY_MOTION or self.last_mouse_user is None or self.last_mouse_user!=ss.uuid:
804 ss.update_mouse(wid, event.x_root, event.y_root, event.x, event.y)
807 def do_xpra_xkb_event(self, event):
808 #X11: XKBNotify
809 log("WindowModel.do_xpra_xkb_event(%r)" % event)
810 if event.subtype!="bell":
811 log.error("do_xpra_xkb_event(%r) unknown event type: %s" % (event, event.type))
812 return
813 #bell events on our windows will come through the bell signal,
814 #this method is a catch-all for events on windows we don't manage,
815 #so we use wid=0 for that:
816 wid = 0
817 for ss in self._server_sources.values():
818 name = strtobytes(event.bell_name or "")
819 ss.bell(wid, event.device, event.percent, event.pitch, event.duration, event.bell_class, event.bell_id, name)
822 def _bell_signaled(self, wm, event):
823 log("bell signaled on window %#x", event.window.get_xid())
824 if not self.bell:
825 return
826 wid = 0
827 if event.window!=get_default_root_window() and event.window_model is not None:
828 wid = self._window_to_id.get(event.window_model, 0)
829 log("_bell_signaled(%s,%r) wid=%s", wm, event, wid)
830 from xpra.server.source.windows_mixin import WindowsMixin
831 for ss in self._server_sources.values():
832 if isinstance(ss, WindowsMixin):
833 name = strtobytes(event.bell_name or "")
834 ss.bell(wid, event.device, event.percent, event.pitch, event.duration, event.bell_class, event.bell_id, name)
837 def get_screen_number(self, _wid):
838 #maybe this should be in all cases (it is in desktop_server):
839 #model = self._id_to_window.get(wid)
840 #return model.client_window.get_screen().get_number()
841 #return Gdk.Display.get_default().get_default_screen().get_number()
842 #-1 uses the current screen
843 return -1
846 def cleanup_input_devices(self):
847 pass
850 def setup_input_devices(self):
851 from xpra.server import server_features
852 xinputlog("setup_input_devices() input_devices feature=%s", server_features.input_devices)
853 if not server_features.input_devices:
854 return
855 xinputlog("setup_input_devices() format=%s, input_devices=%s", self.input_devices_format, self.input_devices)
856 xinputlog("setup_input_devices() input_devices_data=%s", self.input_devices_data)
857 #xinputlog("setup_input_devices() input_devices_data=%s", self.input_devices_data)
858 xinputlog("setup_input_devices() pointer device=%s", self.pointer_device)
859 xinputlog("setup_input_devices() touchpad device=%s", self.touchpad_device)
860 self.pointer_device_map = {}
861 if not self.touchpad_device:
862 #no need to assign anything, we only have one device anyway
863 return
864 #if we find any absolute pointer devices,
865 #map them to the "touchpad_device"
866 XIModeAbsolute = 1
867 for deviceid, device_data in self.input_devices_data.items():
868 name = device_data.get("name")
869 #xinputlog("[%i]=%s", deviceid, device_data)
870 xinputlog("[%i]=%s", deviceid, name)
871 if device_data.get("use")!="slave pointer":
872 continue
873 classes = device_data.get("classes")
874 if not classes:
875 continue
876 #look for absolute pointer devices:
877 touchpad_axes = []
878 for i, defs in classes.items():
879 xinputlog(" [%i]=%s", i, defs)
880 mode = defs.get("mode")
881 label = defs.get("label")
882 if not mode or mode!=XIModeAbsolute:
883 continue
884 if defs.get("min", -1)==0 and defs.get("max", -1)==(2**24-1):
885 touchpad_axes.append((i, label))
886 if len(touchpad_axes)==2:
887 xinputlog.info("found touchpad device: %s", name)
888 xinputlog("axes: %s", touchpad_axes)
889 self.pointer_device_map[deviceid] = self.touchpad_device
892 def _process_wheel_motion(self, proto, packet):
893 assert self.pointer_device.has_precise_wheel()
894 wid, button, distance, pointer, modifiers, _buttons = packet[1:7]
895 with xsync:
896 if self.do_process_mouse_common(proto, wid, pointer):
897 self._update_modifiers(proto, wid, modifiers)
898 self.pointer_device.wheel_motion(button, distance/1000.0) #pylint: disable=no-member
900 def get_pointer_device(self, deviceid):
901 #mouselog("get_pointer_device(%i) input_devices_data=%s", deviceid, self.input_devices_data)
902 if self.input_devices_data:
903 device_data = self.input_devices_data.get(deviceid)
904 if device_data:
905 mouselog("get_pointer_device(%i) device=%s", deviceid, device_data.get("name"))
906 device = self.pointer_device_map.get(deviceid) or self.pointer_device
907 return device
910 def _get_pointer_abs_coordinates(self, wid, pos):
911 #simple absolute coordinates
912 x, y = pos[:2]
913 from xpra.server.mixins.window_server import WindowServer
914 if len(pos)>=4 and isinstance(self, WindowServer):
915 #relative coordinates
916 model = self._id_to_window.get(wid)
917 if model:
918 rx, ry = pos[2:4]
919 geom = model.get_geometry()
920 x = geom[0]+rx
921 y = geom[1]+ry
922 log("_get_pointer_abs_coordinates(%i, %s)=%s window geometry=%s", wid, pos, (x, y), geom)
923 return x, y
925 def _move_pointer(self, wid, pos, deviceid=-1, *args):
926 #(this is called within an xswallow context)
927 screen_no = self.get_screen_number(wid)
928 device = self.get_pointer_device(deviceid)
929 x, y = self._get_pointer_abs_coordinates(wid, pos)
930 mouselog("move_pointer(%s, %s, %s) screen_no=%i, device=%s, position=%s",
931 wid, pos, deviceid, screen_no, device, (x, y))
932 try:
933 device.move_pointer(screen_no, x, y, *args)
934 except Exception as e:
935 mouselog.error("Error: failed to move the pointer to %sx%s using %s", x, y, device)
936 mouselog.error(" %s", e)
938 def do_process_mouse_common(self, proto, wid, pointer, deviceid=-1, *args):
939 mouselog("do_process_mouse_common%s", tuple([proto, wid, pointer, deviceid]+list(args)))
940 if self.readonly:
941 return None
942 pos = self.root_window.get_pointer()[-3:-1]
943 uuid = None
944 if proto:
945 ss = self.get_server_source(proto)
946 if ss:
947 uuid = ss.uuid
948 if pos!=pointer[:2] or self.input_devices=="xi":
949 self.last_mouse_user = uuid
950 with xswallow:
951 self._move_pointer(wid, pointer, deviceid, *args)
952 return pointer
954 def _update_modifiers(self, proto, wid, modifiers):
955 if self.readonly:
956 return
957 ss = self.get_server_source(proto)
958 if ss:
959 if self.ui_driver and self.ui_driver!=ss.uuid:
960 return
961 ss.make_keymask_match(modifiers)
962 if wid==self.get_focus():
963 ss.user_event()
965 def do_process_button_action(self, proto, wid, button, pressed, pointer, modifiers, _buttons=(), deviceid=-1, *_args):
966 self._update_modifiers(proto, wid, modifiers)
967 #TODO: pass extra args
968 if self._process_mouse_common(proto, wid, pointer, deviceid):
969 self.button_action(pointer, button, pressed, deviceid)
971 def button_action(self, pointer, button, pressed, deviceid=-1, *args):
972 device = self.get_pointer_device(deviceid)
973 assert device, "pointer device %s not found" % deviceid
974 try:
975 log("%s%s", device.click, (button, pressed, args))
976 with xsync:
977 device.click(button, pressed, *args)
978 except XError:
979 log("button_action(%s, %s, %s, %s, %s)", pointer, button, pressed, deviceid, args, exc_info=True)
980 log.error("Error: failed (un)press mouse button %s", button)
981 if button>=4:
982 log.error(" (perhaps your Xvfb does not support mousewheels?)")
985 def make_screenshot_packet_from_regions(self, regions):
986 #regions = array of (wid, x, y, PIL.Image)
987 if not regions:
988 log("screenshot: no regions found, returning empty 0x0 image!")
989 return ["screenshot", 0, 0, "png", -1, ""]
990 #in theory, we could run the rest in a non-UI thread since we're done with GTK..
991 minx = min(x for (_,x,_,_) in regions)
992 miny = min(y for (_,_,y,_) in regions)
993 maxx = max((x+img.get_width()) for (_,x,_,img) in regions)
994 maxy = max((y+img.get_height()) for (_,_,y,img) in regions)
995 width = maxx-minx
996 height = maxy-miny
997 log("screenshot: %sx%s, min x=%s y=%s", width, height, minx, miny)
998 from PIL import Image #@UnresolvedImport
999 screenshot = Image.new("RGBA", (width, height))
1000 for wid, x, y, img in reversed(regions):
1001 pixel_format = img.get_pixel_format()
1002 target_format = {
1003 "XRGB" : "RGB",
1004 "BGRX" : "RGB",
1005 "BGRA" : "RGBA"}.get(pixel_format, pixel_format)
1006 pixels = img.get_pixels()
1007 w = img.get_width()
1008 h = img.get_height()
1009 #PIL cannot use the memoryview directly:
1010 if isinstance(pixels, memoryview):
1011 pixels = pixels.tobytes()
1012 try:
1013 window_image = Image.frombuffer(target_format, (w, h), pixels, "raw", pixel_format, img.get_rowstride())
1014 except Exception:
1015 log.error("Error parsing window pixels in %s format for window %i", pixel_format, wid, exc_info=True)
1016 continue
1017 tx = x-minx
1018 ty = y-miny
1019 screenshot.paste(window_image, (tx, ty))
1020 from io import BytesIO
1021 buf = BytesIO()
1022 screenshot.save(buf, "png")
1023 data = buf.getvalue()
1024 buf.close()
1025 packet = ["screenshot", width, height, "png", width*4, Compressed("png", data)]
1026 log("screenshot: %sx%s %s", packet[1], packet[2], packet[-1])
1027 return packet