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 __init__(self, selection, _log): self.clipboard = GetClipboard(selection) self.selection = selection self._log = _log self.owned_label = label() self.get_targets = gtk.combo_box_new_text() self.get_targets.set_sensitive(False) self.get_targets.connect("changed", self.get_target_changed) self.set_targets = gtk.combo_box_new_text() self.set_targets.append_text("STRING") self.set_targets.append_text("UTF8_STRING") self.set_targets.set_active(0) self.set_targets.connect("changed", self.set_target_changed) self.value_label = label() self.value_entry = gtk.Entry() self.value_entry.set_max_length(100) self.value_entry.set_width_chars(32) self.clear_label_btn = gtk.Button("X") self.clear_label_btn.connect("clicked", self.clear_label) self.clear_entry_btn = gtk.Button("X") self.clear_entry_btn.connect("clicked", self.clear_entry) self.get_get_targets_btn = gtk.Button("Get Targets") self.get_get_targets_btn.connect("clicked", self.do_get_targets) self.get_target_btn = gtk.Button("Get Target") self.get_target_btn.connect("clicked", self.do_get_target) self.get_target_btn.set_sensitive(False) self.set_target_btn = gtk.Button("Set Target") self.set_target_btn.connect("clicked", self.do_set_target) self.get_string_btn = gtk.Button("Get String") self.get_string_btn.connect("clicked", self.do_get_string) self.set_string_btn = gtk.Button("Set String") self.set_string_btn.connect("clicked", self.do_set_string) self.clipboard.connect("owner-change", self.owner_changed) self.log("ready")
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 = () if is_X11(): try: from xpra.x11.gtk_x11.prop import prop_get self.prop_get = prop_get except ImportError as e: log.warn("Warning: limited support for clipboard properties") log.warn(" %s", e) self.prop_get = None self._loop_uuid = "" self._clipboard.connect("owner-change", self.do_owner_changed)
class ClipboardInstance(object): def __init__(self, selection, _log): self.clipboard = GetClipboard(selection) self.selection = selection self._log = _log self.owned_label = label() self.get_targets = gtk.combo_box_new_text() self.get_targets.set_sensitive(False) self.get_targets.connect("changed", self.get_target_changed) self.set_targets = gtk.combo_box_new_text() self.set_targets.append_text("STRING") self.set_targets.append_text("UTF8_STRING") self.set_targets.set_active(0) self.set_targets.connect("changed", self.set_target_changed) self.value_label = label() self.value_entry = gtk.Entry() self.value_entry.set_max_length(100) self.value_entry.set_width_chars(32) self.clear_label_btn = gtk.Button("X") self.clear_label_btn.connect("clicked", self.clear_label) self.clear_entry_btn = gtk.Button("X") self.clear_entry_btn.connect("clicked", self.clear_entry) self.get_get_targets_btn = gtk.Button("Get Targets") self.get_get_targets_btn.connect("clicked", self.do_get_targets) self.get_target_btn = gtk.Button("Get Target") self.get_target_btn.connect("clicked", self.do_get_target) self.get_target_btn.set_sensitive(False) self.set_target_btn = gtk.Button("Set Target") self.set_target_btn.connect("clicked", self.do_set_target) self.get_string_btn = gtk.Button("Get String") self.get_string_btn.connect("clicked", self.do_get_string) self.set_string_btn = gtk.Button("Set String") self.set_string_btn.connect("clicked", self.do_set_string) self.clipboard.connect("owner-change", self.owner_changed) self.log("ready") def __repr__(self): return "ClipboardInstance(%s)" % self.selection def log(self, msg): self._log(self.selection, msg) def clear_entry(self, *_args): self.value_entry.set_text("") def clear_label(self, *_args): self.value_label.set_text("") def get_targets_callback(self, _c, targets, *_args): self.log("got targets: %s" % str(targets)) if hasattr(targets, "name"): self.log("target is atom: %s" % targets.name()) targets = [] filtered = [ x for x in (targets or []) if x not in ("MULTIPLE", "TARGETS") ] ct = self.get_targets.get_active_text() if not ct: #choose a good default target: for x in ("STRING", "UTF8_STRING"): if x in filtered: ct = x break self.get_targets.get_model().clear() self.get_targets.set_sensitive(True) i = 0 for t in filtered: self.get_targets.append_text(t) if t == ct: self.get_targets.set_active(i) i += 1 self.get_targets.show_all() def do_get_targets(self, *_args): self.clipboard.request_targets(self.get_targets_callback, None) def get_target_changed(self, _cb): target = self.get_targets.get_active_text() self.get_target_btn.set_sensitive(bool(target)) def set_target_changed(self, cb): pass def ellipsis(self, val): if len(val) > 24: return val[:24] + ".." return val def selection_value_callback(self, _cb, selection_data, *_args): #print("selection_value_callback(%s, %s, %s)" % (cb, selection_data, args)) try: if selection_data.data is None: s = "" else: s = "type=%s, format=%s, data=%s" % ( selection_data.type, selection_data.format, self.ellipsis(re.escape(selection_data.data))) except TypeError: try: s = self.ellipsis("\\".join( [str(x) for x in bytearray(selection_data.data)])) except Exception: s = "!ERROR! binary data?" self.log("Got selection data: '%s'" % s) self.value_label.set_text(s) def do_get_target(self, *_args): self.clear_label() target = self.get_targets.get_active_text() self.log("Requesting %s" % target) self.clipboard.request_contents(target, self.selection_value_callback, None) def selection_clear_cb(self, _clipboard, _data): #print("selection_clear_cb(%s, %s)", clipboard, data) self.log("Selection has been cleared") def selection_get_callback(self, _clipboard, selectiondata, _info, *_args): #log("selection_get_callback(%s, %s, %s, %s) targets=%s", # clipboard, selectiondata, info, args, selectiondata.get_targets()) value = self.value_entry.get_text() self.log("Answering selection request with value: '%s'" % self.ellipsis(value)) selectiondata.set("STRING", 8, value) def do_set_target(self, *_args): target = self.set_targets.get_active_text() self.log("Target set to %s" % target) self.clipboard.set_with_data([(target, 0, 0)], self.selection_get_callback, self.selection_clear_cb) def string_value_callback(self, _cb, value, *_args): if value is None: value = "" assert isinstance(value, str), "value is not a string!" self.log("Got string selection data: '%s'" % value) self.value_label.set_text(self.ellipsis(value)) def do_get_string(self, *_args): #self.log("do_get_string%s on %s.%s" % (args, self, self.clipboard)) self.clipboard.request_text(self.string_value_callback, None) def do_set_string(self, *_args): self.clipboard.set_text(self.ellipsis(self.value_entry.get_text())) def owner_changed(self, _cb, event): r = {} if not is_gtk3(): r = { gtk.gdk.OWNER_CHANGE_CLOSE: "close", gtk.gdk.OWNER_CHANGE_DESTROY: "destroy", gtk.gdk.OWNER_CHANGE_NEW_OWNER: "new owner", } owner = self.clipboard.get_owner() #print("xid=%s, owner=%s" % (self.value_entry.get_window().xid, event.owner)) weownit = (owner is not None) if weownit: owner_info = "(us)" else: owner_info = hex(event.owner) self.log("Owner changed, reason: %s, new owner=%s" % (r.get(event.reason, event.reason), owner_info))
def __init__(self, screen_number=0): self._selection = "_XSETTINGS_S%s" % screen_number self._clipboard = GetClipboard(self._selection)
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)
def __init__(self, selection): gobject.GObject.__init__(self) self.atom = selection self.clipboard = GetClipboard(selection) self._xwindow = None
class ManagerSelection(gobject.GObject): __gsignals__ = { "selection-lost": no_arg_signal, "xpra-destroy-event": one_arg_signal, } def __str__(self): return "ManagerSelection(%s)" % self.atom def __init__(self, selection): gobject.GObject.__init__(self) self.atom = selection self.clipboard = GetClipboard(selection) self._xwindow = None def _owner(self): return X11WindowBindings().XGetSelectionOwner(self.atom) def owned(self): "Returns True if someone owns the given selection." return self._owner() != XNone # If the selection is already owned, then raise AlreadyOwned rather # than stealing it. IF_UNOWNED = "if_unowned" # If the selection is already owned, then steal it, and then block until # the previous owner has signaled that they are done cleaning up. FORCE = "force" # If the selection is already owned, then steal it and return immediately. # Created for the use of tests. FORCE_AND_RETURN = "force_and_return" def acquire(self, when): old_owner = self._owner() if when is self.IF_UNOWNED and old_owner != XNone: raise AlreadyOwned if is_gtk3(): set_clipboard_data(self.clipboard, "VERSION") else: self.clipboard.set_with_data([("VERSION", 0, 0)], self._get, self._clear, None) # Having acquired the selection, we have to announce our existence # (ICCCM 2.8, still). The details here probably don't matter too # much; I've never heard of an app that cares about these messages, # and metacity actually gets the format wrong in several ways (no # MANAGER or owner_window atoms). But might as well get it as right # as possible. # To announce our existence, we need: # -- the timestamp we arrived at # -- the manager selection atom # -- the window that registered the selection # Of course, because Gtk is doing so much magic for us, we have to do # some weird tricks to get at these. # Ask ourselves when we acquired the selection: contents = wait_for_contents(self.clipboard, "TIMESTAMP") ts_data = selectiondata_get_data(contents) #data is a timestamp, X11 datatype is Time which is CARD32, #(which is 64 bits on 64-bit systems!) Lsize = calcsize("@L") if len(ts_data)==Lsize: ts_num = unpack("@L", ts_data[:Lsize])[0] else: ts_num = 0 #CurrentTime log.warn("invalid data for 'TIMESTAMP': %s", ([hex(ord(x)) for x in ts_data])) # Calculate the X atom for this selection: selection_xatom = get_xatom(self.atom) # Ask X what window we used: self._xwindow = X11WindowBindings().XGetSelectionOwner(self.atom) root = self.clipboard.get_display().get_default_screen().get_root_window() xid = get_xwindow(root) X11WindowBindings().sendClientMessage(xid, xid, False, StructureNotifyMask, "MANAGER", ts_num, selection_xatom, self._xwindow) if old_owner != XNone and when is self.FORCE: # Block in a recursive mainloop until the previous owner has # cleared out. try: with xsync: window = get_pywindow(self.clipboard, old_owner) window.set_events(window.get_events() | STRUCTURE_MASK) log("got window") except XError: log("Previous owner is already gone, not blocking") else: log("Waiting for previous owner to exit...") add_event_receiver(window, self) gtk_main() log("...they did.") window = get_pywindow(self.clipboard, self._xwindow) window.set_title("Xpra-ManagerSelection") if is_gtk3(): #we can't use set_with_data(..), #so we have to listen for owner-change: self.clipboard.connect("owner-change", self._owner_change) def _owner_change(self, clipboard, event): log("owner_change(%s, %s)", clipboard, event) if str(event.selection)!=self.atom: #log("_owner_change(..) not our selection: %s vs %s", event.selection, self.atom) return if event.owner: owner = event.owner.get_xid() if owner==self._xwindow: log("_owner_change(..) we still own %s", event.selection) return if self._xwindow: self._xwindow = None self.emit("selection-lost") def do_xpra_destroy_event(self, event): remove_event_receiver(event.window, self) gtk_main_quit() def _get(self, _clipboard, outdata, _which, _userdata): # We are compliant with ICCCM version 2.0 (see section 4.3) outdata.set("INTEGER", 32, pack("@ii", 2, 0)) def _clear(self, _clipboard, _userdata): self._xwindow = None self.emit("selection-lost") def window(self): if self._xwindow is None: return None return get_pywindow(self.clipboard, self._xwindow)
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 ClipboardInstance(object): def __init__(self, selection, _log): self.clipboard = GetClipboard(selection) self.selection = selection self._log = _log self.owned_label = label() self.get_targets = gtk.combo_box_new_text() self.get_targets.set_sensitive(False) self.get_targets.connect("changed", self.get_target_changed) self.set_targets = gtk.combo_box_new_text() self.set_targets.append_text("STRING") self.set_targets.append_text("UTF8_STRING") self.set_targets.set_active(0) self.set_targets.connect("changed", self.set_target_changed) self.value_label = label() self.value_entry = gtk.Entry() self.value_entry.set_max_length(100) self.value_entry.set_width_chars(32) self.clear_label_btn = gtk.Button("X") self.clear_label_btn.connect("clicked", self.clear_label) self.clear_entry_btn = gtk.Button("X") self.clear_entry_btn.connect("clicked", self.clear_entry) self.get_get_targets_btn = gtk.Button("Get Targets") self.get_get_targets_btn.connect("clicked", self.do_get_targets) self.get_target_btn = gtk.Button("Get Target") self.get_target_btn.connect("clicked", self.do_get_target) self.get_target_btn.set_sensitive(False) self.set_target_btn = gtk.Button("Set Target") self.set_target_btn.connect("clicked", self.do_set_target) self.get_string_btn = gtk.Button("Get String") self.get_string_btn.connect("clicked", self.do_get_string) self.set_string_btn = gtk.Button("Set String") self.set_string_btn.connect("clicked", self.do_set_string) self.clipboard.connect("owner-change", self.owner_changed) self.log("ready") def __repr__(self): return "ClipboardInstance(%s)" % self.selection def log(self, msg): self._log(self.selection, msg) def clear_entry(self, *args): self.value_entry.set_text("") def clear_label(self, *args): self.value_label.set_text("") def get_targets_callback(self, c, targets, *args): self.log("got targets: %s" % str(targets)) if hasattr(targets, "name"): self.log("target is atom: %s" % targets.name()) targets = [] filtered = [x for x in (targets or []) if x not in ("MULTIPLE", "TARGETS")] ct = self.get_targets.get_active_text() if not ct: #choose a good default target: for x in ("STRING", "UTF8_STRING"): if x in filtered: ct = x break self.get_targets.get_model().clear() self.get_targets.set_sensitive(True) i = 0 for t in filtered: self.get_targets.append_text(t) if t==ct: self.get_targets.set_active(i) i += 1 self.get_targets.show_all() def do_get_targets(self, *args): self.clipboard.request_targets(self.get_targets_callback, None) def get_target_changed(self, cb): target = self.get_targets.get_active_text() self.get_target_btn.set_sensitive(bool(target)) def set_target_changed(self, cb): pass def ellipsis(self, val): if len(val)>24: return val[:24]+".." return val def selection_value_callback(self, cb, selection_data, *args): #print("selection_value_callback(%s, %s, %s)" % (cb, selection_data, args)) try: if selection_data.data is None: s = "" else: s = "type=%s, format=%s, data=%s" % ( selection_data.type, selection_data.format, self.ellipsis(re.escape(selection_data.data))) except TypeError: try: s = self.ellipsis("\\".join([str(x) for x in bytearray(selection_data.data)])) except: s = "!ERROR! binary data?" self.log("Got selection data: '%s'" % s) self.value_label.set_text(s) def do_get_target(self, *args): self.clear_label() target = self.get_targets.get_active_text() self.log("Requesting %s" % target) self.clipboard.request_contents(target, self.selection_value_callback, None) def selection_clear_cb(self, clipboard, data): #print("selection_clear_cb(%s, %s)", clipboard, data) self.log("Selection has been cleared") def selection_get_callback(self, clipboard, selectiondata, info, *args): #print("selection_get_callback(%s, %s, %s, %s) targets=%s" % (clipboard, selectiondata, info, args, selectiondata.get_targets())) value = self.value_entry.get_text() self.log("Answering selection request with value: '%s'" % self.ellipsis(value)) selectiondata.set("STRING", 8, value) def do_set_target(self, *args): target = self.set_targets.get_active_text() self.log("Target set to %s" % target) self.clipboard.set_with_data([(target, 0, 0)], self.selection_get_callback, self.selection_clear_cb) def string_value_callback(self, cb, value, *args): if value is None: value = "" assert type(value)==str, "value is not a string!" self.log("Got string selection data: '%s'" % value) self.value_label.set_text(self.ellipsis(value)) def do_get_string(self, *args): #self.log("do_get_string%s on %s.%s" % (args, self, self.clipboard)) self.clipboard.request_text(self.string_value_callback, None) def do_set_string(self, *args): self.clipboard.set_text(self.ellipsis(self.value_entry.get_text())) def owner_changed(self, cb, event): r = {} if not is_gtk3(): r = {gtk.gdk.OWNER_CHANGE_CLOSE : "close", gtk.gdk.OWNER_CHANGE_DESTROY : "destroy", gtk.gdk.OWNER_CHANGE_NEW_OWNER : "new owner"} owner = self.clipboard.get_owner() #print("xid=%s, owner=%s" % (self.value_entry.get_window().xid, event.owner)) weownit = (owner is not None) if weownit: owner_info="(us)" else: owner_info = hex(event.owner) self.log("Owner changed, reason: %s, new owner=%s" % ( r.get(event.reason, event.reason), owner_info))
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)