1#!/usr/local/bin/python3.8
2
3import math
4import gi
5gi.require_version('Gtk', '3.0')
6gi.require_version('XApp', '1.0')
7from gi.repository import Gio, Gtk, GObject, Gdk, GLib, XApp
8
9settings_objects = {}
10
11class EditableEntry (Gtk.Stack):
12
13    __gsignals__ = {
14        'changed': (GObject.SignalFlags.RUN_FIRST, None,
15                    (str,))
16    }
17
18    def __init__ (self):
19        super(EditableEntry, self).__init__()
20
21        self.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
22        self.set_transition_duration(150)
23
24        self.label = Gtk.Label()
25        self.entry = Gtk.Entry()
26        self.button = Gtk.Button()
27
28        self.button.set_alignment(1.0, 0.5)
29        self.button.set_relief(Gtk.ReliefStyle.NONE)
30        self.add_named(self.button, "button");
31        self.add_named(self.entry, "entry");
32        self.set_visible_child_name("button")
33        self.editable = False
34        self.current_text = None
35        self.show_all()
36
37        self.button.connect("released", self._on_button_clicked)
38        self.button.connect("activate", self._on_button_clicked)
39        self.entry.connect("activate", self._on_entry_validated)
40        self.entry.connect("changed", self._on_entry_changed)
41        self.entry.connect("focus-out-event", self._on_focus_lost)
42
43    def set_text(self, text):
44        self.button.set_label(text)
45        self.entry.set_text(text)
46        self.current_text = text
47
48    def _on_focus_lost(self, widget, event):
49        self.button.set_label(self.current_text)
50        self.entry.set_text(self.current_text)
51
52        self.set_editable(False)
53
54    def _on_button_clicked(self, button):
55        self.set_editable(True)
56        self.entry.grab_focus()
57
58    def _on_entry_validated(self, entry):
59        self.set_editable(False)
60        self.emit("changed", entry.get_text())
61        self.current_text = entry.get_text()
62
63    def _on_entry_changed(self, entry):
64        self.button.set_label(entry.get_text())
65
66    def set_editable(self, editable):
67        if (editable):
68            self.set_visible_child_name("entry")
69        else:
70            self.set_visible_child_name("button")
71        self.editable = editable
72
73    def set_tooltip_text(self, tooltip):
74        self.button.set_tooltip_text(tooltip)
75
76    def get_editable(self):
77        return self.editable
78
79    def get_text(self):
80        return self.entry.get_text()
81
82class SettingsStack(Gtk.Stack):
83    def __init__(self):
84        Gtk.Stack.__init__(self)
85        self.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
86        self.set_transition_duration(150)
87        self.expand = True
88
89class SettingsRevealer(Gtk.Revealer):
90    def __init__(self, schema=None, key=None, values=None, check_func=None):
91        Gtk.Revealer.__init__(self)
92
93        self.check_func = check_func
94
95        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=15)
96        Gtk.Revealer.add(self, self.box)
97
98        self.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN)
99        self.set_transition_duration(150)
100
101        if schema:
102            self.settings = Gio.Settings.new(schema)
103            # if there aren't values or a function provided to determine visibility we can do a simple bind
104            if values is None and check_func is None:
105                self.settings.bind(key, self, "reveal-child", Gio.SettingsBindFlags.GET)
106            else:
107                self.values = values
108                self.settings.connect("changed::" + key, self.on_settings_changed)
109                self.on_settings_changed(self.settings, key)
110
111    def add(self, widget):
112        self.box.pack_start(widget, False, True, 0)
113
114    #only used when checking values
115    def on_settings_changed(self, settings, key):
116        value = settings.get_value(key).unpack()
117        if self.check_func is None:
118            self.set_reveal_child(value in self.values)
119        else:
120            self.set_reveal_child(self.check_func(value, self.values))
121
122class SettingsPage(Gtk.Box):
123    def __init__(self):
124        Gtk.Box.__init__(self)
125        self.set_orientation(Gtk.Orientation.VERTICAL)
126        self.set_spacing(30)
127        self.set_margin_left(80)
128        self.set_margin_right(80)
129        self.set_margin_top(15)
130        self.set_margin_bottom(15)
131
132    def add_section(self, title=None, subtitle=None):
133        section = SettingsSection(title, subtitle)
134        self.pack_start(section, False, False, 0)
135
136        return section
137
138    def add_reveal_section(self, title, schema=None, key=None, values=None, revealer=None):
139        section = SettingsSection(title)
140        if revealer is None:
141            revealer = SettingsRevealer(schema, key, values)
142        revealer.add(section)
143        section._revealer = revealer
144        self.pack_start(revealer, False, False, 0)
145
146        return section
147
148class SettingsSection(Gtk.Box):
149    def __init__(self, title=None, subtitle=None):
150        Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
151        self.set_spacing(10)
152
153        self.always_show = False
154        self.revealers = []
155
156        if title or subtitle:
157            header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
158            header_box.set_spacing(5)
159            self.add(header_box)
160
161            if title:
162                label = Gtk.Label()
163                label.set_markup("<b>%s</b>" % title)
164                label.set_alignment(0, 0.5)
165                header_box.add(label)
166
167            if subtitle:
168                sub = Gtk.Label()
169                sub.set_text(subtitle)
170                sub.get_style_context().add_class("dim-label")
171                sub.set_alignment(0, 0.5)
172                header_box.add(sub)
173
174        self.frame = Gtk.Frame()
175        self.frame.set_no_show_all(True)
176        self.frame.set_shadow_type(Gtk.ShadowType.IN)
177        frame_style = self.frame.get_style_context()
178        frame_style.add_class("view")
179        self.size_group = Gtk.SizeGroup()
180        self.size_group.set_mode(Gtk.SizeGroupMode.VERTICAL)
181
182        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
183        self.frame.add(self.box)
184        self.add(self.frame)
185
186        self.need_separator = False
187
188    def add_row(self, widget):
189        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
190        if self.need_separator:
191            vbox.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
192        list_box = Gtk.ListBox()
193        list_box.set_selection_mode(Gtk.SelectionMode.NONE)
194        row = Gtk.ListBoxRow(can_focus=False)
195        row.add(widget)
196        if isinstance(widget, Switch):
197            list_box.connect("row-activated", widget.clicked)
198        list_box.add(row)
199        vbox.add(list_box)
200        self.box.add(vbox)
201
202        self.update_always_show_state()
203
204        self.need_separator = True
205
206    def add_reveal_row(self, widget, schema=None, key=None, values=None, check_func=None, revealer=None):
207        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
208        if self.need_separator:
209            vbox.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
210        list_box = Gtk.ListBox()
211        list_box.set_selection_mode(Gtk.SelectionMode.NONE)
212        row = Gtk.ListBoxRow(can_focus=False)
213        row.add(widget)
214        if isinstance(widget, Switch):
215            list_box.connect("row-activated", widget.clicked)
216        list_box.add(row)
217        vbox.add(list_box)
218        if revealer is None:
219            revealer = SettingsRevealer(schema, key, values, check_func)
220        widget.revealer = revealer
221        revealer.add(vbox)
222        self.box.add(revealer)
223
224        self.need_separator = True
225
226        self.revealers.append(revealer)
227        if not self.always_show:
228            revealer.notify_id = revealer.connect('notify::child-revealed', self.check_reveal_state)
229            self.check_reveal_state()
230
231        return revealer
232
233    def add_note(self, text):
234        label = Gtk.Label()
235        label.set_alignment(0, 0.5)
236        label.set_markup(text)
237        label.set_line_wrap(True)
238        self.add(label)
239        return label
240
241    def update_always_show_state(self):
242        if self.always_show:
243            return
244
245        self.frame.set_no_show_all(False)
246        self.frame.show_all()
247        self.always_show = True
248
249        for revealer in self.revealers:
250            revealer.disconnect(revealer.notify_id)
251
252    def check_reveal_state(self, *args):
253        for revealer in self.revealers:
254            if revealer.props.child_revealed:
255                self.box.show_all()
256                self.frame.show()
257                return
258
259        self.frame.hide()
260
261class SettingsWidget(Gtk.Box):
262    def __init__(self, dep_key=None):
263        Gtk.Box.__init__(self)
264        self.set_orientation(Gtk.Orientation.HORIZONTAL)
265        self.set_spacing(20)
266        self.set_border_width(5)
267        self.set_margin_left(20)
268        self.set_margin_right(20)
269
270        if dep_key:
271            self.set_dep_key(dep_key)
272
273    def set_dep_key(self, dep_key):
274        flag = Gio.SettingsBindFlags.GET
275        if dep_key[0] == "!":
276            dep_key = dep_key[1:]
277            flag |= Gio.Settings.BindFlags.INVERT_BOOLEAN
278
279        split = dep_key.split("/")
280        dep_settings = Gio.Settings.new(split[0])
281        dep_settings.bind(split[1], self, "sensitive", flag)
282
283    def add_to_size_group(self, group):
284        group.add_widget(self.content_widget)
285
286    def fill_row(self):
287        self.set_border_width(0)
288        self.set_margin_left(0)
289        self.set_margin_right(0)
290
291    def get_settings(self, schema):
292        global settings_objects
293        try:
294            return settings_objects[schema]
295        except:
296            settings_objects[schema] = Gio.Settings.new(schema)
297            return settings_objects[schema]
298
299class SettingsLabel(Gtk.Label):
300    def __init__(self, text=None):
301        Gtk.Label.__init__(self)
302        if text:
303            self.set_label(text)
304
305        self.set_alignment(0.0, 0.5)
306        self.set_line_wrap(True)
307
308    def set_label_text(self, text):
309        self.set_label(text)
310
311class Switch(SettingsWidget):
312    bind_prop = "active"
313    bind_dir = Gio.SettingsBindFlags.DEFAULT
314
315    def __init__(self, label, dep_key=None, tooltip=""):
316        super(Switch, self).__init__(dep_key=dep_key)
317
318        self.content_widget = Gtk.Switch(valign=Gtk.Align.CENTER)
319        self.label = SettingsLabel(label)
320        self.pack_start(self.label, False, False, 0)
321        self.pack_end(self.content_widget, False, False, 0)
322
323        self.set_tooltip_text(tooltip)
324
325    def clicked(self, *args):
326        if self.is_sensitive():
327            self.content_widget.set_active(not self.content_widget.get_active())
328
329class SpinButton(SettingsWidget):
330    bind_prop = "value"
331    bind_dir = Gio.SettingsBindFlags.GET
332
333    def __init__(self, label, units="", mini=None, maxi=None, step=1, page=None, size_group=None, dep_key=None, tooltip=""):
334        super(SpinButton, self).__init__(dep_key=dep_key)
335
336        self.timer = None
337
338        if units:
339            label += " (%s)" % units
340        self.label = SettingsLabel(label)
341        self.content_widget = Gtk.SpinButton()
342
343        self.pack_start(self.label, False, False, 0)
344        self.pack_end(self.content_widget, False, False, 0)
345
346        range = self.get_range()
347        if mini == None or maxi == None:
348            mini = range[0]
349            maxi = range[1]
350        elif range is not None:
351            mini = max(mini, range[0])
352            maxi = min(maxi, range[1])
353
354        if not page:
355            page = step
356
357        self.content_widget.set_range(mini, maxi)
358        self.content_widget.set_increments(step, page)
359
360        digits = 0
361        if (step and '.' in str(step)):
362            digits = len(str(step).split('.')[1])
363        self.content_widget.set_digits(digits)
364
365        self.content_widget.connect("value-changed", self.apply_later)
366
367        self.set_tooltip_text(tooltip)
368
369        if size_group:
370            self.add_to_size_group(size_group)
371
372    def apply_later(self, *args):
373        def apply(self):
374            self.set_value(self.content_widget.get_value())
375            self.timer = None
376
377        if self.timer:
378            GLib.source_remove(self.timer)
379        self.timer = GLib.timeout_add(300, apply, self)
380
381class Entry(SettingsWidget):
382    bind_prop = "text"
383    bind_dir = Gio.SettingsBindFlags.DEFAULT
384
385    def __init__(self, label, expand_width=False, size_group=None, dep_key=None, tooltip=""):
386        super(Entry, self).__init__(dep_key=dep_key)
387
388        self.label = SettingsLabel(label)
389        self.content_widget = Gtk.Entry()
390        self.content_widget.set_valign(Gtk.Align.CENTER)
391
392        self.pack_start(self.label, False, False, 0)
393        self.pack_end(self.content_widget, expand_width, expand_width, 0)
394
395        self.set_tooltip_text(tooltip)
396
397        if size_group:
398            self.add_to_size_group(size_group)
399
400class TextView(SettingsWidget):
401    bind_prop = "text"
402    bind_dir = Gio.SettingsBindFlags.DEFAULT
403
404    def __init__(self, label, height=200, dep_key=None, tooltip=""):
405        super(TextView, self).__init__(dep_key=dep_key)
406
407        self.set_orientation(Gtk.Orientation.VERTICAL)
408        self.set_spacing(8)
409
410        self.label = Gtk.Label.new(label)
411        self.label.set_halign(Gtk.Align.CENTER)
412
413        self.scrolledwindow = Gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
414        self.scrolledwindow.set_size_request(width=-1, height=height)
415        self.scrolledwindow.set_policy(hscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
416                                       vscrollbar_policy=Gtk.PolicyType.AUTOMATIC)
417        self.scrolledwindow.set_shadow_type(type=Gtk.ShadowType.ETCHED_IN)
418        self.content_widget = Gtk.TextView()
419        self.content_widget.set_border_width(3)
420        self.content_widget.set_wrap_mode(wrap_mode=Gtk.WrapMode.NONE)
421        self.bind_object = self.content_widget.get_buffer()
422
423        self.pack_start(self.label, False, False, 0)
424        self.add(self.scrolledwindow)
425        self.scrolledwindow.add(self.content_widget)
426        self._value_changed_timer = None
427
428class FontButton(SettingsWidget):
429    bind_prop = "font-name"
430    bind_dir = Gio.SettingsBindFlags.DEFAULT
431
432    def __init__(self, label, size_group=None, dep_key=None, tooltip=""):
433        super(FontButton, self).__init__(dep_key=dep_key)
434
435        self.label = SettingsLabel(label)
436
437        self.content_widget = Gtk.FontButton()
438        self.content_widget.set_valign(Gtk.Align.CENTER)
439
440        self.pack_start(self.label, False, False, 0)
441        self.pack_end(self.content_widget, False, False, 0)
442
443        self.set_tooltip_text(tooltip)
444
445        if size_group:
446            self.add_to_size_group(size_group)
447
448class Range(SettingsWidget):
449    bind_prop = "value"
450    bind_dir = Gio.SettingsBindFlags.GET | Gio.SettingsBindFlags.NO_SENSITIVITY
451
452    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=""):
453        super(Range, self).__init__(dep_key=dep_key)
454
455        self.set_orientation(Gtk.Orientation.VERTICAL)
456        self.set_spacing(0)
457
458        self.log = log
459        self.invert = invert
460        self.flipped = flipped
461        self.timer = None
462        self.value = 0
463
464        hbox = Gtk.Box()
465
466        if units:
467            label += " ({})".format(units)
468
469        self.label = Gtk.Label.new(label)
470        self.label.set_halign(Gtk.Align.CENTER)
471
472        self.min_label= Gtk.Label()
473        self.max_label = Gtk.Label()
474        self.min_label.set_alignment(1.0, 0.75)
475        self.max_label.set_alignment(1.0, 0.75)
476        self.min_label.set_margin_right(6)
477        self.max_label.set_margin_left(6)
478        self.min_label.set_markup("<i><small>%s</small></i>" % min_label)
479        self.max_label.set_markup("<i><small>%s</small></i>" % max_label)
480
481        range = self.get_range()
482        if mini == None or maxi == None:
483            mini = range[0]
484            maxi = range[1]
485        elif range is not None:
486            mini = max(mini, range[0])
487            maxi = min(maxi, range[1])
488
489        if log:
490            mini = math.log(mini)
491            maxi = math.log(maxi)
492            if self.flipped:
493                self.map_get = lambda x: -1 * (math.log(x))
494                self.map_set = lambda x: math.exp(x)
495            else:
496                self.map_get = lambda x: math.log(x)
497                self.map_set = lambda x: math.exp(x)
498        elif self.flipped:
499            self.map_get = lambda x: x * -1
500            self.map_set = lambda x: x * -1
501
502        if self.flipped:
503            tmp_mini = mini
504            mini = maxi * -1
505            maxi = tmp_mini * -1
506
507        if step is None:
508            self.step = (maxi - mini) * 0.02
509        else:
510            self.step = math.log(step) if log else step
511
512        self.content_widget = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, mini, maxi, self.step)
513        self.content_widget.set_inverted(invert)
514        self.content_widget.set_draw_value(show_value and not self.flipped)
515        self.bind_object = self.content_widget.get_adjustment()
516
517        if invert:
518            self.step *= -1 # Gtk.Scale.new_with_range want a positive value, but our custom scroll handler wants a negative value
519
520        hbox.pack_start(self.min_label, False, False, 0)
521        hbox.pack_start(self.content_widget, True, True, 0)
522        hbox.pack_start(self.max_label, False, False, 0)
523
524        self.pack_start(self.label, False, False, 0)
525        self.pack_start(hbox, True, True, 6)
526
527        self.content_widget.connect("scroll-event", self.on_scroll_event)
528        self.content_widget.connect("value-changed", self.apply_later)
529
530        self.set_tooltip_text(tooltip)
531
532    def apply_later(self, *args):
533        def apply(self):
534            if self.log:
535                self.set_value(math.exp(abs(self.content_widget.get_value())))
536            else:
537                if self.flipped:
538                    self.set_value(self.content_widget.get_value() * -1)
539                else:
540                    self.set_value(self.content_widget.get_value())
541            self.timer = None
542
543        if self.timer:
544            GLib.source_remove(self.timer)
545        self.timer = GLib.timeout_add(300, apply, self)
546
547    def on_scroll_event(self, widget, event):
548        found, delta_x, delta_y = event.get_scroll_deltas()
549
550        # If you scroll up, delta_y < 0. This is a weird world
551        widget.set_value(widget.get_value() - delta_y * self.step)
552
553        return True
554
555    def add_mark(self, value, position, markup):
556        if self.log:
557            self.content_widget.add_mark(math.log(value), position, markup)
558        else:
559            self.content_widget.add_mark(value, position, markup)
560
561    def set_rounding(self, digits):
562        if not self.log:
563            self.content_widget.set_round_digits(digits)
564            self.content_widget.set_digits(digits)
565
566class ComboBox(SettingsWidget):
567    bind_dir = None
568
569    def __init__(self, label, options=[], valtype=None, separator=None, size_group=None, dep_key=None, tooltip=""):
570        super(ComboBox, self).__init__(dep_key=dep_key)
571
572        self.valtype = valtype
573        self.separator = separator
574        self.option_map = {}
575
576        self.label = SettingsLabel(label)
577
578        self.content_widget = Gtk.ComboBox()
579        renderer_text = Gtk.CellRendererText()
580        self.content_widget.pack_start(renderer_text, True)
581        self.content_widget.add_attribute(renderer_text, "text", 1)
582
583        self.pack_start(self.label, False, False, 0)
584        self.pack_end(self.content_widget, False, False, 0)
585        self.content_widget.set_valign(Gtk.Align.CENTER)
586
587        self.set_options(options)
588
589        if separator:
590            self.content_widget.set_row_separator_func(self.is_separator_row)
591
592        self.set_tooltip_text(tooltip)
593
594        if size_group:
595            self.add_to_size_group(size_group)
596
597    def on_my_value_changed(self, widget):
598        tree_iter = widget.get_active_iter()
599        if tree_iter != None:
600            self.value = self.model[tree_iter][0]
601            self.set_value(self.value)
602
603    def on_setting_changed(self, *args):
604        self.value = self.get_value()
605        try:
606            self.content_widget.set_active_iter(self.option_map[self.value])
607        except:
608            self.content_widget.set_active_iter(None)
609
610    def connect_widget_handlers(self, *args):
611        self.content_widget.connect('changed', self.on_my_value_changed)
612
613    def set_options(self, options):
614        if self.valtype is not None:
615            var_type = self.valtype
616        else:
617            # assume all keys are the same type (mixing types is going to cause an error somewhere)
618            var_type = type(options[0][0])
619        self.model = Gtk.ListStore(var_type, str)
620
621        for option in options:
622            self.option_map[option[0]] = self.model.append([option[0], option[1]])
623
624        self.content_widget.set_model(self.model)
625        self.content_widget.set_id_column(0)
626
627    def is_separator_row(self, model, tree_iter):
628        if model[tree_iter][0] == self.separator:
629            return True
630        else:
631            return False
632
633class ColorChooser(SettingsWidget):
634    bind_dir = None
635
636    def __init__(self, label, legacy_string=False, size_group=None, dep_key=None, tooltip=""):
637        super(ColorChooser, self).__init__(dep_key=dep_key)
638        # note: Gdk.Color is deprecated in favor of Gdk.RGBA, but as the hex format is still used
639        # in some places (most notably the desktop background handling in cinnamon-desktop) we
640        # still support it for now by adding the legacy_string argument
641        self.legacy_string = legacy_string
642
643        self.label = SettingsLabel(label)
644        self.content_widget = Gtk.ColorButton()
645        self.content_widget.set_use_alpha(True)
646        self.pack_start(self.label, False, False, 0)
647        self.pack_end(self.content_widget, False, False, 0)
648
649        self.set_tooltip_text(tooltip)
650
651        if size_group:
652            self.add_to_size_group(size_group)
653
654    def on_setting_changed(self, *args):
655        color_string = self.get_value()
656        rgba = Gdk.RGBA()
657        rgba.parse(color_string)
658        self.content_widget.set_rgba(rgba)
659
660    def connect_widget_handlers(self, *args):
661        self.content_widget.connect('color-set', self.on_my_value_changed)
662
663    def on_my_value_changed(self, widget):
664        if self.legacy_string:
665            color_string = self.content_widget.get_color().to_string()
666        else:
667            color_string = self.content_widget.get_rgba().to_string()
668        self.set_value(color_string)
669
670class FileChooser(SettingsWidget):
671    bind_dir = None
672
673    def __init__(self, label, dir_select=False, size_group=None, dep_key=None, tooltip=""):
674        super(FileChooser, self).__init__(dep_key=dep_key)
675        if dir_select:
676            action = Gtk.FileChooserAction.SELECT_FOLDER
677        else:
678            action = Gtk.FileChooserAction.OPEN
679
680        self.label = SettingsLabel(label)
681        self.content_widget = Gtk.FileChooserButton(action=action)
682        self.pack_start(self.label, False, False, 0)
683        self.pack_end(self.content_widget, False, False, 0)
684
685        self.set_tooltip_text(tooltip)
686
687        if size_group:
688            self.add_to_size_group(size_group)
689
690    def on_file_selected(self, *args):
691        self.set_value(self.content_widget.get_uri())
692
693    def on_setting_changed(self, *args):
694        self.content_widget.set_uri(self.get_value())
695
696    def connect_widget_handlers(self, *args):
697        self.content_widget.connect("file-set", self.on_file_selected)
698
699class IconChooser(SettingsWidget):
700    bind_prop = "icon"
701    bind_dir = Gio.SettingsBindFlags.DEFAULT
702
703    def __init__(self, label, default_icon=None, icon_categories=[], default_category=None, expand_width=False, size_group=None, dep_key=None, tooltip=""):
704        super(IconChooser, self).__init__(dep_key=dep_key)
705
706        self.label = SettingsLabel(label)
707
708        self.content_widget = XApp.IconChooserButton()
709        self.content_widget.set_icon_size(Gtk.IconSize.BUTTON)
710
711        dialog = self.content_widget.get_dialog()
712        if default_icon:
713            dialog.set_default_icon(default_icon)
714
715        for category in icon_categories:
716            dialog.add_custom_category(category['name'], category['icons'])
717
718        if default_category is not None:
719            self.content_widget.set_default_category(default_category)
720
721        self.pack_start(self.label, False, False, 0)
722        self.pack_end(self.content_widget, expand_width, expand_width, 0)
723
724        self.set_tooltip_text(tooltip)
725
726        if size_group:
727            self.add_to_size_group(size_group)
728
729class Button(SettingsWidget):
730    def __init__(self, label, callback=None):
731        super(Button, self).__init__()
732        self.label = label
733        self.callback = callback
734
735        self.content_widget = Gtk.Button(label=label)
736        self.pack_start(self.content_widget, True, True, 0)
737        self.content_widget.connect("clicked", self._on_button_clicked)
738
739    def _on_button_clicked(self, *args):
740        if self.callback is not None:
741            self.callback(self)
742        elif hasattr(self, "on_activated"):
743            self.on_activated()
744        else:
745            print("warning: button '%s' does nothing" % self.label)
746
747    def set_label(self, label):
748        self.label = label
749        self.content_widget.set_label(label)
750
751class Text(SettingsWidget):
752    def __init__(self, label, align=Gtk.Align.START):
753        super(Text, self).__init__()
754        self.label = label
755
756        if align == Gtk.Align.END:
757            xalign = 1.0
758            justification = Gtk.Justification.RIGHT
759        elif align == Gtk.Align.CENTER:
760            xalign = 0.5
761            justification = Gtk.Justification.CENTER
762        else: # START and FILL align left
763            xalign = 0
764            justification = Gtk.Justification.LEFT
765
766        self.content_widget = Gtk.Label(label, halign=align, xalign=xalign, justify=justification)
767        self.content_widget.set_line_wrap(True)
768        self.pack_start(self.content_widget, True, True, 0)
769