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# This file is part of Xpra. 

2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>) 

3# Copyright (C) 2010-2019 Antoine Martin <antoine@xpra.org> 

4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com> 

5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

6# later version. See the file COPYING for details. 

7 

8#pylint: disable-msg=E1101 

9 

10import os 

11import errno 

12import signal 

13import datetime 

14from collections import deque 

15from time import sleep, time 

16from queue import Queue 

17from gi.repository import GLib 

18 

19from xpra.platform.gui import ( 

20 get_vrefresh, get_window_min_size, get_window_max_size, 

21 get_double_click_time, get_double_click_distance, get_native_system_tray_classes, 

22 ) 

23from xpra.platform.features import SYSTEM_TRAY_SUPPORTED 

24from xpra.platform.paths import get_icon_filename 

25from xpra.scripts.config import FALSE_OPTIONS 

26from xpra.make_thread import make_thread 

27from xpra.os_util import ( 

28 bytestostr, monotonic_time, memoryview_to_bytes, 

29 OSX, POSIX, is_Ubuntu, 

30 ) 

31from xpra.util import ( 

32 iround, envint, envbool, typedict, 

33 make_instance, updict, repr_ellipsized, csv, 

34 ) 

35from xpra.client.mixins.stub_client_mixin import StubClientMixin 

36from xpra.log import Logger 

37 

38log = Logger("window") 

39geomlog = Logger("geometry") 

40paintlog = Logger("paint") 

41drawlog = Logger("draw") 

42focuslog = Logger("focus") 

43grablog = Logger("grab") 

44iconlog = Logger("icon") 

45mouselog = Logger("mouse") 

46cursorlog = Logger("cursor") 

47metalog = Logger("metadata") 

48traylog = Logger("client", "tray") 

49 

50MOUSE_SHOW = envbool("XPRA_MOUSE_SHOW", True) 

51 

52PAINT_FAULT_RATE = envint("XPRA_PAINT_FAULT_INJECTION_RATE") 

53PAINT_FAULT_TELL = envbool("XPRA_PAINT_FAULT_INJECTION_TELL", True) 

54PAINT_DELAY = envint("XPRA_PAINT_DELAY", 0) 

55 

56WM_CLASS_CLOSEEXIT = os.environ.get("XPRA_WM_CLASS_CLOSEEXIT", "Xephyr").split(",") 

57TITLE_CLOSEEXIT = os.environ.get("XPRA_TITLE_CLOSEEXIT", "Xnest").split(",") 

58 

59OR_FORCE_GRAB_STR = os.environ.get("XPRA_OR_FORCE_GRAB", "DIALOG:sun-awt-X11") 

60OR_FORCE_GRAB = {} 

61for s in OR_FORCE_GRAB_STR.split(","): 

62 parts = s.split(":") 

63 if len(parts)==1: 

64 OR_FORCE_GRAB.setdefault("*", []).append(s) 

65 else: 

66 OR_FORCE_GRAB.setdefault(parts[0], []).append(parts[1]) 

67 

68SKIP_DUPLICATE_BUTTON_EVENTS = envbool("XPRA_SKIP_DUPLICATE_BUTTON_EVENTS", True) 

69REVERSE_HORIZONTAL_SCROLLING = envbool("XPRA_REVERSE_HORIZONTAL_SCROLLING", OSX) 

70 

71DYNAMIC_TRAY_ICON = envbool("XPRA_DYNAMIC_TRAY_ICON", not OSX and not is_Ubuntu()) 

72ICON_OVERLAY = envint("XPRA_ICON_OVERLAY", 50) 

73ICON_SHRINKAGE = envint("XPRA_ICON_SHRINKAGE", 75) 

74SAVE_WINDOW_ICONS = envbool("XPRA_SAVE_WINDOW_ICONS", False) 

75SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False) 

76SIGNAL_WATCHER = envbool("XPRA_SIGNAL_WATCHER", True) 

77 

78FAKE_SUSPEND_RESUME = envint("XPRA_FAKE_SUSPEND_RESUME", 0) 

79 

80 

81DRAW_TYPES = {bytes : "bytes", str : "bytes", tuple : "arrays", list : "arrays"} 

82 

83 

84""" 

85Utility superclass for clients that handle windows: 

86create, resize, paint, grabs, cursors, etc 

87""" 

88class WindowClient(StubClientMixin): 

89 

90 def __init__(self): 

91 StubClientMixin.__init__(self) 

92 self._window_to_id = {} 

93 self._id_to_window = {} 

94 

95 self.auto_refresh_delay = -1 

96 self.min_window_size = 0, 0 

97 self.max_window_size = 0, 0 

98 

99 #draw thread: 

100 self._draw_queue = None 

101 self._draw_thread = None 

102 self._draw_counter = 0 

103 

104 #statistics and server info: 

105 self.pixel_counter = deque(maxlen=1000) 

106 

107 self.readonly = False 

108 self.windows_enabled = True 

109 self.pixel_depth = 0 

110 

111 self.server_window_frame_extents = False 

112 self.server_is_desktop = False 

113 self.server_window_states = [] 

114 self.server_window_signals = () 

115 

116 self.server_input_devices = None 

117 self.server_precise_wheel = False 

118 self.server_pointer_relative = False 

119 

120 self.input_devices = "auto" 

121 

122 self.overlay_image = None 

123 

124 self.server_cursors = False 

125 self.client_supports_system_tray = False 

126 self.client_supports_cursors = False 

127 self.client_supports_bell = False 

128 self.cursors_enabled = False 

129 self.default_cursor_data = None 

130 self.server_bell = False 

131 self.bell_enabled = False 

132 

133 self.border = None 

134 self.window_close_action = "forward" 

135 self.modal_windows = True 

136 

137 self._pid_to_signalwatcher = {} 

138 self._signalwatcher_to_wids = {} 

139 

140 self.wheel_map = {} 

141 self.wheel_deltax = 0 

142 self.wheel_deltay = 0 

143 

144 #state: 

145 self.lost_focus_timer = None 

146 self._focused = None 

147 self._window_with_grab = None 

148 self._suspended_at = 0 

149 self._button_state = {} 

150 

151 def init(self, opts): 

152 if opts.system_tray and SYSTEM_TRAY_SUPPORTED: 

153 try: 

154 from xpra.client import client_tray 

155 assert client_tray 

156 except ImportError: 

157 log.warn("Warning: the tray forwarding module is missing") 

158 else: 

159 self.client_supports_system_tray = True 

160 self.client_supports_cursors = opts.cursors 

161 self.client_supports_bell = opts.bell 

162 self.input_devices = opts.input_devices 

163 self.auto_refresh_delay = opts.auto_refresh_delay 

164 def parse_window_size(v, attribute="max-size"): 

165 if v: 

166 try: 

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

168 assert len(pv)==2 

169 return pv 

170 except: 

171 #the main script does some checking, but we could be called from a config file launch 

172 log.warn("Warning: invalid window %s specified: %s", attribute, v) 

173 return None 

174 self.min_window_size = parse_window_size(opts.min_size) or get_window_min_size() 

175 self.max_window_size = parse_window_size(opts.max_size) or get_window_max_size() 

176 self.pixel_depth = int(opts.pixel_depth) 

177 if self.pixel_depth not in (0, 16, 24, 30) and self.pixel_depth<32: 

178 log.warn("Warning: invalid pixel depth %i", self.pixel_depth) 

179 self.pixel_depth = 0 

180 

181 self.windows_enabled = opts.windows 

182 if self.windows_enabled: 

183 if opts.window_close not in ("forward", "ignore", "disconnect", "shutdown", "auto"): 

184 self.window_close_action = "forward" 

185 log.warn("Warning: invalid 'window-close' option: '%s'", opts.window_close) 

186 log.warn(" using '%s'", self.window_close_action) 

187 else: 

188 self.window_close_action = opts.window_close 

189 self.modal_windows = self.windows_enabled and opts.modal_windows 

190 

191 self.border_str = opts.border 

192 if opts.border: 

193 self.parse_border() 

194 

195 #mouse wheel: 

196 mw = (opts.mousewheel or "").lower().replace("-", "") 

197 if mw not in FALSE_OPTIONS: 

198 UP = 4 

199 LEFT = 6 

200 Z1 = 8 

201 for i in range(20): 

202 btn = 4+i*2 

203 invert = ( 

204 mw=="invert" or 

205 mw=="invertall" or 

206 (btn==UP and mw=="inverty") or 

207 (btn==LEFT and mw=="invertx") or 

208 (btn==Z1 and mw=="invertz") 

209 ) 

210 if not invert: 

211 self.wheel_map[btn] = btn 

212 self.wheel_map[btn+1] = btn+1 

213 else: 

214 self.wheel_map[btn+1] = btn 

215 self.wheel_map[btn] = btn+1 

216 mouselog("wheel_map(%s)=%s", mw, self.wheel_map) 

217 

218 if 0<ICON_OVERLAY<=100: 

219 icon_filename = get_icon_filename("xpra") 

220 if icon_filename: 

221 try: 

222 #make sure Pillow's PNG image loader doesn't spam the output with debug messages: 

223 import logging 

224 logging.getLogger("PIL.PngImagePlugin").setLevel(logging.INFO) 

225 from PIL import Image #@UnresolvedImport 

226 self.overlay_image = Image.open(icon_filename) 

227 except Exception as e: 

228 log.error("Error: failed to load overlay icon '%s':", icon_filename, exc_info=True) 

229 log.error(" %s", e) 

230 traylog("overlay_image=%s", self.overlay_image) 

231 self._draw_queue = Queue() 

232 self._draw_thread = make_thread(self._draw_thread_loop, "draw") 

233 

234 

235 def parse_border(self): 

236 #not implemented here (see gtk3 client) 

237 pass 

238 

239 

240 def run(self): 

241 #we decode pixel data in this thread 

242 self._draw_thread.start() 

243 if FAKE_SUSPEND_RESUME: 

244 self.timeout_add(FAKE_SUSPEND_RESUME*1000, self.suspend) 

245 self.timeout_add(FAKE_SUSPEND_RESUME*1000*2, self.resume) 

246 

247 

248 def cleanup(self): 

249 log("WindowClient.cleanup()") 

250 #tell the draw thread to exit: 

251 dq = self._draw_queue 

252 if dq: 

253 dq.put(None) 

254 #the protocol has been closed, it is now safe to close all the windows: 

255 #(cleaner and needed when we run embedded in the client launcher) 

256 self.destroy_all_windows() 

257 self.cancel_lost_focus_timer() 

258 if dq: 

259 dq.put(None) 

260 log("WindowClient.cleanup() done") 

261 

262 

263 def set_modal_windows(self, modal_windows): 

264 self.modal_windows = modal_windows 

265 #re-set flag on all the windows: 

266 for w in self._id_to_window.values(): 

267 modal = w._metadata.boolget("modal", False) 

268 w.set_modal(modal) 

269 

270 def set_windows_cursor(self, client_windows, new_cursor): 

271 raise NotImplementedError() 

272 

273 def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name): 

274 raise NotImplementedError() 

275 

276 

277 def get_info(self): 

278 info = { 

279 "count" : len(self._window_to_id), 

280 "min-size" : self.min_window_size, 

281 "max-size" : self.max_window_size, 

282 "draw-counter" : self._draw_counter, 

283 "read-only" : self.readonly, 

284 "wheel" : { 

285 "delta-x" : self.wheel_deltax, 

286 "delta-y" : self.wheel_deltay, 

287 }, 

288 "focused" : self._focused or 0, 

289 "grabbed" : self._window_with_grab or 0, 

290 "buttons" : self._button_state, 

291 } 

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

293 info[wid] = window.get_info() 

294 return {"windows" : info} 

295 

296 

297 ###################################################################### 

298 # hello: 

299 def get_caps(self) -> dict: 

300 #FIXME: the messy bits without proper namespace: 

301 caps = { 

302 #generic server flags: 

303 #mouse and cursors: 

304 "mouse.show" : MOUSE_SHOW, 

305 "mouse.initial-position" : self.get_mouse_position(), 

306 "named_cursors" : False, 

307 "cursors" : self.client_supports_cursors, 

308 "double_click.time" : get_double_click_time(), 

309 "double_click.distance" : get_double_click_distance(), 

310 #features: 

311 "bell" : self.client_supports_bell, 

312 "vrefresh" : get_vrefresh(), 

313 "windows" : self.windows_enabled, 

314 "auto_refresh_delay" : int(self.auto_refresh_delay*1000), 

315 #system tray forwarding: 

316 "system_tray" : self.client_supports_system_tray, 

317 "wants_default_cursor" : True, 

318 } 

319 updict(caps, "window", self.get_window_caps()) 

320 updict(caps, "encoding", { 

321 "eos" : True, 

322 }) 

323 return caps 

324 

325 def get_window_caps(self) -> dict: 

326 return { 

327 #implemented in the gtk client: 

328 "min-size" : self.min_window_size, 

329 "max-size" : self.max_window_size, 

330 "restack" : True, 

331 } 

332 

333 

334 def parse_server_capabilities(self, c : typedict) -> bool: 

335 self.server_window_frame_extents = c.boolget("window.frame-extents") 

336 self.server_cursors = c.boolget("cursors", True) #added in 0.5, default to True! 

337 self.cursors_enabled = self.server_cursors and self.client_supports_cursors 

338 self.default_cursor_data = c.tupleget("cursor.default", None) 

339 self.server_bell = c.boolget("bell") #added in 0.5, default to True! 

340 self.bell_enabled = self.server_bell and self.client_supports_bell 

341 if c.boolget("windows", True): 

342 if self.windows_enabled: 

343 server_auto_refresh_delay = c.intget("auto_refresh_delay", 0)/1000.0 

344 if server_auto_refresh_delay==0 and self.auto_refresh_delay>0: 

345 log.warn("Warning: server does not support auto-refresh!") 

346 else: 

347 log.warn("Warning: window forwarding is not enabled on this server") 

348 self.server_window_signals = c.strtupleget("window.signals") 

349 self.server_window_states = c.strtupleget("window.states", ( 

350 "iconified", "fullscreen", 

351 "above", "below", 

352 "sticky", "iconified", "maximized", 

353 )) 

354 self.server_is_desktop = c.boolget("shadow") or c.boolget("desktop") 

355 #input devices: 

356 self.server_input_devices = c.strget("input-devices") 

357 self.server_precise_wheel = c.boolget("wheel.precise", False) 

358 self.server_pointer_relative = c.boolget("pointer.relative", False) 

359 return True 

360 

361 

362 ###################################################################### 

363 # pointer: 

364 def _process_pointer_position(self, packet): 

365 wid, x, y = packet[1:4] 

366 if len(packet)>=6: 

367 rx, ry = packet[4:6] 

368 else: 

369 rx, ry = -1, -1 

370 cx, cy = self.get_mouse_position() 

371 start_time = monotonic_time() 

372 mouselog("process_pointer_position: %i,%i (%i,%i relative to wid %i) - current position is %i,%i", 

373 x, y, rx, ry, wid, cx, cy) 

374 size = 10 

375 for i,w in self._id_to_window.items(): 

376 #not all window implementations have this method: 

377 #(but GLClientWindow does) 

378 show_pointer_overlay = getattr(w, "show_pointer_overlay", None) 

379 if show_pointer_overlay: 

380 if i==wid: 

381 value = rx, ry, size, start_time 

382 else: 

383 value = None 

384 show_pointer_overlay(value) 

385 

386 def send_wheel_delta(self, wid, button, distance, *args): 

387 modifiers = self.get_current_modifiers() 

388 pointer = self.get_mouse_position() 

389 buttons = [] 

390 mouselog("send_wheel_delta(%i, %i, %.4f, %s) precise wheel=%s, modifiers=%s, pointer=%s", 

391 wid, button, distance, args, self.server_precise_wheel, modifiers, pointer) 

392 if self.server_precise_wheel: 

393 #send the exact value multiplied by 1000 (as an int) 

394 idist = int(distance*1000) 

395 if abs(idist)>0: 

396 packet = ["wheel-motion", wid, 

397 button, idist, 

398 pointer, modifiers, buttons] + list(args) 

399 mouselog("send_wheel_delta(..) %s", packet) 

400 self.send_positional(packet) 

401 return 0 

402 else: 

403 #server cannot handle precise wheel, 

404 #so we have to use discrete events, 

405 #and send a click for each step: 

406 steps = abs(int(distance)) 

407 for _ in range(steps): 

408 self.send_button(wid, button, True, pointer, modifiers, buttons) 

409 self.send_button(wid, button, False, pointer, modifiers, buttons) 

410 #return remainder: 

411 return float(distance) - int(distance) 

412 

413 def wheel_event(self, wid, deltax=0, deltay=0, deviceid=0): 

414 #this is a different entry point for mouse wheel events, 

415 #which provides finer grained deltas (if supported by the server) 

416 #accumulate deltas: 

417 if REVERSE_HORIZONTAL_SCROLLING: 

418 deltax = -deltax 

419 self.wheel_deltax += deltax 

420 self.wheel_deltay += deltay 

421 button = self.wheel_map.get(6+int(self.wheel_deltax>0)) #RIGHT=7, LEFT=6 

422 if button>0: 

423 self.wheel_deltax = self.send_wheel_delta(wid, button, self.wheel_deltax, deviceid) 

424 button = self.wheel_map.get(5-int(self.wheel_deltay>0)) #UP=4, DOWN=5 

425 if button>0: 

426 self.wheel_deltay = self.send_wheel_delta(wid, button, self.wheel_deltay, deviceid) 

427 mouselog("wheel_event%s new deltas=%s,%s", 

428 (wid, deltax, deltay, deviceid), self.wheel_deltax, self.wheel_deltay) 

429 

430 def send_button(self, wid, button, pressed, pointer, modifiers, buttons, *args): 

431 pressed_state = self._button_state.get(button, False) 

432 if SKIP_DUPLICATE_BUTTON_EVENTS and pressed_state==pressed: 

433 mouselog("button action: unchanged state, ignoring event") 

434 return 

435 self._button_state[button] = pressed 

436 packet = ["button-action", wid, 

437 button, pressed, 

438 pointer, modifiers, buttons] + list(args) 

439 mouselog("button packet: %s", packet) 

440 self.send_positional(packet) 

441 

442 def scale_pointer(self, pointer): 

443 #subclass may scale this: 

444 #return int(pointer[0]/self.xscale), int(pointer[1]/self.yscale) 

445 return int(pointer[0]), int(pointer[1]) 

446 

447 def send_input_devices(self, fmt, input_devices): 

448 assert self.server_input_devices 

449 self.send("input-devices", fmt, input_devices) 

450 

451 

452 ###################################################################### 

453 # cursor: 

454 def _process_cursor(self, packet): 

455 if not self.cursors_enabled: 

456 return 

457 if len(packet)==2: 

458 #marker telling us to use the default cursor: 

459 new_cursor = packet[1] 

460 else: 

461 if len(packet)<9: 

462 raise Exception("invalid cursor packet: %s items" % len(packet)) 

463 encoding = packet[1] 

464 if not isinstance(encoding, bytes): 

465 log.warn("Warning: received an invalid cursor packet:") 

466 tmp_packet = list(packet) 

467 try: 

468 tmp_packet[9] = ".." 

469 except IndexError: 

470 pass 

471 log.warn(" %s", repr_ellipsized(tmp_packet)) 

472 log.warn(" data types:") 

473 log.warn(" %s", csv(type(x) for x in packet)) 

474 raise Exception("invalid cursor packet format: cursor type is a %s" % type(encoding)) 

475 #trim packet-type: 

476 new_cursor = packet[1:] 

477 pixels = new_cursor[8] 

478 if encoding==b"png": 

479 if SAVE_CURSORS: 

480 serial = new_cursor[7] 

481 with open("raw-cursor-%#x.png" % serial, 'wb') as f: 

482 f.write(pixels) 

483 from xpra.codecs.pillow.decoder import open_only 

484 img = open_only(pixels, ("png",)) 

485 new_cursor[8] = img.tobytes("raw", "BGRA") 

486 cursorlog("used PIL to convert png cursor to raw") 

487 new_cursor[0] = b"raw" 

488 elif encoding!=b"raw": 

489 cursorlog.warn("Warning: invalid cursor encoding: %s", encoding) 

490 return 

491 self.set_windows_cursor(self._id_to_window.values(), new_cursor) 

492 

493 def reset_cursor(self): 

494 self.set_windows_cursor(self._id_to_window.values(), []) 

495 

496 

497 def cook_metadata(self, _new_window, metadata): 

498 #subclasses can apply tweaks here: 

499 return typedict(metadata) 

500 

501 

502 ###################################################################### 

503 # system tray 

504 def _process_new_tray(self, packet): 

505 assert self.client_supports_system_tray 

506 self._ui_event() 

507 wid, w, h = packet[1:4] 

508 w = max(1, self.sx(w)) 

509 h = max(1, self.sy(h)) 

510 metadata = typedict() 

511 if len(packet)>=5: 

512 metadata = typedict(packet[4]) 

513 traylog("tray %i metadata=%s", wid, metadata) 

514 assert wid not in self._id_to_window, "we already have a window %s: %s" % (wid, self._id_to_window.get(wid)) 

515 app_id = wid 

516 tray = self.setup_system_tray(self, app_id, wid, w, h, metadata) 

517 traylog("process_new_tray(%s) tray=%s", packet, tray) 

518 self._id_to_window[wid] = tray 

519 self._window_to_id[tray] = wid 

520 

521 

522 def make_system_tray(self, *args): 

523 """ tray used for application systray forwarding """ 

524 tc = self.get_system_tray_classes() 

525 traylog("make_system_tray%s system tray classes=%s", args, tc) 

526 return make_instance(tc, self, *args) 

527 

528 def get_system_tray_classes(self): 

529 #subclasses may add their toolkit specific variants, if any 

530 #by overriding this method 

531 #use the native ones first: 

532 return get_native_system_tray_classes() 

533 

534 def setup_system_tray(self, client, app_id, wid, w, h, metadata): 

535 tray_widget = None 

536 #this is a tray forwarded for a remote application 

537 def tray_click(button, pressed, event_time=0): 

538 tray = self._id_to_window.get(wid) 

539 traylog("tray_click(%s, %s, %s) tray=%s", button, pressed, event_time, tray) 

540 if tray: 

541 x, y = self.get_mouse_position() 

542 modifiers = self.get_current_modifiers() 

543 button_packet = ["button-action", wid, button, pressed, (x, y), modifiers] 

544 traylog("button_packet=%s", button_packet) 

545 self.send_positional(button_packet) 

546 tray.reconfigure() 

547 def tray_mouseover(x, y): 

548 tray = self._id_to_window.get(wid) 

549 traylog("tray_mouseover(%s, %s) tray=%s", x, y, tray) 

550 if tray: 

551 modifiers = self.get_current_modifiers() 

552 buttons = [] 

553 pointer_packet = ["pointer-position", wid, self.cp(x, y), modifiers, buttons] 

554 traylog("pointer_packet=%s", pointer_packet) 

555 self.send_mouse_position(pointer_packet) 

556 def do_tray_geometry(*args): 

557 #tell the "ClientTray" where it now lives 

558 #which should also update the location on the server if it has changed 

559 tray = self._id_to_window.get(wid) 

560 if tray_widget: 

561 geom = tray_widget.get_geometry() 

562 else: 

563 geom = None 

564 traylog("tray_geometry(%s) widget=%s, geometry=%s tray=%s", args, tray_widget, geom, tray) 

565 if tray and geom: 

566 tray.move_resize(*geom) 

567 def tray_geometry(*args): 

568 #the tray widget may still be None if we haven't returned from make_system_tray yet, 

569 #in which case we will check the geometry a little bit later: 

570 if tray_widget: 

571 do_tray_geometry(*args) 

572 else: 

573 self.idle_add(do_tray_geometry, *args) 

574 def tray_exit(*args): 

575 traylog("tray_exit(%s)", args) 

576 title = metadata.strget("title", "") 

577 tray_widget = self.make_system_tray(app_id, None, title, None, tray_geometry, tray_click, tray_mouseover, tray_exit) 

578 traylog("setup_system_tray%s tray_widget=%s", (client, app_id, wid, w, h, title), tray_widget) 

579 assert tray_widget, "could not instantiate a system tray for tray id %s" % wid 

580 tray_widget.show() 

581 from xpra.client.client_tray import ClientTray 

582 mmap = getattr(self, "mmap", None) 

583 return ClientTray(client, wid, w, h, metadata, tray_widget, self.mmap_enabled, mmap) 

584 

585 

586 def get_tray_window(self, app_name, hints): 

587 #try to identify the application tray that generated this notification, 

588 #so we can show it as coming from the correct systray icon 

589 #on platforms that support it (ie: win32) 

590 trays = tuple(w for w in self._id_to_window.values() if w.is_tray()) 

591 if trays: 

592 try: 

593 pid = int(hints.get("pid") or 0) 

594 except (TypeError, ValueError): 

595 pass 

596 else: 

597 if pid: 

598 for tray in trays: 

599 metadata = getattr(tray, "_metadata", typedict()) 

600 if metadata.intget("pid")==pid: 

601 traylog("tray window: matched pid=%i", pid) 

602 return tray.tray_widget 

603 if app_name and app_name.lower()!="xpra": 

604 #exact match: 

605 for tray in trays: 

606 #traylog("window %s: is_tray=%s, title=%s", window, 

607 # window.is_tray(), getattr(window, "title", None)) 

608 if tray.title==app_name: 

609 return tray.tray_widget 

610 for tray in trays: 

611 if tray.title.find(app_name)>=0: 

612 return tray.tray_widget 

613 return self.tray 

614 

615 

616 def set_tray_icon(self): 

617 #find all the window icons, 

618 #and if they are all using the same one, then use it as tray icon 

619 #otherwise use the default icon 

620 traylog("set_tray_icon() DYNAMIC_TRAY_ICON=%s, tray=%s", DYNAMIC_TRAY_ICON, self.tray) 

621 if not self.tray: 

622 return 

623 if not DYNAMIC_TRAY_ICON: 

624 #the icon ends up looking garbled on win32, 

625 #and we somehow also lose the settings that can keep us in the visible systray list 

626 #so don't bother 

627 return 

628 windows = tuple(w for w in self._window_to_id if not w.is_tray()) 

629 #get all the icons: 

630 icons = tuple(getattr(w, "_current_icon", None) for w in windows) 

631 missing = sum(1 for icon in icons if icon is None) 

632 traylog("set_tray_icon() %i windows, %i icons, %i missing", len(windows), len(icons), missing) 

633 if icons and not missing: 

634 icon = icons[0] 

635 for i in icons[1:]: 

636 if i!=icon: 

637 #found a different icon 

638 icon = None 

639 break 

640 if icon: 

641 has_alpha = icon.mode=="RGBA" 

642 width, height = icon.size 

643 traylog("set_tray_icon() using unique %s icon: %ix%i (has-alpha=%s)", 

644 icon.mode, width, height, has_alpha) 

645 rowstride = width * (3+int(has_alpha)) 

646 rgb_data = icon.tobytes("raw", icon.mode) 

647 self.tray.set_icon_from_data(rgb_data, has_alpha, width, height, rowstride) 

648 return 

649 #this sets the default icon (badly named function!) 

650 traylog("set_tray_icon() using default icon") 

651 self.tray.set_icon() 

652 

653 

654 ###################################################################### 

655 # combine the window icon with our own icon 

656 def _window_icon_image(self, wid, width, height, coding, data): 

657 #convert the data into a pillow image, 

658 #adding the icon overlay (if enabled) 

659 from PIL import Image 

660 coding = bytestostr(coding) 

661 iconlog("%s.update_icon(%s, %s, %s, %s bytes) ICON_SHRINKAGE=%s, ICON_OVERLAY=%s", 

662 self, width, height, coding, len(data), ICON_SHRINKAGE, ICON_OVERLAY) 

663 if coding=="default": 

664 img = self.overlay_image 

665 elif coding == "BGRA": 

666 rowstride = width*4 

667 img = Image.frombytes("RGBA", (width,height), memoryview_to_bytes(data), "raw", "BGRA", rowstride, 1) 

668 has_alpha = True 

669 elif coding in ("BGRA", "premult_argb32"): 

670 if coding == "premult_argb32": 

671 #we usually cannot do in-place and this is not performance critical 

672 from xpra.codecs.argb.argb import unpremultiply_argb #@UnresolvedImport 

673 data = unpremultiply_argb(data) 

674 rowstride = width*4 

675 img = Image.frombytes("RGBA", (width,height), memoryview_to_bytes(data), "raw", "BGRA", rowstride, 1) 

676 has_alpha = True 

677 else: 

678 from xpra.codecs.pillow.decoder import open_only 

679 img = open_only(data, ("png", )) 

680 if img.mode not in ("RGB", "RGBA"): 

681 img = img.convert("RGBA") 

682 has_alpha = img.mode=="RGBA" 

683 rowstride = width * (3+int(has_alpha)) 

684 icon = img 

685 save_time = int(time()) 

686 if SAVE_WINDOW_ICONS: 

687 filename = "client-window-%i-icon-%i.png" % (wid, save_time) 

688 icon.save(filename, "png") 

689 iconlog("client window icon saved to %s", filename) 

690 if self.overlay_image and self.overlay_image!=img: 

691 if 0<ICON_SHRINKAGE<100: 

692 #paste the application icon in the top-left corner, 

693 #shrunk by ICON_SHRINKAGE pct 

694 shrunk_width = max(1, width*ICON_SHRINKAGE//100) 

695 shrunk_height = max(1, height*ICON_SHRINKAGE//100) 

696 icon_resized = icon.resize((shrunk_width, shrunk_height), Image.ANTIALIAS) 

697 icon = Image.new("RGBA", (width, height)) 

698 icon.paste(icon_resized, (0, 0, shrunk_width, shrunk_height)) 

699 if SAVE_WINDOW_ICONS: 

700 filename = "client-window-%i-icon-shrunk-%i.png" % (wid, save_time) 

701 icon.save(filename, "png") 

702 iconlog("client shrunk window icon saved to %s", filename) 

703 assert 0<ICON_OVERLAY<=100 

704 overlay_width = max(1, width*ICON_OVERLAY//100) 

705 overlay_height = max(1, height*ICON_OVERLAY//100) 

706 xpra_resized = self.overlay_image.resize((overlay_width, overlay_height), Image.ANTIALIAS) 

707 xpra_corner = Image.new("RGBA", (width, height)) 

708 xpra_corner.paste(xpra_resized, (width-overlay_width, height-overlay_height, width, height)) 

709 if SAVE_WINDOW_ICONS: 

710 filename = "client-window-%i-icon-xpracorner-%i.png" % (wid, save_time) 

711 xpra_corner.save(filename, "png") 

712 iconlog("client xpracorner window icon saved to %s", filename) 

713 composite = Image.alpha_composite(icon, xpra_corner) 

714 icon = composite 

715 if SAVE_WINDOW_ICONS: 

716 filename = "client-window-%i-icon-composited-%i.png" % (wid, save_time) 

717 icon.save(filename, "png") 

718 iconlog("client composited window icon saved to %s", filename) 

719 return icon 

720 

721 

722 ###################################################################### 

723 # regular windows: 

724 def _process_new_common(self, packet, override_redirect): 

725 self._ui_event() 

726 wid, x, y, w, h = packet[1:6] 

727 assert 0<=w<32768 and 0<=h<32768 

728 metadata = self.cook_metadata(True, packet[6]) 

729 metalog("process_new_common: %s, metadata=%s, OR=%s", packet[1:7], metadata, override_redirect) 

730 assert wid not in self._id_to_window, "we already have a window %s: %s" % (wid, self._id_to_window.get(wid)) 

731 if w<1 or h<1: 

732 log.error("Error: window %i dimensions %ix%i are invalid", wid, w, h) 

733 w, h = 1, 1 

734 #scaled dimensions of window: 

735 wx = self.sx(x) 

736 wy = self.sy(y) 

737 ww = max(1, self.sx(w)) 

738 wh = max(1, self.sy(h)) 

739 #backing size, same as original (server-side): 

740 bw, bh = w, h 

741 client_properties = {} 

742 if len(packet)>=8: 

743 client_properties = packet[7] 

744 geomlog("process_new_common: wid=%i, OR=%s, geometry(%s)=%s / %s", 

745 wid, override_redirect, packet[2:6], (wx, wy, ww, wh), (bw, bh)) 

746 self.make_new_window(wid, wx, wy, ww, wh, bw, bh, metadata, override_redirect, client_properties) 

747 

748 def make_new_window(self, wid, wx, wy, ww, wh, bw, bh, metadata, override_redirect, client_properties): 

749 client_window_classes = self.get_client_window_classes(ww, wh, metadata, override_redirect) 

750 group_leader_window = self.get_group_leader(wid, metadata, override_redirect) 

751 #workaround for "popup" OR windows without a transient-for (like: google chrome popups): 

752 #prevents them from being pushed under other windows on OSX 

753 #find a "transient-for" value using the pid to find a suitable window 

754 #if possible, choosing the currently focused window (if there is one..) 

755 pid = metadata.intget("pid", 0) 

756 watcher_pid = self.assign_signal_watcher_pid(wid, pid) 

757 if override_redirect and pid>0 and metadata.intget("transient-for", 0)==0 and metadata.strget("role")=="popup": 

758 tfor = None 

759 for twid, twin in self._id_to_window.items(): 

760 if not twin._override_redirect and twin._metadata.intget("pid", -1)==pid: 

761 tfor = twin 

762 if twid==self._focused: 

763 break 

764 if tfor: 

765 log("forcing transient for=%s for new window %s", twid, wid) 

766 metadata["transient-for"] = twid 

767 border = None 

768 if self.border: 

769 border = self.border.clone() 

770 window = None 

771 log("make_new_window(..) client_window_classes=%s, group_leader_window=%s", 

772 client_window_classes, group_leader_window) 

773 for cwc in client_window_classes: 

774 try: 

775 window = cwc(self, group_leader_window, watcher_pid, wid, 

776 wx, wy, ww, wh, bw, bh, 

777 metadata, override_redirect, client_properties, 

778 border, self.max_window_size, self.default_cursor_data, self.pixel_depth, 

779 self.headerbar) 

780 break 

781 except Exception: 

782 log.warn("failed to instantiate %s", cwc, exc_info=True) 

783 if window is None: 

784 log.warn("no more options.. this window will not be shown, sorry") 

785 return None 

786 log("make_new_window(..) window(%i)=%s", wid, window) 

787 self._id_to_window[wid] = window 

788 self._window_to_id[window] = wid 

789 window.show_all() 

790 if override_redirect: 

791 if self.should_force_grab(metadata): 

792 grablog.warn("forcing grab for OR window %i, matches %s", wid, OR_FORCE_GRAB) 

793 self.window_grab(window) 

794 return window 

795 

796 def should_force_grab(self, metadata): 

797 if not OR_FORCE_GRAB: 

798 return False 

799 window_types = metadata.get("window-type", []) 

800 wm_class = metadata.strtupleget("class-instance", (None, None), 2, 2) 

801 c = None 

802 if wm_class: 

803 c = wm_class[0] 

804 if c: 

805 for window_type, force_wm_classes in OR_FORCE_GRAB.items(): 

806 #ie: DIALOG : ["sun-awt-X11"] 

807 if window_type=="*" or window_type in window_types: 

808 for wmc in force_wm_classes: 

809 if wmc=="*" or c and c.startswith(wmc): 

810 return True 

811 return False 

812 

813 ###################################################################### 

814 # listen for process signals using a watcher process: 

815 def assign_signal_watcher_pid(self, wid, pid): 

816 if not SIGNAL_WATCHER: 

817 return 0 

818 if not POSIX or OSX or not pid: 

819 return 0 

820 proc = self._pid_to_signalwatcher.get(pid) 

821 if proc is None or proc.poll(): 

822 from xpra.child_reaper import getChildReaper 

823 from subprocess import Popen, PIPE, STDOUT 

824 try: 

825 proc = Popen("xpra_signal_listener", 

826 stdin=PIPE, stdout=PIPE, stderr=STDOUT, 

827 start_new_session=True) 

828 except OSError as e: 

829 log("assign_signal_watcher_pid(%s, %s)", wid, pid, exc_info=True) 

830 log.error("Error: cannot execute signal listener") 

831 log.error(" %s", e) 

832 proc = None 

833 if proc and proc.poll() is None: 

834 #def add_process(self, process, name, command, ignore=False, forget=False, callback=None): 

835 proc.stdout_io_watch = None 

836 def watcher_terminated(*args): 

837 #watcher process terminated, remove io watch: 

838 #this may be redundant since we also return False from signal_watcher_event 

839 log("watcher_terminated%s", args) 

840 source = proc.stdout_io_watch 

841 if source: 

842 proc.stdout_io_watch = None 

843 self.source_remove(source) 

844 getChildReaper().add_process(proc, "signal listener for remote process %s" % pid, 

845 command="xpra_signal_listener", ignore=True, forget=True, 

846 callback=watcher_terminated) 

847 log("using watcher pid=%i for server pid=%i", proc.pid, pid) 

848 self._pid_to_signalwatcher[pid] = proc 

849 proc.stdout_io_watch = GLib.io_add_watch(proc.stdout, 

850 GLib.PRIORITY_DEFAULT, GLib.IO_IN, 

851 self.signal_watcher_event, proc, pid, wid) 

852 if proc: 

853 self._signalwatcher_to_wids.setdefault(proc, []).append(wid) 

854 return proc.pid 

855 return 0 

856 

857 def signal_watcher_event(self, fd, cb_condition, proc, pid, wid): 

858 log("signal_watcher_event%s", (fd, cb_condition, proc, pid, wid)) 

859 if cb_condition==GLib.IO_HUP: 

860 proc.stdout_io_watch = None 

861 return False 

862 if proc.stdout_io_watch is None: 

863 #no longer watched 

864 return False 

865 if cb_condition==GLib.IO_IN: 

866 try: 

867 signame = bytestostr(proc.stdout.readline()).strip("\n\r") 

868 log("signal_watcher_event: %s", signame) 

869 if not signame: 

870 pass 

871 elif signame not in self.server_window_signals: 

872 log("Warning: signal %s cannot be forwarded to this server", signame) 

873 else: 

874 self.send("window-signal", wid, signame) 

875 except Exception as e: 

876 log.error("signal_watcher_event%s", (fd, cb_condition, proc, pid, wid), exc_info=True) 

877 log.error("Error: processing signal watcher output for pid %i of window %i", pid, wid) 

878 log.error(" %s", e) 

879 if proc.poll(): 

880 #watcher ended, stop watching its stdout 

881 proc.stdout_io_watch = None 

882 return False 

883 return True 

884 

885 

886 def freeze(self): 

887 log("freeze()") 

888 for window in self._id_to_window.values(): 

889 window.freeze() 

890 

891 def unfreeze(self): 

892 log("unfreeze()") 

893 for window in self._id_to_window.values(): 

894 window.unfreeze() 

895 

896 

897 def deiconify_windows(self): 

898 log("deiconify_windows()") 

899 for window in self._id_to_window.values(): 

900 deiconify = getattr(window, "deiconify", None) 

901 if deiconify: 

902 deiconify() 

903 

904 

905 def resize_windows(self, new_size_fn): 

906 for window in self._id_to_window.values(): 

907 if window: 

908 ww, wh = window._size 

909 nw, nh = new_size_fn(ww, wh) 

910 #this will apply the new scaling value to the size constraints: 

911 window.reset_size_constraints() 

912 window.resize(nw, nh) 

913 self.send_refresh_all() 

914 

915 

916 def reinit_window_icons(self): 

917 #make sure the window icons are the ones we want: 

918 iconlog("reinit_window_icons()") 

919 for wid in tuple(self._id_to_window.keys()): 

920 window = self._id_to_window.get(wid) 

921 if window: 

922 reset_icon = getattr(window, "reset_icon", None) 

923 if reset_icon: 

924 reset_icon() 

925 

926 def reinit_windows(self, new_size_fn=None): 

927 #now replace all the windows with new ones: 

928 for wid in tuple(self._id_to_window.keys()): 

929 window = self._id_to_window.get(wid) 

930 if window: 

931 self.reinit_window(wid, window, new_size_fn) 

932 self.send_refresh_all() 

933 

934 def reinit_window(self, wid, window, new_size_fn=None): 

935 geomlog("reinit_window%s", (wid, window, new_size_fn)) 

936 def fake_send(*args): 

937 log("fake_send%s", args) 

938 if window.is_tray(): 

939 #trays are never GL enabled, so don't bother re-creating them 

940 #might cause problems anyway if we did 

941 #just send a configure event in case they are moved / scaled 

942 window.send_configure() 

943 return 

944 #ignore packets from old window: 

945 window.send = fake_send 

946 #copy attributes: 

947 x, y = window._pos 

948 ww, wh = window._size 

949 if new_size_fn: 

950 ww, wh = new_size_fn(ww, wh) 

951 try: 

952 bw, bh = window._backing.size 

953 except: 

954 bw, bh = ww, wh 

955 client_properties = window._client_properties 

956 resize_counter = window._resize_counter 

957 metadata = window._metadata 

958 override_redirect = window._override_redirect 

959 backing = window._backing 

960 current_icon = window._current_icon 

961 video_decoder, csc_decoder, decoder_lock = None, None, None 

962 try: 

963 if backing: 

964 video_decoder = backing._video_decoder 

965 csc_decoder = backing._csc_decoder 

966 decoder_lock = backing._decoder_lock 

967 if decoder_lock: 

968 decoder_lock.acquire() 

969 log("reinit_windows() will preserve video=%s and csc=%s for %s", video_decoder, csc_decoder, wid) 

970 backing._video_decoder = None 

971 backing._csc_decoder = None 

972 backing._decoder_lock = None 

973 backing.close() 

974 

975 #now we can unmap it: 

976 self.destroy_window(wid, window) 

977 #explicitly tell the server we have unmapped it: 

978 #(so it will reset the video encoders, etc) 

979 if not window.is_OR(): 

980 self.send("unmap-window", wid) 

981 self._id_to_window.pop(wid, None) 

982 self._window_to_id.pop(window, None) 

983 #create the new window, 

984 #which should honour the new state of the opengl_enabled flag if that's what we changed, 

985 #or the new dimensions, etc 

986 window = self.make_new_window(wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties) 

987 window._resize_counter = resize_counter 

988 #if we had a backing already, 

989 #restore the attributes we had saved from it 

990 if backing: 

991 backing = window._backing 

992 backing._video_decoder = video_decoder 

993 backing._csc_decoder = csc_decoder 

994 backing._decoder_lock = decoder_lock 

995 if current_icon: 

996 window.update_icon(current_icon) 

997 finally: 

998 if decoder_lock: 

999 decoder_lock.release() 

1000 

1001 

1002 def get_group_leader(self, _wid, _metadata, _override_redirect): 

1003 #subclasses that wish to implement the feature may override this method 

1004 return None 

1005 

1006 

1007 def get_client_window_classes(self, _w, _h, _metadata, _override_redirect): 

1008 return (self.ClientWindowClass,) 

1009 

1010 

1011 def _process_new_window(self, packet): 

1012 self._process_new_common(packet, False) 

1013 

1014 def _process_new_override_redirect(self, packet): 

1015 if self.modal_windows: 

1016 #find any modal windows and remove the flag 

1017 #so that the OR window can get the focus 

1018 #(it will be re-enabled when the OR window disappears) 

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

1020 if window.is_OR() or window.is_tray(): 

1021 continue 

1022 if window.get_modal(): 

1023 metalog("temporarily removing modal flag from %s", wid) 

1024 window.set_modal(False) 

1025 self._process_new_common(packet, True) 

1026 

1027 

1028 def _process_initiate_moveresize(self, packet): 

1029 wid = packet[1] 

1030 window = self._id_to_window.get(wid) 

1031 if window: 

1032 x_root, y_root, direction, button, source_indication = packet[2:7] 

1033 window.initiate_moveresize(self.sx(x_root), self.sy(y_root), direction, button, source_indication) 

1034 

1035 def _process_window_metadata(self, packet): 

1036 wid, metadata = packet[1:3] 

1037 metalog("metadata update for window %i: %s", wid, metadata) 

1038 window = self._id_to_window.get(wid) 

1039 if window: 

1040 metadata = self.cook_metadata(False, metadata) 

1041 window.update_metadata(metadata) 

1042 

1043 def _process_window_icon(self, packet): 

1044 wid, w, h, coding, data = packet[1:6] 

1045 img = self._window_icon_image(wid, w, h, coding, data) 

1046 window = self._id_to_window.get(wid) 

1047 iconlog("_process_window_icon(%s, %s, %s, %s, %s bytes) image=%s, window=%s", 

1048 wid, w, h, coding, len(data), img, window) 

1049 if window and img: 

1050 window.update_icon(img) 

1051 self.set_tray_icon() 

1052 

1053 def _process_window_move_resize(self, packet): 

1054 wid, x, y, w, h = packet[1:6] 

1055 ax = self.sx(x) 

1056 ay = self.sy(y) 

1057 aw = max(1, self.sx(w)) 

1058 ah = max(1, self.sy(h)) 

1059 resize_counter = -1 

1060 if len(packet)>4: 

1061 resize_counter = packet[4] 

1062 window = self._id_to_window.get(wid) 

1063 geomlog("_process_window_move_resize%s moving / resizing window %s (id=%s) to %s", 

1064 packet[1:], window, wid, (ax, ay, aw, ah)) 

1065 if window: 

1066 window.move_resize(ax, ay, aw, ah, resize_counter) 

1067 

1068 def _process_window_resized(self, packet): 

1069 wid, w, h = packet[1:4] 

1070 aw = max(1, self.sx(w)) 

1071 ah = max(1, self.sy(h)) 

1072 resize_counter = -1 

1073 if len(packet)>4: 

1074 resize_counter = packet[4] 

1075 window = self._id_to_window.get(wid) 

1076 geomlog("_process_window_resized%s resizing window %s (id=%s) to %s", packet[1:], window, wid, (aw,ah)) 

1077 if window: 

1078 window.resize(aw, ah, resize_counter) 

1079 

1080 def _process_raise_window(self, packet): 

1081 #implemented in gtk subclass 

1082 pass 

1083 

1084 def _process_restack_window(self, packet): 

1085 #implemented in gtk subclass 

1086 pass 

1087 

1088 

1089 def _process_configure_override_redirect(self, packet): 

1090 wid, x, y, w, h = packet[1:6] 

1091 window = self._id_to_window[wid] 

1092 ax = self.sx(x) 

1093 ay = self.sy(y) 

1094 aw = max(1, self.sx(w)) 

1095 ah = max(1, self.sy(h)) 

1096 geomlog("_process_configure_override_redirect%s move resize window %s (id=%s) to %s", 

1097 packet[1:], window, wid, (ax,ay,aw,ah)) 

1098 window.move_resize(ax, ay, aw, ah, -1) 

1099 

1100 

1101 def window_close_event(self, wid): 

1102 log("window_close_event(%s) close window action=%s", wid, self.window_close_action) 

1103 if self.window_close_action=="forward": 

1104 self.send("close-window", wid) 

1105 elif self.window_close_action=="ignore": 

1106 log("close event for window %i ignored", wid) 

1107 elif self.window_close_action=="disconnect": 

1108 log.info("window-close set to disconnect, exiting (window %i)", wid) 

1109 self.quit(0) 

1110 elif self.window_close_action=="shutdown": 

1111 self.send("shutdown-server", "shutdown on window close") 

1112 elif self.window_close_action=="auto": 

1113 #forward unless this looks like a desktop 

1114 #this allows us behave more like VNC: 

1115 window = self._id_to_window.get(wid) 

1116 log("window_close_event(%i) window=%s", wid, window) 

1117 if self.server_is_desktop: 

1118 log.info("window-close event on desktop or shadow window, disconnecting") 

1119 self.quit(0) 

1120 return True 

1121 if window: 

1122 metadata = getattr(window, "_metadata", {}) 

1123 log("window_close_event(%i) metadata=%s", wid, metadata) 

1124 class_instance = metadata.strtupleget("class-instance", (None, None), 2, 2) 

1125 title = metadata.get("title", "") 

1126 log("window_close_event(%i) title=%s, class-instance=%s", wid, title, class_instance) 

1127 matching_title_close = [x for x in TITLE_CLOSEEXIT if x and title.startswith(x)] 

1128 close = None 

1129 if matching_title_close: 

1130 close = "window-close event on %s window" % title 

1131 elif class_instance and class_instance[1] in WM_CLASS_CLOSEEXIT: 

1132 close = "window-close event on %s window" % class_instance[0] 

1133 if close: 

1134 #honour this close request if there are no other windows: 

1135 if len(self._id_to_window)==1: 

1136 log.info("%s, disconnecting", close) 

1137 self.quit(0) 

1138 return True 

1139 log("there are %i windows, so forwarding %s", len(self._id_to_window), close) 

1140 #default to forward: 

1141 self.send("close-window", wid) 

1142 else: 

1143 log.warn("unknown close-window action: %s", self.window_close_action) 

1144 return True 

1145 

1146 

1147 def _process_lost_window(self, packet): 

1148 wid = packet[1] 

1149 window = self._id_to_window.get(wid) 

1150 if window: 

1151 if window.is_OR() and self.modal_windows: 

1152 self.may_reenable_modal_windows(window) 

1153 del self._id_to_window[wid] 

1154 del self._window_to_id[window] 

1155 self.destroy_window(wid, window) 

1156 self.set_tray_icon() 

1157 

1158 def may_reenable_modal_windows(self, window): 

1159 orwids = tuple(wid for wid, w in self._id_to_window.items() if w.is_OR() and w!=window) 

1160 if orwids: 

1161 #there are other OR windows left, don't do anything 

1162 return 

1163 for wid, w in self._id_to_window.items(): 

1164 if w.is_OR() or w.is_tray(): 

1165 #trays and OR windows cannot be made modal 

1166 continue 

1167 if w._metadata.boolget("modal") and not w.get_modal(): 

1168 metalog("re-enabling modal flag on %s", wid) 

1169 window.set_modal(True) 

1170 

1171 

1172 def destroy_window(self, wid, window): 

1173 log("destroy_window(%s, %s)", wid, window) 

1174 window.destroy() 

1175 if self._window_with_grab==wid: 

1176 log("destroying window %s which has grab, ungrabbing!", wid) 

1177 self.window_ungrab() 

1178 self._window_with_grab = None 

1179 #deal with signal watchers: 

1180 log("looking for window %i in %s", wid, self._signalwatcher_to_wids) 

1181 for signalwatcher, wids in tuple(self._signalwatcher_to_wids.items()): 

1182 if wid in wids: 

1183 log("removing %i from %s for signalwatcher %s", wid, wids, signalwatcher) 

1184 wids.remove(wid) 

1185 if not wids: 

1186 log("last window, removing watcher %s", signalwatcher) 

1187 self._signalwatcher_to_wids.pop(signalwatcher, None) 

1188 self.kill_signalwatcher(signalwatcher) 

1189 #now remove any pids that use this watcher: 

1190 for pid, w in tuple(self._pid_to_signalwatcher.items()): 

1191 if w==signalwatcher: 

1192 del self._pid_to_signalwatcher[pid] 

1193 

1194 def kill_signalwatcher(self, proc): 

1195 log("kill_signalwatcher(%s)", proc) 

1196 if proc.poll() is None: 

1197 stdout_io_watch = proc.stdout_io_watch 

1198 if stdout_io_watch: 

1199 proc.stdout_io_watch = None 

1200 self.source_remove(stdout_io_watch) 

1201 try: 

1202 proc.stdin.write(b"exit\n") 

1203 proc.stdin.flush() 

1204 proc.stdin.close() 

1205 except IOError: 

1206 log.warn("Warning: failed to tell the signal watcher to exit", exc_info=True) 

1207 try: 

1208 os.kill(proc.pid, signal.SIGKILL) 

1209 except OSError as e: 

1210 if e.errno!=errno.ESRCH: 

1211 log.warn("Warning: failed to tell the signal watcher to exit", exc_info=True) 

1212 

1213 def destroy_all_windows(self): 

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

1215 try: 

1216 log("destroy_all_windows() destroying %s / %s", wid, window) 

1217 self.destroy_window(wid, window) 

1218 except Exception: 

1219 pass 

1220 self._id_to_window = {} 

1221 self._window_to_id = {} 

1222 #signal watchers should have been killed in destroy_window(), 

1223 #make sure we don't leave any behind: 

1224 for signalwatcher in tuple(self._signalwatcher_to_wids.keys()): 

1225 try: 

1226 self.kill_signalwatcher(signalwatcher) 

1227 except Exception: 

1228 log("destroy_all_windows() error killing signal watcher %s", signalwatcher, exc_info=True) 

1229 

1230 

1231 ###################################################################### 

1232 # bell 

1233 def _process_bell(self, packet): 

1234 if not self.bell_enabled: 

1235 return 

1236 (wid, device, percent, pitch, duration, bell_class, bell_id, bell_name) = packet[1:9] 

1237 window = self._id_to_window.get(wid) 

1238 self.window_bell(window, device, percent, pitch, duration, bell_class, bell_id, bell_name) 

1239 

1240 

1241 ###################################################################### 

1242 # focus: 

1243 def send_focus(self, wid): 

1244 focuslog("send_focus(%s)", wid) 

1245 self.send("focus", wid, self.get_current_modifiers()) 

1246 

1247 def update_focus(self, wid, gotit): 

1248 focuslog("update_focus(%s, %s) focused=%s, grabbed=%s", wid, gotit, self._focused, self._window_with_grab) 

1249 if gotit: 

1250 if self._focused is not wid: 

1251 self.send_focus(wid) 

1252 self._focused = wid 

1253 self.cancel_lost_focus_timer() 

1254 else: 

1255 if self._window_with_grab: 

1256 self.window_ungrab() 

1257 self.do_force_ungrab(self._window_with_grab) 

1258 self._window_with_grab = None 

1259 if wid and self._focused and self._focused!=wid: 

1260 #if this window lost focus, it must have had it! 

1261 #(catch up - makes things like OR windows work: 

1262 # their parent receives the focus-out event) 

1263 focuslog("window %s lost a focus it did not have!? (simulating focus before losing it)", wid) 

1264 self.send_focus(wid) 

1265 if self._focused and not self.lost_focus_timer: 

1266 #send the lost-focus via a timer and re-check it 

1267 #(this allows a new window to gain focus without having to do a reset_focus) 

1268 self.lost_focus_timer = self.timeout_add(20, self.send_lost_focus) 

1269 self._focused = None 

1270 

1271 def send_lost_focus(self): 

1272 focuslog("send_lost_focus() focused=%s", self._focused) 

1273 self.lost_focus_timer = None 

1274 #check that a new window has not gained focus since: 

1275 if self._focused is None: 

1276 self.send_focus(0) 

1277 

1278 def cancel_lost_focus_timer(self): 

1279 lft = self.lost_focus_timer 

1280 if lft: 

1281 self.lost_focus_timer = None 

1282 self.source_remove(lft) 

1283 

1284 

1285 ###################################################################### 

1286 # grabs: 

1287 def window_grab(self, _window): 

1288 grablog.warn("Warning: window grab not implemented in %s", self.client_type()) 

1289 

1290 def window_ungrab(self): 

1291 grablog.warn("Warning: window ungrab not implemented in %s", self.client_type()) 

1292 

1293 def do_force_ungrab(self, wid): 

1294 grablog("do_force_ungrab(%s)", wid) 

1295 #ungrab via dedicated server packet: 

1296 self.send_force_ungrab(wid) 

1297 

1298 def _process_pointer_grab(self, packet): 

1299 wid = packet[1] 

1300 window = self._id_to_window.get(wid) 

1301 grablog("grabbing %s: %s", wid, window) 

1302 if window: 

1303 self.window_grab(window) 

1304 self._window_with_grab = wid 

1305 

1306 def _process_pointer_ungrab(self, packet): 

1307 wid = packet[1] 

1308 window = self._id_to_window.get(wid) 

1309 grablog("ungrabbing %s: %s", wid, window) 

1310 self.window_ungrab() 

1311 self._window_with_grab = None 

1312 

1313 

1314 ###################################################################### 

1315 # window refresh: 

1316 def suspend(self): 

1317 log.info("system is suspending") 

1318 self._suspended_at = time() 

1319 #tell the server to slow down refresh for all the windows: 

1320 self.control_refresh(-1, True, False) 

1321 

1322 def resume(self): 

1323 elapsed = 0 

1324 if self._suspended_at>0: 

1325 elapsed = max(0, time()-self._suspended_at) 

1326 self._suspended_at = 0 

1327 self.send_refresh_all() 

1328 if elapsed<1: 

1329 #not really suspended 

1330 #happens on macos when switching workspace! 

1331 return 

1332 delta = datetime.timedelta(seconds=int(elapsed)) 

1333 log.info("system resumed, was suspended for %s", str(delta).lstrip("0:")) 

1334 #this will reset the refresh rate too: 

1335 if self.opengl_enabled: 

1336 #with opengl, the buffers sometimes contain garbage after resuming, 

1337 #this should create new backing buffers: 

1338 self.reinit_windows() 

1339 self.reinit_window_icons() 

1340 

1341 def control_refresh(self, wid, suspend_resume, refresh, quality=100, options=None, client_properties=None): 

1342 packet = ["buffer-refresh", wid, 0, quality] 

1343 options = options or {} 

1344 client_properties = client_properties or {} 

1345 options["refresh-now"] = bool(refresh) 

1346 if suspend_resume is True: 

1347 options["batch"] = { 

1348 "reset" : True, 

1349 "delay" : 1000, 

1350 "locked" : True, 

1351 "always" : True, 

1352 } 

1353 elif suspend_resume is False: 

1354 options["batch"] = {"reset" : True} 

1355 else: 

1356 pass #batch unchanged 

1357 log("sending buffer refresh: options=%s, client_properties=%s", options, client_properties) 

1358 packet.append(options) 

1359 packet.append(client_properties) 

1360 self.send(*packet) 

1361 

1362 def send_refresh(self, wid): 

1363 packet = ["buffer-refresh", wid, 0, 100, 

1364 #explicit refresh (should be assumed True anyway), 

1365 #also force a reset of batch configs: 

1366 { 

1367 "refresh-now" : True, 

1368 "batch" : {"reset" : True} 

1369 }, 

1370 {} #no client_properties 

1371 ] 

1372 self.send(*packet) 

1373 

1374 def send_refresh_all(self): 

1375 log("Automatic refresh for all windows ") 

1376 self.send_refresh(-1) 

1377 

1378 

1379 ###################################################################### 

1380 # painting windows: 

1381 def _process_draw(self, packet): 

1382 if PAINT_DELAY>0: 

1383 self.timeout_add(PAINT_DELAY, self._draw_queue.put, packet) 

1384 else: 

1385 self._draw_queue.put(packet) 

1386 

1387 def _process_eos(self, packet): 

1388 self._draw_queue.put(packet) 

1389 

1390 def send_damage_sequence(self, wid, packet_sequence, width, height, decode_time, message=""): 

1391 packet = "damage-sequence", packet_sequence, wid, width, height, decode_time, message 

1392 drawlog("sending ack: %s", packet) 

1393 self.send_now(*packet) 

1394 

1395 def _draw_thread_loop(self): 

1396 while self.exit_code is None: 

1397 packet = self._draw_queue.get() 

1398 if packet is None: 

1399 break 

1400 try: 

1401 self._do_draw(packet) 

1402 sleep(0) 

1403 except Exception as e: 

1404 log.error("Error '%s' processing %s packet", e, packet[0], exc_info=True) 

1405 log("draw thread ended") 

1406 

1407 def _do_draw(self, packet): 

1408 """ this runs from the draw thread above """ 

1409 wid = packet[1] 

1410 window = self._id_to_window.get(wid) 

1411 if bytestostr(packet[0])=="eos": 

1412 if window: 

1413 window.eos() 

1414 return 

1415 x, y, width, height, coding, data, packet_sequence, rowstride = packet[2:10] 

1416 coding = bytestostr(coding) 

1417 if not window: 

1418 #window is gone 

1419 def draw_cleanup(): 

1420 if coding=="mmap": 

1421 assert self.mmap_enabled 

1422 from xpra.net.mmap_pipe import int_from_buffer 

1423 #we need to ack the data to free the space! 

1424 data_start = int_from_buffer(self.mmap, 0) 

1425 offset, length = data[-1] 

1426 data_start.value = offset+length 

1427 #clear the mmap area via idle_add so any pending draw requests 

1428 #will get a chance to run first (preserving the order) 

1429 self.send_damage_sequence(wid, packet_sequence, width, height, -1) 

1430 self.idle_add(draw_cleanup) 

1431 return 

1432 #rename old encoding aliases early: 

1433 options = {} 

1434 if len(packet)>10: 

1435 options = packet[10] 

1436 options = typedict(options) 

1437 dtype = DRAW_TYPES.get(type(data), type(data)) 

1438 drawlog("process_draw: %7i %8s for window %3i, sequence %8i, %4ix%-4i at %4i,%-4i using %6s encoding with options=%s", 

1439 len(data), dtype, wid, packet_sequence, width, height, x, y, coding, options) 

1440 start = monotonic_time() 

1441 def record_decode_time(success, message=""): 

1442 if success>0: 

1443 end = monotonic_time() 

1444 decode_time = int(end*1000*1000-start*1000*1000) 

1445 self.pixel_counter.append((start, end, width*height)) 

1446 dms = "%sms" % (int(decode_time/100)/10.0) 

1447 paintlog("record_decode_time(%s, %s) wid=%s, %s: %sx%s, %s", 

1448 success, message, wid, coding, width, height, dms) 

1449 elif success==0: 

1450 decode_time = -1 

1451 paintlog("record_decode_time(%s, %s) decoding error on wid=%s, %s: %sx%s", 

1452 success, message, wid, coding, width, height) 

1453 else: 

1454 assert success<0 

1455 decode_time = 0 

1456 paintlog("record_decode_time(%s, %s) decoding or painting skipped on wid=%s, %s: %sx%s", 

1457 success, message, wid, coding, width, height) 

1458 self.send_damage_sequence(wid, packet_sequence, width, height, decode_time, repr_ellipsized(message, 512)) 

1459 self._draw_counter += 1 

1460 if PAINT_FAULT_RATE>0 and (self._draw_counter % PAINT_FAULT_RATE)==0: 

1461 drawlog.warn("injecting paint fault for %s draw packet %i, sequence number=%i", 

1462 coding, self._draw_counter, packet_sequence) 

1463 if PAINT_FAULT_TELL: 

1464 self.idle_add(record_decode_time, False, "fault injection for %s draw packet %i, sequence number=%i" % (coding, self._draw_counter, packet_sequence)) 

1465 return 

1466 #we could expose this to the csc step? (not sure how this could be used) 

1467 #if self.xscale!=1 or self.yscale!=1: 

1468 # options["client-scaling"] = self.xscale, self.yscale 

1469 try: 

1470 window.draw_region(x, y, width, height, coding, data, rowstride, 

1471 packet_sequence, options, [record_decode_time]) 

1472 except KeyboardInterrupt: 

1473 raise 

1474 except Exception as e: 

1475 drawlog.error("Error drawing on window %i", wid, exc_info=True) 

1476 self.idle_add(record_decode_time, False, str(e)) 

1477 raise 

1478 

1479 

1480 ###################################################################### 

1481 # screen scaling: 

1482 def fsx(self, v): 

1483 """ convert X coordinate from server to client """ 

1484 return v 

1485 def fsy(self, v): 

1486 """ convert Y coordinate from server to client """ 

1487 return v 

1488 def sx(self, v) -> int: 

1489 """ convert X coordinate from server to client """ 

1490 return iround(v) 

1491 def sy(self, v) -> int: 

1492 """ convert Y coordinate from server to client """ 

1493 return iround(v) 

1494 def srect(self, x, y, w, h): 

1495 """ convert rectangle coordinates from server to client """ 

1496 return self.sx(x), self.sy(y), self.sx(w), self.sy(h) 

1497 def sp(self, x, y): 

1498 """ convert X,Y coordinates from server to client """ 

1499 return self.sx(x), self.sy(y) 

1500 

1501 def cx(self, v) -> int: 

1502 """ convert X coordinate from client to server """ 

1503 return iround(v) 

1504 def cy(self, v) -> int: 

1505 """ convert Y coordinate from client to server """ 

1506 return iround(v) 

1507 def crect(self, x, y, w, h): 

1508 """ convert rectangle coordinates from client to server """ 

1509 return self.cx(x), self.cy(y), self.cx(w), self.cy(h) 

1510 def cp(self, x, y): 

1511 """ convert X,Y coordinates from client to server """ 

1512 return self.cx(x), self.cy(y) 

1513 

1514 

1515 def redraw_spinners(self): 

1516 #draws spinner on top of the window, or not (plain repaint) 

1517 #depending on whether the server is ok or not 

1518 ok = self.server_ok() 

1519 log("redraw_spinners() ok=%s", ok) 

1520 for w in self._id_to_window.values(): 

1521 if not w.is_tray(): 

1522 w.spinner(ok) 

1523 

1524 ###################################################################### 

1525 # packets: 

1526 def init_authenticated_packet_handlers(self): 

1527 for packet_type, handler in { 

1528 "new-window": self._process_new_window, 

1529 "new-override-redirect":self._process_new_override_redirect, 

1530 "new-tray": self._process_new_tray, 

1531 "raise-window": self._process_raise_window, 

1532 "restack-window": self._process_restack_window, 

1533 "initiate-moveresize": self._process_initiate_moveresize, 

1534 "window-move-resize": self._process_window_move_resize, 

1535 "window-resized": self._process_window_resized, 

1536 "window-metadata": self._process_window_metadata, 

1537 "configure-override-redirect": self._process_configure_override_redirect, 

1538 "lost-window": self._process_lost_window, 

1539 "window-icon": self._process_window_icon, 

1540 "draw": self._process_draw, 

1541 "eos": self._process_eos, 

1542 "cursor": self._process_cursor, 

1543 "bell": self._process_bell, 

1544 "pointer-position": self._process_pointer_position, 

1545 "pointer-grab": self._process_pointer_grab, 

1546 "pointer-ungrab": self._process_pointer_ungrab, 

1547 }.items(): 

1548 self.add_packet_handler(packet_type, handler)