Gtk.Actionable - NotImplementedError

Gtk: 4.0
Python: 3.12

I have been playing around with custom widgets, specifically one that reacts when a user selects it and issues a signal. I know that Gtk.Actionable interface is required but my question is: has PyGObject actually implemented it. When I try and use it the following error is reported:

actionable: Gtk.Actionable = Gtk.Actionable() # FIXME: raises NotImplementedError
NotImplementedError: Actionable can not be constructed

The some what odd thing is that Gtk.ActionableInterface (GTK3) does not raise an error either. Not even a warning.

You’re trying to instantiate the Gtk.Actionable interface, which is not possible to do—regardless of the version of GTK in use.

Interfaces, in GObject, are not instantiatable. You need to inherit from it.

Thank you - I follow.
In that case is there an example / reference implementation or how to I could look at?

Thank you Emmanuele I have figured out what is required. However I cannot figure out how implementing ‘Actionable’ or any of the other interfaces actually results in a custom actionable widget being rendered as selected. A pointer would be appreciated.

Thanks again Emmanuale for your help but I could still do with some help please.

I sub-classed Gtk.Actionable and tried to debug it. But what ever I did the following error crops up:

/home/greg/PycharmProjects/ListOrganiser/.venv/lib64/python3.12/site-packages/gi/types.py:217: Warning: cannot derive 'Actionable' from non-fundamental parent type 'GtkActionable'
  _gi.type_register(cls, namespace.get('__gtype_name__'))
Traceback (most recent call last):
  File "/home/greg/PycharmProjects/ListOrganiser/sc/main.py", line 7, in <module>
    from organiser import ListPriority
  File "/home/greg/PycharmProjects/ListOrganiser/sc/organiser.py", line 122, in <module>
    class Actionable(Gtk.Actionable):
  File "/home/greg/PycharmProjects/ListOrganiser/.venv/lib64/python3.12/site-packages/gi/types.py", line 226, in __init__
    super(GObjectMeta, cls).__init__(name, bases, dict_)
  File "/home/greg/PycharmProjects/ListOrganiser/.venv/lib64/python3.12/site-packages/gi/types.py", line 205, in __init__
    cls._type_register(cls.__dict__)
  File "/home/greg/PycharmProjects/ListOrganiser/.venv/lib64/python3.12/site-packages/gi/types.py", line 217, in _type_register
    _gi.type_register(cls, namespace.get('__gtype_name__'))
RuntimeError: could not create new GType: Actionable (subclass of GtkActionable)

I get the error just with a simple class definition:

class Actionable(Gtk.Actionable):
      __gtype_name__ = "Actionable"
      def __init__(self):
          ic()
          return

Is this occurring because Gtk.Actionable is a final class?

Hi,

class Actionable(Gtk.Actionable):

This is not correct, an interface shall be used in conjunction with a GObject (or derived, like any gtk Widgets)

There is an example in the official documentation:

class PersonsModel(GObject.GObject, Gio.ListModel):

here the PersonsModel is created by extending a GObject.GObject with the Gio.ListModel interface.

In your case, to implement an actionable widget, you will have to do something like this:

class MyActionableWidget(Gtk.Widget, Gtk.Actionable):
    __gtype_name__ = __qualname__

    action_name = GObject.Property(type=str, default='window.toggle-maximized')
    action_target = GObject.Property(type=GLib.Variant.__gtype__, default=None)

    def __init__(self):
        super().__init__()
        ...

here is inherits from a base Gtk.Widget, but you can use for example Gtk.Image is you want an actionable image.

Thank you gwillems, Your points were most helpful - so much so I rewrote my implementation. However I still have questions even after re-reading the docs. Incidently your last comment was the most insightful of any I have read to date.

  1. In the code you posted __gtype__ is written. I checked the documentation for GLib.Variant and could not find any reference to it. Could it be a typo?

  2. It is not clear to me, even after reading various Docs and interweb searches just how a custom widget (a Gtk.Label in my case) can be made to respond to a user click. How it gets rendered when clicked.

I’m expecting to have to add a controller. How to do that is clear enough. I’m also expecting to have to use css to render the label click. I think I can even achieve that . But am I on the right track? Is that what is intended? That is what is unclear to me at this point.

Many thanks in anticipation.

If you’re using GTK3, GtkLabel cannot respond to pointer events. You need to put a GtkLabel into a GtkEventBox, and then use the event box to handle pointer events.

Sorry I should have said I’m using Gtk4 and Python 3.12

  1. It’s kind of dark magic, I’m not even sure myself how it works :slight_smile: If I understood well, GObject.Property() needs a reference to a C type, while GLib.Variant is a Python type generated from the introspection data. The __gtype__ attribute is a reference to the original C type from which the Python one is derived, so GObject.Property will accept it. But. __gtype__ is specific to Python’s bindings, you won’t find it in the official C docs.

  2. What do you mean by “respond to a user click” / “gets rendered when clicked”? Can you describe what you’re trying to achieve? If you for example just want to color a label pink when you click on it, then yes you can just use a GtkGestureClick controller and a CSS style. No need of an actionable widget for that.

Oh, you did: I just got confused for a second.

The __gtype__ field is a pygobject binding specific field used to report the GType of a class. It has nothing specific to do with GLib.Variant, and it’s more of a reflection of the underlying GObject type system.

You add an event controller to your widget:

controller = Gtk.GestureClick()
controller.set_name("label click")
controller.connect('pressed', on_gesture_pressed)
controller.connect('released', on_gesture_released)
label = Gtk.Label()
label.add_controller(controller)

It mainly depends on what you want to achieve, here; but you can load CSS that defines new classes, and then add or remove CSS classes on your label, like:

def on_gesture_pressed(controller, n_press, x, y):
    label = controller.get_widget()
    label.add_css_class('label-pressed')

def on_gesture_released(controller, n_press, x, y):
    label = controllet.get_widget()
    label.remove_css_class('label-pressed')
1 Like

Hi gwillems, Thanks,

  1. I query __gtype__ because my linter complines. But the background info was interesting thought. Could not find it PyGObject API either.

  2. I’m happily to provide my full widget spec if necessary but I’ll try a simpler description first if I may.

Think of the Gtk.ListBox widget but orientated horizontally. Each row element would be a tuple of enum and label. The label is presented by the HorizontalListBox widget. I then need to be able to select an row and have that selection result in a callback. As with Gtk.ListBox the selected row gets highlighted.

Incidentally I know I could use a FlowBox but its a sledge hammer for a nut solution.

Are the light appears out of the gloom. Thanks Emmanuale.

Our comments have crossed with my response to gwillems, so I’ll go away and ponder for a while.

Sounds like a perfect description of a horizontal ListView! They already manage the selection for you.

Here a small example:

#!/usr/bin/env python3

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import GObject, Gio, Gtk


class MyApp(Gtk.Application):
    __gtype_name__ = __qualname__

    def __init__(self):
        super().__init__()
        self.connect('activate', self.on_activate)

    def on_activate(self, app):
        self.add_window(MyAppWindow())


class MyAppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = __qualname__

    def __init__(self):
        super().__init__(default_width=600, default_height=400)
        rowlist = Gio.ListStore.new(MyRowElement)
        model = Gtk.SingleSelection.new(rowlist)
        model.connect('selection-changed', self.on_selection_changed)
        rowfactory = Gtk.SignalListItemFactory()
        rowfactory.connect('setup', self.on_factory_setup)
        rowfactory.connect('bind', self.on_factory_bind)
        rowview = Gtk.ListView.new(model, rowfactory)
        rowview.set_orientation(Gtk.Orientation.HORIZONTAL)
        rowview.set_halign(Gtk.Align.CENTER)
        rowview.set_valign(Gtk.Align.CENTER)
        ##rowview.add_css_class('navigation-sidebar')
        rowlist.append(MyRowElement("label1", "enum1"))
        rowlist.append(MyRowElement("label2", "enum2"))
        rowlist.append(MyRowElement("label3", "enum3"))
        self.set_child(rowview)
        self.present()

    def on_selection_changed(self, model, position, n_items):
        print(model.get_selected_item().enum)

    def on_factory_setup(self, factory, listitem):
        listitem.set_child(Gtk.Label(single_line_mode=True))

    def on_factory_bind(self, factory, listitem):
        label = listitem.get_child()
        item = listitem.get_item()
        label.set_text(item.title)


class MyRowElement(GObject.Object):
    __gtype_name__ = __qualname__

    def __init__(self, title, enum):
        super().__init__()
        self.title = title
        self.enum = enum


if __name__ == '__main__':
    sys.exit(MyApp().run(sys.argv))

You can of course add a CSS stylesheet to customize the appearance of the row items.

Hi

Thanks, I didn’t event consider ListView simply because I have only ever seen vertical examples examples. I will keep it in my back pocket so to speak but I would like to gain a deeper understanding of custom widget development. Currently I’m playing around with Emmanuele’s suggestions, with some success. I need to understand css better (I know, read the docs :wink:

Ok, I gave up. This side project was taking more time than I intended. I’m sure it could be achieved but may be another day. However I returned to an earlier draft an now I have something to share - a solution and I thought I would share it. I have also given some background as comments in the code. Please feel free to give helpful comments but the code works but is not complete and definitely needs a good tidy-up.

###
# A need was identified for an improved widget to control the selection of mutually exclusive calculation options.
# Previously, selection used DropDowns to select the preferred ordering but this was cumbersome and needed
# extensive validation of any selection.  The diagram below is a simplification of the situation.
#
#        /-> o--> Function A -->\             o--> Function A -->\         o--> Function A -->\
# >---->/    o--> Function B --->x->---->\    o--> Function B --->x->----> o--> Function B --->x->
#            o--> Function C -->/         \-> o--> Function C -->/         o--> Function C -->/
#
# The proposal was to present a list of the selection order and allow the user to move options to
# provide the required order.  See below:
#
#           <<< | Function A | Function B | Function C | >>>
# to:
#           <<< | Function B | Function C | Function A | >>>
#
# This widget takes a list of tuples where each tuple is an enum and str, a label. The label is used for a
# friendly option presentation of meaning or intention to an enum value.  The user can arrange the list
# to any desired order by selecting an option and using the '<<<' or '>>>' to move the selection. The
# resulting changed order of enums can then be used to control the routing of the calculation options.
#
# The main advantage of this approach is that every possibly option order is valid and this cutout
# the validation required.
#
# This is presented as is. No warranty or guarantee implied Bla, Bla, Bla.  Make use of it, or not, as you see fit.
###
import enum
from typing import Tuple, Any
from icecream import ic
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib, Gio, GObject


class ListElement(Gtk.ToggleButton):
    __gtype_name__ = "ListElement"

    index = GObject.Property(type=int, default=0)
    label = GObject.Property(type=str, default='label')
    op = GObject.Property(default=None)

    def __init__(self, *, index: int = 0, cell: Tuple[enum, str] = (None, '')):
        super().__init__()

        self.index = index
        self.op = cell[0]
        self.label = cell[1]

        self.set_action_name("demo.ordering")
        self.set_action_target_value(GLib.Variant.new_string(self.label))
        self.set_label(self.label)

        self.set_has_frame(False)
        return

    def set_index(self, index: int) -> None:
        self.index = index
        return

    def get_index(self) -> int:
        return self.index

    def set_op(self, op: enum) -> None:
        self.op = op
        return

    def get_op(self) -> enum:
        return self.op


class ListPriority(Gtk.Widget, Gio.ListModel):
    __gtype_name__ = "ListPriority"

    orientation = GObject.Property(type=Gtk.Orientation, default=Gtk.Orientation.HORIZONTAL)
    item_type = GObject.Property(type=ListElement, default=ListElement)
    n_items = GObject.Property(type=int, default=0)  # tracks the number of items in the list store
    selected_index = GObject.Property(type=int, default=0)   # tracks the index of currently ListElement

    list_store: list[ListElement] = []  #

    def __init__(self, *, orientation: Gtk.Orientation):
        super().__init__()

        if orientation:
            self.orientation = orientation

        # Used to control the active state of the ToggleButtons only
        self.action_group = Gio.SimpleActionGroup()
        self.insert_action_group('demo', self.action_group)

        ordering_action = Gio.SimpleAction(
            name="ordering",
            state=GLib.Variant.new_string("first"),
            parameter_type=GLib.VariantType("s"),
        )
        self.action_group.add_action(ordering_action)

        # The layout manager functions - just needs to be assigned to our class
        layout_manager: Gtk.BoxLayout = Gtk.BoxLayout(orientation=self.orientation)
        self.set_layout_manager(layout_manager)

        self.list_store: Gio.ListStore = Gio.ListStore.new(ListElement)  # implementation of Gio.ListModel

        self.move_left_button: Gtk.Button = Gtk.Button(
            label='<<<',
            icon_name="pan-start-symbolic",
            sensitive=False,
        )
        self.move_left_button.set_has_frame(False)
        self.move_left_button.connect('clicked', self._move, 'move-left')

        self.cells: Gtk.Box = Gtk.Box(
            orientation=self.orientation,
            margin_top=6,
            margin_bottom=6,
            margin_start=6,
            margin_end=6,
            hexpand=True,
            homogeneous=True,
        )

        self.move_right_button: Gtk.Button = Gtk.Button(
            label='>>>',
            icon_name="pan-end-symbolic",
            sensitive=False,
        )
        self.move_right_button.set_has_frame(False)
        self.move_right_button.connect('clicked', self._move, 'move-right')

        self.move_left_button.set_parent(self)
        self.cells.set_parent(self)
        self.move_right_button.set_parent(self)
        return

    def set_orientation(self, orientation: Gtk.Orientation) -> None:
        self.orientation = orientation
        return

    def get_orientation(self) -> Gtk.Orientation:
        return self.orientation

    def unselect(self) -> None:
        """
        Removes active indication, focus, from selected
        :returns: Nothing
        """
        item: ListElement = self.list_store.get_item(self.selected_index)
        item.set_active(False)
        self.selected_index = 0
        return

    def get_item(self, position: int) -> ListElement | None:  # was do_get_item
        if position > self.n_items:
            return None
        return self.list_store[position]

    def get_item_type(self) -> ListElement | None:  # was do_get_item_type
        return self.item_type

    def get_n_items(self) -> int:  # was do_get_n_items
        """
        :returns: the number of items in the list i.e. the value of n_items
        """
        return self.n_items

    def append(self, cell: Tuple[Any, str]) -> None:

        index = len(self.list_store)

        list_item = ListElement(index=index, cell=cell)
        list_item.connect('clicked', self._selected_cb)  # TODO: Not convinced this should be here

        self.list_store.append(list_item)
        self.cells.append(list_item)
        self.n_items += 1

        self.items_changed(index, 0, 1)
        return

    def remove(self, position) -> None:
        del self.list_store[position]
        # FIXME: Also need t remove from presentation
        self.items_changed(position, 1, 0)

    def get_list(self) -> list:
        """
        :returns: list of enums ordered according to the
        """
        result: list = []
        item: ListElement
        for item in self.list_store:
            e_num: enum = item.get_op()
            result.append(e_num)
        return result

    def get_list_with_titles(self) -> list:
        """
        :returns: a list of tuples where each tuple is an enum and
        string. The list ordered according to the selection.
        """
        result: list = []
        item: ListElement
        for item in self.list_store:
            e_num: enum = item.get_op()
            title: str = item.get_label()
            result.append((e_num, title))
        return result

    # CALLBACK
    def _move(self, _: Gtk.Button, direction: str) -> None:
        """

        :param direction:
        :returns: Nothing
        """
        # print(f"Enum List Before move: {self.get_list_with_titles()}")  # TODO: Remove these
        # print(f"Enum List Before move: {self.get_list()}")  # TODO: Remove these

        # indices of items to swap
        if direction == "move-left":
            if self.selected_index == 0:
                return
            swap_index = self.selected_index - 1

        if direction == "move-right":
            if self.selected_index == (self.n_items - 1):
                return
            swap_index = self.selected_index + 1

        # get the list items
        selected: ListElement = self.list_store.get_item(self.selected_index)
        swap_with: ListElement = self.list_store.get_item(swap_index)

        # Swap the list_store items
        (self.list_store[self.selected_index], self.list_store[swap_index]) = (
            self.list_store[swap_index], self.list_store[self.selected_index])

        # sort out the presentation order
        if direction == "move-left":
            self.cells.reorder_child_after(swap_with, selected)  # swap left
            swap_index = self.selected_index
            self.selected_index -= 1

        if direction == "move-right":
            self.cells.reorder_child_after(selected, swap_with)  # swap right
            swap_index = self.selected_index
            self.selected_index += 1

        selected.set_index(self.selected_index)
        swap_with.set_index(swap_index)

        # button state after move
        list_min: int = 0
        list_max: int = self.n_items - 1

        self.move_left_button.set_sensitive(True)
        self.move_right_button.set_sensitive(True)

        if self.selected_index == list_min:
            self.move_left_button.set_sensitive(False)
        if self.selected_index == list_max:
            self.move_right_button.set_sensitive(False)

        # TODO: understand why do we need this?
        self.items_changed(self.selected_index, 1, 1)
        self.items_changed(swap_index, 1, 1)

        # print(f"Enum List After move: {self.get_list_with_titles()}")  # TODO: Remove these
        # print(f"Enum List After move: {self.get_list()}")  # TODO: Remove these
        return

    # CALLBACK
    def _selected_cb(self, selected: ListElement, *args) -> None:
        """
        Handles the callback from a selected item in the presented list.
        :param selected:
        :returns: Nothing
        """
        index: int = selected.get_index()
        self.selected_index = index  # keep index of selected for use by _move

        list_min = 0
        list_max = self.n_items - 1

        self.move_left_button.set_sensitive(True)
        self.move_right_button.set_sensitive(True)

        if index == list_min:
            self.move_left_button.set_sensitive(False)
        if index == list_max:
            self.move_right_button.set_sensitive(False)
        return

    def _update(self, selected: int) -> None:
        """
        Removes the current ListElements and re-installs from a changes list_store
        :returns: Noting
        """
        child = self.cells.get_first_child()
        while child is not None:
            self.cells.remove(child)
            child = self.cells.get_first_child()

        for item in self.list_store:
            self.cells.append(item)

        return