Ejemplo n.º 1
0
 def setup_widgets(self, app):
     # Load glade file
     self.builder = UIBuilder()
     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
             vfile = file(os.path.join(get_install_path(), "__version__"),
                          "r")
             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.dialog.set_version(app_ver)
Ejemplo n.º 2
0
 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)
Ejemplo n.º 3
0
 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)
Ejemplo n.º 4
0
 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)
Ejemplo n.º 5
0
class AboutDialog(object):
    """ Standard looking about dialog """
    def __init__(self, app, gladepath):
        self.gladepath = gladepath
        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):
        # Load glade file
        self.builder = UIBuilder()
        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
                vfile = file(os.path.join(get_install_path(), "__version__"),
                             "r")
                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.dialog.set_version(app_ver)

    def on_dialog_response(self, *a):
        self.close()
Ejemplo n.º 6
0
 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")))
Ejemplo n.º 7
0
class AboutDialog(object):
	""" Standard looking about dialog """
	def __init__(self, app, gladepath):
		self.gladepath = gladepath
		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):
		# Load glade file
		self.builder = UIBuilder()
		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
				vfile = file(os.path.join(get_install_path(), "__version__"), "r")
				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.dialog.set_version(app_ver)
	
	def on_dialog_response(self, *a):
		self.close()
Ejemplo n.º 8
0
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())
Ejemplo n.º 9
0
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())
Ejemplo n.º 10
0
	def setup_widgets(self, app):
		# Load glade file
		self.builder = UIBuilder()
		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
				vfile = file(os.path.join(get_install_path(), "__version__"), "r")
				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)
Ejemplo n.º 11
0
	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)
Ejemplo n.º 12
0
	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")
			)
		)
Ejemplo n.º 13
0
	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)
Ejemplo n.º 14
0
	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)
Ejemplo n.º 15
0
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, e:
			if e.code == 14:
				# 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, e:
			log.exception(e)
			return
Ejemplo n.º 16
0
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 overrided by subclass
	MESSAGES = {}
	
	def __init__(self, app, gladefile, title):
		GObject.GObject.__init__(self)
		self.app = app
		self.config = None
		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()
				parent.remove(self["editor-content"])
				sw = Gtk.ScrolledWindow()
				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.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 searchs 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.
		Usualy 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)
			# Brag
			self.emit("loaded")
	
	def on_data_loaded(self, config):
		"""
		Called from cb_data_loaded, should be overrided 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 overriden
		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
		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 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 overrided 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_reuqested to do actual work """
		self.on_save_reuqested()
	
	def on_save_reuqested(self, config):
		"""
		Should be overrided 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_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 overrided 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()
		d = Gtk.MessageDialog(
			self["editor"],
			Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
			Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE,
			"%s\n%s" % (
				_("Failed to save configuration."),
				str(exception)
			))
		d.run()
		d.hide()
		d.destroy()
		self["editor"].set_sensitive(True)
	
	def call_after_loaded(self, callback, *data):
		""" Calls callback when 'loaded' event is emited """
		self.connect("loaded",
			# lambda below throws 'event_source' argument and
			# calls callback with rest of arguments
			lambda obj, callback, *a : callback(*a),
			callback, *data
			)
Ejemplo n.º 17
0
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 enought 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)
Ejemplo n.º 18
0
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, e:
            if e.code == 14:
                # 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, e:
            log.exception(e)
            return
Ejemplo n.º 19
0
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 enought 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)
Ejemplo n.º 20
0
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 overrided by subclass
    MESSAGES = {}

    def __init__(self, app, gladefile, title):
        GObject.GObject.__init__(self)
        self.app = app
        self.config = None
        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()
                parent.remove(self["editor-content"])
                sw = Gtk.ScrolledWindow()
                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.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 searchs 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.
		Usualy 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)
            # Brag
            self.emit("loaded")

    def on_data_loaded(self, config):
        """
		Called from cb_data_loaded, should be overrided 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 overriden
		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
        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 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 overrided 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_reuqested to do actual work """
        self.on_save_reuqested()

    def on_save_reuqested(self, config):
        """
		Should be overrided 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_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 overrided 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()
        d = Gtk.MessageDialog(
            self["editor"],
            Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE,
            "%s\n%s" % (_("Failed to save configuration."), str(exception)))
        d.run()
        d.hide()
        d.destroy()
        self["editor"].set_sensitive(True)

    def call_after_loaded(self, callback, *data):
        """ Calls callback when 'loaded' event is emited """
        self.connect(
            "loaded",
            # lambda below throws 'event_source' argument and
            # calls callback with rest of arguments
            lambda obj, callback, *a: callback(*a),
            callback,
            *data)