diff --git a/src/etc/xpra/xpra.conf.in b/src/etc/xpra/xpra.conf.in index ace4ada3cc..f0fc3da957 100644 --- a/src/etc/xpra/xpra.conf.in +++ b/src/etc/xpra/xpra.conf.in @@ -30,6 +30,11 @@ #clipboard = auto clipboard = yes +# Direction of clipboard transfers: +#clipboard-direction = to-server +#clipboard-direction = to-client +clipboard-direction = both + # Forward notifications: notifications = yes diff --git a/src/man/xpra.1 b/src/man/xpra.1 index 51911ace2f..ff1d0772ca 100644 --- a/src/man/xpra.1 +++ b/src/man/xpra.1 @@ -34,6 +34,8 @@ xpra \- viewer for remote, persistent X applications [\fB\-\-pulseaudio\-command\fP=\fISERVER START COMMAND\fP] [\fB\-\-readonly\fP=\fIyes\fP|\fIno\fP] [\fB\-\-clipboard\fP=\fIyes\fP|\fIno|clipboard\-type\fP] +[\fB\-\-clipboard\-direction\fP=\fIto\-server\fP|\fIto\-client|both|disabled\fP] +[\fB\-\-clipboard\-filter\-file\fP=\fIFILENAME\fP] [\fB\-\-cursors\fP=\fIyes\fP|\fIno\fP] [\fB\-\-notifications\fP=\fIyes\fP|\fIno\fP] [\fB\-\-xsettings\fP=\fIyes\fP|\fIno\fP] @@ -60,7 +62,6 @@ xpra \- viewer for remote, persistent X applications [\fB\-\-vsock\-auth\fP=\fIMODULE\fP] [\fB\-\-idle\-timeout\fP=\fIIDLETIMEOUT\fP] [\fB\-\-server\-idle\-timeout\fP=\fIIDLETIMEOUT\fP] -[\fB\-\-clipboard\-filter\-file\fP=\fIFILENAME\fP] [\fB\-\-dpi\fP=\fIVALUE\fP] [\fB\-\-input\-method\fP=\fIMETHOD\fP] [\fB\-\-socket\-dir\fP=\fIDIR\fP] @@ -75,6 +76,7 @@ xpra \- viewer for remote, persistent X applications [\fB\-\-mmap\fP=\fIyes\fP|\fIno\fP|ABSOLUTEFILENAME] [\fB\-\-windows\fP=\fIyes\fP|\fIno\fP] [\fB\-\-clipboard\fP=\fIyes\fP|\fIno\fP] +[\fB\-\-clipboard\-direction\fP=\fIto\-server\fP|\fIto\-client|both|disabled\fP] [\fB\-\-cursors\fP=\fIyes\fP|\fIno\fP] [\fB\-\-notifications\fP=\fIyes\fP|\fIno\fP] [\fB\-\-xsettings\fP=\fIyes\fP|\fIno\fP] @@ -472,11 +474,14 @@ Clipboard which can translate from one type of selection to another .IP \fBGDK\fP The most complete clipboard implementation, includes full X11 support .IP \fBdefault\fP -Simple clipboard with limited X11 support +Fallback clipboard, with limited X11 support .IP \fBOSX\fP OSX specific clipboard .RE +.TP +\fB\-\-clipboard\-direction\fP=\fIto\-server\fP|\fIto\-client|both|disabled\fP +Choose the direction of the clipboard synchronization. .TP \fB\-\-pulseaudio\fP=\fIyes\fP|\fIno\fP Enable or disable the starting of a pulseaudio server with the session. diff --git a/src/xpra/client/gtk2/client.py b/src/xpra/client/gtk2/client.py index 6ec3bede85..d99c0fcf54 100644 --- a/src/xpra/client/gtk2/client.py +++ b/src/xpra/client/gtk2/client.py @@ -206,8 +206,9 @@ def get_clipboard_helper_classes(self): try: parts = co.split(".") mod = ".".join(parts[:-1]) - __import__(mod, {}, {}, [parts[-1]]) - loadable.append(co) + module = __import__(mod, {}, {}, [parts[-1]]) + helperclass = getattr(module, parts[-1]) + loadable.append(helperclass) except ImportError as e: clipboardlog("cannot load %s: %s", co, e) continue @@ -221,39 +222,26 @@ def make_clipboard_helper(self): """ clipboard_options = self.get_clipboard_helper_classes() clipboardlog("make_clipboard_helper() options=%s", clipboard_options) - for classname in clipboard_options: - module_name, _class = classname.rsplit(".", 1) - c = self.try_load_clipboard_helper(module_name, _class) - if c: - return c - return None - - def try_load_clipboard_helper(self, module, classname): - try: - m = __import__(module, {}, {}, classname) - if m: - if not hasattr(m, classname): - log.warn("cannot load %s from %s, odd", classname, m) - return None - c = getattr(m, classname) - if c: - return self.setup_clipboard_helper(c) - except ImportError as e: - clipboardlog.error("Error: cannot load %s.%s:", module, classname) - clipboardlog.error(" %s", e) - return None - except: - clipboardlog.error("cannot load %s.%s", module, classname, exc_info=True) - return None - clipboardlog.error("cannot load %s.%s", module, classname) + for helperclass in clipboard_options: + try: + return self.setup_clipboard_helper(helperclass) + except ImportError as e: + clipboardlog.error("Error: cannot instantiate %s:", helperclass) + clipboardlog.error(" %s", e) + except: + clipboardlog.error("cannot instantiate %s", helperclass, exc_info=True) return None def setup_clipboard_helper(self, helperClass): clipboardlog("setup_clipboard_helper(%s)", helperClass) #first add the platform specific one, (may be None): from xpra.platform.features import CLIPBOARDS - kwargs= {"clipboards.local" : CLIPBOARDS, #all the local clipboards supported - "clipboards.remote" : self.server_clipboards} #all the remote clipboards supported + kwargs= { + "clipboards.local" : CLIPBOARDS, #all the local clipboards supported + "clipboards.remote" : self.server_clipboards, #all the remote clipboards supported + "can-send" : self.client_clipboard_direction in ("to-server", "both"), + "can-receive" : self.client_clipboard_direction in ("to-client", "both"), + } #only allow translation overrides if we have a way of telling the server about them: if self.server_supports_clipboard_enable_selections: kwargs.update({ diff --git a/src/xpra/client/gtk_base/gtk_tray_menu_base.py b/src/xpra/client/gtk_base/gtk_tray_menu_base.py index d075d4f317..6c59aa457a 100644 --- a/src/xpra/client/gtk_base/gtk_tray_menu_base.py +++ b/src/xpra/client/gtk_base/gtk_tray_menu_base.py @@ -60,11 +60,23 @@ SPEED_OPTIONS[1] = "Lowest Bandwidth" SPEED_OPTIONS[100] = "Lowest Latency" -CLIPBOARD_LABELS = ["Disabled", "Clipboard", "Primary", "Secondary"] -CLIPBOARD_LABEL_TO_NAME = {"Disabled" : None, +CLIPBOARD_LABELS = ["Clipboard", "Primary", "Secondary"] +CLIPBOARD_LABEL_TO_NAME = { "Clipboard" : "CLIPBOARD", "Primary" : "PRIMARY", - "Secondary" : "SECONDARY"} + "Secondary" : "SECONDARY" + } +CLIPBOARD_NAME_TO_LABEL = dict((v,k) for k,v in CLIPBOARD_LABEL_TO_NAME.items()) + +CLIPBOARD_DIRECTION_LABELS = ["Client to server only", "Server to client only", "Both directions", "Disabled"] +CLIPBOARD_DIRECTION_LABEL_TO_NAME = { + "Client to server only" : "to-server", + "Server to client only" : "to-client", + "Both directions" : "both", + "Disabled" : "disabled", + } +CLIPBOARD_DIRECTION_NAME_TO_LABEL = dict((v,k) for k,v in CLIPBOARD_DIRECTION_LABEL_TO_NAME.items()) + def ll(m): try: @@ -456,132 +468,117 @@ def set_notifications_menuitem(*args): self.client.after_handshake(set_notifications_menuitem) return self.notifications_menuitem - def make_clipboard_togglemenuitem(self): - clipboardlog("make_clipboard_togglemenuitem()") - def menu_clipboard_toggled(*args): - new_state = self.clipboard_menuitem.get_active() - clipboardlog("clipboard_toggled(%s) clipboard_enabled=%s, new_state=%s", args, self.client.clipboard_enabled, new_state) - if self.client.clipboard_enabled!=new_state: - self.client.clipboard_enabled = new_state - self.client.emit("clipboard-toggled") - self.clipboard_menuitem = self.checkitem("Clipboard", menu_clipboard_toggled) - set_sensitive(self.clipboard_menuitem, False) - def set_clipboard_menuitem(*args): - clipboardlog("set_clipboard_menuitem%s enabled=%s", args, self.client.clipboard_enabled) - self.clipboard_menuitem.set_active(self.client.clipboard_enabled) - c = self.client - can_clipboard = c.server_supports_clipboard and c.client_supports_clipboard - set_sensitive(self.clipboard_menuitem, can_clipboard) - if can_clipboard: - self.clipboard_menuitem.set_tooltip_text("Enable clipboard synchronization") - else: - self.clipboard_menuitem.set_tooltip_text("Clipboard synchronization cannot be enabled: disabled by server") - self.client.after_handshake(set_clipboard_menuitem) - def clipboard_toggled(*args): - #keep menu in sync with actual "clipboard_enabled" flag: - if self.client.clipboard_enabled != self.clipboard_menuitem.get_active(): - self.clipboard_menuitem.set_active(self.client.clipboard_enabled) - self.client.connect("clipboard-toggled", clipboard_toggled) - return self.clipboard_menuitem - - def _can_handle_clipboard(self): - c = self.client - return c.server_supports_clipboard and c.client_supports_clipboard - - def get_clipboard_helper_class(self): - return TranslatedClipboardProtocolHelper def remote_clipboard_changed(self, item, clipboard_submenu): - if not self._can_handle_clipboard(): - item = None - if item is None: - item = ([x for x in clipboard_submenu.get_children() if x.get_label()=="Disabled"]+[None])[0] - if item is None: - return + c = self.client + if not c or not c.server_supports_clipboard or not c.client_supports_clipboard: + return #prevent infinite recursion where ensure_item_selected #ends up calling here again - ich = getattr(clipboard_submenu, "_in_change_handler_", False) + key = "_in_remote_clipboard_changed" + ich = getattr(clipboard_submenu, key, False) clipboardlog("remote_clipboard_changed%s already in change handler: %s, visible=%s", (ll(item), clipboard_submenu), ich, clipboard_submenu.get_visible()) if ich: # or not clipboard_submenu.get_visible(): return try: - setattr(clipboard_submenu, "_in_change_handler_", True) + setattr(clipboard_submenu, key, True) selected_item = ensure_item_selected(clipboard_submenu, item) selected = selected_item.get_label() remote_clipboard = CLIPBOARD_LABEL_TO_NAME.get(selected) self.set_new_remote_clipboard(remote_clipboard) finally: - setattr(clipboard_submenu, "_in_change_handler_", False) + setattr(clipboard_submenu, key, False) def set_new_remote_clipboard(self, remote_clipboard): clipboardlog("set_new_remote_clipboard(%s)", remote_clipboard) - old_state = self.client.clipboard_enabled - send_tokens = False - if remote_clipboard is not None: - #clipboard is not disabled - if self.client.clipboard_helper is None: - self.client.clipboard_helper = self.client.setup_clipboard_helper(self.get_clipboard_helper_class()) - self.client.clipboard_helper.remote_clipboard = remote_clipboard - self.client.clipboard_helper.remote_clipboards = [remote_clipboard] - send_tokens = True - new_state = True - selections = [remote_clipboard] - else: - self.client.clipboard_helper = None - send_tokens = False - new_state = False - selections = [] - clipboardlog("set_new_remote_clipboard(%s) old_state=%s, new_state=%s", remote_clipboard, old_state, new_state) - if new_state!=old_state: - self.client.clipboard_enabled = new_state - self.client.emit("clipboard-toggled") - send_tokens = True + ch = self.client.clipboard_helper + ch.remote_clipboard = remote_clipboard + ch.remote_clipboards = [remote_clipboard] + selections = [remote_clipboard] + clipboardlog.info("server clipboard synchronization changed to %s selection", remote_clipboard) #tell the server what to look for: #(now that "clipboard-toggled" has re-enabled clipboard if necessary) self.client.send_clipboard_selections(selections) - if send_tokens and self.client.clipboard_helper: - self.client.clipboard_helper.send_all_tokens() + ch.send_all_tokens() def make_translatedclipboard_optionsmenuitem(self): clipboardlog("make_translatedclipboard_optionsmenuitem()") - clipboard_menu = self.menuitem("Clipboard", "clipboard.png", "Choose which remote clipboard to connect to", None) - set_sensitive(clipboard_menu, False) + ch = self.client.clipboard_helper + selection_menu = self.menuitem("Selection", None, "Choose which remote clipboard to connect to") + selection_submenu = gtk.Menu() + selection_menu.set_submenu(selection_submenu) + self.popup_menu_workaround(selection_submenu) + for label in CLIPBOARD_LABELS: + remote_clipboard = CLIPBOARD_LABEL_TO_NAME[label] + selection_item = CheckMenuItem(label) + active = getattr(ch, "remote_clipboard", "CLIPBOARD")==remote_clipboard + selection_item.set_active(active) + selection_item.set_draw_as_radio(True) + def remote_clipboard_changed(item): + self.remote_clipboard_changed(item, selection_submenu) + selection_item.connect("toggled", remote_clipboard_changed) + selection_submenu.append(selection_item) + selection_submenu.show_all() + return selection_menu + + def clipboard_direction_changed(self, item, submenu): + log("clipboard_direction_changed(%s, %s)", item, submenu) + sel = ensure_item_selected(submenu, item, recurse=False) + if not sel: + return + self.do_clipboard_direction_changed(sel.get_label() or "") + + def do_clipboard_direction_changed(self, label): + #find the value matching this item label: + d = CLIPBOARD_DIRECTION_LABEL_TO_NAME.get(label) + if d and d!=self.client.client_clipboard_direction: + log.info("clipboard synchronization direction changed to: %s", label.lower()) + self.client.client_clipboard_direction = d + can_send = d in ("to-server", "both") + can_receive = d in ("to-client", "both") + self.client.clipboard_helper.set_direction(can_send, can_receive) + #will send new tokens and may help reset things: + self.client.emit("clipboard-toggled") + + def make_clipboardmenuitem(self): + clipboardlog("make_clipboardmenuitem()") + self.clipboard_menuitem = self.menuitem("Clipboard", "clipboard.png") + set_sensitive(self.clipboard_menuitem, False) def set_clipboard_menu(*args): + c = self.client + if not c.server_supports_clipboard: + self.clipboard_menuitem.set_tooltip_text("Server does not support clipboard synchronization") + return + if not c.client_supports_clipboard: + self.client_menuitem.set_tooltip_text("Client does not support clipboard synchronization") + return + #add a submenu: + set_sensitive(self.clipboard_menuitem, True) + c = self.client + ch = self.client.clipboard_helper clipboard_submenu = gtk.Menu() - clipboard_menu.set_submenu(clipboard_submenu) + self.clipboard_menuitem.set_submenu(clipboard_submenu) self.popup_menu_workaround(clipboard_submenu) - c = self.client - can_clipboard = self._can_handle_clipboard() - clipboardlog("set_clipboard_menu(%s) can_clipboard=%s, server=%s, client=%s", args, can_clipboard, c.server_supports_clipboard, c.client_supports_clipboard) - set_sensitive(clipboard_menu, can_clipboard) - for label in CLIPBOARD_LABELS: - remote_clipboard = CLIPBOARD_LABEL_TO_NAME[label] - clipboard_item = CheckMenuItem(label) - ch = self.client.clipboard_helper - active = bool(ch) and isinstance(ch, TranslatedClipboardProtocolHelper) \ - and ch.remote_clipboard==remote_clipboard - clipboard_item.set_active(active) - set_sensitive(clipboard_item, can_clipboard) - clipboard_item.set_draw_as_radio(True) - def remote_clipboard_changed(item): - self.remote_clipboard_changed(item, clipboard_submenu) - clipboard_item.connect("toggled", remote_clipboard_changed) - clipboard_submenu.append(clipboard_item) + #figure out if this is a translated clipboard (win32 or osx) + #and if so, add a submenu to change the selection we synchronize with: + try: + from xpra.clipboard.translated_clipboard import TranslatedClipboardProtocolHelper + assert TranslatedClipboardProtocolHelper + clipboardlog("set_clipboard_menu(%s) helper=%s, server=%s, client=%s", args, ch, c.server_supports_clipboard, c.client_supports_clipboard) + if issubclass(type(ch), TranslatedClipboardProtocolHelper): + clipboard_submenu.append(self.make_translatedclipboard_optionsmenuitem()) + clipboard_submenu.append(gtk.SeparatorMenuItem()) + except: + clipboardlog.error("make_clipboardmenuitem()", exc_info=True) + for label in CLIPBOARD_DIRECTION_LABELS: + direction_item = CheckMenuItem(label) + d = CLIPBOARD_DIRECTION_LABEL_TO_NAME.get(label) + direction_item.set_active(d==self.client.client_clipboard_direction) + direction_item.connect("toggled", self.clipboard_direction_changed, clipboard_submenu) + clipboard_submenu.append(direction_item) clipboard_submenu.show_all() self.client.after_handshake(set_clipboard_menu) - return clipboard_menu - - def make_clipboardmenuitem(self): - try: - copts = self.client.get_clipboard_helper_classes() - #ugly alert: the helper does not exist yet.. we just check the helper classnames: - if len(copts)>0: - chname = copts[0].lower() - if chname.find("translated_clipboard")>=0 or chname.find("osxclipboard")>=0: - return self.make_translatedclipboard_optionsmenuitem() - except: - clipboardlog.error("make_clipboardmenuitem()", exc_info=True) - return self.make_clipboard_togglemenuitem() + return self.clipboard_menuitem def make_keyboardsyncmenuitem(self): diff --git a/src/xpra/client/ui_client_base.py b/src/xpra/client/ui_client_base.py index 4ce961031c..089b76eb82 100644 --- a/src/xpra/client/ui_client_base.py +++ b/src/xpra/client/ui_client_base.py @@ -234,6 +234,7 @@ def __init__(self): self.server_encodings_with_speed = () self.server_encodings_with_quality = () self.server_encodings_with_lossless = () + self.server_clipboard_direction = "both" self.readonly = False self.windows_enabled = True self.pings = False @@ -262,6 +263,7 @@ def __init__(self): self.client_supports_remote_logging = False self.log_both = False self.notifications_enabled = False + self.client_clipboard_direction = "both" self.clipboard_enabled = False self.cursors_enabled = False self.bell_enabled = False @@ -392,6 +394,7 @@ def vinfo(k): self.client_supports_notifications = opts.notifications self.client_supports_system_tray = opts.system_tray and SYSTEM_TRAY_SUPPORTED self.client_clipboard_type = opts.clipboard + self.client_clipboard_direction = opts.clipboard_direction self.client_supports_clipboard = not ((opts.clipboard or "").lower() in FALSE_OPTIONS) self.client_supports_cursors = opts.cursors self.client_supports_bell = opts.bell @@ -1736,6 +1739,19 @@ def parse_server_capabilities(self): self.server_supports_bell = c.boolget("bell") #added in 0.5, default to True! self.bell_enabled = self.server_supports_bell and self.client_supports_bell self.server_supports_clipboard = c.boolget("clipboard") + self.server_clipboard_direction = c.strget("clipboard-direction", "both") + if self.server_clipboard_direction!=self.client_clipboard_direction and self.server_clipboard_direction!="both": + if self.client_clipboard_direction=="disabled": + pass + elif self.server_clipboard_direction=="disabled": + log.warn("Warning: server clipboard synchronization is currently disabled") + self.client_clipboard_direction = "disabled" + elif self.client_clipboard_direction=="both": + log.warn("Warning: server only supports '%s' clipboard transfers", self.server_clipboard_direction) + self.client_clipboard_direction = self.server_clipboard_direction + else: + log.warn("Warning: incompatible clipboard direction settings") + log.warn(" server setting: %s, client setting: %s", self.server_clipboard_direction, self.client_clipboard_direction) self.server_supports_clipboard_enable_selections = c.boolget("clipboard.enable-selections") self.server_clipboards = c.strlistget("clipboards", ALL_CLIPBOARDS) self.server_compressors = c.strlistget("compressors", ["zlib"]) @@ -1942,6 +1958,7 @@ def handshake_complete(self): log.error("Error processing handshake callback %s", cb, exc_info=True) def after_handshake(self, cb, *args): + log("after_handshake(%s, %s) on_handshake=%s", cb, args, self._on_handshake) if self._on_handshake is None: #handshake has already occurred, just call it: self.idle_add(cb, *args) diff --git a/src/xpra/clipboard/clipboard_base.py b/src/xpra/clipboard/clipboard_base.py index 42ee453f79..71897a5ffd 100644 --- a/src/xpra/clipboard/clipboard_base.py +++ b/src/xpra/clipboard/clipboard_base.py @@ -63,6 +63,8 @@ class ClipboardProtocolHelperBase(object): def __init__(self, send_packet_cb, progress_cb=None, **kwargs): self.send = send_packet_cb self.progress_cb = progress_cb + self.can_send = kwargs.get("can-send", True) + self.can_receive = kwargs.get("can-receive", True) self.max_clipboard_packet_size = MAX_CLIPBOARD_PACKET_SIZE self.filter_res = [] filter_res = kwargs.get("filters") @@ -83,11 +85,14 @@ def __repr__(self): return "ClipboardProtocolHelperBase" def get_info(self): - info = {"type" : str(self), + info = { + "type" : str(self), "max_size" : self.max_clipboard_packet_size, "filters" : [x.pattern for x in self.filter_res], "requests" : self._clipboard_request_counter, "pending" : self._clipboard_outstanding_requests.keys(), + "can-send" : self.can_send, + "can-receive" : self.can_receive, "want_targets" : self._want_targets, } for clipboard, proxy in self._clipboard_proxies.items(): @@ -102,6 +107,12 @@ def nosend(*args): x.cleanup() self._clipboard_proxies = {} + def set_direction(self, can_send, can_receive): + self.can_send = can_send + self.can_receive = can_receive + for proxy in self._clipboard_proxies.values(): + proxy.set_direction(can_send, can_receive) + def enable_selections(self, selections): #when clients first connect or later through the "clipboard-enable-selections" packet, #they can tell us which clipboard selections they want enabled @@ -135,6 +146,7 @@ def init_proxies(self, clipboards): self._clipboard_proxies = {} for clipboard in clipboards: proxy = self.make_proxy(clipboard) + proxy.set_direction(self.can_send, self.can_receive) proxy.connect("send-clipboard-token", self._send_clipboard_token_handler) proxy.connect("get-clipboard-from-remote", self._get_clipboard_from_remote_handler) proxy.show() @@ -212,6 +224,9 @@ def _clipboard_got_contents(self, request_id, dtype, dformat, data): def _send_clipboard_token_handler(self, proxy, selection): log("send clipboard token: %s", selection) rsel = self.local_to_remote(selection) + def send_token(*args): + proxy._have_token = False + self.send("clipboard-token", *args) if self._want_targets: #send the token with the target and data once we get them: #first get the targets, then get the contents for targets we want to send (if any) @@ -220,7 +235,7 @@ def got_targets(dtype, dformat, targets): #if there is a text target, send that too (just the first one that matches for now..) send_now = [x for x in targets if x in TEXT_TARGETS] def send_targets_only(): - self.send("clipboard-token", rsel, targets) + send_token(rsel, targets) if len(send_now)==0: send_targets_only() return @@ -242,11 +257,11 @@ def got_contents(dtype, dformat, data): return target_data = (target, dtype, dformat, wire_encoding, wire_data) log("sending token with target data: %s", target_data) - self.send("clipboard-token", rsel, targets, *target_data) + send_token(rsel, targets, *target_data) proxy.get_contents(target, got_contents) proxy.get_contents("TARGETS", got_targets) return - self.send("clipboard-token", rsel) + send_token(rsel) def _munge_raw_selection_to_wire(self, target, dtype, dformat, data): # Some types just cannot be marshalled: @@ -457,6 +472,10 @@ def __init__(self, selection): 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, @@ -474,7 +493,7 @@ def get_info(self): return info def cleanup(self): - if not self._have_token and STORE_ON_EXIT: + if self._can_receive and not self._have_token and STORE_ON_EXIT: self._clipboard.store() self.destroy() @@ -493,9 +512,11 @@ 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("do_owner_changed(%s) greedy_client=%s, block_owner_change=%s", args, self._greedy_client, self._block_owner_change) - log("clipboard: %s owner_changed, enabled=%s, have_token=%s, greedy_client=%s, block_owner_change=%s", self._selection, self._enabled, self._have_token, self._greedy_client, self._block_owner_change) - if self._enabled and self._greedy_client and not self._block_owner_change: + 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 self._enabled and self._can_send and not self._block_owner_change and (self._greedy_client or not self._can_receive): self._block_owner_change = True self._have_token = False self.emit("send-clipboard-token", self._selection) @@ -505,7 +526,7 @@ def do_owner_changed(self, *args): def do_selection_request_event(self, event): log("do_selection_request_event(%s)", event) self._selection_request_events += 1 - if not self._enabled: + 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 @@ -567,7 +588,7 @@ def do_selection_get(self, selection_data, info, time): # main loop. def nodata(): selection_data.set("STRING", 8, "") - if not self._enabled: + 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) @@ -591,7 +612,7 @@ 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 self._enabled and self._can_send: #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) @@ -610,14 +631,17 @@ def 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) + log("got token, selection=%s, targets=%s, target data=%s, can-receive=%s", self._selection, targets, target_data, self._can_receive) 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._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() if self._block_owner_change: #re-enable the flag via idle_add so events like do_owner_changed #get a chance to run first. @@ -640,8 +664,8 @@ def claim(self): # 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: + 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 diff --git a/src/xpra/gtk_common/gtk_util.py b/src/xpra/gtk_common/gtk_util.py index f541b04a4e..f09512e5d3 100644 --- a/src/xpra/gtk_common/gtk_util.py +++ b/src/xpra/gtk_common/gtk_util.py @@ -811,7 +811,7 @@ def title_box(label_str): #utility method to ensure there is always only one CheckMenuItem #selected in a submenu: -def ensure_item_selected(submenu, item): +def ensure_item_selected(submenu, item, recurse=True): if not isinstance(item, gtk.CheckMenuItem): return if item.get_active(): @@ -822,7 +822,7 @@ def deactivate(items, skip=None): continue if isinstance(x, gtk.MenuItem): submenu = x.get_submenu() - if submenu: + if submenu and recurse: deactivate(submenu.get_children(), skip) if isinstance(x, gtk.CheckMenuItem): if x!=item and x.get_active(): diff --git a/src/xpra/platform/darwin/osx_menu.py b/src/xpra/platform/darwin/osx_menu.py index 38398959df..d8b6d6661d 100644 --- a/src/xpra/platform/darwin/osx_menu.py +++ b/src/xpra/platform/darwin/osx_menu.py @@ -11,8 +11,8 @@ from xpra.gtk_common.gtk_util import scaled_image from xpra.gtk_common.about import about -from xpra.client.gtk_base.gtk_tray_menu_base import GTKTrayMenuBase, populate_encodingsmenu, CLIPBOARD_LABEL_TO_NAME, CLIPBOARD_LABELS -from xpra.platform.darwin.osx_clipboard import OSXClipboardProtocolHelper +from xpra.client.gtk_base.gtk_tray_menu_base import GTKTrayMenuBase, populate_encodingsmenu, \ + CLIPBOARD_LABEL_TO_NAME, CLIPBOARD_NAME_TO_LABEL, CLIPBOARD_LABELS, CLIPBOARD_DIRECTION_LABELS, CLIPBOARD_DIRECTION_NAME_TO_LABEL from xpra.platform.paths import get_icon from xpra.platform.darwin.gui import get_OSXApplication @@ -193,8 +193,11 @@ def get_extra_menus(self): clipboard_menu = self.make_menu() menus.append(("Clipboard", clipboard_menu)) for label in CLIPBOARD_LABELS: - remote_clipboard = CLIPBOARD_LABEL_TO_NAME[label] - clipboard_menu.add(self.make_clipboard_submenuitem(label, remote_clipboard)) + clipboard_menu.add(self.make_clipboard_submenuitem(label, self._remote_clipboard_changed)) + clipboard_menu.add(gtk.SeparatorMenuItem()) + for label in CLIPBOARD_DIRECTION_LABELS: + clipboard_menu.add(self.make_clipboard_submenuitem(label, self._clipboard_direction_changed)) + clipboard_menu.show_all() self.client.after_handshake(self.set_clipboard_menu, clipboard_menu) if SHOW_SOUND_MENU: sound_menu = self.make_menu() @@ -227,65 +230,80 @@ def add_ah(*args): menus.append((SEPARATOR+"-EXTRAS", None)) return menus - def get_clipboard_helper_class(self): - return OSXClipboardProtocolHelper - def make_clipboard_submenuitem(self, label, selection): + def _clipboard_direction_changed(self, item, label): + clipboardlog("_clipboard_direction_changed(%s, %s) clipboard_change_pending=%s", item, label, self._clipboard_change_pending) + label = self.select_clipboard_menu_option(item, label, CLIPBOARD_DIRECTION_LABELS) + self.do_clipboard_direction_changed(label or "") + + def _remote_clipboard_changed(self, item, label): + clipboardlog("_remote_clipboard_changed(%s, %s) clipboard_change_pending=%s", item, label, self._clipboard_change_pending) + #ensure this is the only clipboard label selected: + label = self.select_clipboard_menu_option(item, label, CLIPBOARD_LABELS) + if not label: + return + remote_clipboard = CLIPBOARD_LABEL_TO_NAME[label] + clipboardlog("will select clipboard menu item with label=%s, for remote_clipboard=%s", label, remote_clipboard) + glib.timeout_add(0, self._do_clipboard_change, remote_clipboard) + + def _do_clipboard_change(self, remote_clipboard): + #why do we look it up again when we could just pass it in + #to make_clipboard_submenuitem as an extra argument? + #because gtk-osx would fall over itself, making a complete mess of the menus in the process + #and why do we use a timer here? again, more trouble with gtk-osx.. + self._clipboard_change_pending = False + self.set_new_remote_clipboard(remote_clipboard) + + def make_clipboard_submenuitem(self, label, cb=None): clipboard_item = self.checkitem(label) clipboard_item.set_draw_as_radio(True) - def remote_clipboard_changed(item): - clipboardlog("remote_clipboard_changed(%s) label=%s - clipboard_change_pending=%s", item, label, self._clipboard_change_pending) - self.select_clipboard_menu_option(item, label) - clipboard_item.connect("toggled", remote_clipboard_changed) + def clipboard_option_changed(item): + clipboardlog("clipboard_option_changed(%s) label=%s, callback=%s clipboard_change_pending=%s", item, label, cb, self._clipboard_change_pending) + if cb: + cb(item, label) + clipboard_item.connect("toggled", clipboard_option_changed) return clipboard_item - def select_clipboard_menu_option(self, item=None, label="Disabled"): - clipboardlog("select_clipboard_menu_option(%s, %s) clipboard_change_pending=%s", item, label, self._clipboard_change_pending) + def select_clipboard_menu_option(self, item=None, label=None, labels=[]): + #ensure that only the matching menu item is selected, + #(can be specified as a menuitem object, or using its label) + #all the other menu items whose labels are specified will be made inactive + #(we use a flag to prevent reentry) + clipboardlog("select_clipboard_menu_option(%s, %s, %s) clipboard_change_pending=%s", item, label, labels, self._clipboard_change_pending) if self._clipboard_change_pending: - return + return None clipboard = self.get_menu("Clipboard") if not clipboard: - log.error("Error: cannot locate Clipboad menu object!") - return - all_items = clipboard.get_submenu().get_children() + log.error("Error: cannot locate Clipboard menu object!") + return None + all_items = [x for x in clipboard.get_submenu().get_children() if x.get_label() in labels] selected_items = [x for x in all_items if x==item] + [x for x in all_items if x.get_label()==label] - default_items = [x for x in all_items if x.get_label()=="Disabled"] - options = selected_items+default_items - if not options: - log.error("Error: cannot find any clipboard menu options to match!") - return + if not selected_items: + log.error("Error: cannot find any clipboard menu options to match '%s'", label) + return None self._clipboard_change_pending = True - sel = options[0] - remote_clipboard = CLIPBOARD_LABEL_TO_NAME.get(sel.get_label()) - clipboardlog("will select clipboard menu item with label=%s, for remote_clipboard=%s", sel.get_label(), remote_clipboard) + sel = selected_items[0] + if not label: + label = sel.get_label() for x in all_items: - x.set_active(x.get_label()==sel.get_label()) - glib.timeout_add(0, self.do_clipboard_change, remote_clipboard) - - def do_clipboard_change(self, remote_clipboard): - #why do we look it up again when we could just pass it in - #to make_clipboard_submenuitem as an extra argument? - #because gtk-osx would fall over itself, making a complete mess of the menus in the process - #and why do we use a timer here? again, more trouble with gtk-osx.. + active = x.get_label()==label + if x.get_active()!=active: + x.set_active(active) self._clipboard_change_pending = False - self.set_new_remote_clipboard(remote_clipboard) + return label def set_clipboard_menu(self, clipboard_menu): #find the menu item matching the current settings, #and select it try: - assert self._can_handle_clipboard() - remote_clipboard = self.client.clipboard_helper.remote_clipboard - label = None + label = CLIPBOARD_NAME_TO_LABEL.get(self.client.clipboard_helper.remote_clipboard) except: - remote_clipboard = None - label = "Disabled" - selected_menu_item = None - for item in clipboard_menu.get_children(): - if remote_clipboard and remote_clipboard==CLIPBOARD_LABEL_TO_NAME.get(item.get_label()): - selected_menu_item = item - break - self.select_clipboard_menu_option(selected_menu_item, label) + label = None + self.select_clipboard_menu_option(None, label, CLIPBOARD_LABELS) + direction_label = CLIPBOARD_DIRECTION_NAME_TO_LABEL.get(self.client.client_clipboard_direction, "Disabled") + clipboardlog("direction(%s)=%s", self.client.client_clipboard_direction, direction_label) + self.select_clipboard_menu_option(None, direction_label, CLIPBOARD_DIRECTION_LABELS) + #these methods are called by the superclass #but we don't have a quality or speed menu, so override and ignore diff --git a/src/xpra/scripts/config.py b/src/xpra/scripts/config.py index 0826dabdac..e346abd42d 100755 --- a/src/xpra/scripts/config.py +++ b/src/xpra/scripts/config.py @@ -304,6 +304,7 @@ def read_xpra_defaults(): "window-icon" : str, "password-file" : str, "clipboard" : str, + "clipboard-direction" : str, "clipboard-filter-file" : str, "remote-clipboard" : str, "local-clipboard" : str, @@ -490,6 +491,7 @@ def addtrailingslash(v): "window-icon" : "", "password-file" : "", "clipboard" : "yes", + "clipboard-direction" : "both", "clipboard-filter-file" : "", "remote-clipboard" : "CLIPBOARD", "local-clipboard" : "CLIPBOARD", @@ -837,12 +839,26 @@ def fixup_packetencoding(options): warn("warning: invalid packet encoder(s) specified: %s" % (", ".join(unknown))) options.packet_encoders = packet_encoders +def fixup_clipboard(options): + cd = options.clipboard_direction.lower().replace("-", "") + if cd=="toserver": + options.clipboard_direction = "to-server" + elif cd=="toclient": + options.clipboard_direction = "to-client" + elif cd=="both": + options.clipboard_direction = "both" + else: + warn("Warning: invalid value for clipboard-direction: '%s'" % options.clipboard_direction) + warn(" specify 'to-server', 'to-client' or 'both'") + options.clipboard_direction = "disabled" + def fixup_options(options): fixup_encodings(options) fixup_compression(options) fixup_packetencoding(options) fixup_video_all_or_none(options) fixup_socketdirs(options) + fixup_clipboard(options) def main(): diff --git a/src/xpra/scripts/main.py b/src/xpra/scripts/main.py index e4066ccd10..14e43c89a6 100755 --- a/src/xpra/scripts/main.py +++ b/src/xpra/scripts/main.py @@ -472,6 +472,9 @@ def ignore(defaults): group.add_option("--clipboard", action="store", metavar="yes|no|clipboard-type", dest="clipboard", default=defaults.clipboard, help="Enable clipboard support. Default: %s." % defaults.clipboard) + group.add_option("--clipboard-direction", action="store", metavar="to-server|to-client|both", + dest="clipboard_direction", default=defaults.clipboard_direction, + help="Direction of clipboard synchronization. Default: %s." % defaults.clipboard_direction) legacy_bool_parse("notifications") group.add_option("--notifications", action="store", metavar="yes|no", dest="notifications", default=defaults.notifications, @@ -511,6 +514,7 @@ def ignore(defaults): group.add_option("--sharing", action="store", metavar="yes|no", dest="sharing", default=defaults.sharing, help="Allow more than one client to connect to the same session. Default: %s." % enabled_str(defaults.sharing)) + legacy_bool_parse("remote-logging") group.add_option("--remote-logging", action="store", metavar="yes|no|both", dest="remote_logging", default=defaults.remote_logging, help="Forward all the client's log output to the server. Default: %s." % enabled_str(defaults.remote_logging)) diff --git a/src/xpra/server/server_base.py b/src/xpra/server/server_base.py index 0e53f7fff3..b3ec6b2c8d 100644 --- a/src/xpra/server/server_base.py +++ b/src/xpra/server/server_base.py @@ -125,6 +125,7 @@ def __init__(self): self.double_click_time = -1 self.double_click_distance = -1, -1 self.supports_clipboard = False + self.clipboard_direction = "both" self.supports_dbus_proxy = False self.dbus_helper = None self.dbus_control = False @@ -228,6 +229,7 @@ def init_options(self, opts): self.default_dpi = int(opts.dpi) self.idle_timeout = opts.idle_timeout self.supports_clipboard = not ((opts.clipboard or "").lower() in FALSE_OPTIONS) + self.clipboard_direction = opts.clipboard_direction self.clipboard_filter_file = opts.clipboard_filter_file self.supports_dbus_proxy = opts.dbus_proxy self.exit_with_children = opts.exit_with_children @@ -574,7 +576,11 @@ def init_clipboard(self): return try: from xpra.clipboard.gdk_clipboard import GDKClipboardProtocolHelper - kwargs = {"filters" : clipboard_filter_res} + kwargs = { + "filters" : clipboard_filter_res, + "can-send" : self.clipboard_direction in ("to-client", "both"), + "can-receive" : self.clipboard_direction in ("to-server", "both"), + } self._clipboard_helper = GDKClipboardProtocolHelper(self.send_clipboard_packet, self.clipboard_progress, **kwargs) self._clipboards = CLIPBOARDS except Exception: @@ -1268,6 +1274,7 @@ def make_hello(self, source): if source.wants_features: capabilities.update({ "clipboards" : self._clipboards, + "clipboard-direction" : self.clipboard_direction, "notifications" : self.notifications, "bell" : self.bell, "cursors" : self.cursors,