class ClipboardProxy(gtk.Invisible): __gsignals__ = { # arguments: (selection, target) "get-clipboard-from-remote": (SIGNAL_RUN_LAST, gobject.TYPE_PYOBJECT, (gobject.TYPE_PYOBJECT,) * 2, ), # arguments: (selection,) "send-clipboard-token": no_arg_signal, } def __init__(self, selection): gtk.Invisible.__init__(self) self.add_events(PROPERTY_CHANGE_MASK) self._selection = selection self._clipboard = GetClipboard(selection) self._enabled = True self._have_token = False #this workaround is only needed on win32 AFAIK: self._strip_nullbyte = sys.platform.startswith("win") #clients that need a new token for every owner-change: (ie: win32 and osx) #(forces the client to request new contents - prevents stale clipboard data) self._greedy_client = False #semaphore to block the sending of the token when we change the owner ourselves: self._block_owner_change = False #counters for info: self._selection_request_events = 0 self._selection_get_events = 0 self._selection_clear_events = 0 self._sent_token_events = 0 self._got_token_events = 0 self._get_contents_events = 0 self._request_contents_events = 0 try: from xpra.x11.gtk_x11.prop import prop_get self.prop_get = prop_get except ImportError: self.prop_get = None self._clipboard.connect("owner-change", self.do_owner_changed) def set_direction(self, can_send, can_receive): self._can_send = can_send self._can_receive = can_receive def get_info(self): info = { "have_token" : self._have_token, "enabled" : self._enabled, "greedy_client" : self._greedy_client, "blocked_owner_change" : self._block_owner_change, "event" : { "selection_request" : self._selection_request_events, "selection_get" : self._selection_get_events, "selection_clear" : self._selection_clear_events, "got_token" : self._got_token_events, "sent_token" : self._sent_token_events, "get_contents" : self._get_contents_events, "request_contents" : self._request_contents_events, }, } return info def cleanup(self): if self._can_receive and not self._have_token and STORE_ON_EXIT: self._clipboard.store() self.destroy() def is_enabled(self): return self._enabled def set_enabled(self, enabled): log("%s.set_enabled(%s)", self, enabled) self._enabled = enabled def set_greedy_client(self, greedy): log("%s.set_greedy_client(%s)", self, greedy) self._greedy_client = greedy def __repr__(self): return "ClipboardProxy(%s)" % self._selection def do_owner_changed(self, *args): #an application on our side owns the clipboard selection #(they are ready to provide something via the clipboard) log("clipboard: %s owner_changed, enabled=%s, can-send=%s, can-receive=%s, have_token=%s, greedy_client=%s, block_owner_change=%s", self._selection, self._enabled, self._can_send, self._can_receive, self._have_token, self._greedy_client, self._block_owner_change) if not self._enabled or self._block_owner_change: return if (self._can_send and self._greedy_client) or (self._have_token and not self._can_receive): self._block_owner_change = True self._have_token = False self.emit("send-clipboard-token") self._sent_token_events += 1 glib.idle_add(self.remove_block) def do_selection_request_event(self, event): log("do_selection_request_event(%s)", event) self._selection_request_events += 1 if not self._enabled or not self._can_receive: gtk.Invisible.do_selection_request_event(self, event) return # Black magic: the superclass default handler for this signal # implements all the hards parts of selection handling, occasionally # calling back to the do_selection_get handler (below) to actually get # the data to be sent. However, it only does this for targets that # have been registered ahead of time; other targets fall through to a # default implementation that cannot be overridden. So, we swoop in # ahead of time and add whatever target was requested to the list of # targets we want to handle! # # Special cases (magic targets defined by ICCCM): # TIMESTAMP: the remote side has a different timeline than us, so # sending TIMESTAMPS across the wire doesn't make any sense. We # ignore TIMESTAMP requests, and let them fall through to GTK+'s # default handler. # TARGET: GTK+ has default handling for this, but we don't want to # use it. Fortunately, if we tell GTK+ that we can handle TARGET # requests, then it will pass them on to us rather than fall # through to the default handler. # MULTIPLE: Ugh. To handle this properly, we need to go out # ourselves and fetch the magic property off the requesting window # (with proper error trapping and all), and interpret its # contents. Probably doable (FIXME), just a pain. # # Another special case is that if an app requests the contents of a # clipboard that it currently owns, then GTK+ will short-circuit the # normal logic and request the contents directly (i.e. it calls # gtk_selection_invoke_handler) -- without giving us a chance to # assert that we can handle the requested sort of target. Fortunately, # Xpra never needs to request the clipboard when it owns it, so that's # okay. assert str(event.selection) == self._selection target = str(event.target) if target == "TIMESTAMP": pass elif target == "MULTIPLE": if not self.prop_get: log("MULTIPLE for property '%s' not handled due to missing xpra.x11.gtk_x11 bindings", event.property) gtk.Invisible.do_selection_request_event(self, event) return atoms = self.prop_get(event.window, event.property, ["multiple-conversion"]) log("MULTIPLE clipboard atoms: %r", atoms) if atoms: targets = atoms[::2] for t in targets: self.selection_add_target(self._selection, t, 0) else: log("target for %s: %r", self._selection, target) self.selection_add_target(self._selection, target, 0) log("do_selection_request_event(%s) target=%s, selection=%s", event, target, self._selection) gtk.Invisible.do_selection_request_event(self, event) # This function is called by GTK+ when we own the clipboard and a local # app is requesting its contents: def do_selection_get(self, selection_data, info, time): # Either call selection_data.set() or don't, and then return. # In practice, send a call across the wire, then block in a recursive # main loop. def nodata(): selection_data.set("STRING", 8, "") if not self._enabled or not self._can_receive: nodata() return log("do_selection_get(%s, %s, %s) selection=%s", selection_data, info, time, selection_data.selection) self._selection_get_events += 1 assert self._selection == str(selection_data.selection) target = str(selection_data.target) self._request_contents_events += 1 result = self.emit("get-clipboard-from-remote", self._selection, target) if result is None or result["type"] is None: log("remote selection fetch timed out or empty") nodata() return data = result["data"] dformat = result["format"] dtype = result["type"] log("do_selection_get(%s,%s,%s) calling selection_data.set(%s, %s, %s:%s)", selection_data, info, time, dtype, dformat, type(data), len(data or "")) selection_data.set(dtype, dformat, data) def do_selection_clear_event(self, event): # Someone else on our side has the selection log("do_selection_clear_event(%s) have_token=%s, block_owner_change=%s selection=%s", event, self._have_token, self._block_owner_change, self._selection) self._selection_clear_events += 1 if self._enabled and self._can_send and not self._block_owner_change: #if greedy_client is set, do_owner_changed will fire the token #so don't bother sending it now (same if we don't have it) send = ((self._greedy_client and not self._block_owner_change) or self._have_token) self._have_token = False # Emit a signal -> send a note to the other side saying "hey its # ours now" # Send off the anti-token. if send: boc = self._block_owner_change self._block_owner_change = True self.emit("send-clipboard-token") if boc is False: glib.idle_add(self.remove_block) gtk.Invisible.do_selection_clear_event(self, event) def got_token(self, targets, target_data, claim): # We got the anti-token. if not self._enabled: return self._got_token_events += 1 log("got token, selection=%s, targets=%s, target data=%s, claim=%s, can-receive=%s", self._selection, targets, target_data, claim, self._can_receive) if self._greedy_client or CLIPBOARD_GREEDY: self._block_owner_change = True #re-enable the flag via idle_add so events like do_owner_changed #get a chance to run first. glib.idle_add(self.remove_block) if CLIPBOARD_GREEDY and self._can_receive: if targets: for target in targets: self.selection_add_target(self._selection, target, 0) self.selection_owner_set(self._selection) if target_data: for text_target in TEXT_TARGETS: if text_target in target_data: text_data = target_data.get(text_target) log("clipboard %s set to '%s'", self._selection, text_data) self._clipboard.set_text(text_data) if not claim: log("token packet without claim, not setting the token flag") #the other end is just telling us to send the token again next time something changes, #not that they want to own the clipboard selection return self._have_token = True if self._can_receive: #if we don't claim the selection (can-receive=False), #we will have to send the token back on owner-change! self.claim() def remove_block(self, *args): log("remove_block: %s", self._selection) self._block_owner_change = False def claim(self): log("claim() selection=%s, enabled=%s", self._selection, self._enabled) if self._enabled and not self.selection_owner_set(self._selection): # I don't know how this can actually fail, given that we pass # CurrentTime, but just in case: log.warn("Failed to acquire local clipboard %s; " % (self._selection,) + "will not be able to pass local apps " + "contents of remote clipboard") # This function is called by the xpra core when the peer has requested the # contents of this clipboard: def get_contents(self, target, cb): log("get_contents(%s,%s) selection=%s, enabled=%s, can-send=%s", target, cb, self._selection, self._enabled, self._can_send) if not self._enabled or not self._can_send: cb(None, None, None) return self._get_contents_events += 1 if self._have_token: log.warn("Our peer requested the contents of the clipboard, but " + "*I* thought *they* had it... weird.") cb(None, None, None) return if target=="TARGETS": #handle TARGETS using "request_targets" def got_targets(c, targets, *args): log("got_targets(%s, %s, %s)", c, targets, args) cb("ATOM", 32, targets) self._clipboard.request_targets(got_targets) return def unpack(clipboard, selection_data, user_data): log("unpack %s: %s", clipboard, type(selection_data)) if selection_data is None: cb(None, None, None) return log("unpack: %s", selection_data) data = selection_data.data log("unpack(..) type=%s, format=%s, data=%s:%s", selection_data.type, selection_data.format, type(data), len(data or "")) if self._strip_nullbyte and selection_data.type in ("UTF8_STRING", "STRING") and selection_data.format==8: #we may have to strip the nullbyte: if data and data[-1]=='\0': log("stripping end of string null byte") data = data[:-1] cb(str(selection_data.type), selection_data.format, data) self._clipboard.request_contents(target, unpack)
class ClipboardProxy(gtk.Invisible): __gsignals__ = { # arguments: (selection, target) "get-clipboard-from-remote": (SIGNAL_RUN_LAST, gobject.TYPE_PYOBJECT, (gobject.TYPE_PYOBJECT,) * 2, ), # arguments: (selection,) "send-clipboard-token": no_arg_signal, } def __init__(self, selection): gtk.Invisible.__init__(self) self.add_events(PROPERTY_CHANGE_MASK) self._selection = selection self._clipboard = GetClipboard(selection) self._enabled = True self._have_token = False #enabled later during setup self._can_send = False self._can_receive = False #this workaround is only needed on win32 AFAIK: self._strip_nullbyte = WIN32 #clients that need a new token for every owner-change: (ie: win32 and osx) #(forces the client to request new contents - prevents stale clipboard data) self._greedy_client = False #semaphore to block the sending of the token when we change the owner ourselves: self._block_owner_change = False self._last_emit_token = 0 self._emit_token_timer = None #counters for info: self._selection_request_events = 0 self._selection_get_events = 0 self._selection_clear_events = 0 self._sent_token_events = 0 self._got_token_events = 0 self._get_contents_events = 0 self._request_contents_events = 0 self._last_targets = () try: from xpra.x11.gtk_x11.prop import prop_get self.prop_get = prop_get except ImportError: self.prop_get = None self._loop_uuid = "" self._clipboard.connect("owner-change", self.do_owner_changed) def init_uuid(self): self._loop_uuid = LOOP_PREFIX+get_hex_uuid() log("init_uuid() %s uuid=%s", self._selection, self._loop_uuid) set_string(self._clipboard, self._loop_uuid) def set_direction(self, can_send, can_receive): self._can_send = can_send self._can_receive = can_receive def get_info(self): info = { "have_token" : self._have_token, "enabled" : self._enabled, "greedy_client" : self._greedy_client, "blocked_owner_change" : self._block_owner_change, "last-targets" : self._last_targets, "loop-uuid" : self._loop_uuid, "event" : { "selection_request" : self._selection_request_events, "selection_get" : self._selection_get_events, "selection_clear" : self._selection_clear_events, "got_token" : self._got_token_events, "sent_token" : self._sent_token_events, "get_contents" : self._get_contents_events, "request_contents" : self._request_contents_events, }, } return info def cleanup(self): self._enabled = False self.cancel_emit_token() if self._can_receive and not self._have_token and STORE_ON_EXIT: self._clipboard.store() self.destroy() def is_enabled(self): return self._enabled def set_enabled(self, enabled): log("%s.set_enabled(%s)", self, enabled) self._enabled = enabled def set_greedy_client(self, greedy): log("%s.set_greedy_client(%s)", self, greedy) self._greedy_client = greedy def __repr__(self): return "ClipboardProxy(%s)" % self._selection def do_owner_changed(self, *_args): #an application on our side owns the clipboard selection #(they are ready to provide something via the clipboard) log("clipboard: %s owner_changed, enabled=%s, can-send=%s, can-receive=%s, have_token=%s, greedy_client=%s, block_owner_change=%s", bytestostr(self._selection), self._enabled, self._can_send, self._can_receive, self._have_token, self._greedy_client, self._block_owner_change) if not self._enabled or self._block_owner_change: return if self._have_token or (self._greedy_client and self._can_send): if self._have_token or DELAY_SEND_TOKEN<0: #token ownership will change or told not to wait glib.idle_add(self.emit_token) elif not self._emit_token_timer: #we had it already, this can wait: #TODO: don't throttle clients without "want-targets" attribute # (sending the token is only expensive for those) self.schedule_emit_token() def schedule_emit_token(self): now = monotonic_time() elapsed = int((now-self._last_emit_token)*1000) log("schedule_emit_token() elapsed=%i (max=%i)", elapsed, DELAY_SEND_TOKEN) if elapsed>=DELAY_SEND_TOKEN: #enough time has passed self.emit_token() else: self._emit_token_timer = glib.timeout_add(DELAY_SEND_TOKEN-elapsed, self.emit_token) def emit_token(self): self._emit_token_timer = None boc = self._block_owner_change self._block_owner_change = True self._have_token = False self._last_emit_token = monotonic_time() self.emit("send-clipboard-token") self._sent_token_events += 1 if boc is False: glib.idle_add(self.remove_block) def cancel_emit_token(self): ett = self._emit_token_timer if ett: self._emit_token_timer = None glib.source_remove(ett) def do_selection_request_event(self, event): log("do_selection_request_event(%s)", event) self._selection_request_events += 1 if not self._enabled or not self._can_receive: gtk.Invisible.do_selection_request_event(self, event) return # Black magic: the superclass default handler for this signal # implements all the hards parts of selection handling, occasionally # calling back to the do_selection_get handler (below) to actually get # the data to be sent. However, it only does this for targets that # have been registered ahead of time; other targets fall through to a # default implementation that cannot be overridden. So, we swoop in # ahead of time and add whatever target was requested to the list of # targets we want to handle! # # Special cases (magic targets defined by ICCCM): # TIMESTAMP: the remote side has a different timeline than us, so # sending TIMESTAMPS across the wire doesn't make any sense. We # ignore TIMESTAMP requests, and let them fall through to GTK+'s # default handler. # TARGET: GTK+ has default handling for this, but we don't want to # use it. Fortunately, if we tell GTK+ that we can handle TARGET # requests, then it will pass them on to us rather than fall # through to the default handler. # MULTIPLE: Ugh. To handle this properly, we need to go out # ourselves and fetch the magic property off the requesting window # (with proper error trapping and all), and interpret its # contents. Probably doable (FIXME), just a pain. # # Another special case is that if an app requests the contents of a # clipboard that it currently owns, then GTK+ will short-circuit the # normal logic and request the contents directly (i.e. it calls # gtk_selection_invoke_handler) -- without giving us a chance to # assert that we can handle the requested sort of target. Fortunately, # Xpra never needs to request the clipboard when it owns it, so that's # okay. assert str(event.selection) == self._selection, "selection does not match: expected %s but got %s" % (event.selection, self._selection) target = str(event.target) if target == "TIMESTAMP": pass elif target == "MULTIPLE": if not self.prop_get: log("MULTIPLE for property '%s' not handled due to missing xpra.x11.gtk_x11 bindings", event.property) gtk.Invisible.do_selection_request_event(self, event) return atoms = self.prop_get(event.window, event.property, ["multiple-conversion"]) log("MULTIPLE clipboard atoms: %r", atoms) if atoms: targets = atoms[::2] for t in targets: selection_add_target(self, self._selection, t, 0) else: log("target for %s: %r", self._selection, target) selection_add_target(self, self._selection, target, 0) log("do_selection_request_event(%s) target=%s, selection=%s", event, target, self._selection) gtk.Invisible.do_selection_request_event(self, event) # This function is called by GTK+ when we own the clipboard and a local # app is requesting its contents: def do_selection_get(self, selection_data, info, time): # Either call selection_data.set() or don't, and then return. # In practice, send a call across the wire, then block in a recursive # main loop. def nodata(): selectiondata_set(selection_data, "STRING", 8, "") if not self._enabled or not self._can_receive: nodata() return selection = selectiondata_get_selection(selection_data) target = selectiondata_get_target(selection_data) log("do_selection_get(%s, %s, %s) selection=%s", selection_data, info, time, selection) self._selection_get_events += 1 assert str(selection) == self._selection, "selection does not match: expected %s but got %s" % (selection, self._selection) self._request_contents_events += 1 result = self.emit("get-clipboard-from-remote", self._selection, target) if result is None or result["type"] is None: log("remote selection fetch timed out or empty") nodata() return data = result["data"] dformat = result["format"] dtype = result["type"] log("do_selection_get(%s,%s,%s) calling selection_data.set(%s, %s, %s:%s)", selection_data, info, time, dtype, dformat, type(data), len(data or "")) boc = self._block_owner_change self._block_owner_change = True if is_gtk3() and dtype in (b"UTF8_STRING", b"STRING") and dformat==8: #GTK3 workaround: can only use set_text and only on the clipboard? s = bytestostr(data) self._clipboard.set_text(s, len(s)) else: selectiondata_set(selection_data, dtype, dformat, data) if boc is False: glib.idle_add(self.remove_block) def do_selection_clear_event(self, event): # Someone else on our side has the selection log("do_selection_clear_event(%s) have_token=%s, block_owner_change=%s selection=%s", event, self._have_token, self._block_owner_change, self._selection) self._selection_clear_events += 1 if self._enabled and not self._block_owner_change: #if greedy_client is set, do_owner_changed will fire the token #so don't bother sending it now (same if we don't have it) send = ((self._greedy_client and not self._block_owner_change) or self._have_token) self._have_token = False # Emit a signal -> send a note to the other side saying "hey its # ours now" # Send off the anti-token. if send: self.emit_token() gtk.Invisible.do_selection_clear_event(self, event) def got_token(self, targets, target_data, claim=True, synchronous_client=False): # We got the anti-token. self.cancel_emit_token() if not self._enabled: return self._got_token_events += 1 log("got token, selection=%s, targets=%s, target data=%s, claim=%s, can-receive=%s", self._selection, targets, target_data, claim, self._can_receive) if self._greedy_client or CLIPBOARD_GREEDY: self._block_owner_change = True #re-enable the flag via idle_add so events like do_owner_changed #get a chance to run first. glib.idle_add(self.remove_block) if (CLIPBOARD_GREEDY or synchronous_client) and self._can_receive: if targets: for target in targets: selection_add_target(self, self._selection, target, 0) selection_owner_set(self, self._selection) if target_data: for text_target in TEXT_TARGETS: if text_target in target_data: text_data = target_data.get(text_target) log("clipboard %s set to '%s'", self._selection, repr_ellipsized(text_data)) set_string(self._clipboard, text_data) if not claim: log("token packet without claim, not setting the token flag") #the other end is just telling us to send the token again next time something changes, #not that they want to own the clipboard selection return self._have_token = True if self._can_receive: if not self._block_owner_change: #if we don't claim the selection (can-receive=False), #we will have to send the token back on owner-change! self._block_owner_change = True glib.idle_add(self.remove_block) self.claim() def remove_block(self, *_args): log("remove_block: %s", self._selection) self._block_owner_change = False def claim(self): log("claim() selection=%s, enabled=%s", self._selection, self._enabled) if self._enabled and not selection_owner_set(self, self._selection): # I don't know how this can actually fail, given that we pass # CurrentTime, but just in case: log.warn("Failed to acquire local clipboard %s; " % (self._selection,) + "will not be able to pass local apps " + "contents of remote clipboard") # This function is called by the xpra core when the peer has requested the # contents of this clipboard: def get_contents(self, target, cb): log("get_contents(%s,%s) selection=%s, enabled=%s, can-send=%s", target, cb, self._selection, self._enabled, self._can_send) if not self._enabled or not self._can_send: cb(None, None, None) return self._get_contents_events += 1 if self._have_token: log.warn("Our peer requested the contents of the clipboard, but " + "*I* thought *they* had it... weird.") cb(None, None, None) return if target=="TARGETS": #handle TARGETS using "request_targets" def got_targets(c, targets, *args): log("got_targets(%s, %s, %s)", c, targets, args) if is_gtk3(): targets = [x.name() for x in targets] cb("ATOM", 32, targets) self._last_targets = targets or () self._clipboard.request_targets(got_targets) return def unpack(clipboard, selection_data, _user_data=None): log("unpack %s: %s", clipboard, type(selection_data)) global sanitize_gtkselectiondata if selection_data and sanitize_gtkselectiondata(selection_data): self._clipboard.set_text("", len=-1) selection_data = None if selection_data is None: cb(None, None, None) return log("unpack: %s", selection_data) data = selectiondata_get_data(selection_data) dtype = selectiondata_get_data_type(selection_data) dformat = selectiondata_get_format(selection_data) log("unpack(..) type=%s, format=%s, data=%s:%s", dtype, dformat, type(data), len(data or "")) isstring = dtype in (b"UTF8_STRING", b"STRING") and dformat==8 if isstring: if self._strip_nullbyte: #we may have to strip the nullbyte: if data and data[-1]=='\0': log("stripping end of string null byte") data = data[:-1] if data and data==self._loop_uuid: log("not sending loop uuid value '%s', returning an empty string instead", data) data= "" cb(str(dtype), dformat, data) #some applications (ie: firefox, thunderbird) can request invalid targets, #when that happens, translate it to something the application can handle (if any) translated_target = TRANSLATED_TARGETS.get(target) if (translated_target is not None) and self._last_targets and (target not in self._last_targets) and \ (translated_target in self._last_targets) and (not must_discard(translated_target)): log("invalid target %s, replaced with %s", target, translated_target) target = translated_target clipboard_request_contents(self._clipboard, target, unpack)
class ClipboardProxy(gtk.Invisible): __gsignals__ = { # arguments: (selection, target) "get-clipboard-from-remote": ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_PYOBJECT, (gobject.TYPE_PYOBJECT, ) * 2, ), # arguments: (selection,) "send-clipboard-token": n_arg_signal(1), } def __init__(self, selection): gtk.Invisible.__init__(self) self.add_events(PROPERTY_CHANGE_MASK) self._selection = selection self._clipboard = GetClipboard(selection) self._enabled = True self._have_token = False #this workaround is only needed on win32 AFAIK: self._strip_nullbyte = sys.platform.startswith("win") #clients that need a new token for every owner-change: (ie: win32 and osx) #(forces the client to request new contents - prevents stale clipboard data) self._greedy_client = False #semaphore to block the sending of the token when we change the owner ourselves: self._block_owner_change = False #counters for info: self._selection_request_events = 0 self._selection_get_events = 0 self._selection_clear_events = 0 self._sent_token_events = 0 self._got_token_events = 0 self._get_contents_events = 0 self._request_contents_events = 0 try: from xpra.x11.gtk_x11.prop import prop_get self.prop_get = prop_get except ImportError: self.prop_get = None self._clipboard.connect("owner-change", self.do_owner_changed) def get_info(self): info = { "have_token": self._have_token, "enabled": self._enabled, "greedy_client": self._greedy_client, "blocked_owner_change": self._block_owner_change, "event.selection_request": self._selection_request_events, "event.selection_get": self._selection_get_events, "event.selection_clear": self._selection_clear_events, "event.got_token": self._got_token_events, "event.sent_token": self._sent_token_events, "event.get_contents": self._get_contents_events, "event.request_contents": self._request_contents_events, } return info def cleanup(self): if not self._have_token: self._clipboard.store() self.destroy() def is_enabled(self): return self._enabled def set_enabled(self, enabled): log("%s.set_enabled(%s)", self, enabled) self._enabled = enabled def set_greedy_client(self, greedy): log("%s.set_greedy_client(%s)", self, greedy) self._greedy_client = greedy def __str__(self): return "ClipboardProxy(%s)" % self._selection def do_owner_changed(self, *args): log("do_owner_changed(%s) greedy_client=%s, block_owner_change=%s", args, self._greedy_client, self._block_owner_change) if self._enabled and self._greedy_client and not self._block_owner_change: self._block_owner_change = True self._have_token = False self.emit("send-clipboard-token", self._selection) self._sent_token_events += 1 gobject.idle_add(self.remove_block) def do_selection_request_event(self, event): log("do_selection_request_event(%s)", event) self._selection_request_events += 1 if not self._enabled: gtk.Invisible.do_selection_request_event(self, event) return # Black magic: the superclass default handler for this signal # implements all the hards parts of selection handling, occasionally # calling back to the do_selection_get handler (below) to actually get # the data to be sent. However, it only does this for targets that # have been registered ahead of time; other targets fall through to a # default implementation that cannot be overridden. So, we swoop in # ahead of time and add whatever target was requested to the list of # targets we want to handle! # # Special cases (magic targets defined by ICCCM): # TIMESTAMP: the remote side has a different timeline than us, so # sending TIMESTAMPS across the wire doesn't make any sense. We # ignore TIMESTAMP requests, and let them fall through to GTK+'s # default handler. # TARGET: GTK+ has default handling for this, but we don't want to # use it. Fortunately, if we tell GTK+ that we can handle TARGET # requests, then it will pass them on to us rather than fall # through to the default handler. # MULTIPLE: Ugh. To handle this properly, we need to go out # ourselves and fetch the magic property off the requesting window # (with proper error trapping and all), and interpret its # contents. Probably doable (FIXME), just a pain. # # Another special case is that if an app requests the contents of a # clipboard that it currently owns, then GTK+ will short-circuit the # normal logic and request the contents directly (i.e. it calls # gtk_selection_invoke_handler) -- without giving us a chance to # assert that we can handle the requested sort of target. Fortunately, # Xpra never needs to request the clipboard when it owns it, so that's # okay. assert str(event.selection) == self._selection target = str(event.target) if target == "TIMESTAMP": pass elif target == "MULTIPLE": if not self.prop_get: log( "MULTIPLE for property '%s' not handled due to missing xpra.x11.gtk_x11 bindings", event.property) gtk.Invisible.do_selection_request_event(self, event) return atoms = self.prop_get(event.window, event.property, ["multiple-conversion"]) log("MULTIPLE clipboard atoms: %r", atoms) if atoms: targets = atoms[::2] for t in targets: self.selection_add_target(self._selection, t, 0) else: log("target for %s: %r", self._selection, target) self.selection_add_target(self._selection, target, 0) log("do_selection_request_event(%s) target=%s, selection=%s", event, target, self._selection) gtk.Invisible.do_selection_request_event(self, event) # This function is called by GTK+ when we own the clipboard and a local # app is requesting its contents: def do_selection_get(self, selection_data, info, time): # Either call selection_data.set() or don't, and then return. # In practice, send a call across the wire, then block in a recursive # main loop. if not self._enabled: return log("do_selection_get(%s, %s, %s) selection=%s", selection_data, info, time, selection_data.selection) self._selection_get_events += 1 assert self._selection == str(selection_data.selection) target = str(selection_data.target) self._request_contents_events += 1 result = self.emit("get-clipboard-from-remote", self._selection, target) if result is None or result["type"] is None: log("remote selection fetch timed out or empty") selection_data.set("STRING", 8, "") return data = result["data"] dformat = result["format"] dtype = result["type"] log( "do_selection_get(%s,%s,%s) calling selection_data.set(%s, %s, %s:%s)", selection_data, info, time, dtype, dformat, type(data), len(data or "")) selection_data.set(dtype, dformat, data) def do_selection_clear_event(self, event): # Someone else on our side has the selection log( "do_selection_clear_event(%s) have_token=%s, block_owner_change=%s selection=%s", event, self._have_token, self._block_owner_change, self._selection) self._selection_clear_events += 1 if self._enabled: #if greedy_client is set, do_owner_changed will fire the token #so don't bother sending it now (same if we don't have it) send = ((self._greedy_client and not self._block_owner_change) or self._have_token) self._have_token = False # Emit a signal -> send a note to the other side saying "hey its # ours now" # Send off the anti-token. if send: boc = self._block_owner_change self._block_owner_change = True self.emit("send-clipboard-token", self._selection) if boc is False: gobject.idle_add(self.remove_block) gtk.Invisible.do_selection_clear_event(self, event) def got_token(self, targets, target_data): # We got the anti-token. log("got token, selection=%s, targets=%s, target_data=%s", self._selection, targets, target_data) if not self._enabled: return self._got_token_events += 1 self._have_token = True if self._greedy_client: self._block_owner_change = True self.claim() if self._block_owner_change: #re-enable the flag via idle_add so events like do_owner_changed #get a chance to run first. gobject.idle_add(self.remove_block) def remove_block(self, *args): log("remove_block(%s)", args) self._block_owner_change = False def claim(self): if self._enabled and not self.selection_owner_set(self._selection): # I don't know how this can actually fail, given that we pass # CurrentTime, but just in case: log.warn("Failed to acquire local clipboard %s; " % (self._selection, ) + "will not be able to pass local apps " + "contents of remote clipboard") # This function is called by the xpra core when the peer has requested the # contents of this clipboard: def get_contents(self, target, cb): log("get_contents(%s,%s) selection=%s", target, cb, self._selection) if not self._enabled: cb(None, None, None) return self._get_contents_events += 1 if self._have_token: log.warn("Our peer requested the contents of the clipboard, but " + "*I* thought *they* had it... weird.") cb(None, None, None) return if target == "TARGETS": #handle TARGETS using "request_targets" def got_targets(c, targets, *args): log("got_targets(%s, %s, %s)", c, targets, args) cb("ATOM", 32, targets) self._clipboard.request_targets(got_targets) return def unpack(clipboard, selection_data, user_data): log("unpack %s: %s", clipboard, type(selection_data)) if selection_data is None: cb(None, None, None) return log("unpack: %s", selection_data) data = selection_data.data log("unpack(..) type=%s, format=%s, data=%s:%s", selection_data.type, selection_data.format, type(data), len(data or "")) if self._strip_nullbyte and selection_data.type in ( "UTF8_STRING", "STRING") and selection_data.format == 8: #we may have to strip the nullbyte: if data and data[-1] == '\0': log("stripping end of string null byte") data = data[:-1] cb(str(selection_data.type), selection_data.format, data) self._clipboard.request_contents(target, unpack)