1#!/usr/local/bin/python3.8
2
3import sys
4import json
5import dbus
6import gi
7gi.require_version('Gtk', '3.0')
8from gi.repository import Gtk, Gdk
9
10from SettingsWidgets import SidePage
11from xapp.GSettingsWidgets import *
12
13
14class Monitor:
15    def __init__(self):
16        self.top = -1
17        self.bottom = -1
18        self.right = -1
19        self.left = -1
20
21
22class PanelSettingsPage(SettingsPage):
23    def __init__(self, panel_id, settings, position):
24        super(PanelSettingsPage, self).__init__()
25        self.set_margin_top(0)
26        self.set_margin_bottom(0)
27        self.panel_id = panel_id
28        self.settings = settings
29
30        center_switcher_label = _("Center Zone")
31
32        if position in ("top", "bottom"):
33            dimension_text = _("Panel height:")
34            left_switcher_label = _("Left Zone")
35            right_switcher_label = _("Right Zone")
36        else:
37            dimension_text = _("Panel width:")
38            left_switcher_label = _("Top Zone")
39            right_switcher_label = _("Bottom Zone")
40
41        def can_show(vlist, possible):
42            for item in vlist:
43                if item.split(":")[0] == panel_id:
44                    return item.split(":")[1] != "false"
45
46        section = SettingsSection(_("Panel Visibility"))
47        self.add(section)
48
49        self.size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
50
51        options = [["true", _("Auto hide panel")], ["false", _("Always show panel")], ["intel", _("Intelligently hide panel")]]
52        widget = PanelComboBox(_("Auto-hide panel"), "org.cinnamon", "panels-autohide", self.panel_id, options, size_group=self.size_group)
53        section.add_row(widget)
54
55        widget = PanelSpinButton(_("Show delay"), "org.cinnamon", "panels-show-delay", self.panel_id, _("milliseconds"), 0, 2000, 50, 200)#, dep_key="org.cinnamon/panels-autohide")
56        section.add_reveal_row(widget, "org.cinnamon", "panels-autohide", check_func=can_show)
57
58        widget = PanelSpinButton(_("Hide delay"), "org.cinnamon", "panels-hide-delay", self.panel_id, _("milliseconds"), 0, 2000, 50, 200)#, dep_key="org.cinnamon/panels-autohide")
59        section.add_reveal_row(widget, "org.cinnamon", "panels-autohide", check_func=can_show)
60
61        section = SettingsSection(_("Customize"))
62        self.add(section)
63
64        widget = PanelRange(dimension_text, "org.cinnamon", "panels-height", self.panel_id, _("Smaller"), _("Larger"), mini=20, maxi=60, show_value=True)
65        widget.set_rounding(0)
66        section.add_row(widget)
67
68        section = SettingsSection(_("Panel appearance"))
69        self.add(section)
70
71        zone_switcher = SettingsWidget()
72        zone_switcher.fill_row()
73
74        switcher_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, border_width=5)
75        zones_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
76
77        stack = Gtk.Stack(transition_type=Gtk.StackTransitionType.SLIDE_LEFT_RIGHT, transition_duration=150)
78        switcher = Gtk.StackSwitcher(stack=stack, halign=Gtk.Align.CENTER)
79
80        section.add_row(switcher_box)
81        switcher_box.get_parent().set_activatable(False)
82
83        switcher_box.pack_start(switcher, False, False, 0)
84        zones_box.pack_start(stack, False, False, 0)
85
86        zone_infos = [
87            [left_switcher_label, "left"],
88            [center_switcher_label, "center"],
89            [right_switcher_label, "right"]
90        ];
91
92        for [zone, label] in (["left", left_switcher_label],
93                              ["center", center_switcher_label],
94                              ["right", right_switcher_label]):
95            page = self.create_zone_page(zone)
96            page.show_all()
97
98            stack.add_titled(page, zone, label)
99
100        section.add_row(zones_box)
101        zones_box.get_parent().set_activatable(False)
102
103        stack.set_visible_child_name("left")
104
105        self.show_all()
106
107    def create_zone_page(self, zone):
108        zone_page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
109
110        text_options = [
111            [0, _("Allow theme to determine font size")]
112        ]
113
114        points = 6.0
115        while points <= 16.0:
116            text_options.append([points, "%.1fpt" % points])
117            points += 0.5
118
119        widget = PanelJSONComboBox(_("Font size"),
120                                     "org.cinnamon", "panel-zone-text-sizes",
121                                     self.panel_id, zone, text_options, valtype=float, size_group=self.size_group)
122        zone_page.pack_start(widget, False, False, 0)
123
124        fullcolor_options = [
125            [-1, _("Scale to panel size exactly")],
126            [0, _("Scale to panel size optimally")],
127            [16, '16px'],
128            [22, '22px'],
129            [24, '24px'],
130            [32, '32px'],
131            [48, '48px']
132        ]
133
134        widget = PanelJSONComboBox(_("Colored icon size"),
135                                   "org.cinnamon", "panel-zone-icon-sizes",
136                                   self.panel_id, zone, fullcolor_options, valtype=int, size_group=self.size_group)
137        zone_page.pack_start(widget, False, False, 0)
138
139        widget = PanelJSONSpinButton(_("Symbolic icon size"),
140                                     "org.cinnamon", "panel-zone-symbolic-icon-sizes",
141                                     self.panel_id, zone, _("px"), 10, 50, 1, 0)
142        zone_page.pack_start(widget, False, False, 0)
143
144        return zone_page
145
146class Module:
147    name = "panel"
148    category = "prefs"
149    comment = _("Manage Cinnamon panel settings")
150
151    def __init__(self, content_box):
152        keywords = _("panel, height, bottom, top, autohide, size, layout")
153        self.sidePage = SidePage(_("Panel"), "cs-panel", keywords, content_box, module=self)
154
155    def on_module_selected(self):
156        if not self.loaded:
157            print("Loading Panel module")
158
159            self.settings = Gio.Settings.new("org.cinnamon")
160
161            try:
162                if len(sys.argv) > 2 and sys.argv[1] == "panel":
163                    self.panel_id = sys.argv[2]
164                else:
165                    self.panel_id = self.settings.get_strv("panels-enabled")[0].split(":")[0]
166            except:
167                self.panel_id = ""
168
169            self.panels = []
170
171            self.previous_button = Gtk.Button(_("Previous panel"))
172            self.next_button = Gtk.Button(_("Next panel"))
173
174            controller = SettingsWidget()
175            controller.fill_row()
176            controller.pack_start(self.previous_button, False, False, 0)
177            controller.pack_end(self.next_button, False, False, 0)
178            self.previous_button.connect("clicked", self.on_previous_panel)
179            self.next_button.connect("clicked", self.on_next_panel)
180
181            self.revealer = SettingsRevealer()
182
183            page = SettingsPage()
184            page.add(controller)
185            page.set_margin_bottom(0)
186            self.revealer.add(page)
187            self.sidePage.add_widget(self.revealer)
188
189            self.config_stack = Gtk.Stack()
190            self.config_stack.set_transition_duration(150)
191            self.revealer.add(self.config_stack)
192
193            page = SettingsPage()
194            self.sidePage.add_widget(page)
195            section = page.add_section(_("General Panel Options"))
196
197            buttons = SettingsWidget()
198            self.add_panel_button = Gtk.Button(label=_("Add new panel"))
199
200            buttons.pack_start(self.add_panel_button, False, False, 2)
201            toggle_button = Gtk.ToggleButton(label=_("Panel edit mode"))
202
203            self.settings.bind("panel-edit-mode", toggle_button, "active", Gio.SettingsBindFlags.DEFAULT)
204            buttons.pack_end(toggle_button, False, False, 2)
205            section.add_row(buttons)
206
207            section.add_row(GSettingsSwitch(_("Allow the pointer to pass through the edges of panels"), "org.cinnamon", "no-adjacent-panel-barriers"))
208
209            self.add_panel_button.set_sensitive(False)
210
211            self.settings.connect("changed::panels-enabled", self.on_panel_list_changed)
212
213            self.proxy = None
214
215            try:
216                Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None,
217                                          "org.Cinnamon", "/org/Cinnamon", "org.Cinnamon", None, self._on_proxy_ready, None)
218            except dbus.exceptions.DBusException as e:
219                print(e)
220                self.proxy = None
221
222        self.on_panel_list_changed()
223
224    def _on_proxy_ready (self, object, result, data=None):
225        self.proxy = Gio.DBusProxy.new_for_bus_finish(result)
226
227        if not self.proxy.get_name_owner():
228            self.proxy = None
229
230        if self.proxy:
231            self.revealer.connect("unmap", self.restore_panels)
232            self.revealer.connect("destroy", self.restore_panels)
233
234            self.add_panel_button.connect("clicked", self.on_add_panel)
235
236            if self.panel_id is not None:
237                self.proxy.highlightPanel('(ib)', int(self.panel_id), True)
238
239    def on_add_panel(self, widget):
240        if self.proxy:
241            self.proxy.addPanelQuery()
242
243    def on_previous_panel(self, widget):
244        if self.panel_id and self.proxy:
245            self.proxy.highlightPanel('(ib)', int(self.panel_id), False)
246
247        current = self.panels.index(self.current_panel)
248
249        if current - 1 >= 0:
250            self.current_panel = self.panels[current - 1]
251            self.panel_id = self.current_panel.panel_id
252        else:
253            self.current_panel = self.panels[len(self.panels) - 1]
254            self.panel_id = self.current_panel.panel_id
255
256        self.config_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT)
257
258        if self.proxy:
259            self.proxy.highlightPanel('(ib)', int(self.panel_id), True)
260
261        self.config_stack.set_visible_child(self.current_panel)
262
263    def on_next_panel(self, widget):
264        if self.panel_id and self.proxy:
265            self.proxy.highlightPanel('(ib)', int(self.panel_id), False)
266
267        current = self.panels.index(self.current_panel)
268
269        if current + 1 < len(self.panels):
270            self.current_panel = self.panels[current + 1]
271            self.panel_id = self.current_panel.panel_id
272        else:
273            self.current_panel = self.panels[0]
274            self.panel_id = self.current_panel.panel_id
275
276        self.config_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
277
278        if self.proxy:
279            self.proxy.highlightPanel('(ib)', int(self.panel_id), True)
280
281        self.config_stack.set_visible_child(self.current_panel)
282
283    def on_panel_list_changed(self, *args):
284        if len(self.panels) > 0:
285            for panel in self.panels:
286                panel.destroy()
287
288        self.panels = []
289        monitor_layout = []
290
291        panels = self.settings.get_strv("panels-enabled")
292        n_mons = Gdk.Screen.get_default().get_n_monitors()
293
294        for i in range(n_mons):
295            monitor_layout.append(Monitor())
296
297        current_found = False
298        for panel in panels:
299            panel_id, monitor_id, position = panel.split(":")
300            monitor_id = int(monitor_id)
301            panel_page = PanelSettingsPage(panel_id, self.settings, position)
302            self.config_stack.add_named(panel_page, panel_id)
303
304            # we may already have a current panel id from the command line or if
305            # if the panels-enabled key changed since everything was loaded
306            if panel_id == self.panel_id:
307                current_found = True
308                self.current_panel = panel_page
309                self.config_stack.set_visible_child(panel_page)
310
311            # we don't currently show panels on monitors that aren't attached
312            # if we decide to change this behavior, we should probably give some visual indication
313            # that the panel is on a detached monitor
314            if monitor_id < n_mons:
315                if "top" in position:
316                    monitor_layout[monitor_id].top = panel_page
317                elif "bottom" in position:
318                    monitor_layout[monitor_id].bottom = panel_page
319                elif "left" in position:
320                    monitor_layout[monitor_id].left = panel_page
321                else:
322                    monitor_layout[monitor_id].right = panel_page
323
324        # Index the panels for the next/previous buttons
325        for monitor in monitor_layout:
326            for panel_page in (monitor.top, monitor.bottom, monitor.left, monitor.right):
327                if panel_page != -1:
328                    self.panels.append(panel_page)
329
330        # if there are no panels, there's no point in showing the stack
331        if len(self.panels) == 0:
332            self.next_button.hide()
333            self.previous_button.hide()
334            self.config_stack.hide()
335            self.add_panel_button.set_sensitive(True)
336            self.current_panel = None
337            self.panel_id = None
338            return
339
340        self.config_stack.show()
341        self.next_button.show()
342        self.previous_button.show()
343
344        # Disable the panel switch buttons if there's only one panel
345        if len(self.panels) == 1:
346            self.next_button.set_sensitive(False)
347            self.previous_button.set_sensitive(False)
348        else:
349            self.next_button.set_sensitive(True)
350            self.previous_button.set_sensitive(True)
351
352        if not current_found:
353            self.current_panel = self.panels[0]
354            self.panel_id = self.current_panel.panel_id
355            self.config_stack.set_visible_child(self.current_panel)
356
357        self.revealer.set_reveal_child(len(self.panels) != 0)
358
359        # If all panel positions are full, we want to disable the add button
360        can_add = False
361        for monitor in monitor_layout:
362            if -1 in (monitor.top, monitor.bottom, monitor.left, monitor.right):
363                can_add = True
364                break
365
366        self.add_panel_button.set_sensitive(can_add)
367
368        try:
369            current_idx = self.panels.index(self.panel_id)
370        except:
371            current_idx = 0
372
373        if self.proxy:
374            self.proxy.highlightPanel('(ib)', int(self.panel_id), True)
375
376    def restore_panels(self, widget):
377        self.proxy.destroyDummyPanels()
378        if self.panel_id:
379            self.proxy.highlightPanel('(ib)', int(self.panel_id), False)
380
381class PanelWidgetBackend(object):
382    def connect_to_settings(self, schema, key):
383        self.key = key
384        self.settings = Gio.Settings.new(schema)
385        self.settings_changed_id = self.settings.connect("changed::"+self.key, self.on_setting_changed)
386        self.connect("destroy", self.on_destroy)
387        self.on_setting_changed()
388
389        # unless we have a binding direction get, we need to connect the handlers after hooking up the settings
390        # this is different from the GSettingsBackend because we cant use a bind here due to the complicated nature
391        # of the getting and setting
392        if self.bind_dir is None or (self.bind_dir & Gio.SettingsBindFlags.GET == 0):
393            self.connect_widget_handlers()
394
395    def set_value(self, value):
396        vals = self.settings[self.key]
397        newvals = []
398        for val in vals:
399            if val.split(":")[0] == self.panel_id:
400                newvals.append(self.panel_id + ":" + self.stringify(value))
401            else:
402                newvals.append(val)
403        self.settings[self.key] = newvals
404
405    def get_value(self):
406        vals = self.settings[self.key]
407        for val in vals:
408            [pid, value] = val.split(":")
409            if pid == self.panel_id:
410                return self.unstringify(value)
411
412    def stringify(self, value):
413        return str(value)
414
415    def on_destroy(self, *args):
416        self.settings.disconnect(self.settings_changed_id)
417
418class PanelSwitch(Switch, PanelWidgetBackend):
419    def __init__(self, label, schema, key, panel_id, *args, **kwargs):
420        self.panel_id = panel_id
421        super(PanelSwitch, self).__init__(label, *args, **kwargs)
422
423        self.connect_to_settings(schema, key)
424
425    def stringify(self, value):
426        return "true" if value else "false"
427
428    def unstringify(self, value):
429        return value != "false"
430
431    def on_setting_changed(self, *args):
432        value = self.get_value()
433        if value != self.content_widget.get_active():
434            self.content_widget.set_active(value)
435
436    def connect_widget_handlers(self, *args):
437        self.content_widget.connect("notify::active", self.on_my_value_changed)
438
439    def on_my_value_changed(self, *args):
440        active = self.content_widget.get_active()
441        if self.get_value() != active:
442            self.set_value(active)
443
444class PanelSpinButton(SpinButton, PanelWidgetBackend):
445    def __init__(self, label, schema, key, panel_id, *args, **kwargs):
446        self.panel_id = panel_id
447        super(PanelSpinButton, self).__init__(label, *args, **kwargs)
448
449        self.content_widget.set_value(0)
450
451        self.connect_to_settings(schema, key)
452
453    def get_range(self):
454        return None
455
456    # We use integer directly here because that is all the panel currently uses.
457    # If that changes in the future, we will need to fix this.
458    def stringify(self, value):
459        return str(int(value))
460
461    def unstringify(self, value):
462        return int(value)
463
464    def on_setting_changed(self, *args):
465        value = self.get_value()
466        if value is not None and value != int(self.content_widget.get_value()):
467            self.content_widget.set_value(value)
468
469class PanelJSONSpinButton(SpinButton, PanelWidgetBackend):
470    def __init__(self, label, schema, key, panel_id, zone, *args, **kwargs):
471        self.panel_id = panel_id
472        self.zone = zone
473        super(PanelJSONSpinButton, self).__init__(label, *args, **kwargs)
474
475        self.connect_to_settings(schema, key)
476
477    def get_range(self):
478        return
479
480    # We use integer directly here because that is all the panel currently uses.
481    # If that changes in the future, we will need to fix this.
482    def stringify(self, value):
483        return str(int(value))
484
485    def unstringify(self, value):
486        return int(value)
487
488    def on_setting_changed(self, *args):
489        self.content_widget.set_value(self.get_value())
490
491    def set_value(self, value):
492        vals = json.loads(self.settings[self.key])
493        for obj in vals:
494            if obj['panelId'] != int(self.panel_id):
495                continue
496            for key, val in obj.items():
497                if key == self.zone:
498                    obj[key] = int(value)
499                    break
500
501        self.settings[self.key] = json.dumps(vals)
502
503    def get_value(self):
504        vals = self.settings[self.key]
505        vals = json.loads(vals)
506        for obj in vals:
507            if obj['panelId'] != int(self.panel_id):
508                continue
509            for key, val in obj.items():
510                if key == self.zone:
511                    return int(val)
512        return 0 # prevent warnings if key is reset
513
514class PanelComboBox(ComboBox, PanelWidgetBackend):
515    def __init__(self, label, schema, key, panel_id, *args, **kwargs):
516        self.panel_id = panel_id
517        super(PanelComboBox, self).__init__(label, *args, **kwargs)
518
519        self.connect_to_settings(schema, key)
520
521    def stringify(self, value):
522        return value
523
524    def unstringify(self, value):
525        return value
526
527class PanelJSONComboBox(ComboBox, PanelWidgetBackend):
528    def __init__(self, label, schema, key, panel_id, zone, *args, **kwargs):
529        self.panel_id = panel_id
530        self.zone = zone
531        super(PanelJSONComboBox, self).__init__(label, *args, **kwargs)
532
533        self.connect_to_settings(schema, key)
534
535    def stringify(self, value):
536        return value
537
538    def unstringify(self, value):
539        return value
540
541    def set_value(self, value):
542        vals = json.loads(self.settings[self.key])
543        for obj in vals:
544            if obj['panelId'] != int(self.panel_id):
545                continue
546            for key, val in obj.items():
547                if key == self.zone:
548                    obj[key] = self.valtype(value)
549                    break
550
551        self.settings[self.key] = json.dumps(vals)
552
553    def get_value(self):
554        vals = self.settings[self.key]
555        vals = json.loads(vals)
556        for obj in vals:
557            if obj['panelId'] != int(self.panel_id):
558                continue
559            for key, val in obj.items():
560                if key == self.zone:
561                    return self.valtype(val)
562
563class PanelRange(Range, PanelWidgetBackend):
564    def __init__(self, label, schema, key, panel_id, *args, **kwargs):
565        self.panel_id = panel_id
566        super(PanelRange, self).__init__(label, *args, **kwargs)
567        self.connect_to_settings(schema, key)
568
569    def get_range(self):
570        return None
571
572    # We use integer directly here because that is all the panel currently uses.
573    # If that changes in the future, we will need to fix this.
574    def stringify(self, value):
575        return str(int(value))
576
577    def unstringify(self, value):
578        return int(value)
579
580    def on_setting_changed(self, *args):
581        value = self.get_value()
582        if value != int(self.bind_object.get_value()):
583            self.bind_object.set_value(value)
584