Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/x11/gtk_x11/clipboard.py : 32%
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.
6import os
7import struct
8from gi.repository import GLib, GObject, Gdk
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
33X11Window = X11WindowBindings()
35log = Logger("x11", "clipboard")
38CurrentTime = constants["CurrentTime"]
39StructureNotifyMask = constants["StructureNotifyMask"]
41sizeof_long = struct.calcsize(b'@L')
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)
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)
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)
87class X11Clipboard(ClipboardTimeoutHelper, GObject.GObject):
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 }
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)
106 def __repr__(self):
107 return "X11Clipboard"
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)
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()
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()
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
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)
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)
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)
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)
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)
195 ############################################################################
196 # x11 specific munging support:
197 ############################################################################
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)
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)
209GObject.type_register(X11Clipboard)
212class ClipboardProxy(ClipboardProxyCore, GObject.GObject):
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 }
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()
238 def __repr__(self):
239 return "X11ClipboardProxy(%s)" % self._selection
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)
260 def init_uuid(self):
261 ClipboardProxyCore.init_uuid(self)
262 self.claim()
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()
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
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)
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
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)
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
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
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
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
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))
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)
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)
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()
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)
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)
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()
575 def do_owner_changed(self):
576 log("do_owner_changed()")
577 self.target_data = {}
578 self.targets = ()
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)
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)
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)
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)
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)
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)
705 def incr_data_timeout(self):
706 self.incr_data_timer = None
707 log.warn("Warning: incremental data timeout")
708 self.incr_data = None
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
716GObject.type_register(ClipboardProxy)