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