Пример #1
0
class ApplicationWindow:

    def    __init__(self):
        # Default connection options
        self.config = make_defaults_struct(extras_defaults=LAUNCHER_DEFAULTS, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation())
        #TODO: the fixup does not belong here?
        from xpra.scripts.main import fixup_options
        fixup_options(self.config)
        #what we save by default:
        self.config_keys = set(SAVED_FIELDS)
        def raise_exception(*args):
            raise Exception(*args)
        self.client = make_client(raise_exception, self.config)
        self.client.init(self.config)
        self.exit_code = None
        self.current_error = None

    def get_connection_modes(self):
        modes = ["ssh"]
        try:
            import ssl
            assert ssl
            modes.append("ssl")
        except:
            pass
        if "AES" in ENCRYPTION_CIPHERS:
            modes.append("tcp + aes")
        modes.append("tcp")
        return modes

    def get_launcher_validation(self):
        crypto_backend_init()
        #TODO: since "mode" is not part of global options
        #this validation should be injected from the launcher instead
        def validate_in_list(x, options):
            if x in options:
                return None
            return "must be in %s" % (", ".join(options))
        modes = self.get_connection_modes()
        return {"mode"              : lambda x : validate_in_list(x, modes)}


    def has_mdns(self):
        try:
            from xpra.net.mdns import get_listener_class
            lc = get_listener_class()
            log("mdns listener class: %s", lc)
            if lc:
                return True
        except ImportError as e:
            log("no mdns support: %s", e)
        return False


    def image_button(self, label="", tooltip="", icon_pixbuf=None, clicked_cb=None):
        button = gtk.Button(label)
        settings = button.get_settings()
        settings.set_property('gtk-button-images', True)
        button.connect("clicked", clicked_cb)
        button.set_tooltip_text(tooltip)
        if icon_pixbuf:
            image = gtk.Image()
            image.set_from_pixbuf(icon_pixbuf)
            button.set_image(image)
        return button

    def create_window(self):
        self.window = gtk.Window()
        self.window.connect("destroy", self.destroy)
        self.window.set_default_size(400, 260)
        self.window.set_border_width(20)
        self.window.set_title("Xpra Launcher")
        self.window.modify_bg(STATE_NORMAL, gdk.Color(red=65535, green=65535, blue=65535))

        self.window.set_position(WIN_POS_CENTER)

        vbox = gtk.VBox(False, 0)
        vbox.set_spacing(15)

        #top row:
        hbox = gtk.HBox(False, 0)
        # About dialog (and window icon):
        icon_pixbuf = self.get_icon("xpra.png")
        if icon_pixbuf:
            self.window.set_icon(icon_pixbuf)
            logo_button = self.image_button("", "About", icon_pixbuf, about)
            hbox.pack_start(logo_button, expand=False, fill=False)
        # Bug report tool link:
        icon_pixbuf = self.get_icon("bugs.png")
        self.bug_tool = None
        if icon_pixbuf:
            def bug(*_args):
                if self.bug_tool==None:
                    from xpra.client.gtk_base.bug_report import BugReport
                    self.bug_tool = BugReport()
                    self.bug_tool.init(show_about=False)
                self.bug_tool.show()
            bug_button = self.image_button("", "Bug Report", icon_pixbuf, bug)
            hbox.pack_start(bug_button, expand=False, fill=False)
        # Session browser link:
        icon_pixbuf = self.get_icon("mdns.png")
        self.mdns_gui = None
        if icon_pixbuf and self.has_mdns():
            def mdns(*_args):
                if self.mdns_gui==None:
                    from xpra.client.gtk_base.mdns_gui import mdns_sessions
                    self.mdns_gui = mdns_sessions(self.config)
                    def close_mdns():
                        self.mdns_gui.destroy()
                        self.mdns_gui = None
                    self.mdns_gui.do_quit = close_mdns
                else:
                    self.mdns_gui.present()
            mdns_button = self.image_button("", "Browse Sessions", icon_pixbuf, mdns)
            hbox.pack_start(mdns_button, expand=False, fill=False)

        # Title
        label = gtk.Label("Connect to xpra server")
        label.modify_font(pango.FontDescription("sans 14"))
        hbox.pack_start(label, expand=True, fill=True)
        vbox.pack_start(hbox)

        # Mode:
        hbox = gtk.HBox(False, 20)
        hbox.set_spacing(20)
        hbox.pack_start(gtk.Label("Mode: "))
        self.mode_combo = gtk.combo_box_new_text()
        for x in self.get_connection_modes():
            self.mode_combo.append_text(x.upper())
        self.mode_combo.connect("changed", self.mode_changed)
        hbox.pack_start(self.mode_combo)
        vbox.pack_start(hbox)

        # Username@Host:Port
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(5)
        self.username_entry = gtk.Entry()
        self.username_entry.set_max_length(128)
        self.username_entry.set_width_chars(16)
        self.username_entry.connect("changed", self.validate)
        self.username_entry.set_tooltip_text("username")
        self.username_label = gtk.Label("@")
        self.host_entry = gtk.Entry()
        self.host_entry.set_max_length(128)
        self.host_entry.set_width_chars(24)
        self.host_entry.connect("changed", self.validate)
        self.host_entry.set_tooltip_text("hostname")
        self.ssh_port_entry = gtk.Entry()
        self.ssh_port_entry.set_max_length(5)
        self.ssh_port_entry.set_width_chars(5)
        self.ssh_port_entry.connect("changed", self.validate)
        self.ssh_port_entry.set_tooltip_text("SSH port")
        self.port_entry = gtk.Entry()
        self.port_entry.set_max_length(5)
        self.port_entry.set_width_chars(5)
        self.port_entry.connect("changed", self.validate)
        self.port_entry.set_tooltip_text("port/display")

        hbox.pack_start(self.username_entry)
        hbox.pack_start(self.username_label)
        hbox.pack_start(self.host_entry)
        hbox.pack_start(self.ssh_port_entry)
        hbox.pack_start(gtk.Label(":"))
        hbox.pack_start(self.port_entry)
        vbox.pack_start(hbox)

        # Password
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(20)
        self.password_entry = gtk.Entry()
        self.password_entry.set_max_length(128)
        self.password_entry.set_width_chars(30)
        self.password_entry.set_text("")
        self.password_entry.set_visibility(False)
        self.password_entry.connect("changed", self.password_ok)
        self.password_entry.connect("changed", self.validate)
        self.password_label = gtk.Label("Password: "******"Disable Strict Host Key Check")
        self.nostrict_host_check.set_active(False)
        al = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0)
        al.add(self.nostrict_host_check)
        hbox.pack_start(al)
        vbox.pack_start(hbox)

        # Info Label
        self.info = gtk.Label()
        self.info.set_line_wrap(True)
        self.info.set_size_request(360, -1)
        self.info.modify_fg(STATE_NORMAL, red)
        vbox.pack_start(self.info)

        #hide encoding options by default
        self.encoding_combo = None
        self.encoding_options_check = None
        self.encoding_box = None
        if not PYTHON3:
            #not implemented for gtk3, where we can't use set_menu()...
            hbox = gtk.HBox(False, 0)
            hbox.set_spacing(20)
            self.encoding_options_check = gtk.CheckButton("Advanced Encoding Options")
            self.encoding_options_check.connect("toggled", self.encoding_options_toggled)
            self.encoding_options_check.set_active(False)
            al = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0)
            al.add(self.encoding_options_check)
            hbox.pack_start(al)
            vbox.pack_start(hbox)
            self.encoding_box = gtk.VBox()
            vbox.pack_start(self.encoding_box)

            # Encoding:
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            hbox.pack_start(gtk.Label("Encoding: "))
            self.encoding_combo = OptionMenu()
            def get_current_encoding():
                return self.config.encoding
            def set_new_encoding(e):
                self.config.encoding = e
            encodings = ["auto"]+[x for x in PREFERED_ENCODING_ORDER if x in self.client.get_encodings()]
            server_encodings = encodings
            es = make_encodingsmenu(get_current_encoding, set_new_encoding, encodings, server_encodings)
            self.encoding_combo.set_menu(es)
            set_history_from_active(self.encoding_combo)
            hbox.pack_start(self.encoding_combo)
            self.encoding_box.pack_start(hbox)
            self.encoding_combo.connect("changed", self.encoding_changed)

            # Quality
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            self.quality_label = gtk.Label("Quality: ")
            hbox.pack_start(self.quality_label)
            self.quality_combo = OptionMenu()
            def set_min_quality(q):
                self.config.min_quality = q
            def set_quality(q):
                self.config.quality = q
            def get_min_quality():
                return self.config.min_quality
            def get_quality():
                return self.config.quality
            sq = make_min_auto_menu("Quality", MIN_QUALITY_OPTIONS, QUALITY_OPTIONS,
                                       get_min_quality, get_quality, set_min_quality, set_quality)
            self.quality_combo.set_menu(sq)
            set_history_from_active(self.quality_combo)
            hbox.pack_start(self.quality_combo)
            self.encoding_box.pack_start(hbox)

            # Speed
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            self.speed_label = gtk.Label("Speed: ")
            hbox.pack_start(self.speed_label)
            self.speed_combo = OptionMenu()
            def set_min_speed(s):
                self.config.min_speed = s
            def set_speed(s):
                self.config.speed = s
            def get_min_speed():
                return self.config.min_speed
            def get_speed():
                return self.config.speed
            ss = make_min_auto_menu("Speed", MIN_SPEED_OPTIONS, SPEED_OPTIONS,
                                       get_min_speed, get_speed, set_min_speed, set_speed)
            self.speed_combo.set_menu(ss)
            set_history_from_active(self.speed_combo)
            hbox.pack_start(self.speed_combo)
            self.encoding_box.pack_start(hbox)
            self.encoding_box.hide()

        # Buttons:
        hbox = gtk.HBox(False, 20)
        vbox.pack_start(hbox)
        #Save:
        self.save_btn = gtk.Button("Save")
        self.save_btn.set_tooltip_text("Save settings to a session file")
        self.save_btn.connect("clicked", self.save_clicked)
        hbox.pack_start(self.save_btn)
        #Load:
        self.load_btn = gtk.Button("Load")
        self.load_btn.set_tooltip_text("Load settings from a session file")
        self.load_btn.connect("clicked", self.load_clicked)
        hbox.pack_start(self.load_btn)
        # Connect button:
        self.button = gtk.Button("Connect")
        self.button.connect("clicked", self.connect_clicked)
        connect_icon = self.get_icon("retry.png")
        if connect_icon:
            self.button.set_image(scaled_image(connect_icon, 24))
        hbox.pack_start(self.button)

        def accel_close(*_args):
            gtk.main_quit()
        add_close_accel(self.window, accel_close)
        vbox.show_all()
        self.encoding_options_toggled()
        self.window.vbox = vbox
        self.window.add(vbox)

    def validate(self, *args):
        ssh = self.mode_combo.get_active_text()=="SSH"
        errs = []
        host = self.host_entry.get_text()
        errs.append((self.host_entry, not bool(host), "specify the host"))
        if ssh:
            #validate ssh port:
            ssh_port = self.ssh_port_entry.get_text()
            try:
                ssh_port = int(ssh_port)
            except:
                ssh_port = -1
            errs.append((self.ssh_port_entry, ssh_port<0 or ssh_port>=2**16, "invalid SSH port number"))
        port = self.port_entry.get_text()
        if ssh and not port:
            port = 0        #port optional with ssh
        else:
            try:
                port = int(port)
            except:
                port = -1
        errs.append((self.port_entry, port<0 or port>=2**16, "invalid port number"))
        err_text = []
        for w, e, text in errs:
            self.set_widget_bg_color(w, e)
            if e:
                err_text.append(text)
        log("validate(%s) err_text=%s, errs=%s", args, err_text, errs)
        self.set_info_text(", ".join(err_text))
        self.set_info_color(len(err_text)>0)
        self.button.set_sensitive(len(err_text)==0)
        return errs

    def show(self):
        self.window.show()
        self.window.present()

    def run(self):
        gtk_main()

    def get_icon(self, icon_name):
        icon_filename = os.path.join(get_icon_dir(), icon_name)
        if os.path.exists(icon_filename):
            return pixbuf_new_from_file(icon_filename)
        return None

    def mode_changed(self, *_args):
        mode = self.mode_combo.get_active_text().lower()
        ssh = mode=="ssh"
        if ssh:
            self.port_entry.set_tooltip_text("Display number (optional)")
            self.port_entry.set_text("")
            self.ssh_port_entry.set_text("22")
            self.ssh_port_entry.show()
            self.password_entry.set_tooltip_text("SSH Password")
            self.username_entry.set_tooltip_text("SSH Username")
        else:
            self.ssh_port_entry.hide()
            self.ssh_port_entry.set_text("")
            port_str = self.port_entry.get_text()
            if not port_str:
                self.port_entry.set_text(str(max(0, self.config.port) or DEFAULT_PORT))
            self.port_entry.set_tooltip_text("xpra server port number")
            self.password_entry.set_tooltip_text("Session Password (optional)")
            self.username_entry.set_tooltip_text("Session Username (optional)")
            if self.config.port>0:
                self.port_entry.set_text("%s" % self.config.port)
        can_use_password = not ssh
        if ssh:
            if WIN32:
                #plink can use password
                pass
            else:
                #can use password if sshpass is installed:
                from xpra.platform.paths import get_sshpass_command
                sshpass = get_sshpass_command()
                can_use_password = bool(sshpass)
        if can_use_password:
            self.password_label.show()
            self.password_entry.show()
        else:
            self.password_label.hide()
            self.password_entry.hide()
        self.validate()
        if mode=="ssl" or (mode=="ssh" and not WIN32):
            self.nostrict_host_check.show()
        else:
            self.nostrict_host_check.hide()

    def get_selected_encoding(self, *_args):
        if not self.encoding_combo:
            return ""
        index = get_active_item_index(self.encoding_combo)
        return self.encoding_combo.get_menu().index_to_encoding.get(index)

    def encoding_changed(self, *args):
        encoding = self.get_selected_encoding()
        uses_quality_option = encoding in ["jpeg", "webp", "h264", "auto"]
        log("encoding_changed(%s) uses_quality_option(%s)=%s", args, encoding, uses_quality_option)
        if uses_quality_option:
            self.quality_combo.show()
            self.quality_label.show()
        else:
            self.quality_combo.hide()
            self.quality_label.hide()

    def encoding_options_toggled(self, *_args):
        if not self.encoding_box:
            return
        show_opts = self.encoding_options_check.get_active()
        if show_opts:
            self.encoding_box.show()
        else:
            self.encoding_box.hide()

    def reset_errors(self):
        self.set_sensitive(True)
        self.set_info_text("")
        for widget in (self.info, self.password_entry, self.username_entry, self.host_entry, self.port_entry):
            self.set_widget_fg_color(self.info, False)
            self.set_widget_bg_color(widget, False)

    def set_info_text(self, text):
        if self.info:
            glib.idle_add(self.info.set_text, text)

    def set_info_color(self, is_error=False):
        self.set_widget_fg_color(self.info, is_error)


    def set_sensitive(self, s):
        glib.idle_add(self.window.set_sensitive, s)

    def connect_clicked(self, *_args):
        self.update_options_from_gui()
        self.do_connect()


    def reset_client(self):
        #lose current client class and make a new one:
        if self.client:
            self.client.cleanup()
            self.client = None
        self.client = make_client(Exception, self.config)
        self.client.init(self.config)


    def handle_exception(self, e):
        log("handle_exception(%s)", e)
        t = str(e)
        if self.config.debug:
            #in debug mode, include the full stacktrace:
            t = traceback.format_exc()
        def ui_handle_exception():
            self.reset_client()
            self.set_sensitive(True)
            if not self.current_error:
                self.current_error = t
                self.set_info_color(True)
                self.set_info_text(t)
            self.window.show()
            self.window.present()
        glib.idle_add(ui_handle_exception)

    def do_connect(self):
        try:
            self.connect_builtin()
        except Exception as e:
            log.error("cannot connect:", exc_info=True)
            self.handle_exception(e)

    def connect_builtin(self):
        #cooked vars used by connect_to
        params = {"type"    : self.config.mode}
        username = self.config.username
        if self.config.mode=="ssh":
            if self.config.socket_dir:
                params["socket_dir"] = self.config.socket_dir
            params["remote_xpra"] = self.config.remote_xpra
            params["proxy_command"] = ["_proxy"]
            if self.config.port and self.config.port>0:
                params["display"] = ":%s" % self.config.port
                params["display_as_args"] = [params["display"]]
            else:
                params["display"] = "auto"
                params["display_as_args"] = []
            full_ssh = shlex.split(self.config.ssh)
            password = self.config.password
            host = self.config.host
            upos = host.find("@")
            if upos>=0:
                #found at sign: username@host
                username = host[:upos]
                host = host[upos+1:]
                ppos = username.find(":")
                if ppos>=0:
                    #found separator: username:password@host
                    password = username[ppos+1:]
                    username = username[:ppos]
            if username:
                params["username"] = username
                full_ssh += ["-l", username]
            full_ssh += ["-T", host]
            if self.nostrict_host_check.get_active():
                full_ssh += ["-o", "StrictHostKeyChecking=no"]
            if str(self.config.ssh_port)!="22":
                if WIN32:
                    full_ssh += ["-P", str(self.config.ssh_port)]
                else:
                    full_ssh += ["-p", str(self.config.ssh_port)]
            params["host"] = host
            params["local"] = is_local(self.config.host)
            params["full_ssh"] = full_ssh
            params["password"] = password
            params["display_name"] = "ssh:%s:%s" % (self.config.host, self.config.port)
        elif self.config.mode=="unix-domain":
            params["display"] = ":%s" % self.config.port
            params["display_name"] = "unix-domain:%s" % self.config.port
        else:
            assert self.config.mode in ("tcp", "ssl"), "invalid / unsupported mode %s" % self.config.mode
            params["host"] = self.config.host
            params["local"] = is_local(self.config.host)
            params["port"] = int(self.config.port)
            params["display_name"] = "%s:%s:%s" % (self.config.mode, self.config.host, self.config.port)
            if self.config.mode=="ssl" and self.nostrict_host_check.get_active():
                params["strict-host-check"] = False

        #print("connect_to(%s)" % params)
        #UGLY warning: the username may have been updated during display parsing,
        #or the config file may contain a username which is different from the default one
        #which is used for initializing the client during init,
        #so update the client now:
        self.client.username = username
        self.set_info_text("Connecting...")
        thread.start_new_thread(self.do_connect_builtin, (params,))

    def ssh_failed(self, message):
        log("ssh_failed(%s)", message)
        if not self.current_error:
            self.current_error = message
            self.set_info_text(message)
            self.set_info_color(True)

    def do_connect_builtin(self, display_desc):
        log("do_connect_builtin(%s)", display_desc)
        self.exit_code = None
        self.current_error = None
        self.set_info_text("Connecting.")
        self.set_sensitive(False)
        try:
            conn = connect_to(display_desc, opts=self.config, debug_cb=self.set_info_text, ssh_fail_cb=self.ssh_failed)
        except Exception as e:
            log.error("failed to connect", exc_info=True)
            self.handle_exception(e)
            return
        glib.idle_add(self.window.hide)
        glib.idle_add(self.start_XpraClient, conn, display_desc)

    def start_XpraClient(self, conn, display_desc):
        try:
            self.do_start_XpraClient(conn, display_desc)
        except Exception as e:
            log.error("failed to start client", exc_info=True)
            self.handle_exception(e)

    def do_start_XpraClient(self, conn, display_desc={}):
        log("do_start_XpraClient(%s, %s) client=%s", conn, display_desc, self.client)
        self.client.encoding = self.config.encoding
        self.client.display_desc = display_desc
        self.client.setup_connection(conn)
        #we have already initialized it,
        #but calling client.init will do it again - so we have to clear it:
        from xpra.codecs.video_helper import getVideoHelper
        getVideoHelper().cleanup()
        self.client.init(self.config)
        self.client.init_ui(self.config)
        log("start_XpraClient() client initialized")

        if self.config.password:
            #pass the password to the class directly:
            def load_password():
                return self.config.password
            self.client.password_file = "FAKE-PASSWORD-FILE-FOR-LAUNCHER"
            self.client.load_password = load_password
        #override exit code:
        warn_and_quit_save = self.client.warn_and_quit
        quit_save = self.client.quit
        def do_quit(*_args):
            self.client.warn_and_quit = warn_and_quit_save
            self.client.quit = quit_save
            self.client.cleanup()
            self.destroy()
            gtk.main_quit()
        def warn_and_quit_override(exit_code, warning):
            log("warn_and_quit_override(%s, %s)", exit_code, warning)
            if self.exit_code == None:
                self.exit_code = exit_code
            password_warning = warning.find("invalid password")>=0
            if password_warning:
                self.password_warning()
            err = exit_code!=0 or password_warning
            if not self.current_error:
                self.current_error = warning
                self.set_info_color(err)
                self.set_info_text(warning)
            if err:
                def ignore_further_quit_events(*args):
                    pass
                if self.client:
                    self.client.cleanup()
                    self.client.warn_and_quit = ignore_further_quit_events
                    self.client.quit = ignore_further_quit_events
                w = self.window
                if w:
                    self.set_sensitive(True)
                    self.reset_client()
                    glib.idle_add(w.show)
            else:
                do_quit()

        def quit_override(exit_code):
            log("quit_override(%s)", exit_code)
            if self.exit_code == None:
                self.exit_code = exit_code
            self.client.cleanup()
            if self.exit_code==0:
                do_quit()
            else:
                self.reset_client()

        self.client.warn_and_quit = warn_and_quit_override
        self.client.quit = quit_override
        try:
            self.client.run()
        except Exception as e:
            log.error("client error", exc_info=True)
            self.handle_exception(e)


    def password_ok(self, *_args):
        self.password_entry.modify_text(STATE_NORMAL, black)

    def password_warning(self, *_args):
        self.password_entry.modify_text(STATE_NORMAL, red)
        self.password_entry.grab_focus()

    def set_widget_bg_color(self, widget, is_error=False):
        if is_error:
            color_obj = red
        else:
            color_obj = white
        if color_obj:
            glib.idle_add(widget.modify_base, STATE_NORMAL, color_obj)

    def set_widget_fg_color(self, widget, is_error=False):
        if is_error:
            color_obj = red
        else:
            color_obj = black
        if color_obj:
            glib.idle_add(widget.modify_fg, STATE_NORMAL, color_obj)


    def update_options_from_gui(self):
        def pint(v):
            try:
                return int(v)
            except ValueError:
                return 0
        self.config.host = self.host_entry.get_text()
        self.config.ssh_port = pint(self.ssh_port_entry.get_text())
        self.config.port = pint(self.port_entry.get_text())
        self.config.username = self.username_entry.get_text()
        self.config.encoding = self.get_selected_encoding() or self.config.encoding
        mode_enc = self.mode_combo.get_active_text().upper()
        if mode_enc.startswith("TCP"):
            self.config.mode = "tcp"
            if mode_enc.find("AES")>0 and "AES" in ENCRYPTION_CIPHERS:
                self.config.encryption = "AES"
        elif mode_enc=="SSL":
            self.config.mode = "ssl"
        else:
            self.config.mode = "ssh"
        self.config.password = self.password_entry.get_text()
        log("update_options_from_gui() %s", (self.config.username, self.config.password, self.config.mode, self.config.encryption, self.config.host, self.config.port, self.config.ssh_port, self.config.encoding))

    def update_gui_from_config(self):
        #mode:
        mode = (self.config.mode or "").lower()
        for i,e in enumerate(self.get_connection_modes()):
            if e.lower()==mode:
                self.mode_combo.set_active(i)
        if self.config.encoding and self.encoding_combo:
            index = self.encoding_combo.get_menu().encoding_to_index.get(self.config.encoding, -1)
            log("setting encoding combo to %s / %s", self.config.encoding, index)
            #make sure the right one is the only one selected:
            for i,item in enumerate(self.encoding_combo.get_menu().get_children()):
                item.set_active(i==index)
            #then select it in the combo:
            if index>=0:
                self.encoding_combo.set_history(index)
        self.username_entry.set_text(self.config.username)
        self.password_entry.set_text(self.config.password)
        self.host_entry.set_text(self.config.host)
        def get_port(v, default_port=DEFAULT_PORT):
            try:
                iport = int(v)
                if iport>0 and iport<2**16:
                    return str(iport)
            except:
                pass
            return str(default_port)
        dport = DEFAULT_PORT
        if mode=="ssh":
            #not required, so don't specify one
            dport = ""
        self.port_entry.set_text(get_port(self.config.port, dport))
        self.ssh_port_entry.set_text(get_port(self.config.ssh_port, 22))

    def destroy(self, *_args):
        w = self.window
        if w:
            self.window = None
            w.destroy()
        gtk.main_quit()

    def update_options_from_URL(self, url):
        from xpra.scripts.main import parse_URL
        address, props = parse_URL(url)
        pa = address.split(":")
        if pa[0] in ("tcp", "ssh") and len(pa)>=2:
            props["mode"] = pa
            host = pa[1]
            ph = host.split("@", 1)
            if len(ph)==2:
                username, host = ph
                props["username"] = username
            props["host"] = host
            if len(pa)>=3:
                props["port"] = pa[2]
        self._apply_props(props)

    def update_options_from_file(self, filename):
        log("update_options_from_file(%s)", filename)
        props = read_config(filename)
        self._apply_props(props)

    def _apply_props(self, props):
        #we rely on "ssh_port" being defined on the config object
        #so try to load it from file, and define it if not present:
        options = validate_config(props, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation())
        for k,v in options.items():
            fn = k.replace("-", "_")
            setattr(self.config, fn, v)
        self.config_keys = self.config_keys.union(set(props.keys()))
        log("_apply_props(%s) populated config with keys '%s', ssh=%s", props, options.keys(), self.config.ssh)

    def choose_session_file(self, title, action, action_button, callback):
        file_filter = gtk.FileFilter()
        file_filter.set_name("Xpra")
        file_filter.add_pattern("*.xpra")
        choose_file(self.window, title, action, action_button, callback, file_filter)

    def save_clicked(self, *_args):
        self.update_options_from_gui()
        def do_save(filename):
            #make sure the file extension is .xpra
            if os.path.splitext(filename)!=".xpra":
                filename += ".xpra"
            save_config(filename, self.config, self.config_keys, extras_types=LAUNCHER_OPTION_TYPES)
        self.choose_session_file("Save session settings to file", FILE_CHOOSER_ACTION_SAVE, gtk.STOCK_SAVE, do_save)

    def load_clicked(self, *_args):
        def do_load(filename):
            self.update_options_from_file(filename)
            self.update_gui_from_config()
        self.choose_session_file("Load session settings from file", FILE_CHOOSER_ACTION_OPEN, gtk.STOCK_OPEN, do_load)
Пример #2
0
class ApplicationWindow:

    def    __init__(self):
        # Default connection options
        self.config = make_defaults_struct(extras_defaults=LAUNCHER_DEFAULTS, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation())
        #TODO: the fixup does not belong here?
        from xpra.scripts.main import fixup_options
        fixup_options(self.config)
        #what we save by default:
        self.config_keys = set(SAVED_FIELDS)
        def raise_exception(*args):
            raise Exception(*args)
        self.client = make_client(raise_exception, self.config)
        self.client.init(self.config)
        self.exit_code = None

    def get_launcher_validation(self):
        crypto_backend_init()
        #TODO: since "mode" is not part of global options
        #this validation should be injected from the launcher instead
        MODES = ["tcp", "ssh"]
        if "AES" in ENCRYPTION_CIPHERS:
            MODES = ["tcp", "tcp + aes", "ssh"]
        def validate_in_list(x, options):
            if x in options:
                return None
            return "must be in %s" % (", ".join(options))
        return {"mode"              : lambda x : validate_in_list(x, MODES)}


    def create_window(self):
        self.window = gtk.Window()
        self.window.connect("destroy", self.destroy)
        self.window.set_default_size(400, 300)
        self.window.set_border_width(20)
        self.window.set_title("Xpra Launcher")
        self.window.modify_bg(STATE_NORMAL, gdk.Color(red=65535, green=65535, blue=65535))

        icon_pixbuf = self.get_icon("xpra.png")
        if icon_pixbuf:
            self.window.set_icon(icon_pixbuf)
        self.window.set_position(WIN_POS_CENTER)

        vbox = gtk.VBox(False, 0)
        vbox.set_spacing(15)

        # Title
        hbox = gtk.HBox(False, 0)
        if icon_pixbuf:
            logo_button = gtk.Button("")
            settings = logo_button.get_settings()
            settings.set_property('gtk-button-images', True)
            logo_button.connect("clicked", about)
            logo_button.set_tooltip_text("About")
            image = gtk.Image()
            image.set_from_pixbuf(icon_pixbuf)
            logo_button.set_image(image)
            hbox.pack_start(logo_button, expand=False, fill=False)
        icon_pixbuf = self.get_icon("bugs.png")
        self.bug_tool = None
        if icon_pixbuf:
            bug_button = gtk.Button("")
            settings = bug_button.get_settings()
            settings.set_property('gtk-button-images', True)
            def bug(*args):
                if self.bug_tool==None:
                    from xpra.client.gtk_base.bug_report import BugReport
                    self.bug_tool = BugReport()
                    self.bug_tool.init(show_about=False)
                self.bug_tool.show()
            bug_button.connect("clicked", bug)
            bug_button.set_tooltip_text("Bug Report")
            image = gtk.Image()
            image.set_from_pixbuf(icon_pixbuf)
            bug_button.set_image(image)
            hbox.pack_start(bug_button, expand=False, fill=False)
        label = gtk.Label("Connect to xpra server")
        label.modify_font(pango.FontDescription("sans 14"))
        hbox.pack_start(label, expand=True, fill=True)
        vbox.pack_start(hbox)

        # Mode:
        hbox = gtk.HBox(False, 20)
        hbox.set_spacing(20)
        hbox.pack_start(gtk.Label("Mode: "))
        self.mode_combo = gtk.combo_box_new_text()
        #self.mode_combo.get_model().clear()
        self.mode_combo.append_text("TCP")
        if "AES" in ENCRYPTION_CIPHERS:
            self.mode_combo.append_text("TCP + AES")
        self.mode_combo.append_text("SSH")
        self.mode_combo.connect("changed", self.mode_changed)
        hbox.pack_start(self.mode_combo)
        vbox.pack_start(hbox)

        self.encoding_combo = None
        if not PYTHON3:
            #not implemented for gtk3, where we can't use set_menu()...

            # Encoding:
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            hbox.pack_start(gtk.Label("Encoding: "))
            self.encoding_combo = OptionMenu()
            def get_current_encoding():
                return self.config.encoding
            def set_new_encoding(e):
                self.config.encoding = e
            encodings = [x for x in PREFERED_ENCODING_ORDER if x in self.client.get_encodings()]
            server_encodings = encodings
            es = make_encodingsmenu(get_current_encoding, set_new_encoding, encodings, server_encodings)
            self.encoding_combo.set_menu(es)
            set_history_from_active(self.encoding_combo)
            hbox.pack_start(self.encoding_combo)
            vbox.pack_start(hbox)
            self.encoding_combo.connect("changed", self.encoding_changed)

            # Quality
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            self.quality_label = gtk.Label("Quality: ")
            hbox.pack_start(self.quality_label)
            self.quality_combo = OptionMenu()
            def set_min_quality(q):
                self.config.min_quality = q
            def set_quality(q):
                self.config.quality = q
            def get_min_quality():
                return self.config.min_quality
            def get_quality():
                return self.config.quality
            sq = make_min_auto_menu("Quality", MIN_QUALITY_OPTIONS, QUALITY_OPTIONS,
                                       get_min_quality, get_quality, set_min_quality, set_quality)
            self.quality_combo.set_menu(sq)
            set_history_from_active(self.quality_combo)
            hbox.pack_start(self.quality_combo)
            vbox.pack_start(hbox)

            # Speed
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            self.speed_label = gtk.Label("Speed: ")
            hbox.pack_start(self.speed_label)
            self.speed_combo = OptionMenu()
            def set_min_speed(s):
                self.config.min_speed = s
            def set_speed(s):
                self.config.speed = s
            def get_min_speed():
                return self.config.min_speed
            def get_speed():
                return self.config.speed
            ss = make_min_auto_menu("Speed", MIN_SPEED_OPTIONS, SPEED_OPTIONS,
                                       get_min_speed, get_speed, set_min_speed, set_speed)
            self.speed_combo.set_menu(ss)
            set_history_from_active(self.speed_combo)
            hbox.pack_start(self.speed_combo)
            vbox.pack_start(hbox)

        # Username@Host:Port
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(5)
        self.username_entry = gtk.Entry()
        self.username_entry.set_max_length(128)
        self.username_entry.set_width_chars(16)
        self.username_entry.connect("changed", self.validate)
        self.username_entry.set_tooltip_text("SSH username")
        self.username_label = gtk.Label("@")
        self.host_entry = gtk.Entry()
        self.host_entry.set_max_length(128)
        self.host_entry.set_width_chars(24)
        self.host_entry.connect("changed", self.validate)
        self.host_entry.set_tooltip_text("hostname")
        self.ssh_port_entry = gtk.Entry()
        self.ssh_port_entry.set_max_length(5)
        self.ssh_port_entry.set_width_chars(5)
        self.ssh_port_entry.connect("changed", self.validate)
        self.ssh_port_entry.set_tooltip_text("SSH port")
        self.port_entry = gtk.Entry()
        self.port_entry.set_max_length(5)
        self.port_entry.set_width_chars(5)
        self.port_entry.connect("changed", self.validate)
        self.port_entry.set_tooltip_text("port/display")

        hbox.pack_start(self.username_entry)
        hbox.pack_start(self.username_label)
        hbox.pack_start(self.host_entry)
        hbox.pack_start(self.ssh_port_entry)
        hbox.pack_start(gtk.Label(":"))
        hbox.pack_start(self.port_entry)
        vbox.pack_start(hbox)

        # Password
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(20)
        self.password_entry = gtk.Entry()
        self.password_entry.set_max_length(128)
        self.password_entry.set_width_chars(30)
        self.password_entry.set_text("")
        self.password_entry.set_visibility(False)
        self.password_entry.connect("changed", self.password_ok)
        self.password_entry.connect("changed", self.validate)
        self.password_label = gtk.Label("Password: "******"Save")
        self.save_btn.set_tooltip_text("Save settings to a session file")
        self.save_btn.connect("clicked", self.save_clicked)
        hbox.pack_start(self.save_btn)
        #Load:
        self.load_btn = gtk.Button("Load")
        self.load_btn.set_tooltip_text("Load settings from a session file")
        self.load_btn.connect("clicked", self.load_clicked)
        hbox.pack_start(self.load_btn)
        # Connect button:
        self.button = gtk.Button("Connect")
        self.button.connect("clicked", self.connect_clicked)
        connect_icon = self.get_icon("retry.png")
        if connect_icon:
            self.button.set_image(scaled_image(connect_icon, 24))
        hbox.pack_start(self.button)

        def accel_close(*args):
            gtk.main_quit()
        add_close_accel(self.window, accel_close)
        vbox.show_all()
        self.window.vbox = vbox
        self.window.add(vbox)

    def validate(self, *args):
        ssh = self.mode_combo.get_active_text()=="SSH"
        errs = []
        host = self.host_entry.get_text()
        errs.append((self.host_entry, not bool(host), "specify the host"))
        if ssh:
            #validate ssh port:
            ssh_port = self.ssh_port_entry.get_text()
            try:
                ssh_port = int(ssh_port)
            except:
                ssh_port = -1
            errs.append((self.ssh_port_entry, ssh_port<0 or ssh_port>=2**16, "invalid SSH port number"))
        port = self.port_entry.get_text()
        if ssh and not port:
            port = 0        #port optional with ssh
        else:
            try:
                port = int(port)
            except:
                port = -1
        errs.append((self.port_entry, port<0 or port>=2**16, "invalid port number"))
        err_text = []
        for w, e, text in errs:
            self.set_widget_bg_color(w, e)
            if e:
                err_text.append(text)
        log("validate(%s) err_text=%s, errs=%s", args, err_text, errs)
        self.set_info_text(", ".join(err_text))
        self.set_info_color(len(err_text)>0)
        self.button.set_sensitive(len(err_text)==0)
        return errs

    def show(self):
        self.window.show()
        self.window.present()

    def run(self):
        gtk_main()

    def get_icon(self, icon_name):
        icon_filename = os.path.join(get_icon_dir(), icon_name)
        if os.path.exists(icon_filename):
            return pixbuf_new_from_file(icon_filename)
        return None

    def mode_changed(self, *args):
        ssh = self.mode_combo.get_active_text()=="SSH"
        self.port_entry.set_text("")
        if ssh:
            self.ssh_port_entry.show()
            self.port_entry.set_tooltip_text("Display number (optional)")
            self.password_entry.set_tooltip_text("SSH Password")
            self.username_entry.show()
            self.username_label.show()
        else:
            self.ssh_port_entry.hide()
            self.port_entry.set_tooltip_text("port number")
            self.password_entry.set_tooltip_text("Session Password")
            self.username_entry.hide()
            self.username_label.hide()
            if self.config.port>0:
                self.port_entry.set_text("%s" % self.config.port)
        if not ssh or sys.platform.startswith("win") or sys.platform.startswith("darwin"):
            #password cannot be used with ssh
            #(except on win32 with plink, and on osx via the SSH_ASKPASS hack)
            self.password_label.show()
            self.password_entry.show()
        else:
            self.password_label.hide()
            self.password_entry.hide()
        self.validate()

    def get_selected_encoding(self, *args):
        if not self.encoding_combo:
            return ""
        index = get_active_item_index(self.encoding_combo)
        return self.encoding_combo.get_menu().index_to_encoding.get(index)

    def encoding_changed(self, *args):
        encoding = self.get_selected_encoding()
        uses_quality_option = encoding in ["jpeg", "webp", "h264"]
        log("encoding_changed(%s) uses_quality_option(%s)=%s", args, encoding, uses_quality_option)
        if uses_quality_option:
            self.quality_combo.show()
            self.quality_label.show()
        else:
            self.quality_combo.hide()
            self.quality_label.hide()

    def reset_errors(self):
        self.set_sensitive(True)
        self.set_info_text("")
        for widget in (self.info, self.password_entry, self.username_entry, self.host_entry, self.port_entry):
            self.set_widget_fg_color(self.info, False)
            self.set_widget_bg_color(widget, False)

    def set_info_text(self, text):
        if self.info:
            glib.idle_add(self.info.set_text, text)

    def set_info_color(self, is_error=False):
        self.set_widget_fg_color(self.info, is_error)


    def set_sensitive(self, s):
        glib.idle_add(self.window.set_sensitive, s)

    def connect_clicked(self, *args):
        self.update_options_from_gui()
        self.do_connect()


    def reset_client(self):
        #lose current client class and make a new one:
        if self.client:
            self.client.cleanup()
            self.client = None
        self.client = make_client(Exception, self.config)
        self.client.init(self.config)


    def handle_exception(self, e):
        t = str(e)
        if self.config.debug:
            #in debug mode, include the full stacktrace:
            t = traceback.format_exc()
        def ui_handle_exception():
            self.reset_client()
            self.set_sensitive(True)
            self.set_info_color(True)
            self.set_info_text(t)
            self.window.show()
            self.window.present()
        glib.idle_add(ui_handle_exception)

    def do_connect(self):
        try:
            self.connect_builtin()
        except Exception as e:
            log.error("cannot connect:", exc_info=True)
            self.handle_exception(e)

    def connect_builtin(self):
        #cooked vars used by connect_to
        params = {"type"    : self.config.mode}
        username = self.config.username
        if self.config.mode=="ssh":
            if self.config.socket_dir:
                params["socket_dir"] = self.config.socket_dir
            params["remote_xpra"] = self.config.remote_xpra
            params["proxy_command"] = ["_proxy"]
            if self.config.port and self.config.port>0:
                params["display"] = ":%s" % self.config.port
                params["display_as_args"] = [params["display"]]
            else:
                params["display"] = "auto"
                params["display_as_args"] = []
            full_ssh = shlex.split(self.config.ssh)
            password = self.config.password
            host = self.config.host
            upos = host.find("@")
            if upos>=0:
                #found at sign: username@host
                username = host[:upos]
                host = host[upos+1:]
                ppos = username.find(":")
                if ppos>=0:
                    #found separator: username:password@host
                    password = username[ppos+1:]
                    username = username[:ppos]
            if username:
                params["username"] = username
                full_ssh += ["-l", username]
            full_ssh += ["-T", host]
            if str(self.config.ssh_port)!="22":
                if sys.platform.startswith("win"):
                    full_ssh += ["-P", str(self.config.ssh_port)]
                else:
                    full_ssh += ["-p", str(self.config.ssh_port)]
            params["full_ssh"] = full_ssh
            params["password"] = password
            params["display_name"] = "ssh:%s:%s" % (self.config.host, self.config.port)
        elif self.config.mode=="unix-domain":
            params["display"] = ":%s" % self.config.port
            params["display_name"] = "unix-domain:%s" % self.config.port
        else:
            #tcp:
            params["host"] = self.config.host
            params["port"] = int(self.config.port)
            params["display_name"] = "tcp:%s:%s" % (self.config.host, self.config.port)

        #print("connect_to(%s)" % params)
        #UGLY warning: the username may have been updated during display parsing,
        #or the config file may contain a username which is different from the default one
        #which is used for initializing the client during init,
        #so update the client now:
        self.client.username = username
        self.set_info_text("Connecting...")
        thread.start_new_thread(self.do_connect_builtin, (params,))

    def ssh_failed(self, message):
        self.set_info_text(message)
        self.set_info_color(True)

    def do_connect_builtin(self, params):
        self.exit_code = None
        self.set_info_text("Connecting.")
        self.set_sensitive(False)
        try:
            conn = connect_to(params, self.set_info_text, ssh_fail_cb=self.ssh_failed)
        except Exception as e:
            log.error("failed to connect", exc_info=True)
            self.handle_exception(e)
            return
        glib.idle_add(self.window.hide)
        glib.idle_add(self.start_XpraClient, conn)

    def start_XpraClient(self, conn):
        try:
            self.do_start_XpraClient(conn)
        except Exception as e:
            log.error("failed to start client", exc_info=True)
            self.handle_exception(e)

    def do_start_XpraClient(self, conn):
        log("start_XpraClient() client=%s", self.client)
        self.client.encoding = self.config.encoding
        self.client.setup_connection(conn)
        self.client.init_ui(self.config)
        log("start_XpraClient() client initialized")

        if self.config.password:
            #pass the password to the class directly:
            def load_password():
                return self.config.password
            self.client.password_file = "FAKE-PASSWORD-FILE-FOR-LAUNCHER"
            self.client.load_password = load_password
        #override exit code:
        warn_and_quit_save = self.client.warn_and_quit
        quit_save = self.client.quit
        def do_quit(*args):
            self.client.warn_and_quit = warn_and_quit_save
            self.client.quit = quit_save
            self.client.cleanup()
            self.destroy()
            gtk.main_quit()
        def warn_and_quit_override(exit_code, warning):
            log("warn_and_quit_override(%s, %s)", exit_code, warning)
            if self.exit_code == None:
                self.exit_code = exit_code
            password_warning = warning.find("invalid password")>=0
            if password_warning:
                self.password_warning()
            err = exit_code!=0 or password_warning
            self.set_info_color(err)
            self.set_info_text(warning)
            if err:
                def ignore_further_quit_events(*args):
                    pass
                if self.client:
                    self.client.cleanup()
                    self.client.warn_and_quit = ignore_further_quit_events
                    self.client.quit = ignore_further_quit_events
                self.set_sensitive(True)
                self.reset_client()
                glib.idle_add(self.window.show)
            else:
                do_quit()

        def quit_override(exit_code):
            log("quit_override(%s)", exit_code)
            if self.exit_code == None:
                self.exit_code = exit_code
            self.client.cleanup()
            if self.exit_code==0:
                do_quit()
            else:
                self.reset_client()

        self.client.warn_and_quit = warn_and_quit_override
        self.client.quit = quit_override
        try:
            self.client.run()
        except Exception as e:
            log.error("client error", exc_info=True)
            self.handle_exception(e)


    def password_ok(self, *args):
        self.password_entry.modify_text(STATE_NORMAL, black)

    def password_warning(self, *args):
        self.password_entry.modify_text(STATE_NORMAL, red)
        self.password_entry.grab_focus()

    def set_widget_bg_color(self, widget, is_error=False):
        if is_error:
            color_obj = red
        else:
            color_obj = white
        if color_obj:
            glib.idle_add(widget.modify_base, STATE_NORMAL, color_obj)

    def set_widget_fg_color(self, widget, is_error=False):
        if is_error:
            color_obj = red
        else:
            color_obj = black
        if color_obj:
            glib.idle_add(widget.modify_fg, STATE_NORMAL, color_obj)


    def update_options_from_gui(self):
        self.config.host = self.host_entry.get_text()
        self.config.ssh_port = self.ssh_port_entry.get_text()
        self.config.port = self.port_entry.get_text()
        self.config.username = self.username_entry.get_text()
        self.config.encoding = self.get_selected_encoding() or self.config.encoding
        mode_enc = self.mode_combo.get_active_text()
        if mode_enc.startswith("TCP"):
            self.config.mode = "tcp"
            if mode_enc.find("AES")>0 and "AES" in ENCRYPTION_CIPHERS:
                self.config.encryption = "AES"
        else:
            self.config.mode = "ssh"
        self.config.password = self.password_entry.get_text()
        log("update_options_from_gui() %s", (self.config.username, self.config.password, self.config.mode, self.config.encryption, self.config.host, self.config.port, self.config.ssh_port, self.config.encoding))

    def update_gui_from_config(self):
        #mode:
        if self.config.mode == "tcp":
            self.mode_combo.set_active(0)
        elif self.config.mode == "tcp + aes" and "AES" in ENCRYPTION_CIPHERS:
            self.mode_combo.set_active(1)
        else:
            self.mode_combo.set_active(2)
        if self.config.encoding and self.encoding_combo:
            index = self.encoding_combo.get_menu().encoding_to_index.get(self.config.encoding, -1)
            log("setting encoding combo to %s / %s", self.config.encoding, index)
            if index>=0:
                self.encoding_combo.set_history(index)
        self.username_entry.set_text(self.config.username)
        self.password_entry.set_text(self.config.password)
        self.host_entry.set_text(self.config.host)
        def get_port(v):
            try:
                iport = int(v)
                if iport>0:
                    return str(iport)
            except:
                pass
            return ""
        self.port_entry.set_text(get_port(self.config.port))
        self.ssh_port_entry.set_text(get_port(self.config.ssh_port))

    def destroy(self, *args):
        self.window.destroy()
        self.window = None
        gtk.main_quit()

    def update_options_from_file(self, filename):
        props = read_config(filename)
        #we rely on "ssh_port" being defined on the config object
        #so try to load it from file, and define it if not present:
        options = validate_config(props, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation())
        for k,v in options.items():
            fn = k.replace("-", "_")
            setattr(self.config, fn, v)
        self.config_keys = self.config_keys.union(set(props.keys()))
        log("update_options_from_file(%s) populated config with keys '%s', ssh=%s", filename, options.keys(), self.config.ssh)

    def choose_session_file(self, title, action, action_button, callback):
        file_filter = gtk.FileFilter()
        file_filter.set_name("Xpra")
        file_filter.add_pattern("*.xpra")
        choose_file(self.window, title, action, action_button, callback, file_filter)

    def save_clicked(self, *args):
        self.update_options_from_gui()
        def do_save(filename):
            save_config(filename, self.config, self.config_keys, extras_types=LAUNCHER_OPTION_TYPES)
        self.choose_session_file("Save session settings to file", FILE_CHOOSER_ACTION_SAVE, gtk.STOCK_SAVE, do_save)

    def load_clicked(self, *args):
        def do_load(filename):
            self.update_options_from_file(filename)
            self.update_gui_from_config()
        self.choose_session_file("Load session settings from file", FILE_CHOOSER_ACTION_OPEN, gtk.STOCK_OPEN, do_load)
Пример #3
0
class ApplicationWindow:
    def __init__(self):
        # Default connection options
        self.config = make_defaults_struct(
            extras_defaults=LAUNCHER_DEFAULTS,
            extras_types=LAUNCHER_OPTION_TYPES,
            extras_validation=self.get_launcher_validation())
        #TODO: the fixup does not belong here?
        from xpra.scripts.main import fixup_options
        fixup_options(self.config)
        #what we save by default:
        self.config_keys = set(SAVED_FIELDS)

        def raise_exception(*args):
            raise Exception(*args)

        self.client = make_client(raise_exception, self.config)
        self.client.init(self.config)
        self.exit_code = None

    def get_launcher_validation(self):
        crypto_backend_init()
        #TODO: since "mode" is not part of global options
        #this validation should be injected from the launcher instead
        MODES = ["tcp", "ssh"]
        if "AES" in ENCRYPTION_CIPHERS:
            MODES = ["tcp", "tcp + aes", "ssh"]

        def validate_in_list(x, options):
            if x in options:
                return None
            return "must be in %s" % (", ".join(options))

        return {"mode": lambda x: validate_in_list(x, MODES)}

    def create_window(self):
        self.window = gtk.Window()
        self.window.connect("destroy", self.destroy)
        self.window.set_default_size(400, 300)
        self.window.set_border_width(20)
        self.window.set_title("Xpra Launcher")
        self.window.modify_bg(STATE_NORMAL,
                              gdk.Color(red=65535, green=65535, blue=65535))

        icon_pixbuf = self.get_icon("xpra.png")
        if icon_pixbuf:
            self.window.set_icon(icon_pixbuf)
        self.window.set_position(WIN_POS_CENTER)

        vbox = gtk.VBox(False, 0)
        vbox.set_spacing(15)

        # Title
        hbox = gtk.HBox(False, 0)
        if icon_pixbuf:
            logo_button = gtk.Button("")
            settings = logo_button.get_settings()
            settings.set_property('gtk-button-images', True)
            logo_button.connect("clicked", about)
            logo_button.set_tooltip_text("About")
            image = gtk.Image()
            image.set_from_pixbuf(icon_pixbuf)
            logo_button.set_image(image)
            hbox.pack_start(logo_button, expand=False, fill=False)
        icon_pixbuf = self.get_icon("bugs.png")
        self.bug_tool = None
        if icon_pixbuf:
            bug_button = gtk.Button("")
            settings = bug_button.get_settings()
            settings.set_property('gtk-button-images', True)

            def bug(*args):
                if self.bug_tool == None:
                    from xpra.client.gtk_base.bug_report import BugReport
                    self.bug_tool = BugReport()
                    self.bug_tool.init(show_about=False)
                self.bug_tool.show()

            bug_button.connect("clicked", bug)
            bug_button.set_tooltip_text("Bug Report")
            image = gtk.Image()
            image.set_from_pixbuf(icon_pixbuf)
            bug_button.set_image(image)
            hbox.pack_start(bug_button, expand=False, fill=False)
        label = gtk.Label("Connect to xpra server")
        label.modify_font(pango.FontDescription("sans 14"))
        hbox.pack_start(label, expand=True, fill=True)
        vbox.pack_start(hbox)

        # Mode:
        hbox = gtk.HBox(False, 20)
        hbox.set_spacing(20)
        hbox.pack_start(gtk.Label("Mode: "))
        self.mode_combo = gtk.combo_box_new_text()
        #self.mode_combo.get_model().clear()
        self.mode_combo.append_text("TCP")
        if "AES" in ENCRYPTION_CIPHERS:
            self.mode_combo.append_text("TCP + AES")
        self.mode_combo.append_text("SSH")
        self.mode_combo.connect("changed", self.mode_changed)
        hbox.pack_start(self.mode_combo)
        vbox.pack_start(hbox)

        self.encoding_combo = None
        if not PYTHON3:
            #not implemented for gtk3, where we can't use set_menu()...

            # Encoding:
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            hbox.pack_start(gtk.Label("Encoding: "))
            self.encoding_combo = OptionMenu()

            def get_current_encoding():
                return self.config.encoding

            def set_new_encoding(e):
                self.config.encoding = e

            encodings = [
                x for x in PREFERED_ENCODING_ORDER
                if x in self.client.get_encodings()
            ]
            server_encodings = encodings
            es = make_encodingsmenu(get_current_encoding, set_new_encoding,
                                    encodings, server_encodings)
            self.encoding_combo.set_menu(es)
            set_history_from_active(self.encoding_combo)
            hbox.pack_start(self.encoding_combo)
            vbox.pack_start(hbox)
            self.encoding_combo.connect("changed", self.encoding_changed)

            # Quality
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            self.quality_label = gtk.Label("Quality: ")
            hbox.pack_start(self.quality_label)
            self.quality_combo = OptionMenu()

            def set_min_quality(q):
                self.config.min_quality = q

            def set_quality(q):
                self.config.quality = q

            def get_min_quality():
                return self.config.min_quality

            def get_quality():
                return self.config.quality

            sq = make_min_auto_menu("Quality", MIN_QUALITY_OPTIONS,
                                    QUALITY_OPTIONS, get_min_quality,
                                    get_quality, set_min_quality, set_quality)
            self.quality_combo.set_menu(sq)
            set_history_from_active(self.quality_combo)
            hbox.pack_start(self.quality_combo)
            vbox.pack_start(hbox)

            # Speed
            hbox = gtk.HBox(False, 20)
            hbox.set_spacing(20)
            self.speed_label = gtk.Label("Speed: ")
            hbox.pack_start(self.speed_label)
            self.speed_combo = OptionMenu()

            def set_min_speed(s):
                self.config.min_speed = s

            def set_speed(s):
                self.config.speed = s

            def get_min_speed():
                return self.config.min_speed

            def get_speed():
                return self.config.speed

            ss = make_min_auto_menu("Speed", MIN_SPEED_OPTIONS, SPEED_OPTIONS,
                                    get_min_speed, get_speed, set_min_speed,
                                    set_speed)
            self.speed_combo.set_menu(ss)
            set_history_from_active(self.speed_combo)
            hbox.pack_start(self.speed_combo)
            vbox.pack_start(hbox)

        # Username@Host:Port
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(5)
        self.username_entry = gtk.Entry()
        self.username_entry.set_max_length(128)
        self.username_entry.set_width_chars(16)
        self.username_entry.connect("changed", self.validate)
        self.username_entry.set_tooltip_text("SSH username")
        self.username_label = gtk.Label("@")
        self.host_entry = gtk.Entry()
        self.host_entry.set_max_length(128)
        self.host_entry.set_width_chars(24)
        self.host_entry.connect("changed", self.validate)
        self.host_entry.set_tooltip_text("hostname")
        self.ssh_port_entry = gtk.Entry()
        self.ssh_port_entry.set_max_length(5)
        self.ssh_port_entry.set_width_chars(5)
        self.ssh_port_entry.connect("changed", self.validate)
        self.ssh_port_entry.set_tooltip_text("SSH port")
        self.port_entry = gtk.Entry()
        self.port_entry.set_max_length(5)
        self.port_entry.set_width_chars(5)
        self.port_entry.connect("changed", self.validate)
        self.port_entry.set_tooltip_text("port/display")

        hbox.pack_start(self.username_entry)
        hbox.pack_start(self.username_label)
        hbox.pack_start(self.host_entry)
        hbox.pack_start(self.ssh_port_entry)
        hbox.pack_start(gtk.Label(":"))
        hbox.pack_start(self.port_entry)
        vbox.pack_start(hbox)

        # Password
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(20)
        self.password_entry = gtk.Entry()
        self.password_entry.set_max_length(128)
        self.password_entry.set_width_chars(30)
        self.password_entry.set_text("")
        self.password_entry.set_visibility(False)
        self.password_entry.connect("changed", self.password_ok)
        self.password_entry.connect("changed", self.validate)
        self.password_label = gtk.Label("Password: "******"Save")
        self.save_btn.set_tooltip_text("Save settings to a session file")
        self.save_btn.connect("clicked", self.save_clicked)
        hbox.pack_start(self.save_btn)
        #Load:
        self.load_btn = gtk.Button("Load")
        self.load_btn.set_tooltip_text("Load settings from a session file")
        self.load_btn.connect("clicked", self.load_clicked)
        hbox.pack_start(self.load_btn)
        # Connect button:
        self.button = gtk.Button("Connect")
        self.button.connect("clicked", self.connect_clicked)
        connect_icon = self.get_icon("retry.png")
        if connect_icon:
            self.button.set_image(scaled_image(connect_icon, 24))
        hbox.pack_start(self.button)

        def accel_close(*args):
            gtk.main_quit()

        add_close_accel(self.window, accel_close)
        vbox.show_all()
        self.window.vbox = vbox
        self.window.add(vbox)

    def validate(self, *args):
        ssh = self.mode_combo.get_active_text() == "SSH"
        errs = []
        host = self.host_entry.get_text()
        errs.append((self.host_entry, not bool(host), "specify the host"))
        if ssh:
            #validate ssh port:
            ssh_port = self.ssh_port_entry.get_text()
            try:
                ssh_port = int(ssh_port)
            except:
                ssh_port = -1
            errs.append((self.ssh_port_entry, ssh_port < 0
                         or ssh_port >= 2**16, "invalid SSH port number"))
        port = self.port_entry.get_text()
        if ssh and not port:
            port = 0  #port optional with ssh
        else:
            try:
                port = int(port)
            except:
                port = -1
        errs.append((self.port_entry, port < 0
                     or port >= 2**16, "invalid port number"))
        err_text = []
        for w, e, text in errs:
            self.set_widget_bg_color(w, e)
            if e:
                err_text.append(text)
        log("validate(%s) err_text=%s, errs=%s", args, err_text, errs)
        self.set_info_text(", ".join(err_text))
        self.set_info_color(len(err_text) > 0)
        self.button.set_sensitive(len(err_text) == 0)
        return errs

    def show(self):
        self.window.show()
        self.window.present()

    def run(self):
        gtk_main()

    def get_icon(self, icon_name):
        icon_filename = os.path.join(get_icon_dir(), icon_name)
        if os.path.exists(icon_filename):
            return pixbuf_new_from_file(icon_filename)
        return None

    def mode_changed(self, *args):
        ssh = self.mode_combo.get_active_text() == "SSH"
        self.port_entry.set_text("")
        if ssh:
            self.ssh_port_entry.show()
            self.port_entry.set_tooltip_text("Display number (optional)")
            self.password_entry.set_tooltip_text("SSH Password")
            self.username_entry.show()
            self.username_label.show()
        else:
            self.ssh_port_entry.hide()
            self.port_entry.set_tooltip_text("port number")
            self.password_entry.set_tooltip_text("Session Password")
            self.username_entry.hide()
            self.username_label.hide()
            if self.config.port > 0:
                self.port_entry.set_text("%s" % self.config.port)
        if not ssh or sys.platform.startswith(
                "win") or sys.platform.startswith("darwin"):
            #password cannot be used with ssh
            #(except on win32 with plink, and on osx via the SSH_ASKPASS hack)
            self.password_label.show()
            self.password_entry.show()
        else:
            self.password_label.hide()
            self.password_entry.hide()
        self.validate()

    def get_selected_encoding(self, *args):
        if not self.encoding_combo:
            return ""
        index = get_active_item_index(self.encoding_combo)
        return self.encoding_combo.get_menu().index_to_encoding.get(index)

    def encoding_changed(self, *args):
        encoding = self.get_selected_encoding()
        uses_quality_option = encoding in ["jpeg", "webp", "h264"]
        log("encoding_changed(%s) uses_quality_option(%s)=%s", args, encoding,
            uses_quality_option)
        if uses_quality_option:
            self.quality_combo.show()
            self.quality_label.show()
        else:
            self.quality_combo.hide()
            self.quality_label.hide()

    def reset_errors(self):
        self.set_sensitive(True)
        self.set_info_text("")
        for widget in (self.info, self.password_entry, self.username_entry,
                       self.host_entry, self.port_entry):
            self.set_widget_fg_color(self.info, False)
            self.set_widget_bg_color(widget, False)

    def set_info_text(self, text):
        if self.info:
            glib.idle_add(self.info.set_text, text)

    def set_info_color(self, is_error=False):
        self.set_widget_fg_color(self.info, is_error)

    def set_sensitive(self, s):
        glib.idle_add(self.window.set_sensitive, s)

    def connect_clicked(self, *args):
        self.update_options_from_gui()
        self.do_connect()

    def reset_client(self):
        #lose current client class and make a new one:
        if self.client:
            self.client.cleanup()
            self.client = None
        self.client = make_client(Exception, self.config)
        self.client.init(self.config)

    def handle_exception(self, e):
        t = str(e)
        if self.config.debug:
            #in debug mode, include the full stacktrace:
            t = traceback.format_exc()

        def ui_handle_exception():
            self.reset_client()
            self.set_sensitive(True)
            self.set_info_color(True)
            self.set_info_text(t)
            self.window.show()
            self.window.present()

        glib.idle_add(ui_handle_exception)

    def do_connect(self):
        try:
            self.connect_builtin()
        except Exception as e:
            log.error("cannot connect:", exc_info=True)
            self.handle_exception(e)

    def connect_builtin(self):
        #cooked vars used by connect_to
        params = {"type": self.config.mode}
        username = self.config.username
        if self.config.mode == "ssh":
            if self.config.socket_dir:
                params["socket_dir"] = self.config.socket_dir
            params["remote_xpra"] = self.config.remote_xpra
            params["proxy_command"] = ["_proxy"]
            if self.config.port and self.config.port > 0:
                params["display"] = ":%s" % self.config.port
                params["display_as_args"] = [params["display"]]
            else:
                params["display"] = "auto"
                params["display_as_args"] = []
            full_ssh = shlex.split(self.config.ssh)
            password = self.config.password
            host = self.config.host
            upos = host.find("@")
            if upos >= 0:
                #found at sign: username@host
                username = host[:upos]
                host = host[upos + 1:]
                ppos = username.find(":")
                if ppos >= 0:
                    #found separator: username:password@host
                    password = username[ppos + 1:]
                    username = username[:ppos]
            if username:
                params["username"] = username
                full_ssh += ["-l", username]
            full_ssh += ["-T", host]
            if str(self.config.ssh_port) != "22":
                if sys.platform.startswith("win"):
                    full_ssh += ["-P", str(self.config.ssh_port)]
                else:
                    full_ssh += ["-p", str(self.config.ssh_port)]
            params["full_ssh"] = full_ssh
            params["password"] = password
            params["display_name"] = "ssh:%s:%s" % (self.config.host,
                                                    self.config.port)
        elif self.config.mode == "unix-domain":
            params["display"] = ":%s" % self.config.port
            params["display_name"] = "unix-domain:%s" % self.config.port
        else:
            #tcp:
            params["host"] = self.config.host
            params["port"] = int(self.config.port)
            params["display_name"] = "tcp:%s:%s" % (self.config.host,
                                                    self.config.port)

        #print("connect_to(%s)" % params)
        #UGLY warning: the username may have been updated during display parsing,
        #or the config file may contain a username which is different from the default one
        #which is used for initializing the client during init,
        #so update the client now:
        self.client.username = username
        self.set_info_text("Connecting...")
        thread.start_new_thread(self.do_connect_builtin, (params, ))

    def ssh_failed(self, message):
        self.set_info_text(message)
        self.set_info_color(True)

    def do_connect_builtin(self, params):
        self.exit_code = None
        self.set_info_text("Connecting.")
        self.set_sensitive(False)
        try:
            conn = connect_to(params,
                              self.set_info_text,
                              ssh_fail_cb=self.ssh_failed)
        except Exception as e:
            log.error("failed to connect", exc_info=True)
            self.handle_exception(e)
            return
        glib.idle_add(self.window.hide)
        glib.idle_add(self.start_XpraClient, conn)

    def start_XpraClient(self, conn):
        try:
            self.do_start_XpraClient(conn)
        except Exception as e:
            log.error("failed to start client", exc_info=True)
            self.handle_exception(e)

    def do_start_XpraClient(self, conn):
        log("start_XpraClient() client=%s", self.client)
        self.client.encoding = self.config.encoding
        self.client.setup_connection(conn)
        self.client.init_ui(self.config)
        log("start_XpraClient() client initialized")

        if self.config.password:
            #pass the password to the class directly:
            def load_password():
                return self.config.password

            self.client.password_file = "FAKE-PASSWORD-FILE-FOR-LAUNCHER"
            self.client.load_password = load_password
        #override exit code:
        warn_and_quit_save = self.client.warn_and_quit
        quit_save = self.client.quit

        def do_quit(*args):
            self.client.warn_and_quit = warn_and_quit_save
            self.client.quit = quit_save
            self.client.cleanup()
            self.destroy()
            gtk.main_quit()

        def warn_and_quit_override(exit_code, warning):
            log("warn_and_quit_override(%s, %s)", exit_code, warning)
            if self.exit_code == None:
                self.exit_code = exit_code
            password_warning = warning.find("invalid password") >= 0
            if password_warning:
                self.password_warning()
            err = exit_code != 0 or password_warning
            self.set_info_color(err)
            self.set_info_text(warning)
            if err:

                def ignore_further_quit_events(*args):
                    pass

                if self.client:
                    self.client.cleanup()
                    self.client.warn_and_quit = ignore_further_quit_events
                    self.client.quit = ignore_further_quit_events
                self.set_sensitive(True)
                self.reset_client()
                glib.idle_add(self.window.show)
            else:
                do_quit()

        def quit_override(exit_code):
            log("quit_override(%s)", exit_code)
            if self.exit_code == None:
                self.exit_code = exit_code
            self.client.cleanup()
            if self.exit_code == 0:
                do_quit()
            else:
                self.reset_client()

        self.client.warn_and_quit = warn_and_quit_override
        self.client.quit = quit_override
        try:
            self.client.run()
        except Exception as e:
            log.error("client error", exc_info=True)
            self.handle_exception(e)

    def password_ok(self, *args):
        self.password_entry.modify_text(STATE_NORMAL, black)

    def password_warning(self, *args):
        self.password_entry.modify_text(STATE_NORMAL, red)
        self.password_entry.grab_focus()

    def set_widget_bg_color(self, widget, is_error=False):
        if is_error:
            color_obj = red
        else:
            color_obj = white
        if color_obj:
            glib.idle_add(widget.modify_base, STATE_NORMAL, color_obj)

    def set_widget_fg_color(self, widget, is_error=False):
        if is_error:
            color_obj = red
        else:
            color_obj = black
        if color_obj:
            glib.idle_add(widget.modify_fg, STATE_NORMAL, color_obj)

    def update_options_from_gui(self):
        self.config.host = self.host_entry.get_text()
        self.config.ssh_port = self.ssh_port_entry.get_text()
        self.config.port = self.port_entry.get_text()
        self.config.username = self.username_entry.get_text()
        self.config.encoding = self.get_selected_encoding(
        ) or self.config.encoding
        mode_enc = self.mode_combo.get_active_text()
        if mode_enc.startswith("TCP"):
            self.config.mode = "tcp"
            if mode_enc.find("AES") > 0 and "AES" in ENCRYPTION_CIPHERS:
                self.config.encryption = "AES"
        else:
            self.config.mode = "ssh"
        self.config.password = self.password_entry.get_text()
        log("update_options_from_gui() %s",
            (self.config.username, self.config.password, self.config.mode,
             self.config.encryption, self.config.host, self.config.port,
             self.config.ssh_port, self.config.encoding))

    def update_gui_from_config(self):
        #mode:
        if self.config.mode == "tcp":
            self.mode_combo.set_active(0)
        elif self.config.mode == "tcp + aes" and "AES" in ENCRYPTION_CIPHERS:
            self.mode_combo.set_active(1)
        else:
            self.mode_combo.set_active(2)
        if self.config.encoding and self.encoding_combo:
            index = self.encoding_combo.get_menu().encoding_to_index.get(
                self.config.encoding, -1)
            log("setting encoding combo to %s / %s", self.config.encoding,
                index)
            if index >= 0:
                self.encoding_combo.set_history(index)
        self.username_entry.set_text(self.config.username)
        self.password_entry.set_text(self.config.password)
        self.host_entry.set_text(self.config.host)

        def get_port(v):
            try:
                iport = int(v)
                if iport > 0:
                    return str(iport)
            except:
                pass
            return ""

        self.port_entry.set_text(get_port(self.config.port))
        self.ssh_port_entry.set_text(get_port(self.config.ssh_port))

    def destroy(self, *args):
        self.window.destroy()
        self.window = None
        gtk.main_quit()

    def update_options_from_file(self, filename):
        props = read_config(filename)
        #we rely on "ssh_port" being defined on the config object
        #so try to load it from file, and define it if not present:
        options = validate_config(
            props,
            extras_types=LAUNCHER_OPTION_TYPES,
            extras_validation=self.get_launcher_validation())
        for k, v in options.items():
            fn = k.replace("-", "_")
            setattr(self.config, fn, v)
        self.config_keys = self.config_keys.union(set(props.keys()))
        log(
            "update_options_from_file(%s) populated config with keys '%s', ssh=%s",
            filename, options.keys(), self.config.ssh)

    def choose_session_file(self, title, action, action_button, callback):
        file_filter = gtk.FileFilter()
        file_filter.set_name("Xpra")
        file_filter.add_pattern("*.xpra")
        choose_file(self.window, title, action, action_button, callback,
                    file_filter)

    def save_clicked(self, *args):
        self.update_options_from_gui()

        def do_save(filename):
            save_config(filename,
                        self.config,
                        self.config_keys,
                        extras_types=LAUNCHER_OPTION_TYPES)

        self.choose_session_file("Save session settings to file",
                                 FILE_CHOOSER_ACTION_SAVE, gtk.STOCK_SAVE,
                                 do_save)

    def load_clicked(self, *args):
        def do_load(filename):
            self.update_options_from_file(filename)
            self.update_gui_from_config()

        self.choose_session_file("Load session settings from file",
                                 FILE_CHOOSER_ACTION_OPEN, gtk.STOCK_OPEN,
                                 do_load)
Пример #4
0
class ApplicationWindow:

    def    __init__(self):
        # Default connection options
        self.config = make_defaults_struct(extras_defaults=LAUNCHER_DEFAULTS, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=LAUNCHER_VALIDATION)
        #TODO: the fixup does not belong here?
        from xpra.scripts.main import fixup_video_all_or_none, fixup_encodings, fixup_compression, fixup_packetencoding
        fixup_video_all_or_none(self.config)
        fixup_encodings(self.config)
        fixup_compression(self.config)
        fixup_packetencoding(self.config)
        #what we save by default:
        self.config_keys = set(SAVED_FIELDS)
        if is_gtk3():
            self.config.client_toolkit = "gtk3"
        else:
            self.config.client_toolkit = "gtk2"
        def raise_exception(*args):
            raise Exception(*args)
        self.client = make_client(raise_exception, self.config)
        self.client.init(self.config)
        self.exit_code = None

    def create_window(self):
        self.window = gtk.Window()
        self.window.connect("destroy", self.destroy)
        self.window.set_default_size(400, 300)
        self.window.set_border_width(20)
        self.window.set_title("Xpra Launcher")
        self.window.modify_bg(STATE_NORMAL, gdk.Color(red=65535, green=65535, blue=65535))

        icon_pixbuf = self.get_icon("xpra.png")
        if icon_pixbuf:
            self.window.set_icon(icon_pixbuf)
        self.window.set_position(WIN_POS_CENTER)

        vbox = gtk.VBox(False, 0)
        vbox.set_spacing(15)

        # Title
        hbox = gtk.HBox(False, 0)
        if icon_pixbuf:
            logo_button = gtk.Button("")
            settings = logo_button.get_settings()
            settings.set_property('gtk-button-images', True)
            logo_button.connect("clicked", about)
            set_tooltip_text(logo_button, "About")
            image = gtk.Image()
            image.set_from_pixbuf(icon_pixbuf)
            logo_button.set_image(image)
            hbox.pack_start(logo_button, expand=False, fill=False)
        label = gtk.Label("Connect to xpra server")
        label.modify_font(pango.FontDescription("sans 14"))
        hbox.pack_start(label, expand=True, fill=True)
        vbox.pack_start(hbox)

        # Mode:
        hbox = gtk.HBox(False, 20)
        hbox.set_spacing(20)
        hbox.pack_start(gtk.Label("Mode: "))
        self.mode_combo = gtk.combo_box_new_text()
        self.mode_combo.get_model().clear()
        self.mode_combo.append_text("TCP")
        if "AES" in ENCRYPTION_CIPHERS:
            self.mode_combo.append_text("TCP + AES")
        self.mode_combo.append_text("SSH")
        self.mode_combo.connect("changed", self.mode_changed)
        hbox.pack_start(self.mode_combo)
        vbox.pack_start(hbox)

        # Encoding:
        hbox = gtk.HBox(False, 20)
        hbox.set_spacing(20)
        hbox.pack_start(gtk.Label("Encoding: "))
        self.encoding_combo = OptionMenu()
        def get_current_encoding():
            return self.config.encoding
        def set_new_encoding(e):
            self.config.encoding = e
        encodings = [x for x in PREFERED_ENCODING_ORDER if x in self.client.get_encodings()]
        server_encodings = encodings
        es = make_encodingsmenu(get_current_encoding, set_new_encoding, encodings, server_encodings)
        self.encoding_combo.set_menu(es)
        set_history_from_active(self.encoding_combo)
        hbox.pack_start(self.encoding_combo)
        vbox.pack_start(hbox)
        self.encoding_combo.connect("changed", self.encoding_changed)

        # Quality
        hbox = gtk.HBox(False, 20)
        hbox.set_spacing(20)
        self.quality_label = gtk.Label("Quality: ")
        hbox.pack_start(self.quality_label)
        self.quality_combo = OptionMenu()
        def set_min_quality(q):
            self.config.min_quality = q
        def set_quality(q):
            self.config.quality = q
        def get_min_quality():
            return self.config.min_quality
        def get_quality():
            return self.config.quality
        sq = make_min_auto_menu("Quality", MIN_QUALITY_OPTIONS, QUALITY_OPTIONS,
                                   get_min_quality, get_quality, set_min_quality, set_quality)
        self.quality_combo.set_menu(sq)
        set_history_from_active(self.quality_combo)
        hbox.pack_start(self.quality_combo)
        vbox.pack_start(hbox)

        # Speed
        hbox = gtk.HBox(False, 20)
        hbox.set_spacing(20)
        self.speed_label = gtk.Label("Speed: ")
        hbox.pack_start(self.speed_label)
        self.speed_combo = OptionMenu()
        def set_min_speed(s):
            self.config.min_speed = s
        def set_speed(s):
            self.config.speed = s
        def get_min_speed():
            return self.config.min_speed
        def get_speed():
            return self.config.speed
        ss = make_min_auto_menu("Speed", MIN_SPEED_OPTIONS, SPEED_OPTIONS,
                                   get_min_speed, get_speed, set_min_speed, set_speed)
        self.speed_combo.set_menu(ss)
        set_history_from_active(self.speed_combo)
        hbox.pack_start(self.speed_combo)
        vbox.pack_start(hbox)

        # Username@Host:Port
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(5)
        self.username_entry = gtk.Entry(max=128)
        self.username_entry.set_width_chars(16)
        self.username_entry.connect("changed", self.validate)
        set_tooltip_text(self.username_entry, "SSH username")
        self.username_label = gtk.Label("@")
        self.host_entry = gtk.Entry(max=128)
        self.host_entry.set_width_chars(24)
        self.host_entry.connect("changed", self.validate)
        set_tooltip_text(self.host_entry, "hostname")
        self.ssh_port_entry = gtk.Entry(max=5)
        self.ssh_port_entry.set_width_chars(5)
        self.ssh_port_entry.connect("changed", self.validate)
        set_tooltip_text(self.ssh_port_entry, "SSH port")
        self.port_entry = gtk.Entry(max=5)
        self.port_entry.set_width_chars(5)
        self.port_entry.connect("changed", self.validate)
        set_tooltip_text(self.port_entry, "port/display")

        hbox.pack_start(self.username_entry)
        hbox.pack_start(self.username_label)
        hbox.pack_start(self.host_entry)
        hbox.pack_start(self.ssh_port_entry)
        hbox.pack_start(gtk.Label(":"))
        hbox.pack_start(self.port_entry)
        vbox.pack_start(hbox)

        # Password
        hbox = gtk.HBox(False, 0)
        hbox.set_spacing(20)
        self.password_entry = gtk.Entry(max=128)
        self.password_entry.set_width_chars(30)
        self.password_entry.set_text("")
        self.password_entry.set_visibility(False)
        self.password_entry.connect("changed", self.password_ok)
        self.password_entry.connect("changed", self.validate)
        self.password_label = gtk.Label("Password: "******"Save")
        set_tooltip_text(self.save_btn, "Save settings to a session file")
        self.save_btn.connect("clicked", self.save_clicked)
        hbox.pack_start(self.save_btn)
        #Load:
        self.load_btn = gtk.Button("Load")
        set_tooltip_text(self.load_btn, "Load settings from a session file")
        self.load_btn.connect("clicked", self.load_clicked)
        hbox.pack_start(self.load_btn)
        # Connect button:
        self.button = gtk.Button("Connect")
        self.button.connect("clicked", self.connect_clicked)
        connect_icon = self.get_icon("retry.png")
        if connect_icon:
            self.button.set_image(scaled_image(connect_icon, 24))
        hbox.pack_start(self.button)

        def accel_close(*args):
            gtk.main_quit()
        add_close_accel(self.window, accel_close)
        vbox.show_all()
        self.window.vbox = vbox
        self.window.add(vbox)

    def validate(self, *args):
        ssh = self.mode_combo.get_active_text()=="SSH"
        errs = []
        host = self.host_entry.get_text()
        errs.append((self.host_entry, not bool(host), "specify the host"))
        if ssh:
            #validate ssh port:
            ssh_port = self.ssh_port_entry.get_text()
            try:
                ssh_port = int(ssh_port)
            except:
                ssh_port = -1
            errs.append((self.ssh_port_entry, ssh_port<0 or ssh_port>=2**16, "invalid SSH port number"))
        port = self.port_entry.get_text()
        if ssh and not port:
            port = 0        #port optional with ssh
        else:
            try:
                port = int(port)
            except:
                port = -1
        errs.append((self.port_entry, port<0 or port>=2**16, "invalid port number"))
        err_text = []
        for w, e, text in errs:
            self.set_widget_bg_color(w, e)
            if e:
                err_text.append(text)
        log.debug("validate(%s) err_text=%s, errs=%s", args, err_text, errs)
        self.set_info_text(", ".join(err_text))
        self.set_info_color(len(err_text)>0)
        self.button.set_sensitive(len(err_text)==0)
        return errs

    def show(self):
        self.window.show()
        self.window.present()

    def run(self):
        from xpra.gtk_common.gtk2common import gtk2main
        gtk2main()

    def get_icon(self, icon_name):
        icon_filename = os.path.join(get_icon_dir(), icon_name)
        if os.path.exists(icon_filename):
            return pixbuf_new_from_file(icon_filename)
        return None

    def mode_changed(self, *args):
        ssh = self.mode_combo.get_active_text()=="SSH"
        self.port_entry.set_text("")
        if ssh:
            self.ssh_port_entry.show()
            set_tooltip_text(self.port_entry, "Display number (optional)")
            set_tooltip_text(self.password_entry, "SSH Password")
            self.username_entry.show()
            self.username_label.show()
        else:
            self.ssh_port_entry.hide()
            set_tooltip_text(self.port_entry, "port number")
            set_tooltip_text(self.password_entry, "Session Password")
            self.username_entry.hide()
            self.username_label.hide()
            if self.config.port>0:
                self.port_entry.set_text("%s" % self.config.port)
        if not ssh or sys.platform.startswith("win") or sys.platform.startswith("darwin"):
            #password cannot be used with ssh
            #(except on win32 with plink, and on osx via the SSH_ASKPASS hack)
            self.password_label.show()
            self.password_entry.show()
        else:
            self.password_label.hide()
            self.password_entry.hide()
        self.validate()

    def get_selected_encoding(self, *args):
        index = get_active_item_index(self.encoding_combo)
        return self.encoding_combo.get_menu().index_to_encoding.get(index)

    def encoding_changed(self, *args):
        encoding = self.get_selected_encoding()
        uses_quality_option = encoding in ["jpeg", "webp", "h264"]
        log("encoding_changed(%s) uses_quality_option(%s)=%s", args, encoding, uses_quality_option)
        #to prevent win32 crashes:
        if uses_quality_option:
            self.quality_combo.show()
            self.quality_label.show()
        else:
            self.quality_combo.hide()
            self.quality_label.hide()

    def reset_errors(self):
        self.set_sensitive(True)
        self.set_info_text("")
        for widget in (self.info, self.password_entry, self.username_entry, self.host_entry, self.port_entry):
            self.set_widget_fg_color(self.info, False)
            self.set_widget_bg_color(widget, False)

    def set_info_text(self, text):
        if self.info:
            gobject.idle_add(self.info.set_text, text)

    def set_info_color(self, is_error=False):
        self.set_widget_fg_color(self.info, is_error)


    def set_sensitive(self, s):
        gobject.idle_add(self.window.set_sensitive, s)

    def connect_clicked(self, *args):
        self.update_options_from_gui()
        self.do_connect()


    def reset_client(self):
        #lose current client class and make a new one:
        if self.client:
            self.client.cleanup()
            self.client = None
        self.client = make_client(Exception, self.config)
        self.client.init(self.config)


    def handle_exception(self, e):
        t = str(e)
        if self.config.debug:
            #in debug mode, include the full stacktrace:
            import traceback
            t = traceback.format_exc()
        def ui_handle_exception():
            self.reset_client()
            self.set_sensitive(True)
            self.set_info_color(True)
            self.set_info_text(t)
            self.window.show()
        gobject.idle_add(ui_handle_exception)

    def do_connect(self):
        try:
            self.connect_builtin()
        except Exception, e:
            log.error("cannot connect:", exc_info=True)
            self.handle_exception(e)