예제 #1
0
    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)
예제 #2
0
 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")
예제 #3
0
    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)
예제 #4
0
 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")
예제 #5
0
    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)
예제 #6
0
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))
예제 #7
0
 def __init__(self, screen_number=0):
     self._selection = "_XSETTINGS_S%s" % screen_number
     self._clipboard = GetClipboard(self._selection)
예제 #8
0
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)
예제 #9
0
 def __init__(self, selection):
     gobject.GObject.__init__(self)
     self.atom = selection
     self.clipboard = GetClipboard(selection)
     self._xwindow = None
예제 #10
0
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)
예제 #11
0
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)
예제 #12
0
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))
예제 #13
0
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)