Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/shadow/gtk_shadow_server_base.py : 65%
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) 2016-2019 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7import os
8from gi.repository import Gtk, GdkPixbuf
10from xpra.util import envbool, XPRA_APP_ID
11from xpra.os_util import POSIX, OSX
12from xpra.server import server_features
13from xpra.server.shadow.root_window_model import RootWindowModel
14from xpra.server.gtk_server_base import GTKServerBase
15from xpra.server.shadow.shadow_server_base import ShadowServerBase
16from xpra.codecs.codec_constants import TransientCodecException, CodecStateException
17from xpra.gtk_common.gtk_util import get_screen_sizes
18from xpra.net.compression import Compressed
19from xpra.log import Logger
21traylog = Logger("tray")
22mouselog = Logger("mouse")
23notifylog = Logger("notify")
24screenlog = Logger("screen")
25log = Logger("shadow")
27MULTI_WINDOW = envbool("XPRA_SHADOW_MULTI_WINDOW", True)
30class GTKShadowServerBase(ShadowServerBase, GTKServerBase):
32 def __init__(self):
33 from xpra.gtk_common.gtk_util import get_default_root_window
34 ShadowServerBase.__init__(self, get_default_root_window())
35 GTKServerBase.__init__(self)
36 self.session_type = "shadow"
37 #for managing the systray
38 self.tray_menu = None
39 self.tray_menu_shown = False
40 self.tray_widget = None
41 self.tray = False
42 self.tray_icon = None
44 def init(self, opts):
45 GTKServerBase.init(self, opts)
46 ShadowServerBase.init(self, opts)
47 self.tray = opts.tray
48 self.tray_icon = opts.tray_icon
49 if self.tray:
50 self.setup_tray()
53 def cleanup(self):
54 self.cleanup_tray()
55 ShadowServerBase.cleanup(self)
56 GTKServerBase.cleanup(self) #@UndefinedVariable
59 def client_startup_complete(self, ss):
60 GTKServerBase.client_startup_complete(self, ss)
61 if not self.tray_icon:
62 self.set_tray_icon("server-connected")
64 def last_client_exited(self):
65 log("last_client_exited() mapped=%s", self.mapped)
66 for wid in tuple(self.mapped):
67 self.stop_refresh(wid)
68 #revert to default icon:
69 if not self.tray_icon:
70 self.set_tray_icon("server-notconnected")
71 GTKServerBase.last_client_exited(self)
74 def make_hello(self, source):
75 caps = ShadowServerBase.make_hello(self, source)
76 caps.update(GTKServerBase.make_hello(self, source))
77 if source.wants_features:
78 caps["screen_sizes"] = get_screen_sizes()
79 return caps
82 def refresh(self):
83 log("refresh() mapped=%s, capture=%s", self.mapped, self.capture)
84 if not self.mapped:
85 self.refresh_timer = None
86 return False
87 if self.capture:
88 try:
89 if not self.capture.refresh():
90 #capture doesn't have any screen updates,
91 #so we can skip calling damage
92 #(this shortcut is only used with nvfbc)
93 return False
94 except TransientCodecException as e:
95 log("refresh()", exc_info=True)
96 log.warn("Warning: transient codec exception:")
97 log.warn(" %s", e)
98 self.recreate_window_models()
99 return False
100 except CodecStateException:
101 log("refresh()", exc_info=True)
102 log.warn("Warning: codec state exception:")
103 log.warn(" %s", e)
104 self.recreate_window_models()
105 return False
106 for window in self._id_to_window.values():
107 self.refresh_window(window)
108 return True
111 ############################################################################
112 # handle monitor changes
114 def send_updated_screen_size(self):
115 log("send_updated_screen_size")
116 super().send_updated_screen_size()
117 if server_features.windows:
118 self.recreate_window_models()
120 def recreate_window_models(self):
121 #remove all existing models and re-create them:
122 for model in tuple(self._window_to_id.keys()):
123 self._remove_window(model)
124 self.cleanup_capture()
125 for model in self.makeRootWindowModels():
126 self._add_new_window(model)
129 def setup_capture(self):
130 raise NotImplementedError()
132 def makeRootWindowModels(self):
133 log("makeRootWindowModels() root=%s", self.root)
134 self.capture = self.setup_capture()
135 if not MULTI_WINDOW:
136 return (RootWindowModel(self.root, self.capture),)
137 models = []
138 screen = self.root.get_screen()
139 n = screen.get_n_monitors()
140 for i in range(n):
141 geom = screen.get_monitor_geometry(i)
142 x, y, width, height = geom.x, geom.y, geom.width, geom.height
143 try:
144 scale_factor = screen.get_monitor_scale_factor(i)
145 except Exception as e:
146 screenlog("no scale factor: %s", e)
147 else:
148 screenlog("scale factor for monitor %i: %i", i, scale_factor)
149 model = RootWindowModel(self.root, self.capture)
150 if hasattr(screen, "get_monitor_plug_name"):
151 plug_name = screen.get_monitor_plug_name(i)
152 if plug_name or n>1:
153 model.title = plug_name or str(i)
154 model.geometry = (x, y, width, height)
155 screenlog("monitor %i: %10s geometry=%s", i, model.get_property("title"), model.get_property("geometry"))
156 models.append(model)
157 log("makeRootWindowModels()=%s", models)
158 return models
161 def _adjust_pointer(self, proto, wid, opointer):
162 window = self._id_to_window.get(wid)
163 if not window:
164 self.suspend_cursor(proto)
165 return None
166 pointer = super()._adjust_pointer(proto, wid, opointer)
167 #the window may be at an offset (multi-window for multi-monitor):
168 wx, wy, ww, wh = window.get_geometry()
169 #or maybe the pointer is off-screen:
170 x, y = pointer[:2]
171 if x<0 or x>=ww or y<0 or y>=wh:
172 self.suspend_cursor(proto)
173 return None
174 self.restore_cursor(proto)
175 #note: with x11 shadow servers,
176 # X11ServerCore._get_pointer_abs_coordinates() will recalculate
177 # the absolute coordinates from the relative ones,
178 # and it should end up with the same values we calculated here
179 ax = x+wx
180 ay = y+wy
181 return [ax, ay]+list(pointer[2:])
183 def get_pointer_position(self):
184 return self.root.get_pointer()[-3:-1]
187 def get_notification_tray(self):
188 return self.tray_widget
190 def get_notifier_classes(self):
191 ncs = ShadowServerBase.get_notifier_classes(self)
192 try:
193 from xpra.gtk_common.gtk_notifier import GTK_Notifier
194 ncs.append(GTK_Notifier)
195 except Exception as e:
196 notifylog("get_notifier_classes()", exc_info=True)
197 notifylog.warn("Warning: cannot load GTK notifier:")
198 notifylog.warn(" %s", e)
199 return ncs
202 ############################################################################
203 # system tray methods, mostly copied from the gtk client...
204 # (most of these should probably be moved to a common location instead)
206 def cleanup_tray(self):
207 tw = self.tray_widget
208 traylog("cleanup_tray() tray_widget=%s", tw)
209 if tw:
210 self.tray_widget = None
211 tw.cleanup()
213 def setup_tray(self):
214 if OSX:
215 return
216 try:
217 #menu:
218 label = "Xpra Shadow Server"
219 display = os.environ.get("DISPLAY")
220 if POSIX and display:
221 label = "Xpra %s Shadow Server" % display
222 self.tray_menu = Gtk.Menu()
223 self.tray_menu.set_title(label)
224 title_item = Gtk.MenuItem()
225 title_item.set_label(label)
226 title_item.set_sensitive(False)
227 title_item.show()
228 self.tray_menu.append(title_item)
229 from xpra.gtk_common.about import about
230 self.tray_menu.append(self.traymenuitem("About Xpra", "information.png", None, about))
231 if server_features.windows:
232 def readonly_toggled(menuitem):
233 log("readonly_toggled(%s)", menuitem)
234 ro = menuitem.get_active()
235 if ro!=self.readonly:
236 self.readonly = ro
237 self.setting_changed("readonly", ro)
238 readonly_menuitem = self.checkitem("Read-only", cb=readonly_toggled, active=self.readonly)
239 self.tray_menu.append(readonly_menuitem)
240 self.tray_menu.append(self.traymenuitem("Exit", "quit.png", None, self.tray_exit_callback))
241 self.tray_menu.append(self.traymenuitem("Close Menu", "close.png", None, self.close_tray_menu))
242 #maybe add: session info, clipboard, sharing, etc
243 #control: disconnect clients
244 self.tray_menu.connect("deactivate", self.tray_menu_deactivated)
245 self.tray_widget = self.make_tray_widget()
246 self.set_tray_icon(self.tray_icon or "server-notconnected")
247 except ImportError as e:
248 traylog.warn("Warning: failed to load systemtray:")
249 traylog.warn(" %s", e)
250 except Exception as e:
251 traylog("error setting up %s", self.tray_widget, exc_info=True)
252 traylog.error("Error setting up system tray:")
253 traylog.error(" %s", e)
256 def make_tray_widget(self):
257 from xpra.platform.gui import get_native_system_tray_classes
258 classes = get_native_system_tray_classes()
259 try:
260 from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray
261 classes.append(GTKStatusIconTray)
262 except ImportError:
263 traylog("no GTKStatusIconTray", exc_info=True)
264 traylog("tray classes: %s", classes)
265 if not classes:
266 traylog.error("Error: no system tray implementation available")
267 return None
268 errs = []
269 for c in classes:
270 try:
271 w = c(self, XPRA_APP_ID, self.tray, "Xpra Shadow Server",
272 None, None, self.tray_click_callback, mouseover_cb=None, exit_cb=self.tray_exit_callback)
273 return w
274 except Exception as e:
275 errs.append((c, e))
276 traylog.error("Error: all system tray implementations have failed")
277 for c, e in errs:
278 traylog.error(" %s: %s", c, e)
279 return None
282 def set_tray_icon(self, filename):
283 if not self.tray_widget:
284 return
285 try:
286 self.tray_widget.set_icon(filename)
287 except Exception as e:
288 traylog.warn("Warning: failed to set tray icon to %s", filename)
289 traylog.warn(" %s", e)
292 def traymenuitem(self, title, icon_name=None, tooltip=None, cb=None):
293 """ Utility method for easily creating an ImageMenuItem """
294 from xpra.gtk_common.gtk_util import menuitem
295 image = None
296 if icon_name:
297 from xpra.platform.gui import get_icon_size
298 icon_size = get_icon_size()
299 image = self.get_image(icon_name, icon_size)
300 return menuitem(title, image, tooltip, cb)
302 def checkitem(self, title, cb=None, active=False):
303 check_item = Gtk.CheckMenuItem(label=title)
304 check_item.set_active(active)
305 if cb:
306 check_item.connect("toggled", cb)
307 check_item.show()
308 return check_item
310 def get_pixbuf(self, icon_name):
311 from xpra.platform.paths import get_icon_filename
312 try:
313 if not icon_name:
314 traylog("get_pixbuf(%s)=None", icon_name)
315 return None
316 icon_filename = get_icon_filename(icon_name)
317 traylog("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename)
318 if icon_filename:
319 return GdkPixbuf.Pixbuf.new_from_file(icon_filename)
320 except Exception:
321 traylog.error("get_pixbuf(%s)", icon_name, exc_info=True)
322 return None
324 def get_image(self, icon_name, size=None):
325 from xpra.gtk_common.gtk_util import scaled_image
326 try:
327 pixbuf = self.get_pixbuf(icon_name)
328 traylog("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf)
329 if not pixbuf:
330 return None
331 return scaled_image(pixbuf, size)
332 except Exception:
333 traylog.error("get_image(%s, %s)", icon_name, size, exc_info=True)
334 return None
337 def tray_menu_deactivated(self, *_args):
338 self.tray_menu_shown = False
340 def tray_click_callback(self, button, pressed, time=0):
341 traylog("tray_click_callback(%s, %s, %i) tray menu=%s, shown=%s",
342 button, pressed, time, self.tray_menu, self.tray_menu_shown)
343 if pressed:
344 self.close_tray_menu()
345 else:
346 #status icon can give us a position function:
347 #except this doesn't work and nothing happens!
348 #position_menu = self.tray_widget.tray_widget.position_menu
349 #pos = position_menu(self.tray_menu, x, y, self.tray_widget.tray_widget)
350 if POSIX and not OSX:
351 self.tray_menu.popup_at_pointer()
352 else:
353 self.tray_menu.popup(None, None, None, None, button, time)
354 self.tray_menu_shown = True
356 def tray_exit_callback(self, *_args):
357 self.close_tray_menu()
358 self.idle_add(self.clean_quit, False)
360 def close_tray_menu(self, *_args):
361 if self.tray_menu_shown:
362 self.tray_menu.popdown()
363 self.tray_menu_shown = False
366 ############################################################################
367 # screenshot
368 def do_make_screenshot_packet(self):
369 assert len(self._id_to_window)==1, "multi root window screenshot not implemented yet"
370 rwm = self._id_to_window.values()[0]
371 w, h, encoding, rowstride, data = rwm.take_screenshot()
372 assert encoding=="png" #use fixed encoding for now
373 return ["screenshot", w, h, encoding, rowstride, Compressed(encoding, data)]