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