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) 2019-2020 Antoine Martin <antoine@xpra.org> 

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

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

5 

6import os 

7import struct 

8from gi.repository import GLib, GObject, Gdk 

9 

10from xpra.gtk_common.error import xsync, xswallow 

11from xpra.gtk_common.gobject_util import one_arg_signal, n_arg_signal 

12from xpra.gtk_common.gtk_util import get_default_root_window, GDKWindow 

13from xpra.x11.gtk_x11.gdk_bindings import ( 

14 add_event_receiver, #@UnresolvedImport 

15 remove_event_receiver, #@UnresolvedImport 

16 init_x11_filter, 

17 cleanup_x11_filter, 

18 ) 

19from xpra.gtk_common.error import XError 

20from xpra.clipboard.clipboard_core import ( 

21 ClipboardProxyCore, TEXT_TARGETS, 

22 must_discard, must_discard_extra, _filter_targets, 

23 ) 

24from xpra.clipboard.clipboard_timeout_helper import ClipboardTimeoutHelper, CONVERT_TIMEOUT 

25from xpra.x11.bindings.window_bindings import ( #@UnresolvedImport 

26 constants, PropertyError, #@UnresolvedImport 

27 X11WindowBindings, #@UnresolvedImport 

28 ) 

29from xpra.os_util import bytestostr 

30from xpra.util import csv, repr_ellipsized, ellipsizer, first_time 

31from xpra.log import Logger 

32 

33X11Window = X11WindowBindings() 

34 

35log = Logger("x11", "clipboard") 

36 

37 

38CurrentTime = constants["CurrentTime"] 

39StructureNotifyMask = constants["StructureNotifyMask"] 

40 

41sizeof_long = struct.calcsize(b'@L') 

42 

43BLACKLISTED_CLIPBOARD_CLIENTS = os.environ.get("XPRA_BLACKLISTED_CLIPBOARD_CLIENTS", "clipit").split(",") 

44log("BLACKLISTED_CLIPBOARD_CLIENTS=%s", BLACKLISTED_CLIPBOARD_CLIENTS) 

45def parse_translated_targets(v): 

46 trans = {} 

47 #we can't use ";" or "/" as separators 

48 #because those are used in mime-types 

49 #and we use "," and ":" ourselves.. 

50 for entry in v.split("#"): 

51 parts = entry.split(":", 1) 

52 if len(parts)!=2: 

53 log.warn("Warning: invalid clipboard translated target:") 

54 log.warn(" '%s'", entry) 

55 continue 

56 src_target = parts[0] 

57 dst_targets = parts[1].split(",") 

58 trans[src_target] = dst_targets 

59 return trans 

60DEFAULT_TRANSLATED_TARGETS = "#".join(( 

61 "text/plain;charset=utf-8:UTF8_STRING,text/plain,public.utf8-plain-text", 

62 "TEXT:text/plain,text/plain;charset=utf-8,UTF8_STRING,public.utf8-plain-text", 

63 "STRING:text/plain,text/plain;charset=utf-8,UTF8_STRING,public.utf8-plain-text", 

64 "UTF8_STRING:text/plain;charset=utf-8,text/plain,public.utf8-plain-text", 

65 "GTK_TEXT_BUFFER_CONTENTS:UTF8_STRING,text/plain,public.utf8-plain-text", 

66 )) 

67TRANSLATED_TARGETS = parse_translated_targets(os.environ.get("XPRA_CLIPBOARD_TRANSLATED_TARGETS", DEFAULT_TRANSLATED_TARGETS)) 

68log("TRANSLATED_TARGETS=%s", TRANSLATED_TARGETS) 

69 

70 

71def xatoms_to_strings(data): 

72 l = len(data) 

73 if l%sizeof_long!=0: 

74 raise Exception("invalid length for atom array: %i, value=%s" % (l, repr_ellipsized(str(data)))) 

75 natoms = l//sizeof_long 

76 atoms = struct.unpack(b"@"+b"L"*natoms, data) 

77 with xsync: 

78 return tuple(bytestostr(name) for name in (X11Window.XGetAtomName(atom) 

79 for atom in atoms if atom) if name is not None) 

80 

81def strings_to_xatoms(data): 

82 with xsync: 

83 atom_array = tuple(X11Window.get_xatom(atom) for atom in data if atom) 

84 return struct.pack(b"@"+b"L"*len(atom_array), *atom_array) 

85 

86 

87class X11Clipboard(ClipboardTimeoutHelper, GObject.GObject): 

88 

89 #handle signals from the X11 bindings, 

90 #and dispatch them to the proxy handling the selection specified: 

91 __gsignals__ = { 

92 "xpra-client-message-event" : one_arg_signal, 

93 "xpra-selection-request" : one_arg_signal, 

94 "xpra-selection-clear" : one_arg_signal, 

95 "xpra-property-notify-event" : one_arg_signal, 

96 "xpra-xfixes-selection-notify-event" : one_arg_signal, 

97 } 

98 

99 def __init__(self, send_packet_cb, progress_cb=None, **kwargs): 

100 GObject.GObject.__init__(self) 

101 self.init_window() 

102 init_x11_filter() 

103 self.x11_filter = True 

104 super().__init__(send_packet_cb, progress_cb, **kwargs) 

105 

106 def __repr__(self): 

107 return "X11Clipboard" 

108 

109 def init_window(self): 

110 root = get_default_root_window() 

111 self.window = GDKWindow(root, width=1, height=1, title="Xpra-Clipboard", wclass=Gdk.WindowWindowClass.INPUT_ONLY) 

112 self.window.set_events(Gdk.EventMask.PROPERTY_CHANGE_MASK | self.window.get_events()) 

113 xid = self.window.get_xid() 

114 with xsync: 

115 X11Window.selectSelectionInput(xid) 

116 add_event_receiver(self.window, self) 

117 

118 def cleanup_window(self): 

119 w = self.window 

120 if w: 

121 self.window = None 

122 remove_event_receiver(w, self) 

123 w.destroy() 

124 

125 def cleanup(self): 

126 if self.x11_filter: 

127 self.x11_filter = False 

128 cleanup_x11_filter() 

129 ClipboardTimeoutHelper.cleanup(self) 

130 self.cleanup_window() 

131 

132 def make_proxy(self, selection): 

133 xid = self.window.get_xid() 

134 proxy = ClipboardProxy(xid, selection) 

135 proxy.set_want_targets(self._want_targets) 

136 proxy.set_direction(self.can_send, self.can_receive) 

137 proxy.connect("send-clipboard-token", self._send_clipboard_token_handler) 

138 proxy.connect("send-clipboard-request", self._send_clipboard_request_handler) 

139 with xsync: 

140 X11Window.selectXFSelectionInput(xid, selection) 

141 return proxy 

142 

143 

144 ############################################################################ 

145 # X11 event handlers: 

146 # we dispatch them to the proxy handling the selection specified 

147 ############################################################################ 

148 def do_xpra_selection_request(self, event): 

149 log("do_xpra_selection_request(%s)", event) 

150 proxy = self._get_proxy(event.selection) 

151 if proxy: 

152 proxy.do_selection_request_event(event) 

153 

154 def do_xpra_selection_clear(self, event): 

155 log("do_xpra_selection_clear(%s)", event) 

156 proxy = self._get_proxy(event.selection) 

157 if proxy: 

158 proxy.do_selection_clear_event(event) 

159 

160 def do_xpra_xfixes_selection_notify_event(self, event): 

161 log("do_xpra_xfixes_selection_notify_event(%s)", event) 

162 proxy = self._get_proxy(event.selection) 

163 if proxy: 

164 proxy.do_selection_notify_event(event) 

165 

166 def do_xpra_client_message_event(self, event): 

167 message_type = event.message_type 

168 if message_type=="_GTK_LOAD_ICONTHEMES": 

169 log("ignored clipboard client message: %s", message_type) 

170 return 

171 log.info("clipboard X11 window %#x received a client message", self.window.get_xid()) 

172 log.info(" %s", event) 

173 

174 def do_xpra_property_notify_event(self, event): 

175 if event.atom in ( 

176 "_NET_WM_NAME", "WM_NAME", "_NET_WM_ICON_NAME", "WM_ICON_NAME", 

177 "WM_PROTOCOLS", "WM_NORMAL_HINTS", "WM_CLIENT_MACHINE", "WM_LOCALE_NAME", 

178 "_NET_WM_PID", "WM_CLIENT_LEADER", "_NET_WM_USER_TIME_WINDOW"): 

179 #these properties are populated by GTK when we create the window, 

180 #no need to log them: 

181 return 

182 log("do_xpra_property_notify_event(%s)", event) 

183 #ie: atom=PRIMARY-TARGETS 

184 #ie: atom=PRIMARY-VALUE 

185 parts = event.atom.split("-", 1) 

186 if len(parts)!=2: 

187 return 

188 selection = parts[0] #ie: PRIMARY 

189 #target = parts[1] #ie: VALUE 

190 proxy = self._get_proxy(selection) 

191 if proxy: 

192 proxy.do_property_notify(event) 

193 

194 

195 ############################################################################ 

196 # x11 specific munging support: 

197 ############################################################################ 

198 

199 def _munge_raw_selection_to_wire(self, target, dtype, dformat, data): 

200 if dformat==32 and dtype in ("ATOM", "ATOM_PAIR"): 

201 return "atoms", _filter_targets(xatoms_to_strings(data)) 

202 return super()._munge_raw_selection_to_wire(target, dtype, dformat, data) 

203 

204 def _munge_wire_selection_to_raw(self, encoding, dtype, dformat, data): 

205 if encoding=="atoms": 

206 return strings_to_xatoms(_filter_targets(data)) 

207 return super()._munge_wire_selection_to_raw(encoding, dtype, dformat, data) 

208 

209GObject.type_register(X11Clipboard) 

210 

211 

212class ClipboardProxy(ClipboardProxyCore, GObject.GObject): 

213 

214 __gsignals__ = { 

215 "xpra-client-message-event" : one_arg_signal, 

216 "xpra-selection-request" : one_arg_signal, 

217 "xpra-selection-clear" : one_arg_signal, 

218 "xpra-property-notify-event" : one_arg_signal, 

219 "xpra-xfixes-selection-notify-event" : one_arg_signal, 

220 # 

221 "send-clipboard-token" : one_arg_signal, 

222 "send-clipboard-request" : n_arg_signal(2), 

223 } 

224 

225 def __init__(self, xid, selection="CLIPBOARD"): 

226 ClipboardProxyCore.__init__(self, selection) 

227 GObject.GObject.__init__(self) 

228 self.xid = xid 

229 self.owned = False 

230 self._want_targets = False 

231 self.remote_requests = {} 

232 self.local_requests = {} 

233 self.local_request_counter = 0 

234 self.targets = () 

235 self.target_data = {} 

236 self.reset_incr_data() 

237 

238 def __repr__(self): 

239 return "X11ClipboardProxy(%s)" % self._selection 

240 

241 def cleanup(self): 

242 log("%s.cleanup()", self) 

243 #give up selection: 

244 #(disabled because this crashes GTK3 on exit) 

245 #if self.owned: 

246 # self.owned = False 

247 # with xswallow: 

248 # X11Window.XSetSelectionOwner(0, self._selection) 

249 #empty replies for all pending requests, 

250 #this will also cancel any pending timers: 

251 rr = self.remote_requests 

252 self.remote_requests = {} 

253 for target in rr: 

254 self.got_contents(target) 

255 lr = self.local_requests 

256 self.local_requests = {} 

257 for target in lr: 

258 self.got_local_contents(target) 

259 

260 def init_uuid(self): 

261 ClipboardProxyCore.init_uuid(self) 

262 self.claim() 

263 

264 def got_token(self, targets, target_data=None, claim=True, synchronous_client=False): 

265 # the remote end now owns the clipboard 

266 self.cancel_emit_token() 

267 if not self._enabled: 

268 return 

269 self._got_token_events += 1 

270 log("got token, selection=%s, targets=%s, target data=%s, claim=%s, can-receive=%s", 

271 self._selection, targets, target_data, claim, self._can_receive) 

272 if claim: 

273 self._have_token = True 

274 if self._can_receive: 

275 self.targets = tuple(bytestostr(x) for x in (targets or ())) 

276 self.target_data = target_data or {} 

277 if targets and claim: 

278 xatoms = strings_to_xatoms(targets) 

279 self.got_contents("TARGETS", "ATOM", 32, xatoms) 

280 if target_data and synchronous_client and claim: 

281 targets = target_data.keys() 

282 text_targets = tuple(x for x in targets if x in TEXT_TARGETS) 

283 if text_targets: 

284 target = text_targets[0] 

285 dtype, dformat, data = target_data.get(target) 

286 dtype = bytestostr(dtype) 

287 self.got_contents(target, dtype, dformat, data) 

288 if self._can_receive and claim: 

289 self.claim() 

290 

291 def claim(self, time=0): 

292 try: 

293 with xsync: 

294 owner = X11Window.XGetSelectionOwner(self._selection) 

295 if owner==self.xid: 

296 log("claim(%i) we already own the '%s' selection", time, self._selection) 

297 return 

298 setsel = X11Window.XSetSelectionOwner(self.xid, self._selection, time) 

299 owner = X11Window.XGetSelectionOwner(self._selection) 

300 self.owned = owner==self.xid 

301 log("claim_selection: set selection owner returned %s, owner=%#x, owned=%s", 

302 setsel, owner, self.owned) 

303 event_mask = StructureNotifyMask 

304 if not self.owned: 

305 log.warn("Warning: we failed to get ownership of the '%s' clipboard selection", self._selection) 

306 return 

307 #send announcement: 

308 log("claim_selection: sending message to root window") 

309 root = get_default_root_window() 

310 root_xid = root.get_xid() 

311 X11Window.sendClientMessage(root_xid, root_xid, False, event_mask, "MANAGER", 

312 time or CurrentTime, self._selection, self.xid) 

313 log("claim_selection: done, owned=%s", self.owned) 

314 except Exception: 

315 log("failed to claim selection '%s'", self._selection, exc_info=True) 

316 raise 

317 

318 def do_xpra_client_message_event(self, event): 

319 if event.message_type=="_GTK_LOAD_ICONTHEMES": 

320 #ignore this crap 

321 return 

322 log.info("clipboard window %#x received an X11 message", event.window.get_xid()) 

323 log.info(" %s", event) 

324 

325 

326 def get_wintitle(self, xid): 

327 data = X11Window.XGetWindowProperty(xid, "WM_NAME", "STRING") 

328 if data: 

329 return data.decode("latin1") 

330 data = X11Window.XGetWindowProperty(xid, "_NET_WM_NAME", "STRING") 

331 if data: 

332 return data.decode("utf8") 

333 xid = X11Window.getParent(xid) 

334 return None 

335 

336 def get_wininfo(self, xid): 

337 with xswallow: 

338 title = self.get_wintitle(xid) 

339 if title: 

340 return "'%s'" % title 

341 with xswallow: 

342 while xid: 

343 title = self.get_wintitle(xid) 

344 if title: 

345 return "child of '%s'" % title 

346 xid = X11Window.getParent(xid) 

347 return hex(xid) 

348 

349 ############################################################################ 

350 # forward local requests to the remote clipboard: 

351 ############################################################################ 

352 def do_selection_request_event(self, event): 

353 #an app is requesting clipboard data from us 

354 log("do_selection_request_event(%s)", event) 

355 requestor = event.requestor 

356 if not requestor: 

357 log.warn("Warning: clipboard selection request without a window, dropped") 

358 return 

359 wininfo = self.get_wininfo(requestor.get_xid()) 

360 prop = event.property 

361 target = str(event.target) 

362 log("clipboard request for %s from window %#x: %s, target=%s, prop=%s", 

363 self._selection, requestor.get_xid(), wininfo, target, prop) 

364 if not target: 

365 log.warn("Warning: ignoring clipboard request without a TARGET") 

366 log.warn(" coming from %s", wininfo) 

367 return 

368 if not prop: 

369 log.warn("Warning: ignoring clipboard request without a property") 

370 log.warn(" coming from %s", wininfo) 

371 return 

372 def nodata(): 

373 self.set_selection_response(requestor, target, prop, "STRING", 8, b"", time=event.time) 

374 if not self._enabled: 

375 nodata() 

376 return 

377 if wininfo and wininfo.strip("'") in BLACKLISTED_CLIPBOARD_CLIENTS: 

378 if first_time("clipboard-blacklisted:%s" % wininfo.strip("'")): 

379 log.warn("receiving clipboard requests from blacklisted client %s", wininfo) 

380 log.warn(" all requests will be silently ignored") 

381 log("responding with nodata for blacklisted client '%s'", wininfo) 

382 return 

383 if not self.owned: 

384 log.warn("Warning: clipboard selection request received,") 

385 log.warn(" coming from %s", wininfo) 

386 log.warn(" but we don't own the selection,") 

387 log.warn(" sending an empty reply") 

388 nodata() 

389 return 

390 if not self._can_receive: 

391 log.warn("Warning: clipboard selection request received,") 

392 log.warn(" coming from %s", wininfo) 

393 log.warn(" but receiving remote data is disabled,") 

394 log.warn(" sending an empty reply") 

395 nodata() 

396 return 

397 if must_discard(target): 

398 log.info("clipboard %s rejecting request for invalid target '%s'", self._selection, target) 

399 log.info(" coming from %s", wininfo) 

400 nodata() 

401 return 

402 

403 if target=="TARGETS": 

404 if self.targets: 

405 log("using existing TARGETS value as response: %s", self.targets) 

406 xatoms = strings_to_xatoms(self.targets) 

407 self.set_selection_response(requestor, target, prop, "ATOM", 32, xatoms, event.time) 

408 return 

409 if "TARGETS" not in self.remote_requests: 

410 self.emit("send-clipboard-request", self._selection, "TARGETS") 

411 #when appending, the time may not be honoured 

412 #and we may reply with data from an older request 

413 self.remote_requests.setdefault("TARGETS", []).append((requestor, target, prop, event.time)) 

414 return 

415 

416 req_target = target 

417 if self.targets and target not in self.targets: 

418 if first_time("client-%s-invalidtarget-%s" % (wininfo, target)): 

419 l = log.info 

420 else: 

421 l = log.debug 

422 l("client %s is requesting an unknown target: '%s'", wininfo, target) 

423 translated_targets = TRANSLATED_TARGETS.get(target, ()) 

424 can_translate = tuple(x for x in translated_targets if x in self.targets) 

425 if can_translate: 

426 req_target = can_translate[0] 

427 l(" using '%s' instead", req_target) 

428 else: 

429 l(" valid targets: %s", csv(self.targets)) 

430 if must_discard_extra(target): 

431 l(" dropping the request") 

432 nodata() 

433 return 

434 

435 target_data = self.target_data.get(req_target) 

436 if target_data and self._have_token: 

437 #we have it already 

438 dtype, dformat, data = target_data 

439 dtype = bytestostr(dtype) 

440 log("setting target data for '%s': %s, %s, %s (%s)", 

441 target, dtype, dformat, ellipsizer(data), type(data)) 

442 self.set_selection_response(requestor, target, prop, dtype, dformat, data, event.time) 

443 return 

444 

445 waiting = self.remote_requests.setdefault(req_target, []) 

446 if waiting: 

447 log("already waiting for '%s' remote request: %s", req_target, waiting) 

448 else: 

449 self.emit("send-clipboard-request", self._selection, req_target) 

450 waiting.append((requestor, target, prop, event.time)) 

451 

452 def set_selection_response(self, requestor, target, prop, dtype, dformat, data, time=0): 

453 log("set_selection_response(%s, %s, %s, %s, %s, %r, %i)", 

454 requestor, target, prop, dtype, dformat, ellipsizer(data), time) 

455 #answer the selection request: 

456 try: 

457 xid = requestor.get_xid() 

458 if not prop: 

459 log.warn("Warning: cannot set clipboard response") 

460 log.warn(" property is unset for requestor %s", self.get_wininfo(xid)) 

461 return 

462 with xsync: 

463 if data is not None: 

464 X11Window.XChangeProperty(xid, prop, dtype, dformat, data) 

465 else: 

466 #maybe even delete the property? 

467 #X11Window.XDeleteProperty(xid, prop) 

468 prop = None 

469 X11Window.sendSelectionNotify(xid, self._selection, target, prop, time) 

470 except XError as e: 

471 log("failed to set selection", exc_info=True) 

472 log.warn("Warning: failed to set selection for target '%s'", target) 

473 log.warn(" on requestor %s", self.get_wininfo(xid)) 

474 log.warn(" property '%s'", prop) 

475 log.warn(" %s", e) 

476 

477 def got_contents(self, target, dtype=None, dformat=None, data=None): 

478 #if this is the special target 'TARGETS', cache the result: 

479 if target=="TARGETS" and dtype=="ATOM" and dformat==32: 

480 self.targets = xatoms_to_strings(data) 

481 #the remote peer sent us a response, 

482 #find all the pending requests for this target 

483 #and give them the response they are waiting for: 

484 pending = self.remote_requests.pop(target, []) 

485 log("got_contents%s pending=%s", 

486 (target, dtype, dformat, ellipsizer(data)), csv(pending)) 

487 for requestor, actual_target, prop, time in pending: 

488 if log.is_debug_enabled(): 

489 log("setting response %s as '%s' on property '%s' of window %s as %s", 

490 ellipsizer(data), actual_target, prop, self.get_wininfo(requestor.get_xid()), dtype) 

491 if actual_target!=target and dtype==target: 

492 dtype = actual_target 

493 self.set_selection_response(requestor, actual_target, prop, dtype, dformat, data, time) 

494 

495 

496 ############################################################################ 

497 # local clipboard events, which may or may not be sent to the remote end 

498 ############################################################################ 

499 def do_selection_notify_event(self, event): 

500 owned = self.owned 

501 xid = 0 

502 if event.owner: 

503 xid = event.owner.get_xid() 

504 self.owned = xid and xid==self.xid 

505 log("do_selection_notify_event(%s) owned=%s, was %s (owner=%#x, xid=%#x), enabled=%s, can-send=%s", 

506 event, self.owned, owned, xid, self.xid, self._enabled, self._can_send) 

507 if not self._enabled: 

508 return 

509 if self.owned or not self._can_send or xid==0: 

510 return 

511 self.do_owner_changed() 

512 self.schedule_emit_token() 

513 

514 def schedule_emit_token(self): 

515 if not (self._want_targets or self._greedy_client): 

516 self._have_token = False 

517 self.emit("send-clipboard-token", ()) 

518 return 

519 #we need the targets, and the target data for greedy clients: 

520 def send_token_with_targets(): 

521 token_data = (self.targets, ) 

522 self._have_token = False 

523 self.emit("send-clipboard-token", token_data) 

524 def with_targets(targets): 

525 if not self._greedy_client: 

526 send_token_with_targets() 

527 return 

528 #find the preferred targets: 

529 targets = self.choose_targets(targets) 

530 if not targets: 

531 send_token_with_targets() 

532 return 

533 target = targets[0] 

534 def got_text_target(dtype, dformat, data): 

535 log("got_text_target(%s, %s, %s)", dtype, dformat, ellipsizer(data)) 

536 if not (dtype and dformat and data): 

537 send_token_with_targets() 

538 return 

539 token_data = (targets, (target, dtype, dformat, data)) 

540 self._have_token = False 

541 self.emit("send-clipboard-token", token_data) 

542 self.get_contents(target, got_text_target) 

543 if self.targets: 

544 with_targets(self.targets) 

545 return 

546 def got_targets(dtype, dformat, data): 

547 assert dtype=="ATOM" and dformat==32 

548 self.targets = xatoms_to_strings(data) 

549 log("got_targets: %s", self.targets) 

550 with_targets(self.targets) 

551 self.get_contents("TARGETS", got_targets) 

552 

553 def choose_targets(self, targets): 

554 if self.preferred_targets: 

555 #prefer PNG, but only if supported by the client: 

556 fmts = [] 

557 for img_fmt in ("image/png", "image/jpeg"): 

558 if img_fmt in targets and img_fmt in self.preferred_targets: 

559 fmts.append(img_fmt) 

560 if fmts: 

561 return fmts 

562 #if we can't choose a text target, at least choose a supported one: 

563 if not any(x for x in targets if x in TEXT_TARGETS and x in self.preferred_targets): 

564 return tuple(x for x in targets if x in self.preferred_targets) 

565 #otherwise choose a text target: 

566 return tuple(x for x in targets if x in TEXT_TARGETS) 

567 

568 def do_selection_clear_event(self, event): 

569 log("do_xpra_selection_clear(%s) was owned=%s", event, self.owned) 

570 if not self._enabled: 

571 return 

572 self.owned = False 

573 self.do_owner_changed() 

574 

575 def do_owner_changed(self): 

576 log("do_owner_changed()") 

577 self.target_data = {} 

578 self.targets = () 

579 

580 def get_contents(self, target, got_contents): 

581 log("get_contents(%s, %s) owned=%s, have-token=%s", 

582 target, got_contents, self.owned, self._have_token) 

583 if target=="TARGETS": 

584 if self.targets: 

585 xatoms = strings_to_xatoms(self.targets) 

586 got_contents("ATOM", 32, xatoms) 

587 return 

588 else: 

589 target_data = self.target_data.get(target) 

590 if target_data: 

591 dtype, dformat, value = target_data 

592 got_contents(dtype, dformat, value) 

593 return 

594 prop = "%s-%s" % (self._selection, target) 

595 with xsync: 

596 owner = X11Window.XGetSelectionOwner(self._selection) 

597 self.owned = owner==self.xid 

598 if self.owned: 

599 #we are the clipboard owner! 

600 log("we are the %s selection owner, using empty reply", self._selection) 

601 got_contents(None, None, None) 

602 return 

603 request_id = self.local_request_counter 

604 self.local_request_counter += 1 

605 timer = GLib.timeout_add(CONVERT_TIMEOUT, self.timeout_get_contents, target, request_id) 

606 self.local_requests.setdefault(target, {})[request_id] = (timer, got_contents) 

607 log("requesting local XConvertSelection from %s as '%s' into '%s'", self.get_wininfo(owner), target, prop) 

608 X11Window.ConvertSelection(self._selection, target, prop, self.xid, time=CurrentTime) 

609 

610 def timeout_get_contents(self, target, request_id): 

611 try: 

612 target_requests = self.local_requests.get(target) 

613 if target_requests is None: 

614 return 

615 timer, got_contents = target_requests.pop(request_id) 

616 if not target_requests: 

617 del self.local_requests[target] 

618 except KeyError: 

619 return 

620 GLib.source_remove(timer) 

621 log.warn("Warning: %s selection request for '%s' timed out", self._selection, target) 

622 log.warn(" request %i", request_id) 

623 if target=="TARGETS": 

624 got_contents("ATOM", 32, b"") 

625 else: 

626 got_contents(None, None, None) 

627 

628 def do_property_notify(self, event): 

629 log("do_property_notify(%s)", event) 

630 if not self._enabled: 

631 return 

632 #ie: atom="PRIMARY-TARGETS", atom="PRIMARY-STRING" 

633 parts = event.atom.split("-", 1) 

634 assert len(parts)==2 

635 #selection = parts[0] #ie: PRIMARY 

636 target = parts[1] #ie: VALUE 

637 dtype = "" 

638 dformat = 8 

639 try: 

640 with xsync: 

641 dtype, dformat = X11Window.GetWindowPropertyType(self.xid, event.atom, True) 

642 dtype = bytestostr(dtype) 

643 MAX_DATA_SIZE = 4*1024*1024 

644 data = X11Window.XGetWindowProperty(self.xid, event.atom, dtype, None, MAX_DATA_SIZE, True) 

645 #all the code below deals with INCRemental transfers: 

646 if dtype=="INCR" and not self.incr_data_size: 

647 #start of an incremental transfer, extract the size 

648 assert dformat==32 

649 self.incr_data_size = struct.unpack("@L", data)[0] 

650 self.incr_data_chunks = [] 

651 self.incr_data_type = None 

652 log("incremental clipboard data of size %s", self.incr_data_size) 

653 self.reschedule_incr_data_timer() 

654 return 

655 if self.incr_data_size>0: 

656 #incremental is now in progress: 

657 if not self.incr_data_type: 

658 self.incr_data_type = dtype 

659 elif self.incr_data_type!=dtype: 

660 log.error("Error: invalid change of data type") 

661 log.error(" from %s to %s", self.incr_data_type, dtype) 

662 self.reset_incr_data() 

663 self.cancel_incr_data_timer() 

664 return 

665 if data: 

666 log("got incremental data: %i bytes", len(data)) 

667 self.incr_data_chunks.append(data) 

668 self.reschedule_incr_data_timer() 

669 return 

670 self.cancel_incr_data_timer() 

671 data = b"".join(self.incr_data_chunks) 

672 log("got incremental data termination, total size=%i bytes", len(data)) 

673 self.reset_incr_data() 

674 self.got_local_contents(target, dtype, dformat, data) 

675 return 

676 except PropertyError: 

677 log("do_property_notify() property '%s' is gone?", event.atom, exc_info=True) 

678 return 

679 log("%s=%s (%s : %s)", event.atom, ellipsizer(data), dtype, dformat) 

680 if target=="TARGETS": 

681 self.targets = xatoms_to_strings(data or b"") 

682 self.got_local_contents(target, dtype, dformat, data) 

683 

684 def got_local_contents(self, target, dtype=None, dformat=None, data=None): 

685 data = self.filter_data(dtype, dformat, data) 

686 target_requests = self.local_requests.pop(target, {}) 

687 for timer, got_contents in target_requests.values(): 

688 if log.is_debug_enabled(): 

689 log("got_local_contents: calling %s%s", 

690 got_contents, (dtype, dformat, ellipsizer(data))) 

691 GLib.source_remove(timer) 

692 got_contents(dtype, dformat, data) 

693 

694 

695 def reschedule_incr_data_timer(self): 

696 self.cancel_incr_data_timer() 

697 self.incr_data_timer = GLib.timeout_add(1*1000, self.incr_data_timeout) 

698 

699 def cancel_incr_data_timer(self): 

700 idt = self.incr_data_timer 

701 if idt: 

702 self.incr_data_timer = None 

703 GLib.source_remove(idt) 

704 

705 def incr_data_timeout(self): 

706 self.incr_data_timer = None 

707 log.warn("Warning: incremental data timeout") 

708 self.incr_data = None 

709 

710 def reset_incr_data(self): 

711 self.incr_data_size = 0 

712 self.incr_data_type = None 

713 self.incr_data_chunks = None 

714 self.incr_data_timer = None 

715 

716GObject.type_register(ClipboardProxy)