1# -*- coding: utf-8 -*-
2#
3# Caribou - text entry and UI navigation application
4#
5# Copyright (C) 2010 Eitan Isaacson <eitan@monotonous.org>
6#
7# This program is free software; you can redistribute it and/or modify it
8# under the terms of the GNU Lesser General Public License as published by the
9# Free Software Foundation; either version 2.1 of the License, or (at your
10# option) any later version.
11#
12# This program is distributed in the hope that it will be useful, but WITHOUT
13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
15# for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with this program; if not, write to the Free Software Foundation,
19# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21from caribou.settings.setting_types import *
22
23import gi
24from gi.repository import GObject
25gi.require_version('Gtk', '3.0')
26gi.require_version('Gdk', '3.0')
27from gi.repository import Gdk
28from gi.repository import Gtk
29
30class AbstractPreferencesUI:
31    def populate_settings(self, settings_manager):
32        if getattr(self, "notebook", None) is None:
33            self.notebook = Gtk.Notebook()
34        self._populate_settings(self.notebook, settings_manager.groups)
35        self.notebook.set_show_tabs(self.notebook.get_n_pages() != 1)
36
37        return self.notebook
38
39    def _populate_settings(self, parent, setting, level=0):
40        if level == 0:
41            for s in setting:
42                vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
43                parent.append_page(vbox, Gtk.Label(label=s.label))
44                self._populate_settings(vbox, s, 1)
45        else:
46            parent.set_border_width(6)
47            table = None
48            row = 0
49            for s in setting:
50                if not isinstance(s, SettingsGroup):
51                    if table is None:
52                        table = Gtk.Table.new(1, 2, False)
53                        table.set_row_spacings(3)
54                        table.set_col_spacings(3)
55                        parent.pack_start(table, False, False, 0)
56                    self._create_widget(table, row, s)
57                    row += 1
58                else:
59                    table = None
60                    frame = Gtk.Frame()
61                    frame.set_shadow_type(Gtk.ShadowType.NONE)
62                    label = Gtk.Label()
63                    label.set_markup('<b>%s</b>' % s.label)
64                    frame.set_label_widget(label)
65                    vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
66                    frame.add(vbox)
67                    parent.pack_start(frame, False, False, 0)
68                    self._sensitivity_changed_cb(s, s.sensitive, frame, None)
69                    s.connect("sensitivity-changed",
70                              self._sensitivity_changed_cb,
71                              frame, None)
72                    self._populate_settings(vbox, s, level + 1)
73
74    def _create_widget(self, table, row, setting, xpadding=0):
75        control = None
76        label = None
77        value_changed_cb = None
78        control_changed_cb = None
79        control_changed_signal = None
80        if isinstance(setting, BooleanSetting):
81            control = Gtk.CheckButton.new_with_label(setting.label)
82            control.set_active(setting.value)
83            value_changed_cb = lambda s, v, w: w.set_active(v)
84            control_changed_cb = self._checkbutton_toggled_cb
85            control_changed_signal = 'toggled'
86        else:
87            label = Gtk.Label(label="%s:" % setting.label)
88            label.set_alignment(0.0, 0.5)
89
90            if setting.entry_type == ENTRY_COLOR:
91                control = Gtk.ColorButton.new_with_color(
92                    Gdk.color_parse(setting.value)[1])
93                value_changed_cb = \
94                    lambda s, v, w: w.set_color(Gdk.color_parse(v))
95                control_changed_cb = self._colorbutton_changed_cb
96                control_changed_signal = 'color-set'
97            elif setting.entry_type == ENTRY_FONT:
98                control = Gtk.FontButton.new_with_font(setting.value)
99                value_changed_cb = lambda s, v, w: w.set_font_name(v)
100                control_changed_cb = self._fontbutton_changed_cb
101                control_changed_signal = 'font-set'
102            elif setting.entry_type == ENTRY_SPIN:
103                control = Gtk.SpinButton()
104                if isinstance(setting.value, float):
105                    control.set_digits(2)
106                    control.set_increments(0.01, 0.1)
107                control.set_range(setting.min, setting.max)
108                control.set_value(setting.value)
109                control.update()
110                value_changed_cb = lambda s, v, w: w.set_value(v)
111                control_changed_cb = self._spinner_changed_cb
112                control_changed_signal = "value-changed"
113            elif setting.entry_type == ENTRY_RADIO and setting.allowed:
114                if setting.children:
115                    assert len(setting.children) == len(setting.allowed), \
116                        "If a radio entry has children, they must be equal " \
117                        "in quantity to the allowed values."
118                label = None
119                control = Gtk.Table.new(
120                    len(setting.allowed) + len(setting.children), 2, False)
121                control.set_row_spacings(3)
122                control.set_col_spacings(3)
123                radios = []
124                for string, localized in setting.allowed:
125                    rb = Gtk.RadioButton.new_with_label([], localized)
126                    radios.append(rb)
127                for radio, allowed in zip(radios[1:], setting.allowed[1:]):
128                    radio.join_group(radios[0])
129                    if allowed[0] == setting.value:
130                        radio.set_active(True)
131
132                hid = setting.connect(
133                    'value-changed',
134                    lambda s, v, rs: \
135                        rs[[a for \
136                                a, b in s.allowed].index(v)].set_active(True),
137                    radios)
138
139                r = 0
140                for i, radio in enumerate(radios):
141                    radio.connect('toggled', self._radio_changed_cb, setting,
142                                  radios, hid)
143                    control.attach(
144                        radio, 0, 2, r, r + 1,
145                        Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
146                        Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
147                        0, 0)
148                    r += 1
149                    if setting.children:
150                        self._create_widget(control, r,
151                                            setting.children[i], 12)
152                        r += 1
153
154            elif setting.entry_type == ENTRY_COMBO or setting.allowed:
155                control = Gtk.ComboBoxText.new()
156                for option in setting.allowed:
157                    control.append(str(option[0]), option[1])
158                control.set_active_id(str(setting.value))
159                value_changed_cb = lambda s, v, w: w.set_active_id(str(v))
160                control_changed_cb = self._combo_changed_cb
161                control_changed_signal = 'changed'
162            else:
163                control = Gtk.Entry()
164                control.set_text(setting.value)
165                value_changed_cb = lambda s, v, w: w.set_text(v)
166                control_changed_cb = self._string_changed_cb
167                control_changed_signal = 'insert-at-cursor'
168
169        if label is not None:
170            table.attach(label, 0, 1, row, row + 1,
171                         Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
172                         Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
173                         xpadding, 0)
174            table.attach(control, 1, 2, row, row + 1,
175                         Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
176                         Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
177                         0, 0)
178        else:
179            table.attach(control, 0, 2, row, row + 1,
180                         Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
181                         Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL,
182                         xpadding, 0)
183
184        self._sensitivity_changed_cb(setting, setting.sensitive, control,
185                                     label)
186        setting.connect("sensitivity-changed", self._sensitivity_changed_cb,
187                        control, label)
188
189        if value_changed_cb and control_changed_signal and control_changed_cb:
190            hid = setting.connect('value-changed', value_changed_cb, control)
191            control.connect(control_changed_signal, control_changed_cb,
192                            setting, hid)
193
194    def _sensitivity_changed_cb(self, setting, sensitive, control, label):
195        for w in (control, label):
196            if w is not None:
197                w.set_sensitive(sensitive)
198
199    def _update_setting(self, setting, value, handler_id):
200        if setting.value == value: return
201        setting.handler_block(handler_id)
202        setting.value = value
203        setting.handler_unblock(handler_id)
204
205    def _radio_changed_cb(self, radio, setting, radios, handler_id):
206        if not radio.get_active():
207            return
208
209        i = radios.index(radio)
210        self._update_setting(setting, setting.allowed[i][0], handler_id)
211
212    def _spinner_changed_cb(self, spinner, setting, handler_id):
213        self._update_setting(setting, spinner.get_value(), handler_id)
214
215    def _checkbutton_toggled_cb(self, checkbutton, setting, handler_id):
216        self._update_setting(setting, checkbutton.get_active(), handler_id)
217
218    def _colorbutton_changed_cb(self, colorbutton, setting, handler_id):
219        self._update_setting(setting, colorbutton.get_color().to_string(),
220                             handler_id)
221
222    def _fontbutton_changed_cb(self, fontbutton, setting, handler_id):
223        self._update_setting(setting, fontbutton.get_font_name(), handler_id)
224
225    def _string_changed_cb(self, entry, text, setting, handler_id):
226        self._update_setting(setting, entry.get_text(), handler_id)
227
228    def _combo_changed_cb(self, combo, setting, handler_id):
229        self._update_setting(setting, combo.get_active_id(),
230                             handler_id)
231
232class PreferencesDialog(Gtk.Dialog, AbstractPreferencesUI):
233    __gtype_name__ = "PreferencesDialog"
234
235    def __init__(self, settings_manager):
236        GObject.GObject.__init__(self)
237        self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
238        self.set_border_width(6)
239        self.set_title(settings_manager.groups.label)
240
241        notebook = self.populate_settings(settings_manager)
242        vbox = self.get_content_area()
243        vbox.add(notebook)
244
245class PreferencesWindow(Gtk.Window, AbstractPreferencesUI):
246    __gtype_name__ = "PreferencesWindow"
247
248    def __init__(self, settings_manager):
249        GObject.GObject.__init__(self)
250        self.set_border_width(6)
251        self.set_title(settings_manager.groups.label)
252
253        notebook = self.populate_settings(settings_manager)
254        self.add(notebook)
255
256if __name__ == "__main__":
257    from caribou.settings.settings_manager import SettingsManager
258    from caribou.settings import CaribouSettings
259
260    import signal
261    signal.signal(signal.SIGINT, signal.SIG_DFL)
262
263    w = PreferencesDialog(CaribouSettings())
264    w.show_all()
265
266    try:
267        w.run()
268    except KeyboardInterrupt:
269        Gtk.main_quit()
270