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


This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.