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) 2010-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#pylint: disable-msg=E1101 

7 

8from xpra.util import typedict 

9from xpra.server.mixins.stub_server_mixin import StubServerMixin 

10from xpra.server.source.windows_mixin import WindowsMixin 

11from xpra.log import Logger 

12 

13log = Logger("window") 

14focuslog = Logger("focus") 

15metalog = Logger("metadata") 

16geomlog = Logger("geometry") 

17eventslog = Logger("events") 

18 

19def noop(*_args): 

20 pass 

21 

22 

23""" 

24Mixin for servers that forward windows. 

25""" 

26class WindowServer(StubServerMixin): 

27 

28 def __init__(self): 

29 # Window id 0 is reserved for "not a window" 

30 self._max_window_id = 1 

31 self._window_to_id = {} 

32 self._id_to_window = {} 

33 self.window_filters = [] 

34 self.window_min_size = 0, 0 

35 self.window_max_size = 2**15-1, 2**15-1 

36 

37 def init(self, opts): 

38 def parse_window_size(v, default_value=(0, 0)): 

39 try: 

40 #split on "," or "x": 

41 pv = tuple(int(x.strip()) for x in v.replace(",", "x").split("x", 1)) 

42 assert len(pv)==2 

43 w, h = pv 

44 assert w>=0 and h>0 and w<32768 and h<32768 

45 return w, h 

46 except: 

47 return default_value 

48 self.window_min_size = parse_window_size(opts.min_size, (0, 0)) 

49 self.window_max_size = parse_window_size(opts.max_size, (2**15-1, 2**15-1)) 

50 minw, minh = self.window_min_size 

51 maxw, maxh = self.window_max_size 

52 self.update_size_constraints(minw, minh, maxw, maxh) 

53 

54 def setup(self): 

55 self.load_existing_windows() 

56 

57 def cleanup(self): 

58 for window in tuple(self._window_to_id.keys()): 

59 window.unmanage() 

60 #this can cause errors if we receive packets during shutdown: 

61 #self._window_to_id = {} 

62 #self._id_to_window = {} 

63 

64 

65 def last_client_exited(self): 

66 self._focus(None, 0, []) 

67 

68 

69 def get_server_features(self, _source) -> dict: 

70 return { 

71 "window_refresh_config" : True, #v4 clients assume this is available 

72 "window-filters" : True, #v4 clients assume this is available 

73 } 

74 

75 def get_info(self, _proto) -> dict: 

76 return { 

77 "state" : { 

78 "windows" : sum(int(window.is_managed()) for window in tuple(self._id_to_window.values())), 

79 }, 

80 "filters" : tuple((uuid,repr(f)) for uuid, f in self.window_filters), 

81 } 

82 

83 def get_ui_info(self, _proto, _client_uuids=None, wids=None, *_args) -> dict: 

84 """ info that must be collected from the UI thread 

85 (ie: things that query the display) 

86 """ 

87 return {"windows" : self.get_windows_info(wids)} 

88 

89 

90 def parse_hello(self, ss, caps, send_ui): 

91 if send_ui: 

92 self.parse_hello_ui_window_settings(ss, caps) 

93 

94 def parse_hello_ui_window_settings(self, ss, c): 

95 pass 

96 

97 

98 def add_new_client(self, *_args): 

99 minw, minh = self.window_min_size 

100 maxw, maxh = self.window_max_size 

101 for ss in tuple(self._server_sources.values()): 

102 if not isinstance(ss, WindowsMixin): 

103 continue 

104 cminw, cminh = ss.window_min_size 

105 cmaxw, cmaxh = ss.window_max_size 

106 minw = max(minw, cminw) 

107 minh = max(minh, cminh) 

108 if cmaxw>0: 

109 maxw = min(maxw, cmaxw) 

110 if cmaxh>0: 

111 maxh = min(maxh, cmaxh) 

112 maxw = max(1, maxw) 

113 maxh = max(1, maxh) 

114 if minw>0 and minw>maxw: 

115 maxw = minw 

116 if minh>0 and minh>maxh: 

117 maxh = minh 

118 self.update_size_constraints(minw, minh, maxw, maxh) 

119 

120 def update_size_constraints(self, minw, minh, maxw, maxh): 

121 #subclasses may update the window models 

122 pass 

123 

124 

125 def send_initial_data(self, ss, caps, send_ui, share_count): 

126 if not send_ui: 

127 return 

128 if not isinstance(ss, WindowsMixin): 

129 return 

130 self.send_initial_windows(ss, share_count>0) 

131 self.send_initial_cursors(ss, share_count>0) 

132 

133 

134 def is_shown(self, _window) -> bool: 

135 return True 

136 

137 

138 def get_window_id(self, window): 

139 return self._window_to_id.get(window) 

140 

141 

142 def reset_window_filters(self): 

143 self.window_filters = [] 

144 

145 

146 def get_windows_info(self, window_ids): 

147 info = {} 

148 for wid, window in self._id_to_window.items(): 

149 if window_ids is not None and wid not in window_ids: 

150 continue 

151 info[wid] = self.get_window_info(window) 

152 return info 

153 

154 def get_window_info(self, window): 

155 from xpra.server.window.metadata import make_window_metadata 

156 info = {} 

157 for prop in window.get_property_names(): 

158 if prop=="icons" or prop is None: 

159 continue 

160 metadata = make_window_metadata(window, prop, get_transient_for=self.get_transient_for) 

161 info.update(metadata) 

162 for prop in window.get_internal_property_names(): 

163 metadata = make_window_metadata(window, prop) 

164 info.update(metadata) 

165 info.update({ 

166 "override-redirect" : window.is_OR(), 

167 "tray" : window.is_tray(), 

168 "size" : window.get_dimensions(), 

169 }) 

170 wid = self._window_to_id.get(window) 

171 if wid: 

172 wprops = self.client_properties.get(wid) 

173 if wprops: 

174 info["client-properties"] = wprops 

175 return info 

176 

177 def _update_metadata(self, window, pspec): 

178 metalog("updating metadata on %s: %s", window, pspec) 

179 wid = self._window_to_id.get(window) 

180 if not wid: 

181 return #window is already gone 

182 for ss in self._server_sources.values(): 

183 if isinstance(ss, WindowsMixin): 

184 ss.window_metadata(wid, window, pspec.name) 

185 

186 

187 def _remove_window(self, window): 

188 wid = self._window_to_id[window] 

189 log("remove_window: %s - %s", wid, window) 

190 for ss in self._server_sources.values(): 

191 if isinstance(ss, WindowsMixin): 

192 ss.lost_window(wid, window) 

193 del self._window_to_id[window] 

194 del self._id_to_window[wid] 

195 for ss in self._server_sources.values(): 

196 if isinstance(ss, WindowsMixin): 

197 ss.remove_window(wid, window) 

198 self.client_properties.pop(wid, None) 

199 return wid 

200 

201 def _add_new_window_common(self, window): 

202 props = window.get_dynamic_property_names() 

203 metalog("add_new_window_common(%s) watching for dynamic properties: %s", window, props) 

204 for prop in props: 

205 window.managed_connect("notify::%s" % prop, self._update_metadata) 

206 wid = self._max_window_id 

207 self._max_window_id += 1 

208 self._window_to_id[window] = wid 

209 self._id_to_window[wid] = window 

210 return wid 

211 

212 def _do_send_new_window_packet(self, ptype, window, geometry): 

213 wid = self._window_to_id[window] 

214 for ss in self._server_sources.values(): 

215 if not isinstance(ss, WindowsMixin): 

216 continue 

217 wprops = self.client_properties.get(wid, {}).get(ss.uuid) 

218 x, y, w, h = geometry 

219 #adjust if the transient-for window is not mapped in the same place by the client we send to: 

220 if "transient-for" in window.get_property_names(): 

221 transient_for = self.get_transient_for(window) 

222 if transient_for>0: 

223 parent = self._id_to_window.get(transient_for) 

224 parent_ws = ss.get_window_source(transient_for) 

225 pos = self.get_window_position(parent) 

226 geomlog("transient-for=%s : %s, ws=%s, pos=%s", transient_for, parent, parent_ws, pos) 

227 if pos and parent and parent_ws: 

228 mapped_at = parent_ws.mapped_at 

229 if mapped_at: 

230 wx, wy = pos 

231 cx, cy = mapped_at[:2] 

232 if wx!=cx or wy!=cy: 

233 dx, dy = cx-wx, cy-wy 

234 if dx or dy: 

235 geomlog("adjusting new window position for client window offset: %s", (dx, dy)) 

236 x += dx 

237 y += dy 

238 ss.new_window(ptype, wid, window, x, y, w, h, wprops) 

239 

240 def _process_damage_sequence(self, proto, packet): 

241 packet_sequence, wid, width, height, decode_time = packet[1:6] 

242 if len(packet)>=7: 

243 message = packet[6] 

244 else: 

245 message = "" 

246 ss = self.get_server_source(proto) 

247 if ss: 

248 ss.client_ack_damage(packet_sequence, wid, width, height, decode_time, message) 

249 

250 def refresh_window(self, window): 

251 ww, wh = window.get_dimensions() 

252 self.refresh_window_area(window, 0, 0, ww, wh) 

253 

254 def refresh_window_area(self, window, x, y, width, height, options=None): 

255 wid = self._window_to_id[window] 

256 for ss in tuple(self._server_sources.values()): 

257 damage = getattr(ss, "damage", noop) 

258 damage(wid, window, x, y, width, height, options) 

259 

260 def _process_buffer_refresh(self, proto, packet): 

261 """ can be used for requesting a refresh, or tuning batch config, or both """ 

262 wid, _, qual = packet[1:4] 

263 if len(packet)>=6: 

264 options = typedict(packet[4]) 

265 client_properties = packet[5] 

266 else: 

267 options = typedict({}) 

268 client_properties = {} 

269 if wid==-1: 

270 wid_windows = self._id_to_window 

271 elif wid in self._id_to_window: 

272 wid_windows = {wid : self._id_to_window.get(wid)} 

273 else: 

274 #may have been destroyed since the request was made 

275 log("invalid window specified for refresh: %s", wid) 

276 return 

277 log("process_buffer_refresh for windows: %s options=%s, client_properties=%s", 

278 wid_windows, options, client_properties) 

279 batch_props = options.dictget("batch", {}) 

280 if batch_props or client_properties: 

281 #change batch config and/or client properties 

282 self.update_batch_config(proto, wid_windows, typedict(batch_props), client_properties) 

283 #default to True for backwards compatibility: 

284 if options.get("refresh-now", True): 

285 refresh_opts = {"quality" : qual, 

286 "override_options" : True} 

287 self._refresh_windows(proto, wid_windows, refresh_opts) 

288 

289 

290 def update_batch_config(self, proto, wid_windows, batch_props, client_properties): 

291 ss = self.get_server_source(proto) 

292 if ss is None: 

293 return 

294 for wid, window in wid_windows.items(): 

295 if window is None or not window.is_managed(): 

296 continue 

297 self._set_client_properties(proto, wid, window, client_properties) 

298 ss.update_batch(wid, window, batch_props) 

299 

300 def _refresh_windows(self, proto, wid_windows, opts): 

301 ss = self.get_server_source(proto) 

302 if ss is None: 

303 return 

304 for wid, window in wid_windows.items(): 

305 if window is None or not window.is_managed(): 

306 continue 

307 if not self.is_shown(window): 

308 log("window is no longer shown, ignoring buffer refresh which would fail") 

309 continue 

310 ss.refresh(wid, window, opts) 

311 

312 def _idle_refresh_all_windows(self, proto): 

313 self.idle_add(self._refresh_windows, proto, self._id_to_window, {}) 

314 

315 

316 def get_window_position(self, _window): 

317 #where the window is actually mapped on the server screen: 

318 return None 

319 

320 def _window_mapped_at(self, proto, wid, window, coords=None): 

321 #record where a window is mapped by a client 

322 #(in order to support multiple clients and different offsets) 

323 ss = self.get_server_source(proto) 

324 if not ss: 

325 return 

326 if coords: 

327 ss.map_window(wid, window, coords) 

328 else: 

329 ss.unmap_window(wid, window) 

330 

331 def get_transient_for(self, _window): 

332 return 0 

333 

334 def _process_map_window(self, proto, packet): 

335 log.info("_process_map_window(%s, %s)", proto, packet) 

336 

337 def _process_unmap_window(self, proto, packet): 

338 log.info("_process_unmap_window(%s, %s)", proto, packet) 

339 

340 def _process_close_window(self, proto, packet): 

341 log.info("_process_close_window(%s, %s)", proto, packet) 

342 

343 def _process_configure_window(self, proto, packet): 

344 log.info("_process_configure_window(%s, %s)", proto, packet) 

345 

346 def _get_window_dict(self, wids) -> dict: 

347 wd = {} 

348 for wid in wids: 

349 window = self._id_to_window.get(wid) 

350 if window: 

351 wd[wid] = window 

352 return wd 

353 

354 def _process_suspend(self, proto, packet): 

355 eventslog("suspend(%s)", packet[1:]) 

356 ui = bool(packet[1]) 

357 wd = self._get_window_dict(packet[2]) 

358 ss = self.get_server_source(proto) 

359 if ss: 

360 ss.suspend(ui, wd) 

361 

362 def _process_resume(self, proto, packet): 

363 eventslog("resume(%s)", packet[1:]) 

364 ui = bool(packet[1]) 

365 wd = self._get_window_dict(packet[2]) 

366 ss = self.get_server_source(proto) 

367 if ss: 

368 ss.resume(ui, wd) 

369 

370 

371 def send_initial_windows(self, ss, sharing=False): 

372 raise NotImplementedError() 

373 

374 

375 def send_initial_cursors(self, ss, sharing=False): 

376 pass 

377 

378 

379 ###################################################################### 

380 # focus: 

381 def _process_focus(self, proto, packet): 

382 if self.readonly: 

383 return 

384 wid = packet[1] 

385 focuslog("process_focus: wid=%s", wid) 

386 if len(packet)>=3: 

387 modifiers = packet[2] 

388 else: 

389 modifiers = None 

390 ss = self.get_server_source(proto) 

391 if ss: 

392 self._focus(ss, wid, modifiers) 

393 #if the client focused one of our windows, count this as a user event: 

394 if wid>0: 

395 ss.user_event() 

396 

397 def _focus(self, _server_source, wid, modifiers): 

398 focuslog("_focus(%s,%s)", wid, modifiers) 

399 

400 def get_focus(self) -> int: 

401 #can be overriden by subclasses that do manage focus 

402 #(ie: not shadow servers which only have a single window) 

403 #default: no focus 

404 return -1 

405 

406 

407 def init_packet_handlers(self): 

408 self.add_packet_handlers({ 

409 "map-window" : self._process_map_window, 

410 "unmap-window" : self._process_unmap_window, 

411 "configure-window" : self._process_configure_window, 

412 "close-window" : self._process_close_window, 

413 "focus" : self._process_focus, 

414 "damage-sequence" : self._process_damage_sequence, 

415 "buffer-refresh" : self._process_buffer_refresh, 

416 "suspend" : self._process_suspend, 

417 "resume" : self._process_resume, 

418 })