1#!/usr/local/bin/python3.8
2
3import getopt
4import sys
5
6import os
7import glob
8import gettext
9import time
10import traceback
11import locale
12import urllib.request as urllib
13from functools import cmp_to_key
14import unicodedata
15import config
16from setproctitle import setproctitle
17
18import gi
19gi.require_version('Gtk', '3.0')
20gi.require_version('XApp', '1.0')
21from gi.repository import Gio, Gtk, Pango, Gdk, XApp
22
23sys.path.append(config.currentPath + "/modules")
24sys.path.append(config.currentPath + "/bin")
25import capi
26import proxygsettings
27import SettingsWidgets
28
29# i18n
30gettext.install("cinnamon", "/usr/local/share/locale", names=["ngettext"])
31
32# Standard setting pages... this can be expanded to include applet dirs maybe?
33mod_files = glob.glob(config.currentPath + "/modules/*.py")
34mod_files.sort()
35if len(mod_files) == 0:
36    print("No settings modules found!!")
37    sys.exit(1)
38
39mod_files = [x.split('/')[-1].split('.')[0] for x in mod_files]
40
41for mod_file in mod_files:
42    if mod_file[0:3] != "cs_":
43        raise Exception("Settings modules must have a prefix of 'cs_' !!")
44
45modules = map(__import__, mod_files)
46
47# i18n for menu item
48menuName = _("System Settings")
49menuComment = _("Control Center")
50
51WIN_WIDTH = 800
52WIN_HEIGHT = 600
53WIN_H_PADDING = 20
54
55MIN_LABEL_WIDTH = 16
56MAX_LABEL_WIDTH = 25
57MIN_PIX_WIDTH = 100
58MAX_PIX_WIDTH = 160
59
60MOUSE_BACK_BUTTON = 8
61
62CATEGORIES = [
63    #        Display name                         ID              Show it? Always False to start              Icon
64    {"label": _("Appearance"),            "id": "appear",      "show": False,                       "icon": "cs-cat-appearance"},
65    {"label": _("Preferences"),           "id": "prefs",       "show": False,                       "icon": "cs-cat-prefs"},
66    {"label": _("Hardware"),              "id": "hardware",    "show": False,                       "icon": "cs-cat-hardware"},
67    {"label": _("Administration"),        "id": "admin",       "show": False,                       "icon": "cs-cat-admin"}
68]
69
70CONTROL_CENTER_MODULES = [
71    #         Label                              Module ID                Icon                         Category      Keywords for filter
72    [_("Color"),                            "color",              "cs-color",                   "hardware",      _("color, profile, display, printer, output")],
73    [_("Graphics Tablet"),                  "wacom",              "cs-tablet",                  "hardware",      _("wacom, digitize, tablet, graphics, calibrate, stylus")]
74]
75
76STANDALONE_MODULES = [
77    # Label                              Executable                             Icon                     Category          Keywords for filter
78    [_("Printers"),                      "system-config-printer",               "cs-printer",            "hardware",       _("printers, laser, inkjet")],
79    [_("Firewall"),                      "gufw",                                "cs-firewall",           "admin",          _("firewall, block, filter, programs")],
80    [_("Firewall"),                      "firewall-config",                     "cs-firewall",           "admin",          _("firewall, block, filter, programs")],
81    [_("Languages"),                     "mintlocale",                          "cs-language",           "prefs",          _("language, install, foreign")],
82    [_("Input Method"),                  "mintlocale-im",                       "cs-input-method",       "prefs",          _("language, install, foreign, input, method, chinese, korean, japanese, typing")],
83    [_("Login Window"),                  "pkexec lightdm-settings",             "cs-login",              "admin",          _("login, lightdm, mdm, gdm, manager, user, password, startup, switch")],
84    [_("Login Window"),                  "lightdm-gtk-greeter-settings-pkexec", "cs-login",              "admin",          _("login, lightdm, manager, settings, editor")],
85    [_("Driver Manager"),                "pkexec driver-manager",               "cs-drivers",            "admin",          _("video, driver, wifi, card, hardware, proprietary, nvidia, radeon, nouveau, fglrx")],
86    [_("Nvidia Settings"),               "nvidia-settings",                     "cs-drivers",            "admin",          _("video, driver, proprietary, nvidia, settings")],
87    [_("Software Sources"),              "pkexec mintsources",                  "cs-sources",            "admin",          _("ppa, repository, package, source, download")],
88    [_("Package Management"),            "dnfdragora",                          "cs-sources",            "admin",          _("update, install, repository, package, source, download")],
89    [_("Package Management"),            "yumex-dnf",                           "cs-sources",            "admin",          _("update, install, repository, package, source, download")],
90    [_("Users and Groups"),              "cinnamon-settings-users",             "cs-user-accounts",      "admin",          _("user, users, account, accounts, group, groups, password")],
91    [_("Manage Services and Units"),     "systemd-manager-pkexec",              "cs-sources",            "admin",          _("systemd, units, services, systemctl, init")],
92    [_("Disks"),                         "gnome-disks",                         "org.gnome.DiskUtility", "hardware",       _("disks, manage, hardware, management, hard, hdd, pendrive, format, erase, test, create, iso, ISO, disk, image")]
93]
94
95TABS = {
96    # KEY (cs_KEY.py) : {"tab_name": tab_number, ... }
97    "universal-access": {"visual": 0, "keyboard": 1, "typing": 2, "mouse": 3},
98    "applets":          {"installed": 0, "more": 1, "download": 1},
99    "backgrounds":      {"images": 0, "settings": 1},
100    "default":          {"preferred": 0, "removable": 1},
101    "desklets":         {"installed": 0, "more": 1, "download": 1, "general": 2},
102    "display":          {"layout": 0, "settings": 1},
103    "effects":          {"effects": 0, "customize": 1},
104    "extensions":       {"installed": 0, "more": 1, "download": 1},
105    "keyboard":         {"typing": 0, "shortcuts": 1, "layouts": 2},
106    "mouse":            {"mouse": 0, "touchpad": 1},
107    "power":            {"power": 0, "batteries": 1, "brightness": 2},
108    "screensaver":      {"settings": 0, "customize": 1},
109    "sound":            {"output": 0, "input": 1, "sounds": 2, "applications": 3, "settings": 4},
110    "themes":           {"themes": 0, "download": 1, "options": 2},
111    "windows":          {"titlebar": 0, "behavior": 1, "alttab": 2},
112    "workspaces":       {"osd": 0, "settings": 1}
113}
114
115ARG_REWRITE = {
116    'accessibility':    'universal-access',
117    'screen':           'display',
118    'screens':          'display',
119    'bluetooth':        'blueberry',
120    'hotcorners':       'hotcorner',
121    'accounts':         'online-accounts',
122    'colors':           'color',
123    'me':               'user',
124    'lightdm-settings': 'pkexec lightdm-settings',
125    'login-screen':     'pkexec lightdm-settings',
126    'window':           'windows',
127    'background':       'backgrounds',
128    'driver-manager':   'pkexec driver-manager',
129    'drivers':          'pkexec driver-manager',
130    'printers':         'system-config-printer',
131    'printer':          'system-config-printer',
132    'infos':            'info',
133    'locale':           'mintlocale',
134    'language':         'mintlocale',
135    'input-method':     'mintlocale-im',
136    'nvidia':           'nvidia-settings',
137    'firewall':         'gufw',
138    'networks':         'network',
139    'sources':          'pkexec mintsources',
140    'mintsources':      'pkexec mintsources',
141    'panels':           'panel',
142    'tablet':           'wacom',
143    'users':            'cinnamon-settings-users'
144}
145
146
147def print_timing(func):
148    # decorate functions with @print_timing to output how long they take to run.
149    def wrapper(*arg):
150        t1 = time.time()
151        res = func(*arg)
152        t2 = time.time()
153        print('%s took %0.3f ms' % (func.func_name, (t2-t1)*1000.0))
154        return res
155    return wrapper
156
157
158def touch(fname, times=None):
159    with open(fname, 'a'):
160        os.utime(fname, times)
161
162
163class MainWindow:
164    # Change pages
165    def side_view_nav(self, side_view, path, cat):
166        selected_items = side_view.get_selected_items()
167        if len(selected_items) > 0:
168            self.deselect(cat)
169            filtered_path = side_view.get_model().convert_path_to_child_path(selected_items[0])
170            if filtered_path is not None:
171                self.go_to_sidepage(cat, filtered_path, user_action=True)
172
173    def _on_sidepage_hide_stack(self):
174        self.stack_switcher.set_opacity(0)
175
176    def _on_sidepage_show_stack(self):
177        self.stack_switcher.set_opacity(1)
178
179    def go_to_sidepage(self, cat, path, user_action=True):
180        iterator = self.store[cat].get_iter(path)
181        sidePage = self.store[cat].get_value(iterator, 2)
182        if not sidePage.is_standalone:
183            if not user_action:
184                self.window.set_title(sidePage.name)
185                self.window.set_icon_name(sidePage.icon)
186            sidePage.build()
187            if sidePage.stack:
188                self.stack_switcher.set_stack(sidePage.stack)
189                l = sidePage.stack.get_children()
190                if len(l) > 0:
191                    if self.tab in range(len(l)):
192                        sidePage.stack.set_visible_child(l[self.tab])
193                        visible_child = sidePage.stack.get_visible_child()
194                        if self.tab == 1 \
195                        and hasattr(visible_child, 'sort_combo') \
196                        and self.sort in range(5):
197                            visible_child.sort_combo.set_active(self.sort)
198                            visible_child.sort_changed()
199                    else:
200                        sidePage.stack.set_visible_child(l[0])
201                    if sidePage.stack.get_visible():
202                        self.stack_switcher.set_opacity(1)
203                    else:
204                        self.stack_switcher.set_opacity(0)
205                    if hasattr(sidePage, "connect_proxy"):
206                        sidePage.connect_proxy("hide_stack", self._on_sidepage_hide_stack)
207                        sidePage.connect_proxy("show_stack", self._on_sidepage_show_stack)
208                else:
209                    self.stack_switcher.set_opacity(0)
210            else:
211                self.stack_switcher.set_opacity(0)
212
213            if user_action:
214                self.main_stack.set_visible_child_name("content_box_page")
215                self.header_stack.set_visible_child_name("content_box")
216
217            else:
218                self.main_stack.set_visible_child_full("content_box_page", Gtk.StackTransitionType.NONE)
219                self.header_stack.set_visible_child_full("content_box", Gtk.StackTransitionType.NONE)
220
221            self.current_sidepage = sidePage
222            width = 0
223            for widget in self.top_bar:
224                m, n = widget.get_preferred_width()
225                width += n
226            self.top_bar.set_size_request(width + 20, -1)
227            self.maybe_resize(sidePage)
228        else:
229            sidePage.build()
230
231    def maybe_resize(self, sidePage):
232        m, n = self.content_box.get_preferred_size()
233
234        # Resize vertically depending on the height requested by the module
235        use_height = WIN_HEIGHT
236        total_height = n.height + self.bar_heights + WIN_H_PADDING
237        if not sidePage.size:
238            # No height requested, resize vertically if the module is taller than the window
239            if total_height > WIN_HEIGHT:
240                use_height = total_height
241        elif sidePage.size > 0:
242            # Height hardcoded by the module
243            use_height = sidePage.size + self.bar_heights + WIN_H_PADDING
244        elif sidePage.size == -1:
245            # Module requested the window to fit it (i.e. shrink the window if necessary)
246            use_height = total_height
247
248        self.window.resize(WIN_WIDTH, use_height)
249
250    def deselect(self, cat):
251        for key in self.side_view:
252            if key is not cat:
253                self.side_view[key].unselect_all()
254
255    # Create the UI
256    def __init__(self):
257        self.builder = Gtk.Builder()
258        self.builder.set_translation_domain('cinnamon')  # let it translate!
259        self.builder.add_from_file(config.currentPath + "/cinnamon-settings.ui")
260        self.window = XApp.GtkWindow(window_position=Gtk.WindowPosition.CENTER,
261                                     default_width=800, default_height=600)
262
263        main_box = self.builder.get_object("main_box")
264        self.window.add(main_box)
265        self.top_bar = self.builder.get_object("top_bar")
266        self.side_view = {}
267        self.main_stack = self.builder.get_object("main_stack")
268        self.main_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
269        self.main_stack.set_transition_duration(150)
270        self.header_stack = self.builder.get_object("header_stack")
271        self.header_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
272        self.header_stack.set_transition_duration(150)
273        self.side_view_container = self.builder.get_object("category_box")
274        self.side_view_sw = self.builder.get_object("side_view_sw")
275        context = self.side_view_sw.get_style_context()
276        context.add_class("cs-category-view")
277        context.add_class("view")
278        self.side_view_sw.show_all()
279        self.content_box = self.builder.get_object("content_box")
280        self.content_box_sw = self.builder.get_object("content_box_sw")
281        self.content_box_sw.show_all()
282        self.button_back = self.builder.get_object("button_back")
283        self.button_back.set_tooltip_text(_("Back to all settings"))
284        button_image = self.builder.get_object("image1")
285        button_image.props.icon_size = Gtk.IconSize.MENU
286
287        self.stack_switcher = self.builder.get_object("stack_switcher")
288
289        self.search_entry = self.builder.get_object("search_box")
290        self.search_entry.set_placeholder_text(_("Search"))
291        self.search_entry.connect("changed", self.onSearchTextChanged)
292        self.search_entry.connect("icon-press", self.onClearSearchBox)
293
294        self.window.connect("destroy", self.quit)
295
296        self.builder.connect_signals(self)
297        self.unsortedSidePages = []
298        self.sidePages = []
299        self.settings = Gio.Settings.new("org.cinnamon")
300        self.current_cat_widget = None
301
302        self.current_sidepage = None
303        self.c_manager = capi.CManager()
304        self.content_box.c_manager = self.c_manager
305        self.bar_heights = 0
306
307        for module in modules:
308            try:
309                mod = module.Module(self.content_box)
310                if self.loadCheck(mod) and self.setParentRefs(mod):
311                    self.unsortedSidePages.append((mod.sidePage, mod.name, mod.category))
312            except:
313                print("Failed to load module %s" % module)
314                traceback.print_exc()
315
316        for item in CONTROL_CENTER_MODULES:
317            ccmodule = SettingsWidgets.CCModule(item[0], item[1], item[2], item[3], item[4], self.content_box)
318            if ccmodule.process(self.c_manager):
319                self.unsortedSidePages.append((ccmodule.sidePage, ccmodule.name, ccmodule.category))
320
321        for item in STANDALONE_MODULES:
322            samodule = SettingsWidgets.SAModule(item[0], item[1], item[2], item[3], item[4], self.content_box)
323            if samodule.process():
324                self.unsortedSidePages.append((samodule.sidePage, samodule.name, samodule.category))
325
326        # sort the modules alphabetically according to the current locale
327        localeStrKey = cmp_to_key(locale.strcoll)
328        # Apply locale key to the field name of each side page.
329        sidePagesKey = lambda m: localeStrKey(m[0].name)
330        self.sidePages = sorted(self.unsortedSidePages, key=sidePagesKey)
331
332        # create the backing stores for the side nav-view.
333        sidePagesIters = {}
334        self.store = {}
335        self.storeFilter = {}
336        for sidepage in self.sidePages:
337            sp, sp_id, sp_cat = sidepage
338            if sp_cat not in self.store:        #       Label         Icon    sidePage    Category
339                self.store[sidepage[2]] = Gtk.ListStore(str,          str,    object,     str)
340                for category in CATEGORIES:
341                    if category["id"] == sp_cat:
342                        category["show"] = True
343
344            # Don't allow item names (and their translations) to be more than 30 chars long. It looks ugly and it creates huge gaps in the icon views
345            name = sp.name
346            if len(name) > 30:
347                name = "%s..." % name[:30]
348            sidePagesIters[sp_id] = (self.store[sp_cat].append([name, sp.icon, sp, sp_cat]), sp_cat)
349
350        self.min_label_length = 0
351        self.min_pix_length = 0
352
353        for key in self.store:
354            char, pix = self.get_label_min_width(self.store[key])
355            self.min_label_length = max(char, self.min_label_length)
356            self.min_pix_length = max(pix, self.min_pix_length)
357            self.storeFilter[key] = self.store[key].filter_new()
358            self.storeFilter[key].set_visible_func(self.filter_visible_function)
359
360        self.min_label_length += 2
361        self.min_pix_length += 4
362
363        self.min_label_length = max(self.min_label_length, MIN_LABEL_WIDTH)
364        self.min_pix_length = max(self.min_pix_length, MIN_PIX_WIDTH)
365
366        self.min_label_length = min(self.min_label_length, MAX_LABEL_WIDTH)
367        self.min_pix_length = min(self.min_pix_length, MAX_PIX_WIDTH)
368
369        self.displayCategories()
370
371        # set up larger components.
372        self.window.set_title(_("System Settings"))
373        self.button_back.connect('clicked', self.back_to_icon_view)
374
375        self.calculate_bar_heights()
376
377        self.tab = 0  # open 'manage' tab by default
378        self.sort = 1  # sorted by 'score' by default
379
380        # Select the first sidePage
381        if len(sys.argv) > 1:
382            arg1 = sys.argv[1]
383            if arg1 in ARG_REWRITE.keys():
384                arg1 = ARG_REWRITE[arg1]
385        if len(sys.argv) > 1 and arg1 in sidePagesIters:
386            # Analyses arguments to know the tab to open
387            # and the sort to apply if the tab is the 'more' one.
388            # Examples:
389            #   cinnamon-settings.py applets --tab=more --sort=date
390            #   cinnamon-settings.py applets --tab=1 --sort=2
391            #   cinnamon-settings.py applets --tab=more --sort=date
392            #   cinnamon-settings.py applets --tab=1 -s 2
393            #   cinnamon-settings.py applets -t 1 -s installed
394            #   cinnamon-settings.py desklets -t 2
395            # Please note that useless or wrong arguments are ignored.
396            opts = []
397            sorts_literal = {"name":0, "score":1, "date":2, "installed":3, "update":4}
398            tabs_literal = {"default":0}
399            if arg1 in TABS.keys():
400                tabs_literal = TABS[arg1]
401
402            try:
403                if len(sys.argv) > 2:
404                    opts = getopt.getopt(sys.argv[2:], "t:s:", ["tab=", "sort="])[0]
405            except getopt.GetoptError:
406                pass
407
408            for opt, arg in opts:
409                if opt in ("-t", "--tab"):
410                    if arg.isdecimal():
411                        self.tab = int(arg)
412                    elif arg in tabs_literal.keys():
413                        self.tab = tabs_literal[arg]
414                if opt in ("-s", "--sort"):
415                    if arg.isdecimal():
416                        self.sort = int(arg)
417                    elif arg in sorts_literal.keys():
418                        self.sort = sorts_literal[arg]
419
420            # If we're launching a module directly, set the WM class so GWL
421            # can consider it as a standalone app and give it its own
422            # group.
423            wm_class = "cinnamon-settings %s" % arg1
424            self.window.set_wmclass(wm_class, wm_class)
425            self.button_back.hide()
426            (iter, cat) = sidePagesIters[arg1]
427            path = self.store[cat].get_path(iter)
428            if path:
429                self.go_to_sidepage(cat, path, user_action=False)
430                self.window.show()
431                if arg1 in ("mintlocale", "blueberry", "system-config-printer", "mintlocale-im", "nvidia-settings"):
432                    # These modules do not need to leave the System Settings window open,
433                    # when selected by command line argument.
434                    self.window.close()
435            else:
436                self.search_entry.grab_focus()
437                self.window.show()
438        else:
439            self.search_entry.grab_focus()
440            self.window.connect("key-press-event", self.on_keypress)
441            self.window.connect("button-press-event", self.on_buttonpress)
442
443            self.window.show()
444
445    def on_keypress(self, widget, event):
446        grab = False
447        device = Gtk.get_current_event_device()
448        if device.get_source() == Gdk.InputSource.KEYBOARD:
449            grab = Gdk.Display.get_default().device_is_grabbed(device)
450        if not grab and event.keyval == Gdk.KEY_BackSpace and (type(self.window.get_focus()) not in
451                                                               (Gtk.TreeView, Gtk.Entry, Gtk.SpinButton, Gtk.TextView)):
452            self.back_to_icon_view(None)
453            return True
454        return False
455
456    def on_buttonpress(self, widget, event):
457        if event.button == MOUSE_BACK_BUTTON:
458            self.back_to_icon_view(None)
459            return True
460        return False
461
462    def calculate_bar_heights(self):
463        h = 0
464        m, n = self.top_bar.get_preferred_size()
465        h += n.height
466        self.bar_heights = h
467
468    def onSearchTextChanged(self, widget):
469        self.displayCategories()
470
471    def onClearSearchBox(self, widget, position, event):
472        if position == Gtk.EntryIconPosition.SECONDARY:
473            self.search_entry.set_text("")
474
475    def strip_accents(self, text):
476        text = unicodedata.normalize('NFKD', text)
477        return ''.join([c for c in text if not unicodedata.combining(c)])
478
479    def filter_visible_function(self, model, iter, user_data = None):
480        sidePage = model.get_value(iter, 2)
481        text = self.strip_accents(self.search_entry.get_text().lower())
482        if self.strip_accents(sidePage.name.lower()).find(text) > -1 or \
483           self.strip_accents(sidePage.keywords.lower()).find(text) > -1:
484            return True
485        else:
486            return False
487
488    def displayCategories(self):
489        widgets = self.side_view_container.get_children()
490        for widget in widgets:
491            widget.destroy()
492        self.first_category_done = False # This is just to prevent an extra separator showing up before the first category
493        for category in CATEGORIES:
494            if category["show"] is True:
495                self.prepCategory(category)
496        self.side_view_container.show_all()
497
498    def get_label_min_width(self, model):
499        min_width_chars = 0
500        min_width_pixels = 0
501        icon_view = Gtk.IconView()
502        iter = model.get_iter_first()
503        while iter != None:
504            string = model.get_value(iter, 0)
505            split_by_word = string.split(" ")
506            for word in split_by_word:
507                layout = icon_view.create_pango_layout(word)
508                item_width, item_height = layout.get_pixel_size()
509                if item_width > min_width_pixels:
510                    min_width_pixels = item_width
511                if len(word) > min_width_chars:
512                    min_width_chars = len(word)
513            iter = model.iter_next(iter)
514        return min_width_chars, min_width_pixels
515
516    def pixbuf_data_func(self, column, cell, model, iter, data=None):
517        wrapper = model.get_value(iter, 1)
518        if wrapper:
519            cell.set_property('surface', wrapper.surface)
520
521    def prepCategory(self, category):
522        self.storeFilter[category["id"]].refilter()
523        if not self.anyVisibleInCategory(category):
524            return
525        if self.first_category_done:
526            widget = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
527            self.side_view_container.pack_start(widget, False, False, 10)
528
529        box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4)
530        img = Gtk.Image.new_from_icon_name(category["icon"], Gtk.IconSize.BUTTON)
531        box.pack_start(img, False, False, 4)
532
533        widget = Gtk.Label(yalign=0.5)
534        widget.set_use_markup(True)
535        widget.set_markup('<span size="12000">%s</span>' % category["label"])
536        box.pack_start(widget, False, False, 1)
537        self.side_view_container.pack_start(box, False, False, 0)
538        widget = Gtk.IconView.new_with_model(self.storeFilter[category["id"]])
539
540        area = widget.get_area()
541
542        widget.set_item_width(self.min_pix_length)
543        widget.set_item_padding(0)
544        widget.set_column_spacing(18)
545        widget.set_row_spacing(18)
546        widget.set_margin(20)
547
548        pixbuf_renderer = Gtk.CellRendererPixbuf()
549        text_renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.NONE, wrap_mode=Pango.WrapMode.WORD_CHAR, wrap_width=0, width_chars=self.min_label_length, alignment=Pango.Alignment.CENTER, xalign=0.5)
550
551        area.pack_start(pixbuf_renderer, True, True, False)
552        area.pack_start(text_renderer, True, True, False)
553        area.add_attribute(pixbuf_renderer, "icon-name", 1)
554        pixbuf_renderer.set_property("stock-size", Gtk.IconSize.DIALOG)
555        pixbuf_renderer.set_property("follow-state", True)
556
557        area.add_attribute(text_renderer, "text", 0)
558
559        self.side_view[category["id"]] = widget
560        self.side_view_container.pack_start(self.side_view[category["id"]], False, False, 0)
561        self.first_category_done = True
562        self.side_view[category["id"]].connect("item-activated", self.side_view_nav, category["id"])
563        self.side_view[category["id"]].connect("button-release-event", self.button_press, category["id"])
564        self.side_view[category["id"]].connect("keynav-failed", self.on_keynav_failed, category["id"])
565        self.side_view[category["id"]].connect("selection-changed", self.on_selection_changed, category["id"])
566
567    def bring_selection_into_view(self, iconview):
568        sel = iconview.get_selected_items()
569
570        if sel:
571            path = sel[0]
572            found, rect = iconview.get_cell_rect(path, None)
573
574            cw = self.side_view_container.get_window()
575            cw_x, cw_y = cw.get_position()
576
577            ivw = iconview.get_window()
578            iv_x, iv_y = ivw.get_position()
579
580            final_y = rect.y + cw_y + iv_y
581
582            adj = self.side_view_sw.get_vadjustment()
583            page = adj.get_page_size()
584            current_pos = adj.get_value()
585
586            if (final_y > 0) and ((final_y + rect.height) < page):
587                return
588
589            if ((final_y + rect.height) > page):
590                adj.set_value(current_pos + final_y + rect.height - page + 10)
591            elif final_y < 0:
592                # We can just add a negative here (since final_y < 0), but it's less
593                # confusing to be explicit that we're decreasing current_pos.
594                adj.set_value(current_pos - abs(final_y) - 10)
595
596    def on_selection_changed(self, widget, category):
597        sel = widget.get_selected_items()
598        if len(sel) > 0:
599            self.current_cat_widget = widget
600            self.bring_selection_into_view(widget)
601        for iv in self.side_view:
602            if self.side_view[iv] == self.current_cat_widget:
603                continue
604            self.side_view[iv].unselect_all()
605
606    def get_cur_cat_index(self, category):
607        i = 0
608        for cat in CATEGORIES:
609            if category == cat["id"]:
610                return i
611            i += 1
612
613    def get_cur_column(self, iconview):
614        s, path, cell = iconview.get_cursor()
615        if path:
616            col = iconview.get_item_column(path)
617            return col
618
619    def reposition_new_cat(self, sel, iconview):
620        iconview.set_cursor(sel, None, False)
621        iconview.select_path(sel)
622        iconview.grab_focus()
623
624    def on_keynav_failed(self, widget, direction, category):
625        num_cats = len(CATEGORIES)
626        current_idx = self.get_cur_cat_index(category)
627        ret = False
628        dist = 1000
629        sel = None
630
631        if direction == Gtk.DirectionType.DOWN and current_idx < num_cats - 1:
632            new_cat = CATEGORIES[current_idx + 1]
633            col = self.get_cur_column(widget)
634            new_cat_view = self.side_view[new_cat["id"]]
635            model = new_cat_view.get_model()
636            iter = model.get_iter_first()
637            while iter is not None:
638                path = model.get_path(iter)
639                c = new_cat_view.get_item_column(path)
640                d = abs(c - col)
641                if d < dist:
642                    sel = path
643                    dist = d
644                iter = model.iter_next(iter)
645            self.reposition_new_cat(sel, new_cat_view)
646            ret = True
647        elif direction == Gtk.DirectionType.UP and current_idx > 0:
648            new_cat = CATEGORIES[current_idx - 1]
649            col = self.get_cur_column(widget)
650            new_cat_view = self.side_view[new_cat["id"]]
651            model = new_cat_view.get_model()
652            iter = model.get_iter_first()
653            while iter is not None:
654                path = model.get_path(iter)
655                c = new_cat_view.get_item_column(path)
656                d = abs(c - col)
657                if d <= dist:
658                    sel = path
659                    dist = d
660                iter = model.iter_next(iter)
661            self.reposition_new_cat(sel, new_cat_view)
662            ret = True
663        return ret
664
665    def button_press(self, widget, event, category):
666        if event.button == 1:
667            self.side_view_nav(widget, None, category)
668
669    def anyVisibleInCategory(self, category):
670        id = category["id"]
671        iter = self.storeFilter[id].get_iter_first()
672        visible = False
673        while iter is not None:
674            cat = self.storeFilter[id].get_value(iter, 3)
675            visible = cat == category["id"]
676            iter = self.storeFilter[id].iter_next(iter)
677        return visible
678
679    def setParentRefs (self, mod):
680        try:
681            mod._setParentRef(self.window)
682        except AttributeError:
683            pass
684        return True
685
686    def loadCheck (self, mod):
687        try:
688            return mod._loadCheck()
689        except:
690            return True
691
692    def back_to_icon_view(self, widget):
693        self.window.set_title(_("System Settings"))
694        self.window.set_icon_name("preferences-desktop")
695        self.window.resize(WIN_WIDTH, WIN_HEIGHT)
696        children = self.content_box.get_children()
697        for child in children:
698            child.hide()
699            if child.get_name() == "c_box":
700                c_widgets = child.get_children()
701                for c_widget in c_widgets:
702                    c_widget.hide()
703        self.main_stack.set_visible_child_name("side_view_page")
704        self.header_stack.set_visible_child_name("side_view")
705        self.search_entry.grab_focus()
706
707        if self.current_sidepage.module and hasattr(self.current_sidepage.module, "on_navigate_out_of_module"):
708            self.current_sidepage.module.on_navigate_out_of_module()
709
710        self.current_sidepage = None
711
712    def quit(self, *args):
713        self.window.destroy()
714        Gtk.main_quit()
715
716
717if __name__ == "__main__":
718    setproctitle("cinnamon-settings")
719    import signal
720
721    ps = proxygsettings.get_proxy_settings()
722    if ps:
723        proxy = urllib.ProxyHandler(ps)
724    else:
725        proxy = urllib.ProxyHandler()
726    urllib.install_opener(urllib.build_opener(proxy))
727
728    window = MainWindow()
729    signal.signal(signal.SIGINT, signal.SIG_DFL)
730    Gtk.main()
731