widgets.py 19.2 KB
Newer Older
1
# Copyright (c) 2011 John Stowers
2 3
# SPDX-License-Identifier: GPL-3.0+
# License-Filename: LICENSES/GPL-3.0
4

5
import logging
6
import os.path
7

8
from gi.repository import GLib, Gtk, Gdk, Gio, Pango
John Stowers's avatar
John Stowers committed
9

10
from gtweak.tweakmodel import Tweak, TweakGroup
11
from gtweak.gsettings import GSettingsSetting, GSettingsFakeSetting, GSettingsMissingError
12
from gtweak.gtksettings import GtkSettingsManager
13
from gtweak.gshellwrapper import GnomeShellFactory
14

15
UI_BOX_SPACING = 4
16
_shell = GnomeShellFactory().get_shell()
17

18 19 20 21 22 23 24 25 26
def build_label_beside_widget(txt, *widget, **kwargs):
    """
    Builds a HBox containing widgets.

    Optional Kwargs:
        hbox: Use an existing HBox, not a new one
        info: Informational text to be shown after the label
        warning: Warning text to be shown after the label
    """
27 28 29 30 31
    def make_image(icon, tip):
        image = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU)
        image.set_tooltip_text(tip)
        return image

32 33 34 35 36 37 38 39
    def show_tooltip_when_ellipsized(label, x, y, keyboard_mode, tooltip):
        layout = label.get_layout()
        if layout.is_ellipsized():
            tooltip.set_text(label.get_text())
            return True
        else:
            return False

40 41 42
    if kwargs.get("hbox"):
        hbox = kwargs.get("hbox")
    else:
43
        hbox = Gtk.Box()
44

45
    hbox.props.spacing = UI_BOX_SPACING
46
    lbl = Gtk.Label(label=txt)
John Stowers's avatar
John Stowers committed
47
    lbl.props.ellipsize = Pango.EllipsizeMode.END
John Stowers's avatar
John Stowers committed
48
    lbl.props.xalign = 0.0
49 50
    lbl.set_has_tooltip(True)
    lbl.connect("query-tooltip", show_tooltip_when_ellipsized)
Alex Muñoz's avatar
Alex Muñoz committed
51
    hbox.pack_start(lbl, True, True, 0)
52 53

    if kwargs.get("info"):
54 55 56 57 58 59 60
        hbox.pack_start(
                make_image("dialog-information-symbolic", kwargs.get("info")),
                False, False, 0)
    if kwargs.get("warning"):
        hbox.pack_start(
                make_image("dialog-warning-symbolic", kwargs.get("warning")),
                False, False, 0)
61 62 63 64

    for w in widget:
        hbox.pack_start(w, False, False, 0)

65 66 67 68
    #For Atk, indicate that the rightmost widget, usually the switch relates to the
    #label. By convention this is true in the great majority of cases. Settings that
    #construct their own widgets will need to set this themselves
    lbl.set_mnemonic_widget(widget[-1])
69

Alex Muñoz's avatar
Alex Muñoz committed
70
    return hbox
John Stowers's avatar
John Stowers committed
71 72

def build_combo_box_text(selected, *values):
73 74 75 76
    """
    builds a GtkComboBox and model containing the supplied values.
    @values: a list of 2-tuples (value, name)
    """
John Stowers's avatar
John Stowers committed
77
    store = Gtk.ListStore(str, str)
78
    store.set_sort_column_id(0, Gtk.SortType.ASCENDING)
John Stowers's avatar
John Stowers committed
79 80 81 82 83 84 85 86 87 88

    selected_iter = None
    for (val, name) in values:
        _iter = store.append( (val, name) )
        if val == selected:
            selected_iter = _iter

    combo = Gtk.ComboBox(model=store)
    renderer = Gtk.CellRendererText()
    combo.pack_start(renderer, True)
89
    combo.add_attribute(renderer, 'markup', 1)
John Stowers's avatar
John Stowers committed
90 91 92 93 94
    if selected_iter:
        combo.set_active_iter(selected_iter)

    return combo

95 96 97 98 99
def build_horizontal_sizegroup():
    sg = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
    sg.props.ignore_hidden = True
    return sg

100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
def build_tight_button(stock_id):
    button = Gtk.Button()
    button.set_relief(Gtk.ReliefStyle.NONE)
    button.set_focus_on_click(False)
    button.add(Gtk.Image.new_from_stock(stock_id, Gtk.IconSize.MENU))
    data =  ".button {\n" \
            "-GtkButton-default-border : 0px;\n" \
            "-GtkButton-default-outside-border : 0px;\n" \
            "-GtkButton-inner-border: 0px;\n" \
            "-GtkWidget-focus-line-width : 0px;\n" \
            "-GtkWidget-focus-padding : 0px;\n" \
            "padding: 0px;\n" \
            "}"
    provider = Gtk.CssProvider()
    provider.load_from_data(data)
    # 600 = GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
116
    button.get_style_context().add_provider(provider, 600)
117 118
    return button

119

John Stowers's avatar
John Stowers committed
120
class _GSettingsTweak(Tweak):
Alex Muñoz's avatar
Alex Muñoz committed
121
    def __init__(self, name, schema_name, key_name, **options):
John Stowers's avatar
John Stowers committed
122 123
        self.schema_name = schema_name
        self.key_name = key_name
Alex Muñoz's avatar
Alex Muñoz committed
124
        self._extra_info = None
125 126
        if 'uid' not in options:
            options['uid'] = key_name
127 128 129
        try:
            self.settings = GSettingsSetting(schema_name, **options)
            Tweak.__init__(self,
Alex Muñoz's avatar
Alex Muñoz committed
130
                name,
131 132
                options.get("description",self.settings.schema_get_description(key_name)),
                **options)
133
        except GSettingsMissingError as e:
134 135 136
            self.settings = GSettingsFakeSetting()
            Tweak.__init__(self,"","")
            self.loaded = False
137
            logging.info("GSetting missing %s", e)
138 139 140 141
        except KeyError:
            self.settings = GSettingsFakeSetting()
            Tweak.__init__(self,"","")
            self.loaded = False
John Stowers's avatar
John Stowers committed
142
            logging.info("GSettings missing key %s (key %s)" % (schema_name, key_name))
143

144 145 146 147
        if options.get("logout_required") and self.loaded:
            self.settings.connect("changed::%s" % key_name, self._on_changed_notify_logout)

    def _on_changed_notify_logout(self, settings, key_name):
148
        self.notify_logout()
149

Alex Muñoz's avatar
Alex Muñoz committed
150 151 152 153 154 155
    @property
    def extra_info(self):
        if self._extra_info is None:
            self._extra_info = self.settings.schema_get_summary(self.key_name)
        return self._extra_info

156
class _DependableMixin(object):
John Stowers's avatar
John Stowers committed
157

158 159 160 161 162 163 164 165 166 167 168
    def add_dependency_on_tweak(self, depends, depends_how):
        if isinstance(depends, Tweak):
            self._depends = depends
            if depends_how is None:
                depends_how = lambda x,kn: x.get_boolean(kn)
            self._depends_how = depends_how

            sensitive = self._depends_how(
                                depends.settings,
                                depends.key_name,
            )
169
            self.set_sensitive(sensitive)
170 171 172 173 174

            depends.settings.connect("changed::%s" % depends.key_name, self._on_changed_depend)

    def _on_changed_depend(self, settings, key_name):
        sensitive = self._depends_how(settings,key_name)
175
        self.set_sensitive(sensitive)
176

177
class ListBoxTweakGroup(Gtk.ListBox, TweakGroup):
178 179 180
    def __init__(self, name, *tweaks, **options):
        if 'uid' not in options:
            options['uid'] = self.__class__.__name__
181
        Gtk.ListBox.__init__(self,
182 183
                        selection_mode=Gtk.SelectionMode.NONE,
                        name=options['uid'])
184 185
        self.get_style_context().add_class(
                        options.get('css_class','tweak-group'))
186
        self.props.margin = 20
187 188 189 190
        self.props.vexpand = False
        self.props.valign = Gtk.Align.START

        self._sg = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
191 192
        self._sg.props.ignore_hidden = True

193 194 195 196 197 198 199 200 201 202
        TweakGroup.__init__(self, name, **options)

        for t in tweaks:
            self.add_tweak_row(t)

    #FIXME: need to add remove_tweak_row and remove_tweak (which clears
    #the search cache etc)

    def add_tweak_row(self, t, position=None):
        if self.add_tweak(t):
203 204 205 206 207 208 209 210
            if isinstance(t, Gtk.ListBoxRow):
                row = t
            else:
                row = Gtk.ListBoxRow(name=t.uid)
                row.get_style_context().add_class("tweak")
                if isinstance(t, Title):
                    row.get_style_context().add_class("title")
                row.add(t)
211 212 213 214
            if position is None:
                self.add(row)
            else:
                self.insert(row, position)
215 216
            if t.widget_for_size_group:
                self._sg.add_widget(t.widget_for_size_group)
217
            return row
218

219
class GSettingsCheckTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
John Stowers's avatar
John Stowers committed
220
    def __init__(self, name, schema_name, key_name, **options):
221
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
222
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
John Stowers's avatar
John Stowers committed
223

224
        widget = Gtk.CheckButton.new_with_label(name)
John Stowers's avatar
John Stowers committed
225 226
        self.settings.bind(
                key_name,
227
                widget,
John Stowers's avatar
John Stowers committed
228
                "active", Gio.SettingsBindFlags.DEFAULT)
229
        self.add(widget)
John Stowers's avatar
John Stowers committed
230 231 232 233 234 235 236
        self.widget_for_size_group = None

        self.add_dependency_on_tweak(
                options.get("depends_on"),
                options.get("depends_how")
        )

237
class GSettingsSwitchTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
238
    def __init__(self, name, schema_name, key_name, **options):
239
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
240
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
John Stowers's avatar
John Stowers committed
241

John Stowers's avatar
John Stowers committed
242 243
        w = Gtk.Switch()
        self.settings.bind(key_name, w, "active", Gio.SettingsBindFlags.DEFAULT)
244

245 246 247 248 249
        self.add_dependency_on_tweak(
                options.get("depends_on"),
                options.get("depends_how")
        )

250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
        vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox1.props.spacing = UI_BOX_SPACING
        lbl = Gtk.Label(label=name)
        lbl.props.ellipsize = Pango.EllipsizeMode.END
        lbl.props.xalign = 0.0
        vbox1.pack_start(lbl, True, True, 0)

        if options.get("desc"):
            description = options.get("desc")
            lbl_desc = Gtk.Label()
            lbl_desc.props.xalign = 0.0
            lbl_desc.set_line_wrap(True)
            lbl_desc.get_style_context().add_class("dim-label")
            lbl_desc.set_markup("<span size='small'>"+GLib.markup_escape_text(description)+"</span>")
            vbox1.pack_start(lbl_desc, True, True, 0)

        vbox2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox2_upper = Gtk.Box()
        vbox2_lower = Gtk.Box()
        vbox2.pack_start(vbox2_upper, True, True, 0)
        vbox2.pack_start(w, False, False, 0)
        vbox2.pack_start(vbox2_lower, True, True, 0)

        self.pack_start(vbox1, True, True, 0)
        self.pack_start(vbox2, False, False, 0)
        self.widget_for_size_group = None

277
class GSettingsFontButtonTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
278
    def __init__(self, name, schema_name, key_name, **options):
279
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
280
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
John Stowers's avatar
John Stowers committed
281 282

        w = Gtk.FontButton()
283
        w.set_use_font(True)
John Stowers's avatar
John Stowers committed
284
        self.settings.bind(key_name, w, "font-name", Gio.SettingsBindFlags.DEFAULT)
285
        build_label_beside_widget(name, w, hbox=self)
286
        self.widget_for_size_group = w
John Stowers's avatar
John Stowers committed
287

288
class GSettingsRangeTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
Alex Muñoz's avatar
Alex Muñoz committed
289
    def __init__(self, name, schema_name, key_name, **options):
290
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
291
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
292 293 294 295 296 297

        #returned variant is range:(min, max)
        _min, _max = self.settings.get_range(key_name)[1]

        w = Gtk.HScale.new_with_range(_min, _max, options.get('adjustment_step', 1))
        self.settings.bind(key_name, w.get_adjustment(), "value", Gio.SettingsBindFlags.DEFAULT)
298 299

        build_label_beside_widget(self.name, w, hbox=self)
300 301
        self.widget_for_size_group = w

302
class GSettingsSpinButtonTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
303
    def __init__(self, name, schema_name, key_name, **options):
304
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
305
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
306 307 308 309

        #returned variant is range:(min, max)
        _min, _max = self.settings.get_range(key_name)[1]

310
        adjustment = Gtk.Adjustment(value=0, lower=_min, upper=_max, step_increment=options.get('adjustment_step', 1))
311 312 313 314
        w = Gtk.SpinButton()
        w.set_adjustment(adjustment)
        w.set_digits(options.get('digits', 0))
        self.settings.bind(key_name, adjustment, "value", Gio.SettingsBindFlags.DEFAULT)
315

316
        build_label_beside_widget(name, w, hbox=self)
317
        self.widget_for_size_group = w
318

Alex Muñoz's avatar
Alex Muñoz committed
319 320 321 322
        self.add_dependency_on_tweak(
                options.get("depends_on"),
                options.get("depends_how")
        )
323

324
class GSettingsComboEnumTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
325
    def __init__(self, name, schema_name, key_name, **options):
326
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
327
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
John Stowers's avatar
John Stowers committed
328 329 330 331 332

        _type, values = self.settings.get_range(key_name)
        value = self.settings.get_string(key_name)
        self.settings.connect('changed::'+self.key_name, self._on_setting_changed)

333
        w = build_combo_box_text(value, *[(v,v.replace("-"," ").title()) for v in values])
John Stowers's avatar
John Stowers committed
334 335 336
        w.connect('changed', self._on_combo_changed)
        self.combo = w

337
        build_label_beside_widget(name, w, hbox=self)
John Stowers's avatar
John Stowers committed
338 339
        self.widget_for_size_group = w

340 341 342
    def _values_are_different(self):
        #to stop bouncing back and forth between changed signals. I suspect there must be a nicer
        #Gio.settings_bind way to fix this
John Stowers's avatar
John Stowers committed
343
        return self.settings.get_string(self.key_name) != \
344
               self.combo.get_model().get_value(self.combo.get_active_iter(), 0)
John Stowers's avatar
John Stowers committed
345 346

    def _on_setting_changed(self, setting, key):
347 348 349 350 351
        assert key == self.key_name
        val = self.settings.get_string(key)
        model = self.combo.get_model()
        for row in model:
            if val == row[0]:
352
                self.combo.set_active_iter(row.iter)
353
                break
John Stowers's avatar
John Stowers committed
354 355

    def _on_combo_changed(self, combo):
356 357
        val = self.combo.get_model().get_value(self.combo.get_active_iter(), 0)
        if self._values_are_different():
358
            self.settings.set_string(self.key_name, val)
John Stowers's avatar
John Stowers committed
359

360
class GSettingsComboTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
361
    def __init__(self, name, schema_name, key_name, key_options, **options):
362
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
363
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
364

365 366 367 368 369
        #check key_options is iterable
        #and if supplied, check it is a list of 2-tuples
        assert len(key_options) >= 0
        if len(key_options):
            assert len(key_options[0]) == 2
Alex Muñoz's avatar
Alex Muñoz committed
370
        self._key_options = key_options
371

372
        self.combo = build_combo_box_text(
373
                    self.settings.get_string(self.key_name),
374
                    *key_options)
375
        self.combo.connect('changed', self._on_combo_changed)
376 377 378
        self.settings.connect('changed::'+self.key_name, self._on_setting_changed)

        build_label_beside_widget(name, self.combo,hbox=self)
379 380 381 382 383 384 385 386 387 388 389 390
        self.widget_for_size_group = self.combo

    def _on_setting_changed(self, setting, key):
        assert key == self.key_name
        val = self.settings.get_string(key)
        model = self.combo.get_model()
        for row in model:
            if val == row[0]:
                self.combo.set_active_iter(row.iter)
                return

        self.combo.set_active(-1)
391 392 393 394 395

    def _on_combo_changed(self, combo):
        _iter = combo.get_active_iter()
        if _iter:
            value = combo.get_model().get_value(_iter, 0)
396
            self.settings.set_string(self.key_name, value)
397

Alex Muñoz's avatar
Alex Muñoz committed
398 399 400 401 402 403 404
    @property
    def extra_info(self):
        if self._extra_info is None:
           self._extra_info = self.settings.schema_get_summary(self.key_name)
           self._extra_info += " " + " ".join(op[0] for op in self._key_options)
        return self._extra_info

405 406
class FileChooserButton(Gtk.FileChooserButton):
    def __init__(self, title, local_only, mimetypes):
407 408
        Gtk.FileChooserButton.__init__(self, title=title)

409 410 411 412 413
        if mimetypes:
            f = Gtk.FileFilter()
            for m in mimetypes:
                f.add_mime_type(m)
            self.set_filter(f)
414 415

        #self.set_width_chars(15)
416
        self.set_local_only(local_only)
417 418
        self.set_action(Gtk.FileChooserAction.OPEN)

419
class GSettingsFileChooserButtonTweak(Gtk.Box, _GSettingsTweak, _DependableMixin):
420
    def __init__(self, name, schema_name, key_name, local_only, mimetypes, **options):
421
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
422
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
423 424 425

        self.settings.connect('changed::'+self.key_name, self._on_setting_changed)

426
        self.filechooser = FileChooserButton(name,local_only,mimetypes)
427 428 429
        self.filechooser.set_uri(self.settings.get_string(self.key_name))
        self.filechooser.connect("file-set", self._on_file_set)

430
        build_label_beside_widget(name, self.filechooser, hbox=self)
431 432 433 434 435 436 437 438 439 440 441 442
        self.widget_for_size_group = self.filechooser

    def _values_are_different(self):
        return self.settings.get_string(self.key_name) != self.filechooser.get_uri()

    def _on_setting_changed(self, setting, key):
        self.filechooser.set_uri(self.settings.get_string(key))

    def _on_file_set(self, chooser):
        uri = self.filechooser.get_uri()
        if uri and self._values_are_different():
            self.settings.set_string(self.key_name, uri)
443

444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
class GetterSetterSwitchTweak(Gtk.Box, Tweak):
    def __init__(self, name, **options):
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
        Tweak.__init__(self, name, options.get("description",""), **options)

        sw = Gtk.Switch()
        sw.set_active(self.get_active())
        sw.connect("notify::active", self._on_toggled)

        build_label_beside_widget(name, sw, hbox=self)

    def _on_toggled(self, sw, pspec):
        self.set_active(sw.get_active())

    def get_active(self):
        raise NotImplementedError()

    def set_active(self, v):
        raise NotImplementedError()

464
class Title(Gtk.Box, Tweak):
465
    def __init__(self, name, desc, **options):
466
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
467
        Tweak.__init__(self, name, desc, **options)
468
        widget = Gtk.Label()
469
        widget.set_markup("<b>"+GLib.markup_escape_text(name)+"</b>")
470
        widget.props.xalign = 0.0
471 472
        if not options.get("top"):
            widget.set_margin_top(10)
473
        self.add(widget)
474

Alex Muñoz's avatar
Alex Muñoz committed
475
class GSettingsSwitchTweakValue(Gtk.Box, _GSettingsTweak):
476

Alex Muñoz's avatar
Alex Muñoz committed
477 478
    def __init__(self, name, schema_name, key_name, **options):
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
Alex Muñoz's avatar
Alex Muñoz committed
479
        _GSettingsTweak.__init__(self, name, schema_name, key_name, **options)
Alex Muñoz's avatar
Alex Muñoz committed
480 481 482 483

        sw = Gtk.Switch()
        sw.set_active(self.get_active())
        sw.connect("notify::active", self._on_toggled)
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510

        vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox1.props.spacing = UI_BOX_SPACING
        lbl = Gtk.Label(label=name)
        lbl.props.ellipsize = Pango.EllipsizeMode.END
        lbl.props.xalign = 0.0
        vbox1.pack_start(lbl, True, True, 0)

        if options.get("desc"):
            description = options.get("desc")
            lbl_desc = Gtk.Label()
            lbl_desc.props.xalign = 0.0
            lbl_desc.set_line_wrap(True)
            lbl_desc.get_style_context().add_class("dim-label")
            lbl_desc.set_markup("<span size='small'>"+GLib.markup_escape_text(description)+"</span>")
            vbox1.pack_start(lbl_desc, True, True, 0)

        vbox2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox2_upper = Gtk.Box()
        vbox2_lower = Gtk.Box()
        vbox2.pack_start(vbox2_upper, True, True, 0)
        vbox2.pack_start(sw, False, False, 0)
        vbox2.pack_start(vbox2_lower, True, True, 0)

        self.pack_start(vbox1, True, True, 0)
        self.pack_start(vbox2, False, False, 0)
        self.widget_for_size_group = None
Alex Muñoz's avatar
Alex Muñoz committed
511 512 513

    def _on_toggled(self, sw, pspec):
        self.set_active(sw.get_active())
514

Alex Muñoz's avatar
Alex Muñoz committed
515 516
    def set_active(self, v):
        raise NotImplementedError()
517

Alex Muñoz's avatar
Alex Muñoz committed
518 519
    def get_active(self):
        raise NotImplementedError()