Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/gtk_server_base.py : 57%
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.
9#pylint: disable=wrong-import-position
11import sys
12import os.path
13import gi
14gi.require_version('Gdk', '3.0')
15gi.require_version('Gtk', '3.0')
16gi.require_version('Pango', '1.0')
17from gi.repository import GLib, Gdk, Gtk
19from xpra.util import flatten_dict, envbool
20from xpra.os_util import monotonic_time
21from xpra.gtk_common.gobject_compat import register_os_signals
22from xpra.server import server_features
23from xpra.server.server_base import ServerBase
24from xpra.gtk_common.gtk_util import (
25 get_gtk_version_info, get_root_size,
26 )
27from xpra.log import Logger
29UI_THREAD_WATCHER = envbool("XPRA_UI_THREAD_WATCHER")
31log = Logger("server", "gtk")
32screenlog = Logger("server", "screen")
33cursorlog = Logger("server", "cursor")
34notifylog = Logger("notify")
37class GTKServerBase(ServerBase):
38 """
39 This is the base class for servers.
40 It provides all the generic functions but is not tied
41 to a specific backend (X11 or otherwise).
42 See X11ServerBase, XpraServer and XpraX11ShadowServer
43 """
45 def __init__(self):
46 log("GTKServerBase.__init__()")
47 self.idle_add = GLib.idle_add
48 self.timeout_add = GLib.timeout_add
49 self.source_remove = GLib.source_remove
50 self.cursor_suspended = False
51 self.ui_watcher = None
52 ServerBase.__init__(self)
54 def watch_keymap_changes(self):
55 ### Set up keymap change notification:
56 display = Gdk.Display.get_default()
57 keymap = Gdk.Keymap.get_for_display(display)
58 keymap.connect("keys-changed", self._keys_changed)
60 def install_signal_handlers(self, callback):
61 sstr = "%s server" % self.get_server_mode()
62 register_os_signals(callback, sstr)
63 from xpra.gtk_common.gobject_compat import register_SIGUSR_signals
64 register_SIGUSR_signals(sstr)
66 def do_quit(self):
67 log("do_quit: calling Gtk.main_quit")
68 Gtk.main_quit()
69 log("do_quit: Gtk.main_quit done")
70 #from now on, we can't rely on the main loop:
71 from xpra.os_util import register_SIGUSR_signals
72 register_SIGUSR_signals()
74 def do_cleanup(self):
75 ServerBase.do_cleanup(self)
76 self.close_gtk_display()
77 uiw = self.ui_watcher
78 if uiw:
79 uiw.stop()
81 def close_gtk_display(self):
82 # Close our display(s) first, so the server dying won't kill us.
83 # (if gtk has been loaded)
84 gdk_mod = sys.modules.get("gtk.gdk") or sys.modules.get("gi.repository.Gdk")
85 #bug 2328: python3 shadow server segfault on Ubuntu 16.04
86 #also crashes on Ubuntu 20.04
87 close = envbool("XPRA_CLOSE_GTK_DISPLAY", False)
88 log("close_gtk_display() close=%s, gdk_mod=%s",
89 close, gdk_mod)
90 if close and gdk_mod:
91 displays = Gdk.DisplayManager.get().list_displays()
92 log("close_gtk_display() displays=%s", displays)
93 for d in displays:
94 log("close_gtk_display() closing %s", d)
95 d.close()
98 def do_run(self):
99 if UI_THREAD_WATCHER:
100 from xpra.platform.ui_thread_watcher import get_UI_watcher
101 self.ui_watcher = get_UI_watcher(GLib.timeout_add, GLib.source_remove)
102 self.ui_watcher.start()
103 if server_features.windows:
104 display = Gdk.Display.get_default()
105 i=0
106 while i<display.get_n_screens():
107 screen = display.get_screen(i)
108 screen.connect("size-changed", self._screen_size_changed)
109 screen.connect("monitors-changed", self._monitors_changed)
110 i += 1
111 log("do_run() calling %s", Gtk.main)
112 Gtk.main()
113 log("do_run() end of gtk.main()")
116 def make_hello(self, source):
117 capabilities = super().make_hello(source)
118 if source.wants_display:
119 display = Gdk.Display.get_default()
120 max_size = tuple(display.get_maximal_cursor_size())
121 capabilities.update({
122 "display" : display.get_name(),
123 "cursor.default_size" : display.get_default_cursor_size(),
124 "cursor.max_size" : max_size,
125 })
126 if source.wants_versions:
127 capabilities.update(flatten_dict(get_gtk_version_info()))
128 return capabilities
130 def get_ui_info(self, proto, *args):
131 info = super().get_ui_info(proto, *args)
132 info.setdefault("server", {}).update({
133 "display" : Gdk.Display.get_default().get_name(),
134 "root_window_size" : self.get_root_window_size(),
135 })
136 info.setdefault("cursor", {}).update(self.get_ui_cursor_info())
137 return info
140 def suspend_cursor(self, proto):
141 #this is called by shadow and desktop servers
142 #when we're receiving pointer events but the pointer
143 #is no longer over the active window area,
144 #so we have to tell the client to switch back to the default cursor
145 if self.cursor_suspended:
146 return
147 self.cursor_suspended = True
148 ss = self.get_server_source(proto)
149 if ss:
150 ss.cancel_cursor_timer()
151 ss.send_empty_cursor()
153 def restore_cursor(self, proto):
154 #see suspend_cursor
155 if not self.cursor_suspended:
156 return
157 self.cursor_suspended = False
158 ss = self.get_server_source(proto)
159 if ss:
160 ss.send_cursor()
163 def send_initial_cursors(self, ss, _sharing=False):
164 #cursors: get sizes and send:
165 display = Gdk.Display.get_default()
166 self.cursor_sizes = int(display.get_default_cursor_size()), display.get_maximal_cursor_size()
167 cursorlog("send_initial_cursors() cursor_sizes=%s", self.cursor_sizes)
168 ss.send_cursor()
170 def get_ui_cursor_info(self) -> dict:
171 #(from UI thread)
172 #now cursor size info:
173 display = Gdk.Display.get_default()
174 pos = display.get_default_screen().get_root_window().get_pointer()[-3:-1]
175 cinfo = {"position" : pos}
176 for prop, size in {
177 "default" : display.get_default_cursor_size(),
178 "max" : tuple(display.get_maximal_cursor_size()),
179 }.items():
180 if size is None:
181 continue
182 cinfo["%s_size" % prop] = size
183 return cinfo
185 def do_get_info(self, proto, *args):
186 start = monotonic_time()
187 info = super().do_get_info(proto, *args)
188 vi = get_gtk_version_info()
189 vi["type"] = "Python/gtk"
190 info.setdefault("server", {}).update(vi)
191 log("GTKServerBase.do_get_info took %ims", (monotonic_time()-start)*1000)
192 return info
194 def get_root_window_size(self):
195 return get_root_size()
197 def get_max_screen_size(self):
198 max_w, max_h = get_root_size()
199 return max_w, max_h
201 def configure_best_screen_size(self):
202 root_w, root_h = get_root_size()
203 return root_w, root_h
205 def calculate_workarea(self, maxw, maxh):
206 screenlog("calculate_workarea(%s, %s)", maxw, maxh)
207 workarea = Gdk.Rectangle()
208 workarea.width = maxw
209 workarea.height = maxh
210 for ss in self._server_sources.values():
211 screen_sizes = ss.screen_sizes
212 screenlog("calculate_workarea() screen_sizes(%s)=%s", ss, screen_sizes)
213 if not screen_sizes:
214 continue
215 for display in screen_sizes:
216 #avoid error with old/broken clients:
217 if not display or not isinstance(display, (list, tuple)):
218 continue
219 #display: [':0.0', 2560, 1600, 677, 423, [['DFP2', 0, 0, 2560, 1600, 646, 406]], 0, 0, 2560, 1574]
220 if len(display)>=10:
221 work_x, work_y, work_w, work_h = display[6:10]
222 display_workarea = Gdk.Rectangle()
223 display_workarea.x = work_x
224 display_workarea.y = work_y
225 display_workarea.width = work_w
226 display_workarea.height = work_h
227 screenlog("calculate_workarea() found %s for display %s", display_workarea, display[0])
228 success, workarea = workarea.intersect(display_workarea)
229 if not success:
230 log.warn("Warning: failed to calculate workarea")
231 log.warn(" as intersection of %s and %s", (maxw, maxh), (work_x, work_y, work_w, work_h))
232 #sanity checks:
233 screenlog("calculate_workarea(%s, %s) workarea=%s", maxw, maxh, workarea)
234 if workarea.width==0 or workarea.height==0 or workarea.width>=32768-8192 or workarea.height>=32768-8192:
235 screenlog.warn("Warning: failed to calculate a common workarea")
236 screenlog.warn(" using the full display area: %ix%i", maxw, maxh)
237 workarea = Gdk.Rectangle()
238 workarea.width = maxw
239 workarea.height = maxh
240 self.set_workarea(workarea)
242 def set_workarea(self, workarea):
243 pass
245 def set_desktop_geometry(self, width, height):
246 pass
248 def set_dpi(self, xdpi, ydpi):
249 pass
252 def _move_pointer(self, _wid, pos, *_args):
253 x, y = pos
254 display = Gdk.Display.get_default()
255 display.warp_pointer(display.get_default_screen(), x, y)
257 def do_process_button_action(self, *args):
258 pass
261 def _process_map_window(self, proto, packet):
262 log.info("_process_map_window(%s, %s)", proto, packet)
264 def _process_unmap_window(self, proto, packet):
265 log.info("_process_unmap_window(%s, %s)", proto, packet)
267 def _process_close_window(self, proto, packet):
268 log.info("_process_close_window(%s, %s)", proto, packet)
270 def _process_configure_window(self, proto, packet):
271 log.info("_process_configure_window(%s, %s)", proto, packet)
274 def get_notification_icon(self, icon_string):
275 #the string may be:
276 # * a path which we will load using pillow
277 # * a name we lookup in the current them
278 if not icon_string:
279 return ()
280 img = None
281 from PIL import Image
282 if os.path.isabs(icon_string):
283 if os.path.exists(icon_string) and os.path.isfile(icon_string):
284 img = Image.open(icon_string)
285 w, h = img.size
286 else:
287 #try to find it in the theme:
288 theme = Gtk.IconTheme.get_default()
289 if theme:
290 try:
291 icon = theme.load_icon(icon_string, Gtk.IconSize.BUTTON, 0)
292 except Exception as e:
293 notifylog("failed to load icon '%s' from default theme: %s", icon_string, e)
294 else:
295 data = icon.get_pixels()
296 w = icon.get_width()
297 h = icon.get_height()
298 rowstride = icon.get_rowstride()
299 mode = "RGB"
300 if icon.get_has_alpha():
301 mode = "RGBA"
302 img = Image.frombytes(mode, (w, h), data, "raw", mode, rowstride)
303 if img:
304 if w>256 or h>256:
305 img = img.resize((256, 256), Image.ANTIALIAS)
306 w = h = 256
307 from io import BytesIO
308 buf = BytesIO()
309 img.save(buf, "PNG")
310 cpixels = buf.getvalue()
311 buf.close()
312 return "png", w, h, cpixels
313 return ()