Skip to content

Commit

Permalink
PluginDialog: Replace custom list with Gtk.ListBox
Browse files Browse the repository at this point in the history
  • Loading branch information
infirit committed Oct 20, 2023
1 parent 099a543 commit 154c0c3
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 88 deletions.
202 changes: 115 additions & 87 deletions blueman/gui/applet/PluginDialog.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,12 +10,29 @@
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"
icon_name = cast(str, GObject.Property(type=str))
plugin_name = cast(str, GObject.Property(type=str))
description = cast(str, GObject.Property(type=str))
enabled = cast(bool, GObject.Property(type=bool, default=False))
activatable = cast(bool, GObject.Property(type=bool, default=False))

def __init__(self, icon_name: str, plugin_name: str, description: str, enabled: bool, activatable: bool):
super().__init__()
self.icon_name = icon_name
self.plugin_name = plugin_name
self.description = description
self.enabled = enabled
self.activatable = activatable


class SettingsWidget(Gtk.Box):
def __init__(self, inst: AppletPlugin, orientation: Gtk.Orientation = Gtk.Orientation.VERTICAL) -> None:
super().__init__(
Expand Down Expand Up @@ -124,29 +140,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:
Expand All @@ -159,45 +159,98 @@ 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(False)
)
action.set_property("enabled", activatable)
action.set_state(GLib.Variant.new_boolean(state))
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"]:
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.activatable)
box.add(checkbutton)
checkbutton.set_action_name(f"win.{item.plugin_name}")

self._add_plugin_action(item.plugin_name, item.enabled, item.activatable)
# Set active after adding action
checkbutton.set_active(item.enabled)

plugin_im = Gtk.Image(icon_name=item.icon_name, visible=True)
box.add(plugin_im)

label = Gtk.Label(label=item.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.activatable and item2.activatable) or (not item1.activatable and not item2.activatable):
if item1.plugin_name == item2.plugin_name:
return 0
if a["name"] < b["name"]:
if item1.plugin_name < item2.plugin_name:
return -1
else:
return 1

else:
if a["activatable"] and not b["activatable"]:
if item1.activatable and not item2.activatable:
return -1
elif not a["activatable"] and b["activatable"]:
elif not item1.activatable and item2.activatable:
return 1
else:
return 0

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_plugin_toggle(self, action: Gio.SimpleAction, state: GLib.Variant) -> None:
action.set_state(state)
plugin_name = action.get_name()

def on_selection_changed(self, _selection: Gtk.TreeSelection) -> None:
tree_iter = self.list.selected()
assert tree_iter is not None
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]

name = self.list.get(tree_iter, "name")["name"]
cls: Type[AppletPlugin] = self.applet.Plugins.get_classes()[name]
self.plugin_name.props.label = "<b>" + name + "</b>"
if to_unload:
if not self._ask_unload(
_("Plugin <b>\"%(0)s\"</b> depends on <b>%(1)s</b>. Unloading <b>%(1)s</b> will also unload <b>"
"\"%(0)s\"</b>.\nProceed?") % {"0": ", ".join(to_unload), "1": plugin_name}
):
action.set_state(GLib.Variant.new_boolean(not state))
return
else:
conflicts = self.applet.Plugins.get_conflicts()[plugin_name]
to_unload = [conf for conf in conflicts if conf in loaded]

if to_unload and not self._ask_unload(
_("Plugin <b>%(0)s</b> conflicts with <b>%(1)s</b>. Loading <b>%(1)s</b> will unload <b>%(0)s</b>."
"\nProceed?") % {"0": ", ".join(to_unload), "1": plugin_name}
):
action.set_state(GLib.Variant.new_boolean(not state))
return

for p in to_unload:
logging.debug(f"unloading {p}")
self.applet.Plugins.set_config(p, False)

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.plugin_name]
self.plugin_name.props.label = "<b>" + item.plugin_name + "</b>"
self.icon.props.icon_name = cls.__icon__
self.author_txt.props.label = cls.__author__
self.description.props.label = cls.__description__
Expand All @@ -212,18 +265,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.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.plugin_name]

self.update_config_widget(cls)

Expand Down Expand Up @@ -261,14 +319,15 @@ def populate(self) -> None:
desc = f"<span weight=\"bold\">{name}</span>"
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:
Expand All @@ -277,37 +336,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 <b>\"%(0)s\"</b> depends on <b>%(1)s</b>. Unloading <b>%(1)s</b> will also unload <b>"
"\"%(0)s\"</b>.\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 <b>%(0)s</b> conflicts with <b>%(1)s</b>. Loading <b>%(1)s</b> will unload <b>%(0)s</b>."
"\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
Expand Down
11 changes: 10 additions & 1 deletion data/ui/applet-plugins-widget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@
<property name="hscrollbar-policy">never</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkListBox" id="plugin_listbox">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
</child>
</object>
</child>
</object>
<packing>
Expand Down
2 changes: 2 additions & 0 deletions stubs/gi/repository/GObject.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down

0 comments on commit 154c0c3

Please sign in to comment.