Hide keyboard shortcuts

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. 

6 

7import os 

8from gi.repository import Gtk, GdkPixbuf 

9 

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 

20 

21traylog = Logger("tray") 

22mouselog = Logger("mouse") 

23notifylog = Logger("notify") 

24screenlog = Logger("screen") 

25log = Logger("shadow") 

26 

27MULTI_WINDOW = envbool("XPRA_SHADOW_MULTI_WINDOW", True) 

28 

29 

30class GTKShadowServerBase(ShadowServerBase, GTKServerBase): 

31 

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 

43 

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() 

51 

52 

53 def cleanup(self): 

54 self.cleanup_tray() 

55 ShadowServerBase.cleanup(self) 

56 GTKServerBase.cleanup(self) #@UndefinedVariable 

57 

58 

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") 

63 

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) 

72 

73 

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 

80 

81 

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 

109 

110 

111 ############################################################################ 

112 # handle monitor changes 

113 

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() 

119 

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) 

127 

128 

129 def setup_capture(self): 

130 raise NotImplementedError() 

131 

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 

159 

160 

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:]) 

182 

183 def get_pointer_position(self): 

184 return self.root.get_pointer()[-3:-1] 

185 

186 

187 def get_notification_tray(self): 

188 return self.tray_widget 

189 

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 

200 

201 

202 ############################################################################ 

203 # system tray methods, mostly copied from the gtk client... 

204 # (most of these should probably be moved to a common location instead) 

205 

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() 

212 

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) 

254 

255 

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 

280 

281 

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) 

290 

291 

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) 

301 

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 

309 

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 

323 

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 

335 

336 

337 def tray_menu_deactivated(self, *_args): 

338 self.tray_menu_shown = False 

339 

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 

355 

356 def tray_exit_callback(self, *_args): 

357 self.close_tray_menu() 

358 self.idle_add(self.clean_quit, False) 

359 

360 def close_tray_menu(self, *_args): 

361 if self.tray_menu_shown: 

362 self.tray_menu.popdown() 

363 self.tray_menu_shown = False 

364 

365 

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)]