1#!/usr/local/bin/python3.8
2
3import os
4import subprocess
5
6import dbus
7import gi
8gi.require_version('Gtk', '3.0')
9from gi.repository import Gio, Gtk, GObject, GLib
10
11from xapp.SettingsWidgets import SettingsWidget, SettingsLabel
12from xapp.GSettingsWidgets import PXGSettingsBackend
13from ChooserButtonWidgets import DateChooserButton, TweenChooserButton, EffectChooserButton, TimeChooserButton
14from KeybindingWidgets import ButtonKeybinding
15
16settings_objects = {}
17
18CAN_BACKEND = ["SoundFileChooser", "TweenChooser", "EffectChooser", "DateChooser", "TimeChooser", "Keybinding"]
19
20class BinFileMonitor(GObject.GObject):
21    __gsignals__ = {
22        'changed': (GObject.SignalFlags.RUN_LAST, None, ()),
23    }
24    def __init__(self):
25        super(BinFileMonitor, self).__init__()
26
27        self.changed_id = 0
28
29        env = GLib.getenv("PATH")
30
31        if env == None:
32            env = "/bin:/usr/local/bin:."
33
34        self.paths = env.split(":")
35
36        self.monitors = []
37
38        for path in self.paths:
39            file = Gio.File.new_for_path(path)
40            mon = file.monitor_directory(Gio.FileMonitorFlags.SEND_MOVED, None)
41            mon.connect("changed", self.queue_emit_changed)
42            self.monitors.append(mon)
43
44    def _emit_changed(self):
45        self.emit("changed")
46        self.changed_id = 0
47        return False
48
49    def queue_emit_changed(self, file, other, event_type, data=None):
50        if self.changed_id > 0:
51            GLib.source_remove(self.changed_id)
52            self.changed_id = 0
53
54        self.changed_id = GLib.idle_add(self._emit_changed)
55
56file_monitor = None
57
58def get_file_monitor():
59    global file_monitor
60
61    if file_monitor == None:
62        file_monitor = BinFileMonitor()
63
64    return file_monitor
65
66class DependencyCheckInstallButton(Gtk.Box):
67    def __init__(self, checking_text, install_button_text, binfiles, final_widget=None, satisfied_cb=None):
68        super(DependencyCheckInstallButton, self).__init__(orientation=Gtk.Orientation.HORIZONTAL)
69
70        self.binfiles = binfiles
71        self.satisfied_cb = satisfied_cb
72
73        self.checking_text = checking_text
74        self.install_button_text = install_button_text
75
76        self.stack = Gtk.Stack()
77        self.pack_start(self.stack, False, False, 0)
78
79        self.progress_bar = Gtk.ProgressBar()
80        self.stack.add_named(self.progress_bar, "progress")
81
82        self.progress_bar.set_show_text(True)
83        self.progress_bar.set_text(self.checking_text)
84
85        self.install_warning = Gtk.Label(label=install_button_text, margin=5)
86        frame = Gtk.Frame()
87        frame.add(self.install_warning)
88        frame.set_shadow_type(Gtk.ShadowType.OUT)
89        frame.show_all()
90        self.stack.add_named(frame, "install")
91
92        if final_widget:
93            self.stack.add_named(final_widget, "final")
94        else:
95            self.stack.add_named(Gtk.Alignment(), "final")
96
97        self.stack.set_visible_child_name("progress")
98        self.progress_source_id = 0
99
100        self.file_listener = get_file_monitor()
101        self.file_listener_id = self.file_listener.connect("changed", self.on_file_listener_ping)
102
103        self.connect("destroy", self.on_destroy)
104
105        GLib.idle_add(self.check)
106
107    def check(self):
108        self.start_pulse()
109
110        success = True
111
112        for program in self.binfiles:
113            if not GLib.find_program_in_path(program):
114                success = False
115                break
116
117        GLib.idle_add(self.on_check_complete, success)
118
119        return False
120
121    def pulse_progress(self):
122        self.progress_bar.pulse()
123        return True
124
125    def start_pulse(self):
126        self.cancel_pulse()
127        self.progress_source_id = GLib.timeout_add(200, self.pulse_progress)
128
129    def cancel_pulse(self):
130        if (self.progress_source_id > 0):
131            GLib.source_remove(self.progress_source_id)
132            self.progress_source_id = 0
133
134    def on_check_complete(self, result, data=None):
135        self.cancel_pulse()
136        if result:
137            self.stack.set_visible_child_name("final")
138            if self.satisfied_cb:
139                self.satisfied_cb()
140        else:
141            self.stack.set_visible_child_name("install")
142
143    def on_file_listener_ping(self, monitor, data=None):
144        self.stack.set_visible_child_name("progress")
145        self.progress_bar.set_text(self.checking_text)
146        self.check()
147
148    def on_destroy(self, widget):
149        self.file_listener.disconnect(self.file_listener_id)
150        self.file_listener_id = 0
151
152class GSettingsDependencySwitch(SettingsWidget):
153    def __init__(self, label, schema=None, key=None, dep_key=None, binfiles=None, packages=None):
154        super(GSettingsDependencySwitch, self).__init__(dep_key=dep_key)
155
156        self.binfiles = binfiles
157        self.packages = packages
158
159        self.content_widget = Gtk.Alignment()
160        self.label = Gtk.Label(label)
161        self.pack_start(self.label, False, False, 0)
162        self.pack_end(self.content_widget, False, False, 0)
163
164        self.switch = Gtk.Switch()
165        self.switch.set_halign(Gtk.Align.END)
166        self.switch.set_valign(Gtk.Align.CENTER)
167
168        pkg_string = ""
169        for pkg in packages:
170            if pkg_string != "":
171                pkg_string += ", "
172            pkg_string += pkg
173
174        self.dep_button = DependencyCheckInstallButton(_("Checking dependencies"),
175                                                       _("Please install: %s") % (pkg_string),
176                                                       binfiles,
177                                                       self.switch)
178        self.content_widget.add(self.dep_button)
179
180        if schema:
181            self.settings = self.get_settings(schema)
182            self.settings.bind(key, self.switch, "active", Gio.SettingsBindFlags.DEFAULT)
183
184class SidePage(object):
185    def __init__(self, name, icon, keywords, content_box = None, size = None, is_c_mod = False, is_standalone = False, exec_name = None, module=None):
186        self.name = name
187        self.icon = icon
188        self.content_box = content_box
189        self.widgets = []
190        self.is_c_mod = is_c_mod
191        self.is_standalone = is_standalone
192        self.exec_name = exec_name
193        self.module = module # Optionally set by the module so we can call on_module_selected() on it when we show it.
194        self.keywords = keywords
195        self.size = size
196        self.topWindow = None
197        self.builder = None
198        self.stack = None
199        if self.module != None:
200            self.module.loaded = False
201
202    def add_widget(self, widget):
203        self.widgets.append(widget)
204
205    def build(self):
206        # Clear all the widgets from the content box
207        widgets = self.content_box.get_children()
208        for widget in widgets:
209            self.content_box.remove(widget)
210
211        if (self.module is not None):
212            self.module.on_module_selected()
213            self.module.loaded = True
214
215        if self.is_standalone:
216            subprocess.Popen(self.exec_name.split())
217            return
218
219        # Add our own widgets
220        for widget in self.widgets:
221            if hasattr(widget, 'expand'):
222                self.content_box.pack_start(widget, True, True, 2)
223            else:
224                self.content_box.pack_start(widget, False, False, 2)
225
226        # C modules are sort of messy - they check the desktop type
227        # (for Unity or GNOME) and show/hide UI items depending on
228        # the result - so we cannot just show_all on the widget, it will
229        # mess up these modifications - so for these, we just show the
230        # top-level widget
231        if not self.is_c_mod:
232            self.content_box.show_all()
233            try:
234                self.check_third_arg()
235            except:
236                pass
237            return
238
239        self.content_box.show()
240        for child in self.content_box:
241            child.show()
242
243            # C modules can have non-C parts. C parts are all named c_box
244            if child.get_name() != "c_box":
245                pass
246
247            c_widgets = child.get_children()
248            if not c_widgets:
249                c_widget = self.content_box.c_manager.get_c_widget(self.exec_name)
250                if c_widget is not None:
251                    child.pack_start(c_widget, False, False, 2)
252                    c_widget.show()
253            else:
254                for c_widget in c_widgets:
255                    c_widget.show()
256
257            def recursively_iterate(parent):
258                if self.stack:
259                    return
260                for child in parent:
261                    if isinstance(child, Gtk.Stack):
262                        self.stack = child
263                        break
264                    elif isinstance(child, Gtk.Container):
265                        recursively_iterate(child)
266
267            # Look for a stack recursively
268            recursively_iterate(child)
269
270class CCModule:
271    def __init__(self, label, mod_id, icon, category, keywords, content_box):
272        sidePage = SidePage(label, icon, keywords, content_box, size=-1, is_c_mod=True, is_standalone=False, exec_name=mod_id, module=None)
273        self.sidePage = sidePage
274        self.name = mod_id
275        self.category = category
276
277    def process (self, c_manager):
278        if c_manager.lookup_c_module(self.name):
279            c_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 2)
280            c_box.set_vexpand(False)
281            c_box.set_name("c_box")
282            self.sidePage.add_widget(c_box)
283            return True
284        else:
285            return False
286
287class SAModule:
288    def __init__(self, label, mod_id, icon, category, keywords, content_box):
289        sidePage = SidePage(label, icon, keywords, content_box, False, False, True, mod_id)
290        self.sidePage = sidePage
291        self.name = mod_id
292        self.category = category
293
294    def process (self):
295        name = self.name.replace("pkexec ", "")
296        name = name.split()[0]
297
298        return GLib.find_program_in_path(name) is not None
299
300def walk_directories(dirs, filter_func, return_directories=False):
301    # If return_directories is False: returns a list of valid subdir names
302    # Else: returns a list of valid tuples (subdir-names, parent-directory)
303    valid = []
304    try:
305        for thdir in dirs:
306            if os.path.isdir(thdir):
307                for t in os.listdir(thdir):
308                    if filter_func(os.path.join(thdir, t)):
309                        if return_directories:
310                            valid.append([t, thdir])
311                        else:
312                            valid.append(t)
313    except:
314        pass
315        #logging.critical("Error parsing directories", exc_info=True)
316    return valid
317
318class LabelRow(SettingsWidget):
319    def __init__(self, text=None, tooltip=None):
320        super(LabelRow, self).__init__()
321
322        self.label = SettingsLabel()
323        self.label.set_hexpand(True)
324        self.pack_start(self.label, False, False, 0)
325        self.label.set_markup(text)
326        self.set_tooltip_text(tooltip)
327
328class SoundFileChooser(SettingsWidget):
329    bind_dir = None
330
331    def __init__(self, label, event_sounds=True, size_group=None, dep_key=None, tooltip=""):
332        super(SoundFileChooser, self).__init__(dep_key=dep_key)
333
334        self.event_sounds = event_sounds
335
336        self.label = SettingsLabel(label)
337        self.content_widget = Gtk.Box()
338
339        c = self.content_widget.get_style_context()
340        c.add_class(Gtk.STYLE_CLASS_LINKED)
341
342        self.file_picker_button = Gtk.Button()
343        self.file_picker_button.connect("clicked", self.on_picker_clicked)
344
345        button_content = Gtk.Box(spacing=5)
346        self.file_picker_button.add(button_content)
347
348        self.button_label = Gtk.Label()
349        button_content.pack_start(Gtk.Image(icon_name="sound"), False, False, 0)
350        button_content.pack_start(self.button_label, False, False, 0)
351
352        self.content_widget.pack_start(self.file_picker_button, True, True, 0)
353
354        self.pack_start(self.label, False, False, 0)
355        self.pack_end(self.content_widget, False, False, 0)
356
357        self.play_button = Gtk.Button()
358        self.play_button.set_image(Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON))
359        self.play_button.connect("clicked", self.on_play_clicked)
360        self.content_widget.pack_start(self.play_button, False, False, 0)
361
362        self._proxy = None
363
364        try:
365            Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None,
366                                      'org.cinnamon.SettingsDaemon.Sound',
367                                      '/org/cinnamon/SettingsDaemon/Sound',
368                                      'org.cinnamon.SettingsDaemon.Sound',
369                                      None, self._on_proxy_ready, None)
370        except dbus.exceptions.DBusException as e:
371            print(e)
372            self._proxy = None
373            self.play_button.set_sensitive(False)
374
375        self.set_tooltip_text(tooltip)
376
377        if size_group:
378            self.add_to_size_group(size_group)
379
380    def _on_proxy_ready (self, object, result, data=None):
381        self._proxy = Gio.DBusProxy.new_for_bus_finish(result)
382
383    def on_play_clicked(self, widget):
384        self._proxy.PlaySoundFile("(us)", 0, self.get_value())
385
386    def on_picker_clicked(self, widget):
387        dialog = Gtk.FileChooserDialog(title=self.label.get_text(),
388                                       action=Gtk.FileChooserAction.OPEN,
389                                       transient_for=self.get_toplevel(),
390                                       buttons=(_("_Cancel"), Gtk.ResponseType.CANCEL,
391                                                _("_Open"), Gtk.ResponseType.ACCEPT))
392
393        if os.path.exists(self.get_value()):
394            dialog.set_filename(self.get_value())
395        else:
396            dialog.set_current_folder('/usr/local/share/sounds')
397
398        sound_filter = Gtk.FileFilter()
399        if self.event_sounds:
400            sound_filter.add_mime_type("audio/x-wav")
401            sound_filter.add_mime_type("audio/x-vorbis+ogg")
402        else:
403            sound_filter.add_mime_type("audio/*")
404        sound_filter.set_name(_("Sound files"))
405        dialog.add_filter(sound_filter)
406
407        if (dialog.run() == Gtk.ResponseType.ACCEPT):
408            name = dialog.get_filename()
409            self.set_value(name)
410            self.update_button_label(name)
411
412        dialog.destroy()
413
414    def update_button_label(self, absolute_path):
415        if absolute_path != "":
416            f = Gio.File.new_for_path(absolute_path)
417            self.button_label.set_label(f.get_basename())
418
419    def on_setting_changed(self, *args):
420        self.update_button_label(self.get_value())
421
422    def connect_widget_handlers(self, *args):
423        pass
424
425class TweenChooser(SettingsWidget):
426    bind_prop = "tween"
427    bind_dir = Gio.SettingsBindFlags.DEFAULT
428
429    def __init__(self, label, size_group=None, dep_key=None, tooltip=""):
430        super(TweenChooser, self).__init__(dep_key=dep_key)
431
432        self.label = SettingsLabel(label)
433
434        self.content_widget = TweenChooserButton()
435
436        self.pack_start(self.label, False, False, 0)
437        self.pack_end(self.content_widget, False, False, 0)
438
439        self.set_tooltip_text(tooltip)
440
441        if size_group:
442            self.add_to_size_group(size_group)
443
444class EffectChooser(SettingsWidget):
445    bind_prop = "effect"
446    bind_dir = Gio.SettingsBindFlags.DEFAULT
447
448    def __init__(self, label, possible=None, size_group=None, dep_key=None, tooltip=""):
449        super(EffectChooser, self).__init__(dep_key=dep_key)
450
451        self.label = SettingsLabel(label)
452
453        self.content_widget = EffectChooserButton(possible)
454
455        self.pack_start(self.label, False, False, 0)
456        self.pack_end(self.content_widget, False, False, 0)
457
458        self.set_tooltip_text(tooltip)
459
460        if size_group:
461            self.add_to_size_group(size_group)
462
463class DateChooser(SettingsWidget):
464    bind_dir = None
465
466    def __init__(self, label, size_group=None, dep_key=None, tooltip=""):
467        super(DateChooser, self).__init__(dep_key=dep_key)
468
469        self.label = SettingsLabel(label)
470
471        self.content_widget = DateChooserButton()
472
473        self.pack_start(self.label, False, False, 0)
474        self.pack_end(self.content_widget, False, False, 0)
475
476        self.set_tooltip_text(tooltip)
477
478        if size_group:
479            self.add_to_size_group(size_group)
480
481    def on_date_changed(self, *args):
482        date = self.content_widget.get_date()
483        self.set_value({"y": date.year, "m": date.month, "d": date.day})
484
485    def on_setting_changed(self, *args):
486        date = self.get_value()
487        self.content_widget.set_date((date["y"], date["m"], date["d"]))
488
489    def connect_widget_handlers(self, *args):
490        self.content_widget.connect("date-changed", self.on_date_changed)
491
492class TimeChooser(SettingsWidget):
493    bind_dir = None
494
495    def __init__(self, label, size_group=None, dep_key=None, tooltip=""):
496        super(TimeChooser, self).__init__(dep_key=dep_key)
497
498        self.label = SettingsLabel(label)
499
500        self.content_widget = TimeChooserButton()
501
502        self.pack_start(self.label, False, False, 0)
503        self.pack_end(self.content_widget, False, False, 0)
504
505        self.set_tooltip_text(tooltip)
506
507        if size_group:
508            self.add_to_size_group(size_group)
509
510    def on_time_changed(self, *args):
511        time = self.content_widget.get_time()
512        self.set_value({"h": time.hour, "m": time.minute, "s": time.second})
513
514    def on_setting_changed(self, *args):
515        time = self.get_value()
516        self.content_widget.set_time((time["h"], time["m"], time["s"]))
517
518    def connect_widget_handlers(self, *args):
519        self.content_widget.connect("time-changed", self.on_time_changed)
520
521class Keybinding(SettingsWidget):
522    bind_dir = None
523
524    def __init__(self, label, num_bind=2, size_group=None, dep_key=None, tooltip=""):
525        super(Keybinding, self).__init__(dep_key=dep_key)
526
527        self.num_bind = num_bind
528
529        self.label = SettingsLabel(label)
530
531        self.buttons = []
532        self.teach_button = None
533
534        self.content_widget = Gtk.Frame(shadow_type=Gtk.ShadowType.IN)
535        self.content_widget.set_valign(Gtk.Align.CENTER)
536        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
537        self.content_widget.add(box)
538
539        self.pack_start(self.label, False, False, 0)
540        self.pack_end(self.content_widget, False, False, 0)
541
542        for x in range(self.num_bind):
543            if x != 0:
544                box.add(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL))
545            kb = ButtonKeybinding()
546            kb.set_size_request(150, -1)
547            kb.connect("accel-edited", self.on_kb_changed)
548            kb.connect("accel-cleared", self.on_kb_changed)
549            box.pack_start(kb, False, False, 0)
550            self.buttons.append(kb)
551
552        self.event_id = None
553        self.teaching = False
554
555        self.set_tooltip_text(tooltip)
556
557        if size_group:
558            self.add_to_size_group(size_group)
559
560    def on_kb_changed(self, *args):
561        bindings = []
562
563        for x in range(self.num_bind):
564            string = self.buttons[x].get_accel_string()
565            bindings.append(string)
566
567        self.set_value("::".join(bindings))
568
569    def on_setting_changed(self, *args):
570        value = self.get_value()
571        bindings = value.split("::")
572
573        for x in range(min(len(bindings), self.num_bind)):
574            self.buttons[x].set_accel_string(bindings[x])
575
576    def connect_widget_handlers(self, *args):
577        pass
578
579def g_settings_factory(subclass):
580    class NewClass(globals()[subclass], PXGSettingsBackend):
581        def __init__(self, label, schema, key, *args, **kwargs):
582            self.key = key
583            if schema not in settings_objects:
584                settings_objects[schema] = Gio.Settings.new(schema)
585            self.settings = settings_objects[schema]
586
587            if "map_get" in kwargs:
588                self.map_get = kwargs["map_get"]
589                del kwargs["map_get"]
590            if "map_set" in kwargs:
591                self.map_set = kwargs["map_set"]
592                del kwargs["map_set"]
593
594            super(NewClass, self).__init__(label, *args, **kwargs)
595            self.bind_settings()
596    return NewClass
597
598for widget in CAN_BACKEND:
599    globals()["GSettings"+widget] = g_settings_factory(widget)
600