From 8f641749f0c6fc0bd0e97a5929b773c32a47a5a6 Mon Sep 17 00:00:00 2001 From: Sander Sweers Date: Sun, 3 Sep 2023 13:45:01 +0200 Subject: [PATCH] PluginDialog: Replace custom list with Gtk.ListBox --- blueman/gui/applet/PluginDialog.py | 222 +++++++++++++++++------------ data/ui/applet-plugins-widget.ui | 11 +- stubs/gi/repository/GObject.pyi | 2 + 3 files changed, 140 insertions(+), 95 deletions(-) diff --git a/blueman/gui/applet/PluginDialog.py b/blueman/gui/applet/PluginDialog.py index 7d339cea5..cdb8566af 100644 --- a/blueman/gui/applet/PluginDialog.py +++ b/blueman/gui/applet/PluginDialog.py @@ -1,8 +1,7 @@ from gettext import gettext as _ import logging -from typing import TYPE_CHECKING, List, Type, Dict +from typing import TYPE_CHECKING, Type, Dict, cast, Optional -from blueman.gui.GenericList import GenericList, ListDataDict from blueman.main.Builder import Builder from blueman.main.PluginManager import PluginManager from blueman.plugins.AppletPlugin import AppletPlugin @@ -11,12 +10,39 @@ import gi gi.require_version("Gtk", "3.0") gi.require_version("Gdk", "3.0") -from gi.repository import Gtk, Gdk, Gio +from gi.repository import Gtk, Gdk, Gio, GLib, GObject if TYPE_CHECKING: from blueman.main.Applet import BluemanApplet +class PluginItem(GObject.Object): + __gtype_name__ = "PluginItem" + + class _Props: + icon_name: str + plugin_name: str + description: str + enabled: bool + activatable: bool + + props: _Props + + icon_name = GObject.Property(type=str) + plugin_name = GObject.Property(type=str) + description = GObject.Property(type=str) + enabled = GObject.Property(type=bool, default=False) + activatable = GObject.Property(type=bool, default=False) + + def __init__(self, icon_name: str, plugin_name: str, description: str, enabled: bool, activatable: bool): + super().__init__() + self.props.icon_name = icon_name + self.props.plugin_name = plugin_name + self.props.description = description + self.props.enabled = enabled + self.props.activatable = activatable + + class SettingsWidget(Gtk.Box): def __init__(self, inst: AppletPlugin, orientation: Gtk.Orientation = Gtk.Orientation.VERTICAL) -> None: super().__init__( @@ -124,29 +150,13 @@ def __init__(self, applet: "BluemanApplet") -> None: self.add(builder.get_widget("all", Gtk.Container)) - cr = Gtk.CellRendererToggle() - cr.connect("toggled", self.on_toggled) - - data: List[ListDataDict] = [ - {"id": "active", "type": bool, "renderer": cr, "render_attrs": {"active": 0, "activatable": 1, - "visible": 1}}, - {"id": "activatable", "type": bool}, - {"id": "icon", "type": str, "renderer": Gtk.CellRendererPixbuf(), "render_attrs": {"icon-name": 2}}, - # device caption - {"id": "desc", "type": str, "renderer": Gtk.CellRendererText(), "render_attrs": {"markup": 3}, - "view_props": {"expand": True}}, - {"id": "name", "type": str}, - ] - - self.list = GenericList(data, headers_visible=False, visible=True) - self.list.liststore.set_sort_column_id(3, Gtk.SortType.ASCENDING) - self.list.liststore.set_sort_func(3, self.list_compare_func) - - self.list.selection.connect("changed", self.on_selection_changed) + self.model = Gio.ListStore.new(PluginItem.__gtype__) + self.listbox = builder.get_widget("plugin_listbox", Gtk.ListBox) + self.listbox.bind_model(self.model, self._widget_factory) + self.listbox.connect("row-selected", self._on_row_selected) plugin_list = builder.get_widget("plugin_list", Gtk.ScrolledWindow) plugin_info = builder.get_widget("main_scrolled_window", Gtk.ScrolledWindow) - plugin_list.add(self.list) # Disable overlay scrolling if Gtk.get_minor_version() >= 16: @@ -159,45 +169,94 @@ def __init__(self, applet: "BluemanApplet") -> None: self.sig_b: int = self.applet.Plugins.connect("plugin-unloaded", self.plugin_state_changed, False) self.connect("delete-event", self._on_close) - self.list.set_cursor(Gtk.TreePath.new_first()) - close_action = Gio.SimpleAction.new("close", None) close_action.connect("activate", lambda x, y: self.close()) + self.add_action(close_action) - def list_compare_func(self, _treemodel: Gtk.TreeModel, iter1: Gtk.TreeIter, iter2: Gtk.TreeIter, _user_data: object - ) -> int: - a = self.list.get(iter1, "activatable", "name") - b = self.list.get(iter2, "activatable", "name") + def _add_plugin_action(self, name: str, state: bool, activatable: bool) -> None: + logging.debug(f"adding action: {name}") + action = Gio.SimpleAction.new_stateful( + name, None, GLib.Variant.new_boolean(state) + ) + action.set_property("enabled", activatable) + self.add_action(action) + action.connect("change-state", self._on_plugin_toggle) - if (a["activatable"] and b["activatable"]) or (not a["activatable"] and not b["activatable"]): - if a["name"] == b["name"]: - return 0 - if a["name"] < b["name"]: - return -1 - else: - return 1 + def _widget_factory(self, item: GObject.Object, _data: Optional[object] = None) -> Gtk.Widget: + assert isinstance(item, PluginItem) + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5, visible=True) + + checkbutton = Gtk.CheckButton(visible=True, sensitive=item.props.activatable) + box.add(checkbutton) + checkbutton.set_action_name(f"win.{item.props.plugin_name}") + + self._add_plugin_action(item.props.plugin_name, item.props.enabled, item.props.activatable) + # Set active after adding action + checkbutton.set_active(item.props.enabled) + + plugin_im = Gtk.Image(icon_name=item.props.icon_name, visible=True) + box.add(plugin_im) + + label = Gtk.Label(label=item.props.description, use_markup=True, visible=True) + box.add(label) + return box + + def _model_sort_func(self, item1: Optional[object], item2: Optional[object], _data: Optional[object] = None) -> int: + assert isinstance(item1, PluginItem) + assert isinstance(item2, PluginItem) + if item1.props.activatable and not item2.props.activatable: + return -1 + elif not item1.props.activatable and item2.props.activatable: + return 1 + + if item1.props.plugin_name < item2.props.plugin_name: + return -1 + elif item1.props.plugin_name > item2.props.plugin_name: + return 1 + + return 0 + + def _on_plugin_toggle(self, action: Gio.SimpleAction, state: GLib.Variant) -> None: + action.set_state(state) + plugin_name = action.get_name() + + deps = self.applet.Plugins.get_dependencies()[plugin_name] + loaded = self.applet.Plugins.get_loaded() + to_unload = [dep for dep in deps if dep in loaded] + + if to_unload: + if not self._ask_unload( + _("Plugin \"%(0)s\" depends on %(1)s. Unloading %(1)s will also unload " + "\"%(0)s\".\nProceed?") % {"0": ", ".join(to_unload), "1": plugin_name} + ): + action.set_state(GLib.Variant.new_boolean(not state)) + return else: - if a["activatable"] and not b["activatable"]: - return -1 - elif not a["activatable"] and b["activatable"]: - return 1 - else: - return 0 + conflicts = self.applet.Plugins.get_conflicts()[plugin_name] + to_unload = [conf for conf in conflicts if conf in loaded] - def _on_close(self, _widget: Gtk.Widget, _event: Gdk.Event) -> bool: - self.applet.Plugins.disconnect(self.sig_a) - self.applet.Plugins.disconnect(self.sig_b) - return False + if to_unload and not self._ask_unload( + _("Plugin %(0)s conflicts with %(1)s. Loading %(1)s will unload %(0)s." + "\nProceed?") % {"0": ", ".join(to_unload), "1": plugin_name} + ): + action.set_state(GLib.Variant.new_boolean(not state)) + return - def on_selection_changed(self, _selection: Gtk.TreeSelection) -> None: - tree_iter = self.list.selected() - assert tree_iter is not None + for p in to_unload: + logging.debug(f"unloading {p}") + self.applet.Plugins.set_config(p, False) - name = self.list.get(tree_iter, "name")["name"] - cls: Type[AppletPlugin] = self.applet.Plugins.get_classes()[name] - self.plugin_name.props.label = "" + name + "" + self.applet.Plugins.set_config(plugin_name, plugin_name not in self.applet.Plugins.get_loaded()) + + def _on_row_selected(self, _lb: Gtk.ListBox, lbrow: Gtk.ListBoxRow) -> None: + pos = lbrow.get_index() + item = self.model.get_item(pos) + assert isinstance(item, PluginItem) + + cls: Type[AppletPlugin] = self.applet.Plugins.get_classes()[item.props.plugin_name] + self.plugin_name.props.label = "" + item.props.plugin_name + "" self.icon.props.icon_name = cls.__icon__ self.author_txt.props.label = cls.__author__ self.description.props.label = cls.__description__ @@ -212,18 +271,23 @@ def on_selection_changed(self, _selection: Gtk.TreeSelection) -> None: else: self.conflicts_txt.props.label = _("No conflicts") - if cls.is_configurable() and name in self.applet.Plugins.get_loaded(): + if cls.is_configurable() and item.props.plugin_name in self.applet.Plugins.get_loaded(): self.b_prefs.props.sensitive = True else: self.b_prefs.props.sensitive = False self.update_config_widget(cls) + def _on_close(self, _widget: Gtk.Widget, _event: Gdk.Event) -> bool: + self.applet.Plugins.disconnect(self.sig_a) + self.applet.Plugins.disconnect(self.sig_b) + return False + def on_prefs_toggled(self, _button: Gtk.ToggleButton) -> None: - tree_iter = self.list.selected() - assert tree_iter is not None - name = self.list.get(tree_iter, "name")["name"] - cls: Type[AppletPlugin] = self.applet.Plugins.get_classes()[name] + row = self.listbox.get_selected_row() + pos = row.get_index() + item = cast(PluginItem, self.model.get_item(pos)) + cls: Type[AppletPlugin] = self.applet.Plugins.get_classes()[item.props.plugin_name] self.update_config_widget(cls) @@ -261,14 +325,15 @@ def populate(self) -> None: desc = f"{name}" else: desc = name - self.list.append(active=(name in loaded), icon=cls.__icon__, activatable=cls.__unloadable__, name=name, - desc=desc) + plugin_item = PluginItem(cls.__icon__, name, desc, name in loaded, activatable=cls.__unloadable__) + self.model.insert_sorted(plugin_item, self._model_sort_func) + self.listbox.select_row(self.listbox.get_row_at_index(0)) def plugin_state_changed(self, _plugins: PluginManager, name: str, loaded: bool) -> None: - for row in self.list.liststore: - if self.list.get(row.iter, "name")["name"] == name: - self.list.set(row.iter, active=loaded) - break + logging.debug(f"{name} {loaded}") + action = self.lookup_action(name) + assert isinstance(action, Gio.SimpleAction) + action.set_state(GLib.Variant.new_boolean(loaded)) cls: Type[AppletPlugin] = self.applet.Plugins.get_classes()[name] if not loaded: @@ -277,37 +342,6 @@ def plugin_state_changed(self, _plugins: PluginManager, name: str, loaded: bool) elif cls.is_configurable(): self.b_prefs.props.sensitive = True - def on_toggled(self, _toggle: Gtk.CellRendererToggle, path: str) -> None: - tree_path = Gtk.TreePath.new_from_string(path) - tree_iter = self.list.get_iter(tree_path) - assert tree_iter - name = self.list.get(tree_iter, "name")["name"] - - deps = self.applet.Plugins.get_dependencies()[name] - loaded = self.applet.Plugins.get_loaded() - to_unload = [dep for dep in deps if dep in loaded] - - if to_unload: - if not self._ask_unload( - _("Plugin \"%(0)s\" depends on %(1)s. Unloading %(1)s will also unload " - "\"%(0)s\".\nProceed?") % {"0": ", ".join(to_unload), "1": name} - ): - return - else: - conflicts = self.applet.Plugins.get_conflicts()[name] - to_unload = [conf for conf in conflicts if conf in loaded] - - if to_unload and not self._ask_unload( - _("Plugin %(0)s conflicts with %(1)s. Loading %(1)s will unload %(0)s." - "\nProceed?") % {"0": ", ".join(to_unload), "1": name} - ): - return - - for p in to_unload: - self.applet.Plugins.set_config(p, False) - - self.applet.Plugins.set_config(name, name not in self.applet.Plugins.get_loaded()) - def _ask_unload(self, text: str) -> bool: dialog = Gtk.MessageDialog(parent=self, type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.YES_NO) dialog.props.secondary_use_markup = True diff --git a/data/ui/applet-plugins-widget.ui b/data/ui/applet-plugins-widget.ui index c92715542..27b8581e7 100644 --- a/data/ui/applet-plugins-widget.ui +++ b/data/ui/applet-plugins-widget.ui @@ -19,7 +19,16 @@ never in - + + True + False + + + True + False + + + diff --git a/stubs/gi/repository/GObject.pyi b/stubs/gi/repository/GObject.pyi index 67e0f597c..a7e9676e0 100644 --- a/stubs/gi/repository/GObject.pyi +++ b/stubs/gi/repository/GObject.pyi @@ -70,6 +70,8 @@ class Object(): qdata: GLib.Data ref_count: builtins.int + __gtype__: GType + def bind_property(self, source_property: builtins.str, target: Object, target_property: builtins.str, flags: BindingFlags) -> Binding: ... def bind_property_full(self, source_property: builtins.str, target: Object, target_property: builtins.str, flags: BindingFlags, transform_to: Closure, transform_from: Closure) -> Binding: ...