diff --git a/xapp/GSettingsWidgets.py b/xapp/GSettingsWidgets.py
new file mode 100644
index 0000000..4672877
--- /dev/null
+++ b/xapp/GSettingsWidgets.py
@@ -0,0 +1,199 @@
+#!/usr/bin/python3
+
+from gi.repository import Gio, GLib
+from xapp.SettingsWidgets import *
+
+CAN_BACKEND = ["Switch", "SpinButton", "Entry", "TextView", "FontButton", "Range", "ComboBox",
+ "ColorChooser", "FileChooser", "IconChooser"]
+
+# Monkey patch Gio.Settings object
+def __setitem__(self, key, value):
+ # set_value() aborts the program on an unknown key
+ if key not in self:
+ raise KeyError('unknown key: %r' % (key,))
+
+ # determine type string of this key
+ range = self.get_range(key)
+ type_ = range.get_child_value(0).get_string()
+ v = range.get_child_value(1)
+ if type_ == 'type':
+ # v is boxed empty array, type of its elements is the allowed value type
+ assert v.get_child_value(0).get_type_string().startswith('a')
+ type_str = v.get_child_value(0).get_type_string()[1:]
+ elif type_ == 'enum':
+ # v is an array with the allowed values
+ assert v.get_child_value(0).get_type_string().startswith('a')
+ type_str = v.get_child_value(0).get_child_value(0).get_type_string()
+ elif type_ == 'flags':
+ # v is an array with the allowed values
+ assert v.get_child_value(0).get_type_string().startswith('a')
+ type_str = v.get_child_value(0).get_type_string()
+ elif type_ == 'range':
+ # type_str is a tuple giving the range
+ assert v.get_child_value(0).get_type_string().startswith('(')
+ type_str = v.get_child_value(0).get_type_string()[1]
+
+ if not self.set_value(key, GLib.Variant(type_str, value)):
+ raise ValueError("value '%s' for key '%s' is outside of valid range" % (value, key))
+
+def bind_with_mapping(self, key, widget, prop, flags, key_to_prop, prop_to_key):
+ self._ignore_key_changed = False
+
+ def key_changed(settings, key):
+ if self._ignore_key_changed:
+ return
+ self._ignore_prop_changed = True
+ widget.set_property(prop, key_to_prop(self[key]))
+ self._ignore_prop_changed = False
+
+ def prop_changed(widget, param):
+ if self._ignore_prop_changed:
+ return
+ self._ignore_key_changed = True
+ self[key] = prop_to_key(widget.get_property(prop))
+ self._ignore_key_changed = False
+
+ if not (flags & (Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET)): # ie Gio.SettingsBindFlags.DEFAULT
+ flags |= Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET
+ if flags & Gio.SettingsBindFlags.GET:
+ key_changed(self, key)
+ if not (flags & Gio.SettingsBindFlags.GET_NO_CHANGES):
+ self.connect('changed::' + key, key_changed)
+ if flags & Gio.SettingsBindFlags.SET:
+ widget.connect('notify::' + prop, prop_changed)
+ if not (flags & Gio.SettingsBindFlags.NO_SENSITIVITY):
+ self.bind_writable(key, widget, "sensitive", False)
+
+Gio.Settings.bind_with_mapping = bind_with_mapping
+Gio.Settings.__setitem__ = __setitem__
+
+class BinFileMonitor(GObject.GObject):
+ __gsignals__ = {
+ 'changed': (GObject.SignalFlags.RUN_LAST, None, ()),
+ }
+ def __init__(self):
+ super(BinFileMonitor, self).__init__()
+
+ self.changed_id = 0
+
+ env = GLib.getenv("PATH")
+
+ if env == None:
+ env = "/bin:/usr/bin:."
+
+ self.paths = env.split(":")
+
+ self.monitors = []
+
+ for path in self.paths:
+ file = Gio.File.new_for_path(path)
+ mon = file.monitor_directory(Gio.FileMonitorFlags.SEND_MOVED, None)
+ mon.connect("changed", self.queue_emit_changed)
+ self.monitors.append(mon)
+
+ def _emit_changed(self):
+ self.emit("changed")
+ self.changed_id = 0
+ return False
+
+ def queue_emit_changed(self, file, other, event_type, data=None):
+ if self.changed_id > 0:
+ GLib.source_remove(self.changed_id)
+ self.changed_id = 0
+
+ self.changed_id = GLib.idle_add(self._emit_changed)
+
+file_monitor = None
+
+def get_file_monitor():
+ global file_monitor
+
+ if file_monitor == None:
+ file_monitor = BinFileMonitor()
+
+ return file_monitor
+
+# This class is not meant to be used directly - it is only a backend for the
+# settings widgets to enable them to bind attributes to gsettings keys. To use
+# the gesttings backend, simply add the "GSettings" prefix to the beginning
+# of the widget class name. The arguments of the backended class will be
+# (label, schema, key, any additional widget-specific args and keyword args).
+# (Note: this only works for classes that are gsettings compatible.)
+#
+# If you wish to make a new widget available to be backended, place it in the
+# CAN_BACKEND list. In addition, you will need to add the following attributes
+# to the widget class:
+#
+# bind_dir - (Gio.SettingsBindFlags) flags to define the binding direction or
+# None if you don't want the setting bound (for example if the
+# setting effects multiple attributes)
+# bind_prop - (string) the attribute in the widget that will be bound to the
+# setting. This property may be omitted if bind_dir is None
+# bind_object - (optional) the object to which to bind to (only needed if the
+# attribute to be bound is not a property of self.content_widget)
+# map_get, map_set - (function, optional) a function to map between setting and
+# bound attribute. May also be passed as a keyword arg during
+# instantiation. These will be ignored if bind_dir=None
+# set_rounding - (function, optional) To be used to set the digits to round to
+# if the setting is an integer
+class PXGSettingsBackend(object):
+ def bind_settings(self):
+ if hasattr(self, "set_rounding"):
+ vtype = self.settings.get_value(self.key).get_type_string()
+ if vtype in ["i", "u"]:
+ self.set_rounding(0)
+ if hasattr(self, "bind_object"):
+ bind_object = self.bind_object
+ else:
+ bind_object = self.content_widget
+ if hasattr(self, "map_get") or hasattr(self, "map_set"):
+ self.settings.bind_with_mapping(self.key, bind_object, self.bind_prop, self.bind_dir, self.map_get, self.map_set)
+ elif self.bind_dir != None:
+ self.settings.bind(self.key, bind_object, self.bind_prop, self.bind_dir)
+ else:
+ self.settings.connect("changed::"+self.key, self.on_setting_changed)
+ self.settings.bind_writable(self.key, bind_object, "sensitive", False)
+ self.on_setting_changed()
+ self.connect_widget_handlers()
+
+ def set_value(self, value):
+ self.settings[self.key] = value
+
+ def get_value(self):
+ return self.settings[self.key]
+
+ def get_range(self):
+ range = self.settings.get_range(self.key)
+ if range[0] == "range":
+ return [range[1][0], range[1][1]]
+ else:
+ return None
+
+ def on_setting_changed(self, *args):
+ raise NotImplementedError("SettingsWidget class must implement on_setting_changed().")
+
+ def connect_widget_handlers(self, *args):
+ if self.bind_dir == None:
+ raise NotImplementedError("SettingsWidget classes with no .bind_dir must implement connect_widget_handlers().")
+
+def g_settings_factory(subclass):
+ class NewClass(globals()[subclass], PXGSettingsBackend):
+ def __init__(self, label, schema, key, *args, **kwargs):
+ self.key = key
+ if schema not in settings_objects:
+ settings_objects[schema] = Gio.Settings.new(schema)
+ self.settings = settings_objects[schema]
+
+ if "map_get" in kwargs:
+ self.map_get = kwargs["map_get"]
+ del kwargs["map_get"]
+ if "map_set" in kwargs:
+ self.map_set = kwargs["map_set"]
+ del kwargs["map_set"]
+
+ super(NewClass, self).__init__(label, *args, **kwargs)
+ self.bind_settings()
+ return NewClass
+
+for widget in CAN_BACKEND:
+ globals()["GSettings"+widget] = g_settings_factory(widget)
diff --git a/xapp/SettingsWidgets.py b/xapp/SettingsWidgets.py
new file mode 100755
index 0000000..ee317a9
--- /dev/null
+++ b/xapp/SettingsWidgets.py
@@ -0,0 +1,719 @@
+#!/usr/bin/python3
+
+import math
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('XApp', '1.0')
+from gi.repository import Gio, Gtk, GObject, Gdk, GLib, XApp
+
+settings_objects = {}
+
+class EditableEntry (Gtk.Stack):
+
+ __gsignals__ = {
+ 'changed': (GObject.SignalFlags.RUN_FIRST, None,
+ (str,))
+ }
+
+ def __init__ (self):
+ super(EditableEntry, self).__init__()
+
+ self.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
+ self.set_transition_duration(150)
+
+ self.label = Gtk.Label()
+ self.entry = Gtk.Entry()
+ self.button = Gtk.Button()
+
+ self.button.set_alignment(1.0, 0.5)
+ self.button.set_relief(Gtk.ReliefStyle.NONE)
+ self.add_named(self.button, "button");
+ self.add_named(self.entry, "entry");
+ self.set_visible_child_name("button")
+ self.editable = False
+ self.current_text = None
+ self.show_all()
+
+ self.button.connect("released", self._on_button_clicked)
+ self.button.connect("activate", self._on_button_clicked)
+ self.entry.connect("activate", self._on_entry_validated)
+ self.entry.connect("changed", self._on_entry_changed)
+ self.entry.connect("focus-out-event", self._on_focus_lost)
+
+ def set_text(self, text):
+ self.button.set_label(text)
+ self.entry.set_text(text)
+ self.current_text = text
+
+ def _on_focus_lost(self, widget, event):
+ self.button.set_label(self.current_text)
+ self.entry.set_text(self.current_text)
+
+ self.set_editable(False)
+
+ def _on_button_clicked(self, button):
+ self.set_editable(True)
+ self.entry.grab_focus()
+
+ def _on_entry_validated(self, entry):
+ self.set_editable(False)
+ self.emit("changed", entry.get_text())
+ self.current_text = entry.get_text()
+
+ def _on_entry_changed(self, entry):
+ self.button.set_label(entry.get_text())
+
+ def set_editable(self, editable):
+ if (editable):
+ self.set_visible_child_name("entry")
+ else:
+ self.set_visible_child_name("button")
+ self.editable = editable
+
+ def set_tooltip_text(self, tooltip):
+ self.button.set_tooltip_text(tooltip)
+
+ def get_editable(self):
+ return self.editable
+
+ def get_text(self):
+ return self.entry.get_text()
+
+class SettingsStack(Gtk.Stack):
+ def __init__(self):
+ Gtk.Stack.__init__(self)
+ self.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
+ self.set_transition_duration(150)
+ self.expand = True
+
+class SettingsRevealer(Gtk.Revealer):
+ def __init__(self, schema=None, key=None, values=None, check_func=None):
+ Gtk.Revealer.__init__(self)
+
+ self.check_func = check_func
+
+ self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=15)
+ Gtk.Revealer.add(self, self.box)
+
+ self.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN)
+ self.set_transition_duration(150)
+
+ if schema:
+ self.settings = Gio.Settings.new(schema)
+ # if there aren't values or a function provided to determine visibility we can do a simple bind
+ if values is None and check_func is None:
+ self.settings.bind(key, self, "reveal-child", Gio.SettingsBindFlags.GET)
+ else:
+ self.values = values
+ self.settings.connect("changed::" + key, self.on_settings_changed)
+ self.on_settings_changed(self.settings, key)
+
+ def add(self, widget):
+ self.box.pack_start(widget, False, True, 0)
+
+ #only used when checking values
+ def on_settings_changed(self, settings, key):
+ value = settings.get_value(key).unpack()
+ if self.check_func is None:
+ self.set_reveal_child(value in self.values)
+ else:
+ self.set_reveal_child(self.check_func(value, self.values))
+
+class SettingsPage(Gtk.Box):
+ def __init__(self):
+ Gtk.Box.__init__(self)
+ self.set_orientation(Gtk.Orientation.VERTICAL)
+ self.set_spacing(30)
+ self.set_margin_left(80)
+ self.set_margin_right(80)
+ self.set_margin_top(15)
+ self.set_margin_bottom(15)
+
+ def add_section(self, title=None, subtitle=None):
+ section = SettingsSection(title, subtitle)
+ self.pack_start(section, False, False, 0)
+
+ return section
+
+ def add_reveal_section(self, title, schema=None, key=None, values=None, revealer=None):
+ section = SettingsSection(title)
+ if revealer is None:
+ revealer = SettingsRevealer(schema, key, values)
+ revealer.add(section)
+ section._revealer = revealer
+ self.pack_start(revealer, False, False, 0)
+
+ return section
+
+class SettingsSection(Gtk.Box):
+ def __init__(self, title=None, subtitle=None):
+ Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
+ self.set_spacing(10)
+
+ if title or subtitle:
+ header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ header_box.set_spacing(5)
+ self.add(header_box)
+
+ if title:
+ label = Gtk.Label()
+ label.set_markup("%s" % title)
+ label.set_alignment(0, 0.5)
+ header_box.add(label)
+
+ if subtitle:
+ sub = Gtk.Label()
+ sub.set_text(subtitle)
+ sub.get_style_context().add_class("dim-label")
+ sub.set_alignment(0, 0.5)
+ header_box.add(sub)
+
+ self.frame = Gtk.Frame()
+ self.frame.set_shadow_type(Gtk.ShadowType.IN)
+ frame_style = self.frame.get_style_context()
+ frame_style.add_class("view")
+ self.size_group = Gtk.SizeGroup()
+ self.size_group.set_mode(Gtk.SizeGroupMode.VERTICAL)
+
+ self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ self.frame.add(self.box)
+
+ self.need_separator = False
+
+ def add_row(self, widget):
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ if self.need_separator:
+ vbox.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
+ list_box = Gtk.ListBox()
+ list_box.set_selection_mode(Gtk.SelectionMode.NONE)
+ row = Gtk.ListBoxRow(can_focus=False)
+ row.add(widget)
+ if isinstance(widget, Switch):
+ list_box.connect("row-activated", widget.clicked)
+ list_box.add(row)
+ vbox.add(list_box)
+ self.box.add(vbox)
+
+ if self.frame.get_parent() is None:
+ self.add(self.frame)
+
+ self.need_separator = True
+
+ def add_reveal_row(self, widget, schema=None, key=None, values=None, check_func=None, revealer=None):
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ if self.need_separator:
+ vbox.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
+ list_box = Gtk.ListBox()
+ list_box.set_selection_mode(Gtk.SelectionMode.NONE)
+ row = Gtk.ListBoxRow(can_focus=False)
+ row.add(widget)
+ if isinstance(widget, Switch):
+ list_box.connect("row-activated", widget.clicked)
+ list_box.add(row)
+ vbox.add(list_box)
+ if revealer is None:
+ revealer = SettingsRevealer(schema, key, values, check_func)
+ widget.revealer = revealer
+ revealer.add(vbox)
+ self.box.add(revealer)
+
+ self.need_separator = True
+
+ return revealer
+
+ def add_note(self, text):
+ label = Gtk.Label()
+ label.set_alignment(0, 0.5)
+ label.set_markup(text)
+ label.set_line_wrap(True)
+ self.add(label)
+ return label
+
+class SettingsWidget(Gtk.Box):
+ def __init__(self, dep_key=None):
+ Gtk.Box.__init__(self)
+ self.set_orientation(Gtk.Orientation.HORIZONTAL)
+ self.set_spacing(20)
+ self.set_border_width(5)
+ self.set_margin_left(20)
+ self.set_margin_right(20)
+
+ if dep_key:
+ self.set_dep_key(dep_key)
+
+ def set_dep_key(self, dep_key):
+ flag = Gio.SettingsBindFlags.GET
+ if dep_key[0] == "!":
+ dep_key = dep_key[1:]
+ flag |= Gio.Settings.BindFlags.INVERT_BOOLEAN
+
+ split = dep_key.split("/")
+ dep_settings = Gio.Settings.new(split[0])
+ dep_settings.bind(split[1], self, "sensitive", flag)
+
+ def add_to_size_group(self, group):
+ group.add_widget(self.content_widget)
+
+ def fill_row(self):
+ self.set_border_width(0)
+ self.set_margin_left(0)
+ self.set_margin_right(0)
+
+ def get_settings(self, schema):
+ global settings_objects
+ try:
+ return settings_objects[schema]
+ except:
+ settings_objects[schema] = Gio.Settings.new(schema)
+ return settings_objects[schema]
+
+class SettingsLabel(Gtk.Label):
+ def __init__(self, text=None):
+ Gtk.Label.__init__(self)
+ if text:
+ self.set_label(text)
+
+ self.set_alignment(0.0, 0.5)
+ self.set_line_wrap(True)
+
+ def set_label_text(self, text):
+ self.set_label(text)
+
+class Switch(SettingsWidget):
+ bind_prop = "active"
+ bind_dir = Gio.SettingsBindFlags.DEFAULT
+
+ def __init__(self, label, dep_key=None, tooltip=""):
+ super(Switch, self).__init__(dep_key=dep_key)
+
+ self.content_widget = Gtk.Switch(valign=Gtk.Align.CENTER)
+ self.label = SettingsLabel(label)
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, False, False, 0)
+
+ self.set_tooltip_text(tooltip)
+
+ def clicked(self, *args):
+ if self.is_sensitive():
+ self.content_widget.set_active(not self.content_widget.get_active())
+
+class SpinButton(SettingsWidget):
+ bind_prop = "value"
+ bind_dir = Gio.SettingsBindFlags.GET
+
+ def __init__(self, label, units="", mini=None, maxi=None, step=1, page=None, size_group=None, dep_key=None, tooltip=""):
+ super(SpinButton, self).__init__(dep_key=dep_key)
+
+ self.timer = None
+
+ if units:
+ label += " (%s)" % units
+ self.label = SettingsLabel(label)
+ self.content_widget = Gtk.SpinButton()
+
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, False, False, 0)
+
+ range = self.get_range()
+ if mini == None or maxi == None:
+ mini = range[0]
+ maxi = range[1]
+ elif range is not None:
+ mini = max(mini, range[0])
+ maxi = min(maxi, range[1])
+
+ if not page:
+ page = step
+
+ self.content_widget.set_range(mini, maxi)
+ self.content_widget.set_increments(step, page)
+
+ digits = 0
+ if (step and '.' in str(step)):
+ digits = len(str(step).split('.')[1])
+ self.content_widget.set_digits(digits)
+
+ self.content_widget.connect("value-changed", self.apply_later)
+
+ self.set_tooltip_text(tooltip)
+
+ if size_group:
+ self.add_to_size_group(size_group)
+
+ def apply_later(self, *args):
+ def apply(self):
+ self.set_value(self.content_widget.get_value())
+ self.timer = None
+
+ if self.timer:
+ GLib.source_remove(self.timer)
+ self.timer = GLib.timeout_add(300, apply, self)
+
+class Entry(SettingsWidget):
+ bind_prop = "text"
+ bind_dir = Gio.SettingsBindFlags.DEFAULT
+
+ def __init__(self, label, expand_width=False, size_group=None, dep_key=None, tooltip=""):
+ super(Entry, self).__init__(dep_key=dep_key)
+
+ self.label = SettingsLabel(label)
+ self.content_widget = Gtk.Entry()
+ self.content_widget.set_valign(Gtk.Align.CENTER)
+
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, expand_width, expand_width, 0)
+
+ self.set_tooltip_text(tooltip)
+
+ if size_group:
+ self.add_to_size_group(size_group)
+
+class TextView(SettingsWidget):
+ bind_prop = "text"
+ bind_dir = Gio.SettingsBindFlags.DEFAULT
+
+ def __init__(self, label, height=200, dep_key=None, tooltip=""):
+ super(TextView, self).__init__(dep_key=dep_key)
+
+ self.set_orientation(Gtk.Orientation.VERTICAL)
+ self.set_spacing(8)
+
+ self.label = Gtk.Label.new(label)
+ self.label.set_halign(Gtk.Align.CENTER)
+
+ self.scrolledwindow = Gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
+ self.scrolledwindow.set_size_request(width=-1, height=height)
+ self.scrolledwindow.set_policy(hscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
+ vscrollbar_policy=Gtk.PolicyType.AUTOMATIC)
+ self.scrolledwindow.set_shadow_type(type=Gtk.ShadowType.ETCHED_IN)
+ self.content_widget = Gtk.TextView()
+ self.content_widget.set_border_width(3)
+ self.content_widget.set_wrap_mode(wrap_mode=Gtk.WrapMode.NONE)
+ self.bind_object = self.content_widget.get_buffer()
+
+ self.pack_start(self.label, False, False, 0)
+ self.add(self.scrolledwindow)
+ self.scrolledwindow.add(self.content_widget)
+ self._value_changed_timer = None
+
+class FontButton(SettingsWidget):
+ bind_prop = "font-name"
+ bind_dir = Gio.SettingsBindFlags.DEFAULT
+
+ def __init__(self, label, size_group=None, dep_key=None, tooltip=""):
+ super(FontButton, self).__init__(dep_key=dep_key)
+
+ self.label = SettingsLabel(label)
+
+ self.content_widget = Gtk.FontButton()
+ self.content_widget.set_valign(Gtk.Align.CENTER)
+
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, False, False, 0)
+
+ self.set_tooltip_text(tooltip)
+
+ if size_group:
+ self.add_to_size_group(size_group)
+
+class Range(SettingsWidget):
+ bind_prop = "value"
+ bind_dir = Gio.SettingsBindFlags.GET | Gio.SettingsBindFlags.NO_SENSITIVITY
+
+ def __init__(self, label, min_label="", max_label="", mini=None, maxi=None, step=None, invert=False, log=False, show_value=True, dep_key=None, tooltip="", flipped=False, units=""):
+ super(Range, self).__init__(dep_key=dep_key)
+
+ self.set_orientation(Gtk.Orientation.VERTICAL)
+ self.set_spacing(0)
+
+ self.log = log
+ self.invert = invert
+ self.flipped = flipped
+ self.timer = None
+ self.value = 0
+
+ hbox = Gtk.Box()
+
+ if units:
+ label += " ({})".format(units)
+
+ self.label = Gtk.Label.new(label)
+ self.label.set_halign(Gtk.Align.CENTER)
+
+ self.min_label= Gtk.Label()
+ self.max_label = Gtk.Label()
+ self.min_label.set_alignment(1.0, 0.75)
+ self.max_label.set_alignment(1.0, 0.75)
+ self.min_label.set_margin_right(6)
+ self.max_label.set_margin_left(6)
+ self.min_label.set_markup("%s" % min_label)
+ self.max_label.set_markup("%s" % max_label)
+
+ range = self.get_range()
+ if mini == None or maxi == None:
+ mini = range[0]
+ maxi = range[1]
+ elif range is not None:
+ mini = max(mini, range[0])
+ maxi = min(maxi, range[1])
+
+ if log:
+ mini = math.log(mini)
+ maxi = math.log(maxi)
+ if self.flipped:
+ self.map_get = lambda x: -1 * (math.log(x))
+ self.map_set = lambda x: math.exp(x)
+ else:
+ self.map_get = lambda x: math.log(x)
+ self.map_set = lambda x: math.exp(x)
+ elif self.flipped:
+ self.map_get = lambda x: x * -1
+ self.map_set = lambda x: x * -1
+
+ if self.flipped:
+ tmp_mini = mini
+ mini = maxi * -1
+ maxi = tmp_mini * -1
+
+ if step is None:
+ self.step = (maxi - mini) * 0.02
+ else:
+ self.step = math.log(step) if log else step
+
+ self.content_widget = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, mini, maxi, self.step)
+ self.content_widget.set_inverted(invert)
+ self.content_widget.set_draw_value(show_value and not self.flipped)
+ self.bind_object = self.content_widget.get_adjustment()
+
+ if invert:
+ self.step *= -1 # Gtk.Scale.new_with_range want a positive value, but our custom scroll handler wants a negative value
+
+ hbox.pack_start(self.min_label, False, False, 0)
+ hbox.pack_start(self.content_widget, True, True, 0)
+ hbox.pack_start(self.max_label, False, False, 0)
+
+ self.pack_start(self.label, False, False, 0)
+ self.pack_start(hbox, True, True, 6)
+
+ self.content_widget.connect("scroll-event", self.on_scroll_event)
+ self.content_widget.connect("value-changed", self.apply_later)
+
+ self.set_tooltip_text(tooltip)
+
+ def apply_later(self, *args):
+ def apply(self):
+ if self.log:
+ self.set_value(math.exp(abs(self.content_widget.get_value())))
+ else:
+ if self.flipped:
+ self.set_value(self.content_widget.get_value() * -1)
+ else:
+ self.set_value(self.content_widget.get_value())
+ self.timer = None
+
+ if self.timer:
+ GLib.source_remove(self.timer)
+ self.timer = GLib.timeout_add(300, apply, self)
+
+ def on_scroll_event(self, widget, event):
+ found, delta_x, delta_y = event.get_scroll_deltas()
+
+ # If you scroll up, delta_y < 0. This is a weird world
+ widget.set_value(widget.get_value() - delta_y * self.step)
+
+ return True
+
+ def add_mark(self, value, position, markup):
+ if self.log:
+ self.content_widget.add_mark(math.log(value), position, markup)
+ else:
+ self.content_widget.add_mark(value, position, markup)
+
+ def set_rounding(self, digits):
+ if not self.log:
+ self.content_widget.set_round_digits(digits)
+ self.content_widget.set_digits(digits)
+
+class ComboBox(SettingsWidget):
+ bind_dir = None
+
+ def __init__(self, label, options=[], valtype=None, size_group=None, dep_key=None, tooltip=""):
+ super(ComboBox, self).__init__(dep_key=dep_key)
+
+ self.valtype = valtype
+ self.option_map = {}
+
+ self.label = SettingsLabel(label)
+
+ self.content_widget = Gtk.ComboBox()
+ renderer_text = Gtk.CellRendererText()
+ self.content_widget.pack_start(renderer_text, True)
+ self.content_widget.add_attribute(renderer_text, "text", 1)
+
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, False, False, 0)
+ self.content_widget.set_valign(Gtk.Align.CENTER)
+
+ self.set_options(options)
+
+ self.set_tooltip_text(tooltip)
+
+ if size_group:
+ self.add_to_size_group(size_group)
+
+ def on_my_value_changed(self, widget):
+ tree_iter = widget.get_active_iter()
+ if tree_iter != None:
+ self.value = self.model[tree_iter][0]
+ self.set_value(self.value)
+
+ def on_setting_changed(self, *args):
+ self.value = self.get_value()
+ try:
+ self.content_widget.set_active_iter(self.option_map[self.value])
+ except:
+ self.content_widget.set_active_iter(None)
+
+ def connect_widget_handlers(self, *args):
+ self.content_widget.connect('changed', self.on_my_value_changed)
+
+ def set_options(self, options):
+ if self.valtype is not None:
+ var_type = self.valtype
+ else:
+ # assume all keys are the same type (mixing types is going to cause an error somewhere)
+ var_type = type(options[0][0])
+ self.model = Gtk.ListStore(var_type, str)
+
+ for option in options:
+ self.option_map[option[0]] = self.model.append([option[0], option[1]])
+
+ self.content_widget.set_model(self.model)
+ self.content_widget.set_id_column(0)
+
+class ColorChooser(SettingsWidget):
+ bind_dir = None
+
+ def __init__(self, label, legacy_string=False, size_group=None, dep_key=None, tooltip=""):
+ super(ColorChooser, self).__init__(dep_key=dep_key)
+ # note: Gdk.Color is deprecated in favor of Gdk.RGBA, but as the hex format is still used
+ # in some places (most notably the desktop background handling in cinnamon-desktop) we
+ # still support it for now by adding the legacy_string argument
+ self.legacy_string = legacy_string
+
+ self.label = SettingsLabel(label)
+ self.content_widget = Gtk.ColorButton()
+ self.content_widget.set_use_alpha(True)
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, False, False, 0)
+
+ self.set_tooltip_text(tooltip)
+
+ if size_group:
+ self.add_to_size_group(size_group)
+
+ def on_setting_changed(self, *args):
+ color_string = self.get_value()
+ rgba = Gdk.RGBA()
+ rgba.parse(color_string)
+ self.content_widget.set_rgba(rgba)
+
+ def connect_widget_handlers(self, *args):
+ self.content_widget.connect('color-set', self.on_my_value_changed)
+
+ def on_my_value_changed(self, widget):
+ if self.legacy_string:
+ color_string = self.content_widget.get_color().to_string()
+ else:
+ color_string = self.content_widget.get_rgba().to_string()
+ self.set_value(color_string)
+
+class FileChooser(SettingsWidget):
+ bind_dir = None
+
+ def __init__(self, label, dir_select=False, size_group=None, dep_key=None, tooltip=""):
+ super(FileChooser, self).__init__(dep_key=dep_key)
+ if dir_select:
+ action = Gtk.FileChooserAction.SELECT_FOLDER
+ else:
+ action = Gtk.FileChooserAction.OPEN
+
+ self.label = SettingsLabel(label)
+ self.content_widget = Gtk.FileChooserButton(action=action)
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, False, False, 0)
+
+ self.set_tooltip_text(tooltip)
+
+ if size_group:
+ self.add_to_size_group(size_group)
+
+ def on_file_selected(self, *args):
+ self.set_value(self.content_widget.get_uri())
+
+ def on_setting_changed(self, *args):
+ self.content_widget.set_uri(self.get_value())
+
+ def connect_widget_handlers(self, *args):
+ self.content_widget.connect("file-set", self.on_file_selected)
+
+class IconChooser(SettingsWidget):
+ bind_prop = "icon"
+ bind_dir = Gio.SettingsBindFlags.DEFAULT
+
+ def __init__(self, label, expand_width=False, size_group=None, dep_key=None, tooltip=""):
+ super(IconChooser, self).__init__(dep_key=dep_key)
+
+ self.label = SettingsLabel(label)
+
+ self.content_widget = XApp.IconChooserButton()
+ self.content_widget.set_icon_size(Gtk.IconSize.BUTTON)
+
+ self.pack_start(self.label, False, False, 0)
+ self.pack_end(self.content_widget, expand_width, expand_width, 0)
+
+ self.set_tooltip_text(tooltip)
+
+ if size_group:
+ self.add_to_size_group(size_group)
+
+class Button(SettingsWidget):
+ def __init__(self, label, callback=None):
+ super(Button, self).__init__()
+ self.label = label
+ self.callback = callback
+
+ self.content_widget = Gtk.Button(label=label)
+ self.pack_start(self.content_widget, True, True, 0)
+ self.content_widget.connect("clicked", self._on_button_clicked)
+
+ def _on_button_clicked(self, *args):
+ if self.callback is not None:
+ self.callback(self)
+ elif hasattr(self, "on_activated"):
+ self.on_activated()
+ else:
+ print("warning: button '%s' does nothing" % self.label)
+
+ def set_label(self, label):
+ self.label = label
+ self.content_widget.set_label(label)
+
+class Text(SettingsWidget):
+ def __init__(self, label, align=Gtk.Align.START):
+ super(Text, self).__init__()
+ self.label = label
+
+ if align == Gtk.Align.END:
+ xalign = 1.0
+ justification = Gtk.Justification.RIGHT
+ elif align == Gtk.Align.CENTER:
+ xalign = 0.5
+ justification = Gtk.Justification.CENTER
+ else: # START and FILL align left
+ xalign = 0
+ justification = Gtk.Justification.LEFT
+
+ self.content_widget = Gtk.Label(label, halign=align, xalign=xalign, justify=justification)
+ self.content_widget.set_line_wrap(True)
+ self.pack_start(self.content_widget, True, True, 0)
diff --git a/xapp/__init__.py b/xapp/__init__.py
index f6843c0..8927040 100644
--- a/xapp/__init__.py
+++ b/xapp/__init__.py
@@ -1,3 +1,3 @@
-__all__ = [ "os" ]
+__all__ = [ "os", "GSettingsWidgets", "SettingsWidgets" ]
__version__ = "1.0.0"