From 3bc4f41f22f4cdd13fab39ed16cc96a85e2e2eb7 Mon Sep 17 00:00:00 2001 From: totaam Date: Tue, 13 Apr 2021 00:29:45 +0700 Subject: [PATCH] #3070 add dialogs for configuring many extra options --- xpra/gtk_common/gtk_util.py | 2 +- xpra/gtk_common/start_gui.py | 510 +++++++++++++++++++++++++++++++++-- xpra/scripts/main.py | 2 +- 3 files changed, 493 insertions(+), 21 deletions(-) diff --git a/xpra/gtk_common/gtk_util.py b/xpra/gtk_common/gtk_util.py index b0a63d47aa..9c73fe800d 100644 --- a/xpra/gtk_common/gtk_util.py +++ b/xpra/gtk_common/gtk_util.py @@ -651,7 +651,7 @@ def add_row(self, widget, *widgets, **kwargs): i += 1 self.inc() - def attach(self, widget, i, count=1, + def attach(self, widget, i=0, count=1, xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL, xpadding=10, ypadding=0): self.table.attach(widget, i, i+count, self.row, self.row+1, diff --git a/xpra/gtk_common/start_gui.py b/xpra/gtk_common/start_gui.py index 4c349d0fd1..c3c35846bb 100644 --- a/xpra/gtk_common/start_gui.py +++ b/xpra/gtk_common/start_gui.py @@ -8,15 +8,26 @@ import os.path import subprocess -from gi.repository import Gtk, Pango, GLib +from gi.repository import Gtk, Gdk, Pango, GLib from xpra.gtk_common.gobject_compat import register_os_signals from xpra.gtk_common.gtk_util import ( add_close_accel, get_icon_pixbuf, imagebutton, + TableBuilder, ) +from xpra.util import repr_ellipsized from xpra.os_util import OSX, WIN32, platform_name +from xpra.simple_stats import std_unit_dec +from xpra.scripts.config import ( + get_defaults, parse_bool, + OPTION_TYPES, FALSE_OPTIONS, TRUE_OPTIONS, + ) +from xpra.client.gtk_base.menu_helper import ( + BANDWIDTH_MENU_OPTIONS, + MIN_QUALITY_OPTIONS, MIN_SPEED_OPTIONS, + ) from xpra.platform.paths import get_xpra_command from xpra.log import Logger @@ -29,6 +40,8 @@ REQUIRE_COMMAND = False +UNSET = object() + def exec_command(cmd): env = os.environ.copy() @@ -52,10 +65,40 @@ def l(label): widget = Gtk.Label(label) return sf(widget) + +def link_btn(link, label=None, icon_name="question.png"): + def open_link(): + import webbrowser + webbrowser.open(link) + def help_clicked(*args): + log("help_clicked%s opening '%s'", args, link) + from xpra.make_thread import start_thread + start_thread(open_link, "open-link", True) + icon = get_icon_pixbuf(icon_name) + btn = imagebutton("" if icon else label, icon, label, help_clicked, 12, False) + return btn + +def attach_label(table, label, tooltip_text, link=None): + lbl = Gtk.Label(label) + if tooltip_text: + lbl.set_tooltip_text(tooltip_text) + hbox = Gtk.HBox(False, 0) + hbox.pack_start(xal(lbl), True, True) + if link: + help_btn = link_btn(link, "About %s" % label) + hbox.pack_start(help_btn, False) + table.attach(hbox) + + class StartSession(Gtk.Window): - def __init__(self): + def __init__(self, options): + self.set_options(options) self.exit_code = None + self.options_window = None + self.default_config = get_defaults() + #log("default_config=%s", self.default_config) + #log("options=%s (%s)", options, type(options)) Gtk.Window.__init__(self) self.set_border_width(20) self.set_title("Start Xpra Session") @@ -187,17 +230,33 @@ def rb(sibling=None, label="", cb=None, tooltip_text=None): self.exit_with_client_cb.set_label("exit with client") hbox.add(xal(self.exit_with_client_cb, 0.5)) self.exit_with_client_cb.set_active(False) - #maybe add: - #clipboard, opengl, sharing? + # session options: + hbox = Gtk.HBox(False, 20) + hbox.pack_start(l("Options:"), True, False) + for label_text, icon_name, tooltip_text, cb in ( + ("Features", "features.png", "Session features", self.configure_features), + ("Network", "connect.png", "Network options", self.configure_network), + ("Encodings", "encoding.png", "Picture compression", self.configure_encoding), + ("Keyboard", "keyboard.png", "Keyboard layout and options", self.configure_keyboard), + ("Audio", "speaker.png", "Audio forwarding options", self.configure_audio), + ("Webcam", "webcam.png", "Webcam forwarding options", self.configure_webcam), + ("Printing", "printer.png", "Printer forwarding options", self.configure_printing), + ): + icon = get_icon_pixbuf(icon_name) + ib = imagebutton("", icon=icon, tooltip=label_text or tooltip_text, + clicked_callback=cb, icon_size=32, + label_font=Pango.FontDescription("sans 14")) + hbox.pack_start(ib, True, False) + options_box.pack_start(hbox, True, False) # Action buttons: hbox = Gtk.HBox(False, 20) vbox.pack_start(hbox, False, True, 20) def btn(label, tooltip, callback, default=False): - btn = imagebutton(label, tooltip=tooltip, clicked_callback=callback, icon_size=32, - default=default, label_font=Pango.FontDescription("sans 16")) - hbox.pack_start(btn) - return btn + ib = imagebutton(label, tooltip=tooltip, clicked_callback=callback, icon_size=32, + default=default, label_font=Pango.FontDescription("sans 16")) + hbox.pack_start(ib) + return ib self.cancel_btn = btn("Cancel", "", self.quit) self.run_btn = btn("Start", "Start the xpra session", @@ -209,6 +268,14 @@ def btn(label, tooltip, callback, default=False): vbox.show_all() self.add(vbox) + def set_options(self, options): + #cook some attributes, + #so they won't trigger "changes" messages later + #- we don't show "auto" as an option, convert to either true or false: + options.splash = (str(options.splash) or "").lower() not in FALSE_OPTIONS + options.sharing = () + self.session_options = options + def app_signal(self, signum): if self.exit_code is None: @@ -226,6 +293,25 @@ def do_quit(self): log("do_quit()") Gtk.main_quit() + def run_dialog(self, WClass): + log("run_dialog(%s) session_options=%s", WClass, repr_ellipsized(self.session_options)) + WClass(self.session_options, self.get_run_mode(), self).show() + + def configure_features(self, *_args): + self.run_dialog(FeaturesWindow) + def configure_network(self, *_args): + self.run_dialog(NetworkWindow) + def configure_encoding(self, *_args): + self.run_dialog(EncodingWindow) + def configure_keyboard(self, *_args): + self.run_dialog(KeyboardWindow) + def configure_audio(self, *_args): + self.run_dialog(AudioWindow) + def configure_webcam(self, *_args): + self.run_dialog(WebcamWindow) + def configure_printing(self, *_args): + self.run_dialog(PrintingWindow) + def populate_menus(self): localhost = self.localhost_btn.get_active() @@ -419,6 +505,7 @@ def runattach_command(self, *_args): def do_run(self, attach=False): self.hide() cmd = self.get_run_command(attach) + log("do_run(%s) cmd=%s", attach, cmd) proc = exec_command(cmd) if proc: from xpra.make_thread import start_thread @@ -429,6 +516,15 @@ def wait_for_subprocess(self, proc): log("return code: %s", proc.returncode) GLib.idle_add(self.show) + def get_run_mode(self): + shadow = self.shadow_btn.get_active() + seamless = self.seamless_btn.get_active() + if seamless: + return "start" + if shadow: + return "shadow" + return "start-desktop" + def get_run_command(self, attach=False): localhost = self.localhost_btn.get_active() if xdg and localhost: @@ -441,17 +537,10 @@ def get_run_command(self, attach=False): command = self.desktop_entry.getExec() else: command = self.entry.get_text() - cmd = get_xpra_command() - shadow = self.shadow_btn.get_active() - seamless = self.seamless_btn.get_active() - if seamless: - cmd.append("start") - elif shadow: - cmd.append("shadow") - else: - cmd.append("start-desktop") + cmd = get_xpra_command() + [self.get_run_mode()] ewc = self.exit_with_client_cb.get_active() cmd.append("--exit-with-client=%s" % ewc) + shadow = self.shadow_btn.get_active() if not shadow: ewc = self.exit_with_children_cb.get_active() cmd.append("--exit-with-children=%s" % ewc) @@ -460,6 +549,22 @@ def get_run_command(self, attach=False): else: cmd.append("--start=%s" % command) cmd.append("--attach=%s" % attach) + #process session_config if we have one: + for k in ( + "splash", "headerbar", "notifications", "system-tray", "cursors", "bell", "modal-windows", + "pixel-depth", "mousewheel", + ): + fn = k.replace("-", "_") + if not hasattr(self.session_options, fn): + continue + value = getattr(self.session_options, fn) + default_value = self.default_config.get(k) + ot = OPTION_TYPES.get(k) + if ot is bool: + value = parse_bool(k, value) + if value!=default_value: + log.warn("%s=%s (%s) - not %s (%s)", k, value, type(value), default_value, type(default_value)) + cmd.append("--%s=%s" % (k, value)) localhost = self.localhost_btn.get_active() display = self.display_entry.get_text().lstrip(":") if localhost: @@ -484,14 +589,381 @@ def get_run_command(self, attach=False): return cmd -def main(): # pragma: no cover +class SessionOptions(Gtk.Window): + def __init__(self, title, icon_name, options, run_mode, parent): + Gtk.Window.__init__(self) + self.options = options + self.run_mode = run_mode + self.set_title(title) + self.set_border_width(20) + self.set_resizable(True) + self.set_decorated(True) + self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_transient_for(parent) + self.set_modal(True) + add_close_accel(self, self.close) + self.connect("delete_event", self.close) + icon = get_icon_pixbuf(icon_name) + if icon: + self.set_icon(icon) + + self.vbox = Gtk.VBox(False, 0) + self.vbox.show() + self.add(self.vbox) + self.widgets = [] + self.populate_form() + self.show() + + def populate_form(self): + raise NotImplementedError() + + def close(self, *_args): #pylint: disable=arguments-differ + self.set_value_from_widgets() + self.destroy() + + + def bool_cb(self, table, label, option_name, tooltip_text=None, link=None): + attach_label(table, label, tooltip_text, link) + fn = option_name.replace("-", "_") + value = getattr(self.options, fn) + cb = Gtk.CheckButton() + cb.set_active(str(value).lower() not in FALSE_OPTIONS) + table.attach(cb, 1) + setattr(self, "%s_widget" % fn, cb) + setattr(self, "%s_widget_type" % fn, "bool") + self.widgets.append(option_name) + table.inc() + return cb + + def radio_cb_auto(self, table, label, option_name, tooltip_text=None, link=None): + return self.radio_cb(table, label, option_name, tooltip_text, link, { + "yes" : TRUE_OPTIONS, + "no" : FALSE_OPTIONS, + "auto" : ("auto", "", None), + }) + + def radio_cb(self, table, label, option_name, tooltip_text=None, link=None, options=None): + attach_label(table, label, tooltip_text, link) + fn = option_name.replace("-", "_") + widget_base_name = "%s_widget" % fn + setattr(self, "%s_options" % fn, options) + setattr(self, "%s_widget_type" % fn, "radio") + self.widgets.append(option_name) + value = getattr(self.options, fn) + #log.warn("%s=%s", fn, value) + hbox = Gtk.HBox(True, 10) + i = 0 + sibling = None + btns = [] + for label, match in options.items(): + btn = Gtk.RadioButton.new_with_label_from_widget(sibling, label) + hbox.add(btn) + setattr(self, "%s_%s" % (widget_base_name, label), btn) + btn.set_active(str(value).lower() in match) + if i==0: + sibling = btn + i += 1 + btns.append(btn) + table.attach(hbox, 1) + table.inc() + return btns + + def combo(self, table, label, option_name, options, link=None): + attach_label(table, label, None, link) + fn = option_name.replace("-", "_") + value = getattr(self.options, fn) + c = Gtk.ComboBoxText() + index = None + for i, (v, vlabel) in enumerate(options.items()): + c.append_text(str(vlabel)) + if index is None or v==value: + index = i + if index is not None: + c.set_active(index) + table.attach(c, 1) + setattr(self, "%s_widget" % fn, c) + setattr(self, "%s_widget_type" % fn, "combo") + setattr(self, "%s_options" % fn, options) + self.widgets.append(option_name) + table.inc() + return c + + def set_value_from_widgets(self): + for option_name in self.widgets: + self.set_value_from_widget(option_name) + + def set_value_from_widget(self, option_name): + fn = option_name.replace("-", "_") + widget_type = getattr(self, "%s_widget_type" % fn) + if widget_type=="bool": + widget = getattr(self, "%s_widget" % fn) + values = (widget.get_active(), ) + elif widget_type=="radio": + values = self.valuesfromradio(option_name) + elif widget_type=="combo": + values = self.valuesfromcombo(option_name) + else: + log.warn("unknown widget type '%s'", widget_type) + if len(values)!=1 or values[0]!=UNSET: + current_value = getattr(self.options, fn) + for v in values: + if current_value==v: + #unchanged + return + #pick the first one: + value = values[0] + log.info("changed: %s=%s (%s) - was %s (%s)", fn, value, type(value), current_value, type(current_value)) + setattr(self.options, fn, value) + + + def valuesfromradio(self, option_name): + fn = option_name.replace("-", "_") + options = getattr(self, "%s_options" % fn) + widget_base_name = "%s_widget" % fn + for label, match in options.items(): + btn = getattr(self, "%s_%s" % (widget_base_name, label)) + if btn.get_active(): + return match + return (UNSET, ) + + def valuesfromcombo(self, option_name): + fn = option_name.replace("-", "_") + widget = getattr(self, "%s_widget" % fn) + options = getattr(self, "%s_options" % fn) + value = widget.get_active_text() + for k,v in options.items(): + if str(v)==value: + return (k, ) + return (UNSET, ) + + def table(self): + tb = TableBuilder() + table = tb.get_table() + al = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=1.0) + al.add(table) + self.vbox.pack_start(al, expand=True, fill=True, padding=20) + return tb + + +class FeaturesWindow(SessionOptions): + def __init__(self, *args): + super().__init__("Session Features", "features.png", *args) + + def populate_form(self): + btn = link_btn("https://github.com/Xpra-org/xpra/blob/master/docs/Features/README.md", + label="Features Documentation", icon_name=None) + self.vbox.pack_start(btn, expand=True, fill=False, padding=20) + + tb = self.table() + self.radio_cb(tb, "Header Bar", "headerbar", None, None, { + "yes" : TRUE_OPTIONS, + "no" : FALSE_OPTIONS, + "force" : ("force",), + }) + self.bool_cb(tb, "Splash Screen", "splash") + self.bool_cb(tb, "Notifications", "notifications", None, + "https://github.com/Xpra-org/xpra/blob/master/docs/Features/Notifications.md") + self.bool_cb(tb, "System Tray", "system-tray", None, + "https://github.com/Xpra-org/xpra/blob/master/docs/Features/System-Tray.md") + self.bool_cb(tb, "Cursors", "cursors") + self.bool_cb(tb, "Bell", "bell") + self.bool_cb(tb, "Modal Windows", "modal-windows") + pixel_depths = {0 : "auto"} + if self.run_mode=="shadow": + pixel_depths[8] = 8 + for pd in (16, 24, 30, 32): + pixel_depths[pd] = pd + self.combo(tb, "Pixel Depth", "pixel-depth", pixel_depths, + "https://github.com/Xpra-org/xpra/blob/master/docs/Features/Image-Depth.md") + self.combo(tb, "Mouse Wheel", "mousewheel", { + "on" : "on", + "no" : "disabled", + "invert-x" : "invert X axis", + "invert-y" : "invert Y axis", + "invert-z" : "invert Z axis", + "invert-all" : "invert all axes", + }) + #tb.attach(Gtk.Label("Window Border"), 0) + #tb = self.table_tab("clipboard.png", "Clipboard")[0] + #tb.attach(Gtk.Label("Enabled"), 0) + #self.clipboard_enabled = Gtk.CheckButton() + #tb.attach(self.clipboard_enabled, 1) + #tb.inc() + #tb.attach(Gtk.Label("Direction"), 0) + self.vbox.show_all() + + +class NetworkWindow(SessionOptions): + def __init__(self, *args): + super().__init__("Network Options", "connect.png", *args) + + def populate_form(self): + btn = link_btn("https://github.com/Xpra-org/xpra/blob/master/docs/Network/README.md", + label="Open Network Documentation", icon_name=None) + self.vbox.pack_start(btn, expand=True, fill=False, padding=20) + + tb = self.table() + self.bool_cb(tb, "Multicast DNS", "mdns", "Publish the session via mDNS", + "https://github.com/Xpra-org/xpra/blob/master/docs/Network/Multicast-DNS.md") + self.radio_cb_auto(tb, "Session Sharing", "sharing") + self.radio_cb_auto(tb, "Session Lock", "lock", "Prevent sessions from being taken over by new clients") + tb.attach(Gtk.Label("")) + tb.inc() + self.bool_cb(tb, "Bandwidth Detection", "bandwidth-detection", "Automatically detect runtime bandwidth limits") + bwoptions = {} + for bwlimit in BANDWIDTH_MENU_OPTIONS: + if bwlimit<=0: + s = "None" + elif bwlimit>=10*1000*1000: + s = "%iMbps" % (bwlimit//(1000*1000)) + else: + s = "%sbps" % std_unit_dec(bwlimit) + bwoptions[bwlimit] = s + self.combo(tb, "Bandwidth Limit", "bandwidth-limit", bwoptions) + #ssl options + #ssh=paramiko | plink + #exit-ssh + #Remote Logging + #open-files + #open-url + #file-size-limit + self.vbox.show_all() + + +class EncodingWindow(SessionOptions): + def __init__(self, *args): + super().__init__("Picture Encoding", "encoding.png", *args) + + def populate_form(self): + btn = link_btn("https://github.com/Xpra-org/xpra/blob/master/docs/Usage/Encodings.md", + label="Open Encodings Documentation", icon_name=None) + self.vbox.pack_start(btn, expand=True, fill=False, padding=20) + + tb = self.table() + qoptions = MIN_QUALITY_OPTIONS.copy() + qoptions.pop(0, None) + self.combo(tb, "Minimum Quality", "min-quality", qoptions) + soptions = MIN_SPEED_OPTIONS.copy() + soptions.pop(0, None) + self.combo(tb, "Minimum Speed", "min-speed", soptions) + self.combo(tb, "Auto-refresh", "auto-refresh-delay", { + 0 : "disabled", + 0.1 : "fast", + 0.15 : "normal", + 0.5 : "slow", + }) + tb.attach(Gtk.Label(""), 0, 2) + tb.inc() + tb.attach(Gtk.HSeparator(), 0, 2) + tb.inc() + tb.attach(Gtk.Label("Colourspace Modules"), 0) + tb.inc() + tb.attach(Gtk.Label("Video Encoders"), 0) + tb.inc() + tb.attach(Gtk.Label("Video Decoders"), 0) + self.vbox.show_all() + + +class KeyboardWindow(SessionOptions): + def __init__(self, *args): + super().__init__("Keyboard Options", "keyboard.png", *args) + + def populate_form(self): + btn = link_btn("https://github.com/Xpra-org/xpra/blob/master/docs/Features/Keyboard.md", + label="Open Keyboard Documentation", icon_name=None) + self.vbox.pack_start(btn, expand=True, fill=False, padding=20) + + tb = self.table() + tb.attach(Gtk.Label("Keyboard Layout")) + self.keyboard_layout_widget = Gtk.ComboBoxText() + tb.attach(self.keyboard_layout_widget, 1) + tb.inc() + self.bool_cb(tb, "State Synchronization", "keyboard-sync") + self.bool_cb(tb, "Raw Mode", "keyboard-raw") + self.vbox.show_all() + + +class AudioWindow(SessionOptions): + def __init__(self, *args): + super().__init__("Audio Options", "speaker.png", *args) + + def populate_form(self): + btn = link_btn("https://github.com/Xpra-org/xpra/blob/master/docs/Features/Audio.md", + label="Open Audio Documentation", icon_name=None) + self.vbox.pack_start(btn, expand=True, fill=False, padding=20) + + tb = self.table() + self.radio_cb(tb, "Speaker", "speaker", None, None, { + "on" : TRUE_OPTIONS, + "off" : FALSE_OPTIONS, + "disabled" : ("disabled", ), + }) + tb.attach(Gtk.Label("Speaker Codec")) + self.speaker_codec_widget = Gtk.ComboBoxText() + for v in ("mp3", "wav"): + self.speaker_codec_widget.append_text(v) + tb.attach(self.speaker_codec_widget, 1) + tb.inc() + self.radio_cb(tb, "Microphone", "microphone", None, None, { + "on" : TRUE_OPTIONS, + "off" : FALSE_OPTIONS, + "disabled" : ("disabled", ), + }) + tb.attach(Gtk.Label("Microphone Codec")) + self.microphone_codec_widget = Gtk.ComboBoxText() + for v in ("mp3", "wav"): + self.microphone_codec_widget.append_text(v) + tb.attach(self.microphone_codec_widget, 1) + tb.inc() + self.bool_cb(tb, "AV Sync", "av-sync") + self.vbox.show_all() + + +class WebcamWindow(SessionOptions): + def __init__(self, *args): + super().__init__("Webcam", "webcam.png", *args) + + def populate_form(self): + btn = link_btn("https://github.com/Xpra-org/xpra/blob/master/docs/Features/Webcam.md", + label="Open Webcam Documentation", icon_name=None) + self.vbox.pack_start(btn, expand=True, fill=False, padding=20) + + tb = self.table() + cb = self.bool_cb(tb, "Webcam", "webcam") + if OSX or WIN32: + cb.set_sensitive(False) + cb.set_active(False) + tb.inc() + tb.attach(Gtk.Label(""), 0, 2) + tb.inc() + tb.attach(Gtk.Label("Webcam forwarding is not supported on %s" % platform_name()), 0, 2) + self.vbox.show_all() + + +class PrintingWindow(SessionOptions): + def __init__(self, *args): + super().__init__("Printer", "printer.png", *args) + + def populate_form(self): + btn = link_btn("https://github.com/Xpra-org/xpra/blob/master/docs/Features/Printing.md", + label="Open Printing Documentation", icon_name=None) + self.vbox.pack_start(btn, expand=True, fill=False, padding=20) + + tb = self.table() + #self.bool_cb(tb, "Printing", "printing") + self.radio_cb_auto(tb, "Printing", "printing") + self.vbox.show_all() + + +def main(options=None): # pragma: no cover from xpra.platform import program_context from xpra.log import enable_color from xpra.platform.gui import init, ready with program_context("xpra-start-gui", "Xpra Start GUI"): enable_color() init() - gui = StartSession() + gui = StartSession(options) register_os_signals(gui.app_signal) ready() gui.populate_menus() diff --git a/xpra/scripts/main.py b/xpra/scripts/main.py index 6b1dadc537..b586b9cc6a 100755 --- a/xpra/scripts/main.py +++ b/xpra/scripts/main.py @@ -466,7 +466,7 @@ def do_run_mode(script_file, error_cb, options, args, mode, defaults): elif mode == "start-gui": check_gtk() from xpra.gtk_common.start_gui import main as gui_main #@Reimport - return gui_main() + return gui_main(options) elif mode == "bug-report": check_gtk() from xpra.scripts.bug_report import main as bug_main #@Reimport