class AboutDialog(object): """ Standard looking about dialog """ def __init__(self, app, gladepath, iconpath): self.gladepath = gladepath self.iconpath = iconpath self.setup_widgets(app) def show(self, parent=None): if not parent is None: self.dialog.set_transient_for(parent) self.dialog.show() def run(self, *a): self.dialog.run() def close(self): if hasattr(self, "dialog"): self.dialog.set_visible(False) self.dialog.destroy() def setup_widgets(self, app): self.builder = UIBuilder() # Fix icon path self.builder.replace_icon_path("icons/", self.iconpath) # Load glade file self.builder.add_from_file(os.path.join(self.gladepath, "about.glade")) self.builder.connect_signals(self) self.dialog = self.builder.get_object("dialog") # Get app version app_ver = "unknown" try: if IS_WINDOWS: # pkg_resources will not work on cx_Frozen package from syncthing_gtk.tools import get_install_path with open(os.path.join(get_install_path(), "__version__"), "r") as vfile: app_ver = vfile.read().strip(" \t\r\n") else: import pkg_resources, syncthing_gtk if syncthing_gtk.__file__.startswith( pkg_resources.require("syncthing-gtk")[0].location): app_ver = pkg_resources.require("syncthing-gtk")[0].version except: # pkg_resources is not available or __version__ file missing # There is no reason to crash on this. pass # Get daemon version try: daemon_ver = app.daemon.get_version() app_ver = "%s (Daemon %s)" % (app_ver, daemon_ver) except: # App is None or daemon version is not yet known pass # Display versions in UI self.builder.get_object("lblVersion").set_label(app_ver) def on_dialog_response(self, *a): self.close()
class AboutDialog(object): """ Standard looking about dialog """ def __init__(self, app, gladepath, iconpath): self.gladepath = gladepath self.iconpath = iconpath self.setup_widgets(app) def show(self, parent=None): if not parent is None: self.dialog.set_transient_for(parent) self.dialog.show() def run(self, *a): self.dialog.run() def close(self): if hasattr(self, "dialog"): self.dialog.set_visible(False) self.dialog.destroy() def setup_widgets(self, app): self.builder = UIBuilder() # Fix icon path self.builder.replace_icon_path("icons/", self.iconpath) # Load glade file self.builder.add_from_file(os.path.join(self.gladepath, "about.glade")) self.builder.connect_signals(self) self.dialog = self.builder.get_object("dialog") # Get app version app_ver = "unknown" try: if IS_WINDOWS: # pkg_resources will not work on cx_Frozen package from syncthing_gtk.tools import get_install_path with open(os.path.join(get_install_path(), "__version__"), "r") as vfile: app_ver = vfile.read().strip(" \t\r\n") else: import pkg_resources, syncthing_gtk if syncthing_gtk.__file__.startswith(pkg_resources.require("syncthing-gtk")[0].location): app_ver = pkg_resources.require("syncthing-gtk")[0].version except: # pkg_resources is not available or __version__ file missing # There is no reason to crash on this. pass # Get daemon version try: daemon_ver = app.daemon.get_version() app_ver = "%s (Daemon %s)" % (app_ver, daemon_ver) except: # App is None or daemon version is not yet known pass # Display versions in UI self.builder.get_object("lblVersion").set_label(app_ver) def on_dialog_response(self, *a): self.close()
class DaemonOutputDialog(object): """ Displays output from daemon subprocess """ def __init__(self, app, proc): self.proc = proc self.app = app self.setup_widgets() self.handler = 0 def __getitem__(self, name): """ Convince method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def show_with_lines(self, lines, parent=None): if not parent is None: self["dialog"].set_transient_for(parent) self["dialog"].show_all() self["tvOutput"].get_buffer().set_text("\n".join(lines)) def show(self, parent=None, title=None): if parent is None: self["dialog"].set_modal(False) else: self["dialog"].set_transient_for(parent) if not title is None: self["dialog"].set_title(title) self["dialog"].show_all() self["tvOutput"].get_buffer().set_text("\n".join( self.proc.get_output())) self.handler = self.proc.connect('line', self.cb_line) def close(self, *a): if self.handler > 0: self.proc.disconnect(self.handler) self["dialog"].hide() self["dialog"].destroy() def setup_widgets(self): # Load glade file self.builder = UIBuilder() self.builder.add_from_file( os.path.join(self.app.gladepath, "daemon-output.glade")) self.builder.connect_signals(self) self["tvOutput"].connect('size-allocate', self.scroll) def cb_line(self, proc, line): b = self["tvOutput"].get_buffer() b.insert(b.get_iter_at_offset(-1), "\n%s" % (line, )) def scroll(self, *a): adj = self["sw"].get_vadjustment() adj.set_value(adj.get_upper() - adj.get_page_size())
class DaemonOutputDialog(object): """ Displays output from daemon subprocess """ def __init__(self, app, proc): self.proc = proc self.app = app self.setup_widgets() self.handler = 0 def __getitem__(self, name): """ Convince method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def show_with_lines(self, lines, parent=None): if not parent is None: self["dialog"].set_transient_for(parent) self["dialog"].show_all() self["tvOutput"].get_buffer().set_text("\n".join(lines)) def show(self, parent=None, title=None): if parent is None: self["dialog"].set_modal(False) else: self["dialog"].set_transient_for(parent) if not title is None: self["dialog"].set_title(title) self["dialog"].show_all() self["tvOutput"].get_buffer().set_text("\n".join(self.proc.get_output())) self.handler = self.proc.connect('line', self.cb_line) def close(self, *a): if self.handler > 0: self.proc.disconnect(self.handler) self["dialog"].hide() self["dialog"].destroy() def setup_widgets(self): # Load glade file self.builder = UIBuilder() self.builder.add_from_file(os.path.join(self.app.gladepath, "daemon-output.glade")) self.builder.connect_signals(self) self["tvOutput"].connect('size-allocate', self.scroll) def cb_line(self, proc, line): b = self["tvOutput"].get_buffer() b.insert(b.get_iter_at_offset(-1), "\n%s" % (line,)) def scroll(self, *a): adj = self["sw"].get_vadjustment() adj.set_value( adj.get_upper() - adj.get_page_size())
class IgnoreEditor(object): """ Standard looking about dialog """ def __init__(self, app, rid, file_location): # Store stuff self.app = app self.rid = rid self.file_location = file_location # Load UI self.setup_widgets() def __getitem__(self, name): """ Convince method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def show(self, parent=None): if not parent is None: self["dialog"].set_transient_for(parent) self["dialog"].show_all() def close(self, *a): self["dialog"].set_visible(False) self["dialog"].destroy() def setup_widgets(self): # Load glade file self.builder = UIBuilder() self.builder.add_from_file( os.path.join(self.app.gladepath, "ignore-editor.glade")) self.builder.connect_signals(self) self["lblLocation"].set_markup( '%s <a href="file://%s">%s</a>' % (_("File location:"), os.path.join(os.path.expanduser(self.file_location), ".stignore"), os.path.join(self.file_location, ".stignore"))) def on_dialog_response(self, *a): self.close() def cb_btClose_clicked(self, *a): self.close() def on_lblLocation_activate_link(self, *a): # Called when user clicks on file location link. Clicking there # should open .stignore file in default text editor, allowing # user to edit it there. Saving file from this dialog afterwards # would overwrite his changes, so dialog closes itself to # prevent that from happening self.close() def btSave_clicked_cb(self, *a): start_iter = self["tbPatterns"].get_start_iter() end_iter = self["tbPatterns"].get_end_iter() text = self["tbPatterns"].get_text(start_iter, end_iter, True) self["tvPatterns"].set_sensitive(False) self["btSave"].set_sensitive(False) # TODO: Expect error and create appropriate callback for it self.app.daemon.write_stignore(self.rid, text, self.close, self.close) def load(self): self.app.daemon.read_stignore(self.rid, self.cb_data_loaded, self.cb_data_failed) def cb_data_failed(self, *a): # This should be next to impossible, so simply closing dialog # should be enough of "solution" log.error("Failed to load .stignore data: %s", a) self.close() def cb_data_loaded(self, text): self["tbPatterns"].set_text(text) self["tvPatterns"].grab_focus() self["tvPatterns"].set_sensitive(True) self["btSave"].set_sensitive(True)
class IDDialog(object): """ Dialog with Device ID and generated QR code """ def __init__(self, app, device_id): self.app = app self.device_id = device_id self.setup_widgets() self.ssl_ctx = create_ssl_context() self.load_data() def __getitem__(self, name): """ Convince method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def show(self, parent=None): if not parent is None: self["dialog"].set_transient_for(parent) self["dialog"].show_all() def close(self): self["dialog"].hide() self["dialog"].destroy() def setup_widgets(self): # Load glade file self.builder = UIBuilder() self.builder.add_from_file( os.path.join(self.app.gladepath, "device-id.glade")) self.builder.connect_signals(self) self["vID"].set_text(self.device_id) def load_data(self): """ Loads QR code from Syncthing daemon """ if IS_WINDOWS: return self.load_data_urllib() uri = "%s/qr/?text=%s" % (self.app.daemon.get_webui_url(), self.device_id) io = Gio.file_new_for_uri(uri) io.load_contents_async(None, self.cb_syncthing_qr, ()) def load_data_urllib(self): """ Loads QR code from Syncthing daemon """ uri = "%s/qr/?text=%s" % (self.app.daemon.get_webui_url(), self.device_id) api_key = self.app.daemon.get_api_key() opener = urllib.request.build_opener(DummyHTTPSHandler(self.ssl_ctx)) if not api_key is None: opener.addheaders = [("X-API-Key", api_key)] a = opener.open(uri) data = a.read() tf = tempfile.NamedTemporaryFile("wb", suffix=".png", delete=False) tf.write(data) tf.close() self["vQR"].set_from_file(tf.name) os.unlink(tf.name) def cb_btClose_clicked(self, *a): self.close() def cb_syncthing_qr(self, io, results, *a): """ Called when QR code is loaded or operation fails. Image is then displayed in dialog, failure is silently ignored. """ try: ok, contents, etag = io.load_contents_finish(results) if ok: # QR is loaded, save it to temp file and let GTK to handle # rest tf = tempfile.NamedTemporaryFile("wb", suffix=".png", delete=False) tf.write(contents) tf.close() self["vQR"].set_from_file(tf.name) os.unlink(tf.name) except GLib.Error as e: if e.code in [14, 15]: # Unauthorized. Grab CSRF token from daemon and try again log.warning( "Failed to load image using glib. Retrying with urllib2.") self.load_data_urllib() except Exception as e: log.exception(e) return finally: del io
class EditorDialog(GObject.GObject): """ Universal dialog handler for all Syncthing settings and editing Signals: close() emitted after dialog is closed loaded() Emitted after dialog loads and parses configuration data """ __gsignals__ = { b"close": (GObject.SIGNAL_RUN_FIRST, None, ()), b"loaded": (GObject.SIGNAL_RUN_FIRST, None, ()), } # Should be overridden by subclass MESSAGES = {} SETTING_NEEDS_RESTART = [] RESTART_NEEDED_WIDGET = "lblRestartNeeded" def __init__(self, app, gladefile, title): GObject.GObject.__init__(self) self.app = app self.config = None self._loading = False self.values = None self.checks = {} # Stores original label value while error message is displayed. self.original_labels = {} # Used by get_widget_id self.widget_to_id = {} self.setup_widgets(gladefile, title) # Move entire dialog content to ScrolledWindow if screen height # is too small if Gdk.Screen.get_default().height() < 900: if not self["editor-content"] is None: parent = self["editor-content"].get_parent() if isinstance(parent, Gtk.Notebook): order, labels = [], {} for c in [] + parent.get_children(): labels[c] = parent.get_tab_label(c) order.append(c) parent.remove(c) for c in order: sw = Gtk.ScrolledWindow() sw.add_with_viewport(c) parent.append_page(sw, labels[c]) else: sw = Gtk.ScrolledWindow() parent.remove(self["editor-content"]) sw.add_with_viewport(self["editor-content"]) parent.pack_start(sw, True, True, 0) self["editor"].resize( self["editor"].get_size()[0], Gdk.Screen.get_default().height() * 2 / 3) def load(self): """ Loads configuration data and pre-fills values to fields """ self._loading = True self.load_data() def __getitem__(self, name): """ Convince method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def __contains__(self, name): """ Returns true if there is such widget """ return self.builder.get_object(name) != None def get_widget_id(self, w): """ Returns glade file ID for specified widget or None, if widget is not known. """ if not w in self.widget_to_id: return None return self.widget_to_id[w] def find_widget_by_id(self, id, parent=None): """ Recursively searches for widget with specified ID """ if parent == None: if id in self: return self[id] # Do things fast if possible parent = self["editor"] for c in parent.get_children(): if hasattr(c, "get_id"): if c.get_id() == id: return c if isinstance(c, Gtk.Container): r = self.find_widget_by_id(id, c) if not r is None: return r return None def show(self, parent=None): if not parent is None: self["editor"].set_transient_for(parent) self["editor"].set_modal(True) self["editor"].show_all() def present(self, values=[]): self["editor"].present() for v in values: if not self[v] is None and self[v].get_sensitive(): self[v].grab_focus() return def close(self): self.emit("close") self["editor"].hide() self["editor"].destroy() def setup_widgets(self, gladefile, title): # Load glade file self.builder = UIBuilder() self.builder.add_from_file(os.path.join(self.app.gladepath, gladefile)) self.builder.connect_signals(self) self["editor"].set_title(title) # Disable everything until configuration is loaded self["editor"].set_sensitive(False) def get_burried_value(self, key, vals, default, convert=lambda a: a): """ Returns value stored deeper in element tree. Method is called recursively for every tree level. If value is not found, default is returned. """ if type(key) != list: # Parse key, split by '/' return self.get_burried_value(key.split("/"), vals, default, convert) try: if len(key) > 1: tkey, key = key[0], key[1:] return self.get_burried_value(key, vals[tkey], default, convert) return convert(vals[key[0]]) except Exception: return default def get_value(self, key): """ Returns value from configuration. Usually returns self.values[key], but overriding methods can handle some special cases """ if key in self.values: return self.values[key] else: log.warning("get_value: Value %s not found", key) raise ValueNotFoundError(key) def set_value(self, key, value): """ Stores value to configuration, handling some special cases in overriding methods """ if key in self.values: self.values[key] = value else: raise ValueNotFoundError(key) def create_dicts(self, parent, keys): """ Creates structure of nested dicts, if they are not in place already. """ if not type(keys) == list: keys = list(keys) if len(keys) == 0: return # Done key, rest = keys[0], keys[1:] if not key in parent: parent[key] = {} if parent[key] in ("", None): parent[key] = {} self.create_dicts(parent[key], rest) def load_data(self): self.app.daemon.read_config(self.cb_data_loaded, self.cb_data_failed) def display_error_message(self, value_id): """ Changes text on associated label to error message """ wid = "lbl%s" % (value_id, ) # widget id if value_id in self.original_labels: # Already done return if not value_id in self.MESSAGES: # Nothing to show return self.original_labels[value_id] = self[wid].get_label() self[wid].set_markup('<span color="red">%s</span>' % (self.MESSAGES[value_id], )) def hide_error_message(self, value_id): """ Changes text on associated label back to normal text """ wid = "lbl%s" % (value_id, ) # widget id if value_id in self.original_labels: self[wid].set_label(self.original_labels[value_id]) del self.original_labels[value_id] def cb_data_loaded(self, config): """ Used as handler in load_data """ self.config = config if self.on_data_loaded(): self.update_special_widgets() # Enable dialog self["editor"].set_sensitive(True) self._loading = False # Brag self.emit("loaded") def on_data_loaded(self, config): """ Called from cb_data_loaded, should be overridden by subclass. Should return True to indicate that everything is OK, false on error. """ raise RuntimeError("Override this!") def display_values(self, values): """ Iterates over all known configuration values and sets UI elements using unholy method. Returns True. """ for key in values: widget = self.find_widget_by_id(key) self.widget_to_id[widget] = key if not key is None: try: self.display_value(key, widget) except ValueNotFoundError: # Value not found, probably old daemon version log.warning("display_values: Value %s not found", key) widget.set_sensitive(False) GLib.idle_add(self.present, values) return True def display_value(self, key, w): """ Sets value on UI element for single key. May be overridden by subclass to handle special values. """ if isinstance(w, Gtk.SpinButton): w.get_adjustment().set_value(ints(self.get_value(strip_v(key)))) elif isinstance(w, Gtk.Entry): w.set_text(unicode(self.get_value(strip_v(key)))) elif isinstance(w, Gtk.ComboBox): val = self.get_value(strip_v(key)) m = w.get_model() for i in xrange(0, len(m)): if str(val) == str(m[i][0]).strip(): w.set_active(i) break else: w.set_active(0) elif isinstance(w, Gtk.CheckButton): w.set_active(self.get_value(strip_v(key))) else: log.warning( "display_value: %s class cannot handle widget %s, key %s", self.__class__.__name__, w, key) if not w is None: w.set_sensitive(False) def ui_value_changed(self, w, *a): """ Handler for widget that controls state of other widgets """ key = self.get_widget_id(w) if not self._loading: if key in self.SETTING_NEEDS_RESTART: self[self.RESTART_NEEDED_WIDGET].set_visible(True) if key != None: if isinstance(w, Gtk.CheckButton): self.set_value(strip_v(key), w.get_active()) self.update_special_widgets() if isinstance(w, Gtk.ComboBox): self.set_value(strip_v(key), str(w.get_model()[w.get_active()][0]).strip()) self.update_special_widgets() def update_special_widgets(self, *a): """ Enables/disables special widgets. Does nothing by default, but may be overridden by subclasses """ if self.mode == "folder-edit": self["vID"].set_sensitive(self.id is None) v = self.get_value("Versioning") if v == "": if self["rvVersioning"].get_reveal_child(): self["rvVersioning"].set_reveal_child(False) else: self["bxVersioningSimple"].set_visible( self.get_value("Versioning") == "simple") self["bxVersioningStaggered"].set_visible( self.get_value("Versioning") == "staggered") if not self["rvVersioning"].get_reveal_child(): self["rvVersioning"].set_reveal_child(True) elif self.mode == "device-edit": self["vDeviceID"].set_sensitive(self.is_new) self["vAddresses"].set_sensitive( self.id != self.app.daemon.get_my_id()) elif self.mode == "daemon-settings": self["vMaxSendKbps"].set_sensitive( self.get_value("MaxSendKbpsEnabled")) self["lblvLocalAnnPort"].set_sensitive( self.get_value("LocalAnnEnabled")) self["vLocalAnnPort"].set_sensitive( self.get_value("LocalAnnEnabled")) self["lblvGlobalAnnServer"].set_sensitive( self.get_value("GlobalAnnEnabled")) self["vGlobalAnnServer"].set_sensitive( self.get_value("GlobalAnnEnabled")) def cb_data_failed(self, exception, *a): """ Failed to load configuration. This shouldn't happen unless daemon dies exactly when user clicks to edit menu. Handled by simple error message. """ # All other errors are fatal for now. Error dialog is displayed and program exits. d = Gtk.MessageDialog( self["editor"], Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, "%s %s\n\n%s %s" % (_("Failed to load configuration from daemon."), _("Try again."), _("Error message:"), str(exception))) d.run() self.close() def cb_btClose_clicked(self, *a): self.close() def cb_check_value(self, *a): self["btSave"].set_sensitive(True) for x in self.checks: value = self[x].get_text().strip() if len(value) == 0: # Empty value in field if self.checks[x](value): # ... but empty value is OK self.hide_error_message(x) else: self["btSave"].set_sensitive(False) self.hide_error_message(x) elif not self.checks[x](value): # Invalid value in any field self["btSave"].set_sensitive(False) self.display_error_message(x) else: self.hide_error_message(x) def cb_btSave_clicked(self, *a): """ Calls on_save_requested to do actual work """ self.on_save_requested() def on_save_requested(self, config): """ Should be overridden by subclass. Should return True to indicate that everything is OK, false on error. """ raise RuntimeError("Override this!") def store_values(self, values): """ 'values' parameter should be same as display_values recieved. Iterates over values configuration values and puts stuff from UI back to self.values dict Returns True. """ for key in values: widget = self.find_widget_by_id(key) if not key is None: try: self.store_value(key, widget) except ValueNotFoundError: pass return True def store_value(self, key, w): """ Loads single value from UI element to self.values dict. May be overriden by subclass to handle special values. """ if isinstance(w, Gtk.SpinButton): self.set_value(strip_v(key), int(w.get_adjustment().get_value())) elif isinstance(w, Gtk.Entry): self.set_value(strip_v(key), w.get_text().decode("utf-8")) elif isinstance(w, Gtk.CheckButton): self.set_value(strip_v(key), w.get_active()) elif isinstance(w, Gtk.ComboBox): self.set_value(strip_v(key), str(w.get_model()[w.get_active()][0]).strip()) # else nothing, unknown widget class cannot be read def cb_format_value_s(self, spinner): """ Formats spinner value """ spinner.get_buffer().set_text( _("%ss") % (int(spinner.get_adjustment().get_value()), ), -1) return True def cb_format_value_s_or_disabed(self, spinner): """ Formats spinner value """ val = int(spinner.get_adjustment().get_value()) if val < 1: spinner.get_buffer().set_text(_("disabled"), -1) else: spinner.get_buffer().set_text(_("%ss") % (val, ), -1) return True def cb_format_value_percent(self, spinner): """ Formats spinner value """ val = int(spinner.get_adjustment().get_value()) spinner.get_buffer().set_text(_("%s%%") % (val, ), -1) return True def cb_format_value_kibps_or_no_limit(self, spinner): """ Formats spinner value """ val = int(spinner.get_adjustment().get_value()) if val < 1: spinner.get_buffer().set_text(_("no limit"), -1) else: spinner.get_buffer().set_text(_("%s KiB/s") % (val, ), -1) return True def cb_format_value_days(self, spinner): """ Formats spinner value """ v = int(spinner.get_adjustment().get_value()) if v == 0: spinner.get_buffer().set_text(_("never delete"), -1) elif v == 1: spinner.get_buffer().set_text(_("%s day") % (v, ), -1) else: spinner.get_buffer().set_text(_("%s days") % (v, ), -1) return True def post_config(self): """ Posts edited configuration back to daemon """ self["editor"].set_sensitive(False) self.app.daemon.write_config(self.config, self.syncthing_cb_post_config, self.syncthing_cb_post_error) def syncthing_cb_post_config(self, *a): # No return value for this call, let's hope for the best log.info("Configuration (probably) saved") # Close editor self["editor"].set_sensitive(True) self.on_saved() def on_saved(self): """ Should be overridden by subclass. Called after post_config saves configuration. """ raise RuntimeError("Override this!") def syncthing_cb_post_error(self, exception, *a): # TODO: Unified error message if isinstance(exception, ConnectionRestarted): # Should be ok, this restart is triggered # by App handler for 'config-saved' event. return self.syncthing_cb_post_config() message = "%s\n%s" % (_("Failed to save configuration."), str(exception)) if hasattr(exception, "full_response"): try: fr = unicode(exception.full_response)[0:1024] except UnicodeError: # ... localized error strings on windows are usually # in anything but unicode :( fr = str(repr(exception.full_response))[0:1024] message += "\n\n" + fr d = Gtk.MessageDialog( self["editor"], Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, message) d.run() d.hide() d.destroy() self["editor"].set_sensitive(True) def call_after_loaded(self, callback, *data): """ Calls callback when 'loaded' event is emitted """ self.connect( "loaded", # lambda below throws 'event_source' argument and # calls callback with rest of arguments lambda obj, callback, *a: callback(*a), callback, *data)
class IgnoreEditor(object): """ Standard looking about dialog """ def __init__(self, app, rid, file_location): # Store stuff self.app = app self.rid = rid self.file_location = file_location # Load UI self.setup_widgets() def __getitem__(self, name): """ Convince method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def show(self, parent=None): if not parent is None: self["dialog"].set_transient_for(parent) self["dialog"].show_all() def close(self, *a): self["dialog"].set_visible(False) self["dialog"].destroy() def setup_widgets(self): # Load glade file self.builder = UIBuilder() self.builder.add_from_file(os.path.join(self.app.gladepath, "ignore-editor.glade")) self.builder.connect_signals(self) self["lblLocation"].set_markup( '%s <a href="file://%s">%s</a>' % ( _("File location:"), os.path.join(os.path.expanduser(self.file_location), ".stignore"), os.path.join(self.file_location, ".stignore") ) ) def on_dialog_response(self, *a): self.close() def cb_btClose_clicked(self, *a): self.close() def on_lblLocation_activate_link(self, *a): # Called when user clicks on file location link. Clicking there # should open .stignore file in default text editor, allowing # user to edit it there. Saving file from this dialog afterwards # would overwrite his changes, so dialog closes itself to # prevent that from happening self.close() def btSave_clicked_cb(self, *a): start_iter = self["tbPatterns"].get_start_iter() end_iter = self["tbPatterns"].get_end_iter() text = self["tbPatterns"].get_text(start_iter, end_iter, True) self["tvPatterns"].set_sensitive(False) self["btSave"].set_sensitive(False) # TODO: Expect error and create appropriate callback for it self.app.daemon.write_stignore(self.rid, text, self.close, self.close) def load(self): self.app.daemon.read_stignore(self.rid, self.cb_data_loaded, self.cb_data_failed) def cb_data_failed(self, *a): # This should be next to impossible, so simply closing dialog # should be enough of "solution" log.error("Failed to load .stignore data: %s", a) self.close() def cb_data_loaded(self, text): self["tbPatterns"].set_text(text) self["tvPatterns"].grab_focus() self["tvPatterns"].set_sensitive(True) self["btSave"].set_sensitive(True)
class EditorDialog(GObject.GObject): """ Universal dialog handler for all Syncthing settings and editing Signals: close() emitted after dialog is closed loaded() Emitted after dialog loads and parses configuration data """ __gsignals__ = { b"close" : (GObject.SIGNAL_RUN_FIRST, None, ()), b"loaded" : (GObject.SIGNAL_RUN_FIRST, None, ()), } # Should be overridden by subclass MESSAGES = {} SETTING_NEEDS_RESTART = [] RESTART_NEEDED_WIDGET = "lblRestartNeeded" def __init__(self, app, gladefile, title): GObject.GObject.__init__(self) self.app = app self.config = None self._loading = False self.values = None self.checks = {} # Stores original label value while error message is displayed. self.original_labels={} # Used by get_widget_id self.widget_to_id = {} self.setup_widgets(gladefile, title) # Move entire dialog content to ScrolledWindow if screen height # is too small if Gdk.Screen.get_default().height() < 900: if not self["editor-content"] is None: parent = self["editor-content"].get_parent() if isinstance(parent, Gtk.Notebook): order, labels = [], {} for c in [] + parent.get_children(): labels[c] = parent.get_tab_label(c) order.append(c) parent.remove(c) for c in order: sw = Gtk.ScrolledWindow() sw.add_with_viewport(c) parent.append_page(sw, labels[c]) else: sw = Gtk.ScrolledWindow() parent.remove(self["editor-content"]) sw.add_with_viewport(self["editor-content"]) parent.pack_start(sw, True, True, 0) self["editor"].resize(self["editor"].get_size()[0], Gdk.Screen.get_default().height() * 2 / 3) def load(self): """ Loads configuration data and pre-fills values to fields """ self._loading = True self.load_data() def __getitem__(self, name): """ Convience method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def __contains__(self, name): """ Returns true if there is such widget """ return self.builder.get_object(name) != None def get_widget_id(self, w): """ Returns glade file ID for specified widget or None, if widget is not known. """ if not w in self.widget_to_id: return None return self.widget_to_id[w] def find_widget_by_id(self, id, parent=None): """ Recursively searches for widget with specified ID """ if parent == None: if id in self: return self[id] # Do things fast if possible parent = self["editor"] for c in parent.get_children(): if hasattr(c, "get_id"): if c.get_id() == id: return c if isinstance(c, Gtk.Container): r = self.find_widget_by_id(id, c) if not r is None: return r return None def show(self, parent=None): if not parent is None: self["editor"].set_transient_for(parent) self["editor"].set_modal(True) self["editor"].show_all() def present(self, values=[]): self["editor"].present() for v in values: if not self[v] is None and self[v].get_sensitive(): self[v].grab_focus() return def close(self): self.emit("close") self["editor"].hide() self["editor"].destroy() def setup_widgets(self, gladefile, title): # Load glade file self.builder = UIBuilder() self.builder.add_from_file(os.path.join(self.app.gladepath, gladefile)) self.builder.connect_signals(self) self["editor"].set_title(title) # Disable everything until configuration is loaded self["editor"].set_sensitive(False) def get_burried_value(self, key, vals, default, convert=lambda a:a): """ Returns value stored deeper in element tree. Method is called recursively for every tree level. If value is not found, default is returned. """ if type(key) != list: # Parse key, split by '/' return self.get_burried_value(key.split("/"), vals, default, convert) try: if len(key) > 1: tkey, key = key[0], key[1:] return self.get_burried_value(key, vals[tkey], default, convert) return convert(vals[key[0]]) except Exception: return default def get_value(self, key): """ Returns value from configuration. Usually returns self.values[key], but overriding methods can handle some special cases """ if key in self.values: return self.values[key] else: log.warning("get_value: Value %s not found", key) raise ValueNotFoundError(key) def set_value(self, key, value): """ Stores value to configuration, handling some special cases in overriding methods """ if key in self.values: self.values[key] = value else: raise ValueNotFoundError(key) def create_dicts(self, parent, keys): """ Creates structure of nested dicts, if they are not in place already. """ if not type(keys) == list: keys = list(keys) if len(keys) == 0 : return # Done key, rest = keys[0], keys[1:] if not key in parent : parent[key] = {} if parent[key] in ("", None ): parent[key] = {} self.create_dicts(parent[key], rest) def load_data(self): self.app.daemon.read_config(self.cb_data_loaded, self.cb_data_failed) def display_error_message(self, value_id): """ Changes text on associated label to error message """ wid = "lbl%s" % (value_id,) # widget id if value_id in self.original_labels: # Already done return if not value_id in self.MESSAGES: # Nothing to show return self.original_labels[value_id] = self[wid].get_label() self[wid].set_markup('<span color="red">%s</span>' % (self.MESSAGES[value_id],)) def hide_error_message(self, value_id): """ Changes text on associated label back to normal text """ wid = "lbl%s" % (value_id,) # widget id if value_id in self.original_labels: self[wid].set_label(self.original_labels[value_id]) del self.original_labels[value_id] def cb_data_loaded(self, config): """ Used as handler in load_data """ self.config = config if self.on_data_loaded(): self.update_special_widgets() # Enable dialog self["editor"].set_sensitive(True) self._loading = False # Brag self.emit("loaded") def on_data_loaded(self, config): """ Called from cb_data_loaded, should be overridden by subclass. Should return True to indicate that everything is OK, false on error. """ raise RuntimeError("Override this!") def display_values(self, values): """ Iterates over all known configuration values and sets UI elements using unholy method. Returns True. """ for key in values: widget = self.find_widget_by_id(key) self.widget_to_id[widget] = key if not key is None: try: self.display_value(key, widget) except ValueNotFoundError: # Value not found, probably old daemon version log.warning("display_values: Value %s not found", key) widget.set_sensitive(False) GLib.idle_add(self.present, values) return True def display_value(self, key, w): """ Sets value on UI element for single key. May be overridden by subclass to handle special values. """ if isinstance(w, Gtk.SpinButton): w.get_adjustment().set_value(ints(self.get_value(strip_v(key)))) elif isinstance(w, Gtk.Entry): w.set_text(unicode(self.get_value(strip_v(key)))) elif isinstance(w, Gtk.ComboBox): val = self.get_value(strip_v(key)) m = w.get_model() for i in xrange(0, len(m)): if str(val) == str(m[i][0]).strip(): w.set_active(i) break else: w.set_active(0) elif isinstance(w, Gtk.CheckButton): w.set_active(self.get_value(strip_v(key))) else: log.warning("display_value: %s class cannot handle widget %s, key %s", self.__class__.__name__, w, key) if not w is None: w.set_sensitive(False) def ui_value_changed(self, w, *a): """ Handler for widget that controls state of other widgets """ key = self.get_widget_id(w) if not self._loading: if key in self.SETTING_NEEDS_RESTART: self[self.RESTART_NEEDED_WIDGET].set_visible(True) if key != None: if isinstance(w, Gtk.CheckButton): self.set_value(strip_v(key), w.get_active()) self.update_special_widgets() if isinstance(w, Gtk.ComboBox): self.set_value(strip_v(key), str(w.get_model()[w.get_active()][0]).strip()) self.update_special_widgets() def update_special_widgets(self, *a): """ Enables/disables special widgets. Does nothing by default, but may be overridden by subclasses """ if self.mode == "folder-edit": self["vID"].set_sensitive(self.id is None) v = self.get_value("Versioning") if v == "": if self["rvVersioning"].get_reveal_child(): self["rvVersioning"].set_reveal_child(False) else: self["bxVersioningSimple"].set_visible(self.get_value("Versioning") == "simple") self["bxVersioningStaggered"].set_visible(self.get_value("Versioning") == "staggered") if not self["rvVersioning"].get_reveal_child(): self["rvVersioning"].set_reveal_child(True) elif self.mode == "device-edit": self["vDeviceID"].set_sensitive(self.is_new) self["vAddresses"].set_sensitive(self.id != self.app.daemon.get_my_id()) elif self.mode == "daemon-settings": self["vMaxSendKbps"].set_sensitive(self.get_value("MaxSendKbpsEnabled")) self["lblvLocalAnnPort"].set_sensitive(self.get_value("LocalAnnEnabled")) self["vLocalAnnPort"].set_sensitive(self.get_value("LocalAnnEnabled")) self["lblvGlobalAnnServer"].set_sensitive(self.get_value("GlobalAnnEnabled")) self["vGlobalAnnServer"].set_sensitive(self.get_value("GlobalAnnEnabled")) def cb_data_failed(self, exception, *a): """ Failed to load configuration. This shouldn't happen unless daemon dies exactly when user clicks to edit menu. Handled by simple error message. """ # All other errors are fatal for now. Error dialog is displayed and program exits. d = Gtk.MessageDialog( self["editor"], Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, "%s %s\n\n%s %s" % ( _("Failed to load configuration from daemon."), _("Try again."), _("Error message:"), str(exception) ) ) d.run() self.close() def cb_btClose_clicked(self, *a): self.close() def cb_check_value(self, *a): self["btSave"].set_sensitive(True) for x in self.checks: value = self[x].get_text().strip() if len(value) == 0: # Empty value in field if self.checks[x](value): # ... but empty value is OK self.hide_error_message(x) else: self["btSave"].set_sensitive(False) self.hide_error_message(x) elif not self.checks[x](value): # Invalid value in any field self["btSave"].set_sensitive(False) self.display_error_message(x) else: self.hide_error_message(x) def cb_btSave_clicked(self, *a): """ Calls on_save_requested to do actual work """ self.on_save_requested() def on_save_requested(self, config): """ Should be overridden by subclass. Should return True to indicate that everything is OK, false on error. """ raise RuntimeError("Override this!") def store_values(self, values): """ 'values' parameter should be same as display_values received. Iterates over values configuration values and puts stuff from UI back to self.values dict Returns True. """ for key in values: widget = self.find_widget_by_id(key) if not key is None: try: self.store_value(key, widget) except ValueNotFoundError: pass return True def store_value(self, key, w): """ Loads single value from UI element to self.values dict. May be overriden by subclass to handle special values. """ if isinstance(w, Gtk.SpinButton): self.set_value(strip_v(key), int(w.get_adjustment().get_value())) elif isinstance(w, Gtk.Entry): self.set_value(strip_v(key), w.get_text().decode("utf-8")) elif isinstance(w, Gtk.CheckButton): self.set_value(strip_v(key), w.get_active()) elif isinstance(w, Gtk.ComboBox): self.set_value(strip_v(key), str(w.get_model()[w.get_active()][0]).strip()) # else nothing, unknown widget class cannot be read def cb_format_value_s(self, spinner): """ Formats spinner value """ spinner.get_buffer().set_text(_("%ss") % (int(spinner.get_adjustment().get_value()),), -1) return True def cb_format_value_s_or_disabed(self, spinner): """ Formats spinner value """ val = int(spinner.get_adjustment().get_value()) if val < 1: spinner.get_buffer().set_text(_("disabled"), -1) else: spinner.get_buffer().set_text(_("%ss") % (val,), -1) return True def cb_format_value_percent(self, spinner): """ Formats spinner value """ val = int(spinner.get_adjustment().get_value()) spinner.get_buffer().set_text(_("%s%%") % (val,), -1) return True def cb_format_value_kibps_or_no_limit(self, spinner): """ Formats spinner value """ val = int(spinner.get_adjustment().get_value()) if val < 1: spinner.get_buffer().set_text(_("no limit"), -1) else: spinner.get_buffer().set_text(_("%s KiB/s") % (val,), -1) return True def cb_format_value_days(self, spinner): """ Formats spinner value """ v = int(spinner.get_adjustment().get_value()) if v == 0: spinner.get_buffer().set_text(_("never delete"), -1) elif v == 1: spinner.get_buffer().set_text(_("%s day") % (v,), -1) else: spinner.get_buffer().set_text(_("%s days") % (v,), -1) return True def post_config(self): """ Posts edited configuration back to daemon """ self["editor"].set_sensitive(False) self.app.daemon.write_config(self.config, self.syncthing_cb_post_config, self.syncthing_cb_post_error) def syncthing_cb_post_config(self, *a): # No return value for this call, let's hope for the best log.info("Configuration (probably) saved") # Close editor self["editor"].set_sensitive(True) self.on_saved() def on_saved(self): """ Should be overridden by subclass. Called after post_config saves configuration. """ raise RuntimeError("Override this!") def syncthing_cb_post_error(self, exception, *a): # TODO: Unified error message if isinstance(exception, ConnectionRestarted): # Should be ok, this restart is triggered # by App handler for 'config-saved' event. return self.syncthing_cb_post_config() message = "%s\n%s" % ( _("Failed to save configuration."), str(exception) ) if hasattr(exception, "full_response"): try: fr = unicode(exception.full_response)[0:1024] except UnicodeError: # ... localized error strings on windows are usually # in anything but unicode :( fr = str(repr(exception.full_response))[0:1024] message += "\n\n" + fr d = Gtk.MessageDialog( self["editor"], Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, message ) d.run() d.hide() d.destroy() self["editor"].set_sensitive(True) def call_after_loaded(self, callback, *data): """ Calls callback when 'loaded' event is emitted """ self.connect("loaded", # lambda below throws 'event_source' argument and # calls callback with rest of arguments lambda obj, callback, *a : callback(*a), callback, *data )
class IDDialog(object): """ Dialog with Device ID and generated QR code """ def __init__(self, app, device_id): self.app = app self.device_id = device_id self.setup_widgets() self.ssl_ctx = create_ssl_context() self.load_data() def __getitem__(self, name): """ Convince method that allows widgets to be accessed via self["widget"] """ return self.builder.get_object(name) def show(self, parent=None): if not parent is None: self["dialog"].set_transient_for(parent) self["dialog"].show_all() def close(self): self["dialog"].hide() self["dialog"].destroy() def setup_widgets(self): # Load glade file self.builder = UIBuilder() self.builder.add_from_file(os.path.join(self.app.gladepath, "device-id.glade")) self.builder.connect_signals(self) self["vID"].set_text(self.device_id) def load_data(self): """ Loads QR code from Syncthing daemon """ if IS_WINDOWS: return self.load_data_urllib() uri = "%s/qr/?text=%s" % (self.app.daemon.get_webui_url(), self.device_id) io = Gio.file_new_for_uri(uri) io.load_contents_async(None, self.cb_syncthing_qr, ()) def load_data_urllib(self): """ Loads QR code from Syncthing daemon """ uri = "%s/qr/?text=%s" % (self.app.daemon.get_webui_url(), self.device_id) api_key = self.app.daemon.get_api_key() opener = urllib2.build_opener(DummyHTTPSHandler(self.ssl_ctx)) if not api_key is None: opener.addheaders = [("X-API-Key", api_key)] a = opener.open(uri) data = a.read() tf = tempfile.NamedTemporaryFile("wb", suffix=".png", delete=False) tf.write(data) tf.close() self["vQR"].set_from_file(tf.name) os.unlink(tf.name) def cb_btClose_clicked(self, *a): self.close() def cb_syncthing_qr(self, io, results, *a): """ Called when QR code is loaded or operation fails. Image is then displayed in dialog, failure is silently ignored. """ try: ok, contents, etag = io.load_contents_finish(results) if ok: # QR is loaded, save it to temp file and let GTK to handle # rest tf = tempfile.NamedTemporaryFile("wb", suffix=".png", delete=False) tf.write(contents) tf.close() self["vQR"].set_from_file(tf.name) os.unlink(tf.name) except GLib.Error as e: if e.code in [14, 15]: # Unauthorized. Grab CSRF token from daemon and try again log.warning("Failed to load image using glib. Retrying with urllib2.") self.load_data_urllib() except Exception as e: log.exception(e) return finally: del io