Compare commits

..

1 Commits

Author SHA1 Message Date
865abb5213 La til gtk tabeell 2023-12-04 12:14:00 +01:00
5 changed files with 830 additions and 82 deletions

View File

@ -1,6 +1,5 @@
import argparse
import file_io
import datetime
import iof_xml
import serial
import subprocess
@ -13,7 +12,6 @@ from rich import print
import search_tui
from rich.traceback import install
from rich import inspect
install(show_locals=True)
# Main entrypoint for now. Cli with two options; init will make the files needed and run will start the program from the specified directory
@ -42,9 +40,7 @@ def main():
parser_init.add_argument('--xml', required=False, dest='xml_path', action='store', default=None, help='Where the xml result file should be saved')
parser_init = subparsers.add_parser('mtr', help='run mtr commands')
parser_init.add_argument('--port', required=True, dest='port', action='store', help='specify a serial port')
parser_init.add_argument('--spool', required=False, dest='mtr_spool', action='store_true', help='Spool all mtr data')
parser_init.add_argument('--status', required=False, dest='mtr_status', action='store_true', help='Spool all mtr data')
parser_init.add_argument('--port', required=False, dest='port', action='store', help='specify a serial port')
args = parser.parse_args()
@ -65,12 +61,7 @@ def main():
gen(args.dir, args.xml_path)
case 'mtr':
mtr = serial.Serial(port=args.port, baudrate=9600, timeout=40)
if args.mtr_spool:
mtr.write(b'/SA')
if args.mtr_status:
mtr.write(b'/ST')
case other:
parser.print_help()
mtr.write(b'/SA')
def init_dir(project_dir, entries_xml_file, courses_xml_file):
# Lager mappe med en config fil, en csv fil med løpere og en fil med mtr data
@ -100,46 +91,25 @@ def run(port='/dev/ttyUSB0', project_dir='./', xml_path='./output/'):
csv_path = project_dir + '/runners.csv'
while True:
if mtr.in_waiting > 0:
block = mtr.read_until(expected=b'\xFF\xFF\xFF\xFF')
size = block[0]
if size == 230:
mtr.read_until(expected=b'\xFF\xFF\xFF\xFF')
size = mtr.read(size=1)
if size == b'\xe6':
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
message = b'\xFF\xFF\xFF\xFF' + block[:230]
if not is_checksum_valid(message):
print('[red]Checksum is not valid![red]')
try:
print(otime.CardDump.from_mtr_bytes(message))
print(runner_info(event, card_dump))
except Exception as error:
print(error)
else:
card_dump = otime.CardDump.from_mtr_bytes(message)
event.card_dumps.append(card_dump)
file_io.write_card_dumps(event, mtr_path)
print(runner_info(event, card_dump))
subprocess.run(['git', 'add', './*'], cwd=project_dir, stdout=subprocess.DEVNULL)
subprocess.run(['git', 'commit', '-m', f'Added {card_dump.card}'], cwd=project_dir, stdout=subprocess.DEVNULL)
iof_xml.create_result_file(event, xml_path + '/results.xml')
pdf.create_result_list(event, project_dir + '/output/results.pdf')
meat = mtr.read(229)
full = b'\xFF\xFF\xFF\xFF' + size + meat
card_dump = otime.CardDump.from_mtr_bytes(full)
event.card_dumps.append(card_dump)
file_io.write_card_dumps(event, mtr_path)
print(runner_info(event, card_dump))
subprocess.run(['git', 'add', './*'], cwd=project_dir, stdout=subprocess.DEVNULL)
subprocess.run(['git', 'commit', '-m', f'Added {card_dump.card}'], cwd=project_dir, stdout=subprocess.DEVNULL)
elif size == 55:
message = b'\xFF\xFF\xFF\xFF' + block[:55]
mtr_id = int.from_bytes(message[6:8], 'little')
year = int.from_bytes(message[8:9], 'little')
month = int.from_bytes(message[9:10], 'little')
day = int.from_bytes(message[10:11], 'little')
hours =int.from_bytes(message[11:12], 'little')
minutes = int.from_bytes(message[12:13], 'little')
seconds = int.from_bytes(message[13:14], 'little')
milliseconds =int.from_bytes(message[14:16], 'little')
battery_status = int.from_bytes(message[16:17], 'little')
time = datetime.datetime(year, month, day, hours, minutes, seconds, milliseconds)
print(f'MTR status message: id: {mtr_id}, time: {time}, battery: {battery_status}')
else:
print('Data not found!')
print(block)
iof_xml.create_result_file(event, xml_path + '/results.xml')
pdf.create_result_list(event, project_dir + '/output/results.pdf')
elif size == b'\x37':
meat = mtr.read(55)
inspect(status)
def gen(project_dir='./', xml_path='./output/'):
config_path = project_dir + '/config.yaml'
@ -155,24 +125,17 @@ def gen(project_dir='./', xml_path='./output/'):
def runner_info(event, card_dump):
runner = next((i for i in event.runners if str(i.card_id) == str(card_dump.card)), None)
if runner is None:
return f'[orange_red1]No runner with ecard {card_dump.card}! It matches these courses: {otime.find_courses_matching_controls(card_dump.controls, event.courses)}[/orange_red1] {card_dump}'
return card_dump
result = event.get_runner_result(runner.id)
if result is None:
return '🧐 Dette skal ikke skje...'
if result.status == 'OK':
result_text = f'[green]Place: {result.place}. Time: [bold][yellow]{format_m_s(result.total_time)}[/yellow][/bold][/green]'
elif result.status == 'MissingPunch':
result_text = f'[red]Missed control(s): {result.missed_controls}! Time: [bold][yellow]{format_m_s(result.total_time)}[/yellow][/bold][/red]'
result_text = f'[red]Missed Control! Time: [bold][yellow]{format_m_s(result.total_time)}[/yellow][/bold][/red]'
else:
result_text = f'[blue]{result.status}, Time: [bold][yellow]{result.total_time}[/yellow][/bold][/blue]'
return result_text + f' [blue]{result.fullname()}[/blue], {result.o_class}, {result.club}, [italic]Card: {result.card_id}[/italic]'
def is_checksum_valid(message):
# Hentet fra https://github.com/knutsiem/mtr-log-extractor
checksum = int.from_bytes(message[232:233], 'little')
# calculate checksum for message bytes up until checksum
calculated_checksum = sum(message[:232]) % 256
return checksum == calculated_checksum
if __name__ == '__main__':
main()

View File

@ -46,9 +46,6 @@ def event_from_yaml_and_csv(config_path, mtr_path, csv_path):
i[7] = i[7]
else:
i[7] = None
# Sjekk om brikkenummer er tomt
if i[5] == '':
i[5] = 0
runners = [otime.Runner(id=i[0], status_override=i[1], first=i[2][0], last=i[2][1].strip(), o_class=i[3], club=i[4], card_id=int(i[5]), fork=int(i[6]), start_time=i[7]) for i in data]
event.card_dumps = card_dumps
event.runners = runners

View File

@ -181,7 +181,7 @@ class OClass:
class RunnerResult:
def __init__(self, runner_id: int, first: str, last: str, status: str, place: int, total_time: int, splits: list[int], end_time,
club=None, club_id=None, country=None, card_id=None, o_class=None, controls=None, fork=0, start_time=datetime.datetime(1973, 1, 1), fee_id=None, missed_controls=None, ran_other_course=None):
club=None, club_id=None, country=None, card_id=None, o_class=None, controls=None, fork=0, start_time=datetime.datetime(1973, 1, 1), fee_id=None):
self.id = runner_id
self.first = first
self.last = last
@ -200,10 +200,6 @@ class RunnerResult:
self.total_time = total_time
self.splits = splits
self.end_time = end_time
# List of controls missed
self.missed_controls = missed_controls
# If the runner ran other course it is named here
self.ran_other_course = ran_other_course
def fullname(self):
return f'{self.first} {self.last}'
@ -402,11 +398,7 @@ def produce_class_result(event, o_class_name) -> ClassResult:
results = [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), ok_runners.index(i)+1, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id), start_time=event.get_card_dump(i.card_id).s_time) for i in ok_runners]
results += [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), 0, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id),
start_time=event.get_card_dump(i.card_id).s_time,
missed_controls=find_missed_controls(event.get_runner_controls(i.id), event.get_course(o_class.course).codes[i.fork]),
ran_other_course=find_courses_matching_controls(event.get_card_dump(i.card_id).controls, event.courses))
for i in dsq_runners]
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id), start_time=event.get_card_dump(i.card_id).s_time) for i in dsq_runners]
results += [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), 0, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id)) for i in other_runners]
@ -460,14 +452,3 @@ def contains(small, big):
return map_bl
else:
return False
def find_missed_controls(punches, codes):
return [i for i in codes if i not in punches]
def find_courses_matching_controls(controls, courses):
matches = []
for i in courses:
for fork in i.codes:
if contains(fork, controls):
matches.append(i.name)
return matches

260
otime/ui.py Normal file
View File

@ -0,0 +1,260 @@
# Copyright (C) 2021 Tim Lauridsen < tla[at]rasmil.dk >
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to
# the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
"""
Sample Python Gtk4 Application
"""
import sys
import time
from typing import List
import copy
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, GObject, Gio, Adw
from widgets import ColumnViewListStore
import otime
import file_io
class ColumnElem(GObject.GObject):
"""custom data element for a ColumnView model (Must be based on GObject)"""
def __init__(self, name: str):
super(ColumnElem, self).__init__()
self.name = name
def __repr__(self):
return f"ColumnElem(name: {self.name})"
"""
class OtimeRunnerView(Gtk.ListView):
__gtype_name__ = "OtimeRunnerRowView"
__gsignals__ = {"refresh": (GObject.SignalFlags.RUN_FIRST, None, ())}
selection = Gtk.Template.Child()
class YumexQueueRow(Gtk.Box):
__gtype_name__ = "YumexQueueRow"
icon = Gtk.Template.Child()
text = Gtk.Template.Child()
dep = Gtk.Template.Child()
def __init__(self, view, **kwargs):
super().__init__(**kwargs)
self.view: OtimeRunnerView = view
self.pkg: YumexPackage = None
"""
class MyColumnViewColumn(ColumnViewListStore):
"""Custom ColumnViewColumn"""
def __init__(
self, win: Gtk.ApplicationWindow, col_view: Gtk.ColumnView, data: List
):
# Init ListView with store model class.
super(MyColumnViewColumn, self).__init__(ColumnElem, col_view)
self.win = win
# put some data into the model
for elem in data:
self.add(ColumnElem(elem))
def factory_setup(self, widget, item: Gtk.ListItem):
"""Gtk.SignalListItemFactory::setup signal callback
Handles the creation widgets to put in the ColumnViewColumn
"""
label = Gtk.Text()
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
label.set_margin_start(10)
item.set_child(label)
def factory_bind(self, widget, item: Gtk.ListItem):
"""Gtk.SignalListItemFactory::bind signal callback
Handles adding data for the model to the widgets created in setup
"""
label = item.get_child() # Get the Gtk.Label stored in the ListItem
data = item.get_item() # get the model item, connected to current ListItem
label.set_text(data.name) # Update Gtk.Label with data from model item
class MyWindow(Adw.ApplicationWindow):
def __init__(self, title, width, height, **kwargs):
super(MyWindow, self).__init__(**kwargs)
self.set_default_size(width, height)
box = Gtk.Box()
box.props.orientation = Gtk.Orientation.VERTICAL
header = Gtk.HeaderBar()
stack = Adw.ViewStack()
switcher = Adw.ViewSwitcherTitle()
switcher.set_stack(stack)
header.set_title_widget(switcher)
box.append(header)
content = self.setup_content()
page1 = stack.add_titled(content, "Løpere", "Løpere")
box_p2 = Gtk.Box()
page2 = stack.add_titled(box_p2, "page2", "Page 2")
box.append(stack)
self.set_content(box)
def setup_content(self):
"""Add a page with a text selector to the stack"""
# ColumnView with custom columns
self.columnview = Gtk.ColumnView()
self.columnview.set_show_column_separators(True)
sw = Gtk.ScrolledWindow()
self.columnview = Gtk.ColumnView()
factory_c1 = Gtk.SignalListItemFactory()
factory_c1.connect("setup", setup_c)
factory_c1.connect("bind", bind_c1)
factory_c2 = Gtk.SignalListItemFactory()
factory_c2.connect("setup", setup_c)
factory_c2.connect("bind", bind_c2)
factory_c3 = Gtk.SignalListItemFactory()
factory_c3.connect("setup", setup_c)
factory_c3.connect("bind", bind_c3)
factory_c4 = Gtk.SignalListItemFactory()
factory_c4.connect("setup", setup_c)
factory_c4.connect("bind", bind_c4)
factory_c5 = Gtk.SignalListItemFactory()
factory_c5.connect("setup", setup_c)
factory_c5.connect("bind", bind_c5)
selection = Gtk.SingleSelection()
store = Gio.ListStore.new(DataObject)
selection.set_model(store)
self.columnview.set_model(selection)
columns = []
columns.append(Gtk.ColumnViewColumn.new("Fornavn", factory_c1))
columns.append(Gtk.ColumnViewColumn.new("Etternavn ", factory_c2))
columns.append(Gtk.ColumnViewColumn.new("Klubb", factory_c3))
columns.append(Gtk.ColumnViewColumn.new("klasse", factory_c4))
columns.append(Gtk.ColumnViewColumn.new("eCard", factory_c5))
for i in columns:
self.columnview.append_column(i)
project_dir = '/home/trygve/Dokumenter/ÅbN&FC_2'
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + '/mtr.yaml'
csv_path = project_dir + '/runners.csv'
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
data = [DataObject(i.first, i.last, i.club, i.o_class, i.card_id) for i in event.runners]
for i in data:
store.append(i)
lw_frame = Gtk.Frame()
lw_frame.set_valign(Gtk.Align.FILL)
lw_frame.set_vexpand(True)
lw_frame.set_margin_start(20)
lw_frame.set_margin_end(20)
lw_frame.set_margin_top(10)
lw_frame.set_margin_bottom(10)
sw.set_child(self.columnview)
lw_frame.set_child(sw)
return lw_frame
class DataObject(GObject.GObject):
__gtype_name__ = 'DataObject'
text = GObject.Property(type=str, default=None)
def __init__(self, f_name, l_name, club, o_class, card):
super().__init__()
self.f_name = f_name
self.l_name = l_name
self.club = club
self.o_class = o_class
self.card = str(card)
def setup_c(widget, item):
"""Setup the widget to show in the Gtk.Listview"""
text = Gtk.Text()
item.set_child(text)
def bind_c1(widget, item):
"""bind data from the store object to the widget"""
label = item.get_child()
obj = item.get_item()
label.set_text(obj.f_name)
label.bind_property("f_name", obj, "f_name")
def bind_c2(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.l_name)
def bind_c3(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.club)
def bind_c4(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.o_class)
def bind_c5(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.card)
class Application(Adw.Application):
"""Main Aplication class"""
def __init__(self):
super().__init__(
application_id="net.trygve.otime.alpha", flags=Gio.ApplicationFlags.FLAGS_NONE
)
def do_activate(self):
win = self.props.active_window
if not win:
win = MyWindow("Otime veldig alpha", 800, 800, application=self)
win.present()
def main():
"""Run the main application"""
app = Application()
return app.run(sys.argv)
if __name__ == "__main__":
main()

547
otime/widgets.py Normal file
View File

@ -0,0 +1,547 @@
# Copyright (C) 2021 Tim Lauridsen < tla[at]rasmil.dk >
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to
# the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
"""
Misc classes to build Gtk4 Applications in python 3.9
It takes some of the hassle away from building Gtk4 application in Python
So you can create an cool application, without all the boilerplate code
"""
import os.path
from abc import abstractmethod
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gio, GLib, Gdk
from material import MATERIAL
def rgb_to_hex(r, g, b):
if isinstance(r, float):
r *= 255
g *= 255
b *= 255
return "#{0:02X}{1:02X}{2:02X}".format(int(r), int(g), int(b))
def color_to_hex(color):
return rgb_to_hex(color.red, color.green, color.blue)
def get_font_markup(fontdesc, text):
return f'<span font_desc="{fontdesc}">{text}</span>'
class MaterialColorDialog(Gtk.ColorChooserDialog):
""" Color chooser dialog with Material design colors """
def __init__(self, title, parent):
Gtk.ColorChooserDialog.__init__(self)
self.set_title(title)
self.set_transient_for(parent)
self.set_modal(True)
# build list of material colors in Gdk.RGBA format
color_values = []
for color_name in MATERIAL.colors:
colors = MATERIAL.get_palette(color_name)
for col in colors:
color = Gdk.RGBA()
color.parse(col)
color_values.append(color)
num_colors = 14
self.add_palette(Gtk.Orientation.HORIZONTAL, num_colors, color_values)
self.set_property('show-editor', False)
def get_color(self):
selected_color = self.get_rgba()
return color_to_hex(selected_color)
class ListViewBase(Gtk.ListView):
""" ListView base class, it setup the basic factory, selection model & data model
handlers must be overloaded & implemented in a sub class
"""
def __init__(self, model_cls):
Gtk.ListView.__init__(self)
# Use the signal Factory, so we can connect our own methods to setup
self.factory = Gtk.SignalListItemFactory()
# connect to Gtk.SignalListItemFactory signals
# check https://docs.gtk.org/gtk4/class.SignalListItemFactory.html for details
self.factory.connect('setup', self.on_factory_setup)
self.factory.connect('bind', self.on_factory_bind)
self.factory.connect('unbind', self.on_factory_unbind)
self.factory.connect('teardown', self.on_factory_teardown)
# Create data model, use our own class as elements
self.set_factory(self.factory)
self.store = self.setup_store(model_cls)
# create a selection model containing our data model
self.model = self.setup_model(self.store)
self.model.connect('selection-changed', self.on_selection_changed)
# set the selection model to the view
self.set_model(self.model)
def setup_model(self, store: Gio.ListModel) -> Gtk.SelectionModel:
""" Setup the selection model to use in Gtk.ListView
Can be overloaded in subclass to use another Gtk.SelectModel model
"""
return Gtk.SingleSelection.new(store)
@abstractmethod
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
must be overloaded in subclass to use another Gio.ListModel
"""
raise NotImplemented
def add(self, elem):
""" add element to the data model """
self.store.append(elem)
# Gtk.SignalListItemFactory signal callbacks
# transfer to some some callback stubs, there can be overloaded in
# a subclass.
def on_factory_setup(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Setup the widgets to go into the ListView """
self.factory_setup(widget, item)
def on_factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" GtkSignalListItemFactory::bind signal callback
apply data from model to widgets set in setup"""
self.factory_bind(widget, item)
def on_factory_unbind(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::unbind signal callback
Undo the the binding done in ::bind if needed
"""
self.factory_unbind(widget, item)
def on_factory_teardown(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Undo the creation done in ::setup if needed
"""
self.factory_teardown(widget, item)
def on_selection_changed(self, widget, position, n_items):
# get the current selection (GtkBitset)
selection = widget.get_selection()
# the the first value in the GtkBitset, that contain the index of the selection in the data model
# as we use Gtk.SingleSelection, there can only be one ;-)
ndx = selection.get_nth(0)
self.selection_changed(widget, ndx)
# --------------------> abstract callback methods <--------------------------------
# Implement these methods in your subclass
@abstractmethod
def factory_setup(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" Setup the widgets to go into the ListView (Overload in subclass) """
pass
@abstractmethod
def factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" apply data from model to widgets set in setup (Overload in subclass)"""
pass
@abstractmethod
def factory_unbind(self, widget: Gtk.ListView, item: Gtk.ListItem):
pass
@abstractmethod
def factory_teardown(self, widget: Gtk.ListView, item: Gtk.ListItem):
pass
@abstractmethod
def selection_changed(self, widget, ndx):
""" trigged when selecting in listview is changed
ndx: is the index in the data store model that is selected
"""
pass
class ListViewListStore(ListViewBase):
""" ListView base with an Gio.ListStore as data model
It can contain misc objects derived from GObject
"""
def __init__(self, model_cls):
super(ListViewListStore, self).__init__(model_cls)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model """
return Gio.ListStore.new(model_cls)
class ListViewStrings(ListViewBase):
""" Add ListView with only strings """
def __init__(self):
super(ListViewStrings, self).__init__(Gtk.StringObject)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
Can be overloaded in subclass to use another Gio.ListModel
"""
return Gtk.StringList()
class SelectorBase(Gtk.ListBox):
""" Selector base class """
def __init__(self):
Gtk.ListBox.__init__(self)
# Setup the listbox
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.connect('row-selected', self.on_row_changes)
self._rows = {}
self.ndx = 0
self.callback = None
def add_row(self, name, markup):
""" Overload this in a subclass"""
raise NotImplemented
def on_row_changes(self, widget, row):
ndx = row.get_index()
if self.callback:
self.callback(self._rows[ndx])
else:
print(f'Row Selected : {self._rows[ndx]}')
def set_callback(self, callback):
self.callback = callback
class TextSelector(SelectorBase):
""" Vertical Selector Widget that contains a number of strings where one can be selected """
def add_row(self, name: str, markup: str):
""" Add a named row to the selector with at given icon name"""
# get the image
label = Gtk.Label()
label.set_markup(markup)
# set the widget size request to 32x32 px, so we get some margins
# label.set_size_request(100, 24)
label.set_single_line_mode(True)
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
label.set_xalign(0)
label.set_margin_start(5)
label.set_margin_end(10)
row = self.append(label)
# store the index names, so we can find it on selection
self._rows[self.ndx] = name
self.ndx += 1
class IconSelector(SelectorBase):
""" Vertical Selector Widget that contains a number of icons where one can be selected """
def add_row(self, name, icon_name):
""" Add a named row to the selector with at given icon name"""
# get the image
pix = Gtk.Image.new_from_icon_name(icon_name)
# set the widget size request to 32x32 px, so we get some margins
pix.set_size_request(32, 32)
row = self.append(pix)
# store the index names, so we can find it on selection
self._rows[self.ndx] = name
self.ndx += 1
class ViewColumnBase(Gtk.ColumnViewColumn):
""" ColumnViewColumn base class, it setup the basic factory, selection model & data model
handlers must be overloaded & implemented in a sub class
"""
def __init__(self, model_cls, col_view):
Gtk.ColumnViewColumn.__init__(self)
self.col_view = col_view
# Use the signal Factory, so we can connect our own methods to setup
self.factory = Gtk.SignalListItemFactory()
# connect to Gtk.SignalListItemFactory signals
# check https://docs.gtk.org/gtk4/class.SignalListItemFactory.html for details
self.factory.connect('setup', self.on_factory_setup)
self.factory.connect('bind', self.on_factory_bind)
self.factory.connect('unbind', self.on_factory_unbind)
self.factory.connect('teardown', self.on_factory_teardown)
# Create data model, use our own class as elements
self.set_factory(self.factory)
self.store = self.setup_store(model_cls)
# create a selection model containing our data model
self.model = self.setup_model(self.store)
self.model.connect('selection-changed', self.on_selection_changed)
# add model to the ColumnView
self.col_view.set_model(self.model)
def setup_model(self, store: Gio.ListModel) -> Gtk.SelectionModel:
""" Setup the selection model to use in Gtk.ListView
Can be overloaded in subclass to use another Gtk.SelectModel model
"""
return Gtk.SingleSelection.new(store)
@abstractmethod
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
must be overloaded in subclass to use another Gio.ListModel
"""
raise NotImplemented
def add(self, elem):
""" add element to the data model """
self.store.append(elem)
# Gtk.SignalListItemFactory signal callbacks
# transfer to some some callback stubs, there can be overloaded in
# a subclass.
def on_factory_setup(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Setup the widgets to go into the ListView """
self.factory_setup(widget, item)
def on_factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" GtkSignalListItemFactory::bind signal callback
apply data from model to widgets set in setup"""
self.factory_bind(widget, item)
def on_factory_unbind(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::unbind signal callback
Undo the the binding done in ::bind if needed
"""
self.factory_unbind(widget, item)
def on_factory_teardown(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Undo the creation done in ::setup if needed
"""
self.factory_teardown(widget, item)
def on_selection_changed(self, widget, position, n_items):
# get the current selection (GtkBitset)
selection = widget.get_selection()
# the the first value in the GtkBitset, that contain the index of the selection in the data model
# as we use Gtk.SingleSelection, there can only be one ;-)
ndx = selection.get_nth(0)
self.selection_changed(widget, ndx)
# --------------------> abstract callback methods <--------------------------------
# Implement these methods in your subclass
@abstractmethod
def factory_setup(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
""" Setup the widgets to go into the ColumnViewColumn (Overload in subclass) """
pass
@abstractmethod
def factory_bind(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
""" apply data from model to widgets set in setup (Overload in subclass)"""
pass
@abstractmethod
def factory_unbind(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
pass
@abstractmethod
def factory_teardown(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
pass
@abstractmethod
def selection_changed(self, widget, ndx):
""" trigged when selecting in listview is changed
ndx: is the index in the data store model that is selected
"""
pass
class ColumnViewListStore(ViewColumnBase):
""" ColumnViewColumn base with an Gio.ListStore as data model
It can contain misc objects derived from GObject
"""
def __init__(self, model_cls, col_view):
super(ColumnViewListStore, self).__init__(model_cls, col_view)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model """
return Gio.ListStore.new(model_cls)
class SearchBar(Gtk.SearchBar):
""" Wrapper for Gtk.Searchbar Gtk.SearchEntry"""
def __init__(self, win=None):
super(SearchBar, self).__init__()
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.set_spacing(10)
# Add SearchEntry
self.entry = Gtk.SearchEntry()
self.entry.set_hexpand(True)
box.append(self.entry)
# Add Search Options button (Menu content need to be added)
btn = Gtk.MenuButton()
btn.set_icon_name('preferences-other-symbolic')
self.search_options = btn
box.append(btn)
self.set_child(box)
# connect search entry to seach bar
self.connect_entry(self.entry)
if win:
# set key capture from main window, it will show searchbar, when you start typing
self.set_key_capture_widget(win)
# show close button in search bar
self.set_show_close_button(True)
# Set search mode to off by default
self.set_search_mode(False)
def set_callback(self, callback):
""" Connect the search entry activate to an callback handler"""
self.entry.connect('activate', callback)
class ButtonRow(Gtk.Box):
""" Row of button"""
def __init__(self, btn_list: list, callback):
super(ButtonRow, self).__init__(orientation=Gtk.Orientation.HORIZONTAL)
# self.set_halign(Gtk.Align.CENTER)
self.set_margin_start(20)
self.set_margin_top(20)
self.set_spacing(10)
for title in btn_list:
btn = Gtk.Button()
btn.set_label(f'{title}')
btn.connect('clicked', callback)
self.append(btn)
class SwitchRow(Gtk.Box):
def __init__(self, text):
super(SwitchRow, self).__init__(orientation=Gtk.Orientation.HORIZONTAL)
# Switch to control overlay visibility
self.label = Gtk.Label.new(text)
self.label.set_halign(Gtk.Align.FILL)
self.label.set_valign(Gtk.Align.CENTER)
self.label.set_hexpand(True)
self.label.set_xalign(0.0)
self.label.set_margin_start(20)
self.label.set_margin_bottom(20)
self.append(self.label)
self.switch = Gtk.Switch()
self.switch.set_halign(Gtk.Align.END)
self.switch.set_margin_end(20)
self.switch.set_margin_bottom(20)
self.append(self.switch)
def connect(self, signal, callback):
self.switch.connect(signal, callback)
def set_state(self, state):
self.switch.set_state(state)
class MenuButton(Gtk.MenuButton):
"""
Wrapper class for at Gtk.Menubutton with a menu defined
in a Gtk.Builder xml string
"""
def __init__(self, xml, name, icon_name='open-menu-symbolic'):
super(MenuButton, self).__init__()
builder = Gtk.Builder()
builder.add_from_string(xml)
menu = builder.get_object(name)
self.set_menu_model(menu)
self.set_icon_name(icon_name)
class Stack(Gtk.Stack):
""" Wrapper for Gtk.Stack with with a StackSwitcher """
def __init__(self):
super(Stack, self).__init__()
self.switcher = Gtk.StackSwitcher()
self.switcher.set_stack(self)
self._pages = {}
def add_page(self, name, title, widget):
page = self.add_child(widget)
page.set_name(name)
page.set_title(title)
self._pages[name] = page
return page
class Window(Gtk.ApplicationWindow):
""" custom Gtk.ApplicationWindow with a headerbar"""
def __init__(self, title, width, height, **kwargs):
super(Window, self).__init__(**kwargs)
self.set_default_size(width, height)
self.headerbar = Gtk.HeaderBar()
self.set_titlebar(self.headerbar)
label = Gtk.Label()
label.set_text(title)
self.headerbar.set_title_widget(label)
# custom CSS provider
self.css_provider = None
def load_css(self, css_fn):
"""create a provider for custom styling"""
if css_fn and os.path.exists(css_fn):
css_provider = Gtk.CssProvider()
try:
css_provider.load_from_path(css_fn)
except GLib.Error as e:
print(f"Error loading CSS : {e} ")
return None
print(f'loading custom styling : {css_fn}')
self.css_provider = css_provider
def _add_widget_styling(self, widget):
if self.css_provider:
context = widget.get_style_context()
context.add_provider(
self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def add_custom_styling(self, widget):
self._add_widget_styling(widget)
# iterate children recursive
for child in widget:
self.add_custom_styling(child)
def create_action(self, name, callback):
""" Add an Action and connect to a callback """
action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback)
self.add_action(action)