diff --git a/otime/ui.py b/otime/ui.py new file mode 100644 index 0000000..eb67b48 --- /dev/null +++ b/otime/ui.py @@ -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() diff --git a/otime/widgets.py b/otime/widgets.py new file mode 100644 index 0000000..4f0f2e3 --- /dev/null +++ b/otime/widgets.py @@ -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'{text}' + + +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)