How to properly extend widget classes and in gtk4?

,

I have been wondering what the best way to extend a widget class in gtk4 would look like. I read through Custom widgets in GTK 4 – Introduction – GTK Development Blog and looked at some discussions in forums and I am still a bit unsure of what the best course of action would be.

So lets take the example of where you need to add custom properties to a button. You would do something like

struct _CustomBtn {
    GtkWidget parent_instance;

    GtkButton *btn;
    // custom properties
};


static void custom_btn_class_init(PkLauncherBtnClass *klass) {
    self->btn = gtk_button_new();

    gtk_widget_class_set_layout_manager_type(
        GTK_WIDGET_CLASS(klass),
        GTK_TYPE_BIN_LAYOUT
    );
}

the only problem with this is if your writing a library of extendable widgets to use then you would still want the be able to use your extended widgets in xml

<object class="CustomBtn">
<property name="label">Test me</property>
<property name="custom-prop">My custom property</property>
</object>

So the custom button would still be able to use the label and all other properties of the original button. The only way I can see this being possible is to basically write the property getters and setters for the entire button. Initialize those properties in init and so on. This would be a pain to do every time.

So I then I figured you could do something like just create a custom button that would extend all the functionality of the gtk button and then I could just extend that button when needed something like:

#include "btn.h"

static void buildable_init(GtkBuildableIface *iface);

G_DEFINE_TYPE_WITH_CODE(
    CustomBtn,
    custom_btn,
    GTK_TYPE_WIDGET,
    G_IMPLEMENT_INTERFACE(GTK_TYPE_BUILDABLE, buildable_init)
);

// default gtk button attributes
enum {
    PROP_0,
    PROP_LABEL,
    PROP_ICON_NAME,
    PROP_HAS_FRAME,
    PROP_USE_UNDERLINE,
    PROP_CAN_SHRINK,
    N_PROPS
};

static GParamSpec *props[N_PROPS];

static void set_property(
    GObject *obj,
    guint prop_id,
    const GValue *value,
    GParamSpec *pspec
) {
    CustomBtn *self = CUSTOM_BTN(obj);

    switch(prop_id) {
        case PROP_LABEL:
            gtk_button_set_label(self->btn, g_value_get_string(value));
            break;
        case PROP_ICON_NAME:
            gtk_button_set_icon_name(self->btn, g_value_get_string(value));
            break;
        case PROP_HAS_FRAME:
            gtk_button_set_has_frame(self->btn, g_value_get_boolean(value));
            break;
        case PROP_CAN_SHRINK:
            gtk_button_set_can_shrink(self->btn, g_value_get_boolean(value));
            break;
        case PROP_USE_UNDERLINE:
            gtk_button_set_use_underline(self->btn, g_value_get_boolean(value));
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    }
}

static void get_property(
    GObject *obj,
    guint prop_id,
    GValue *value,
    GParamSpec *pspec
) {
    CustomBtn *self = CUSTOM_BTN(obj);
    switch(prop_id) {
        case PROP_LABEL:
            g_value_set_string(value, gtk_button_get_label(self->btn));
            break;
        case PROP_ICON_NAME:
            g_value_set_string(value, gtk_button_get_icon_name(self->btn));
            break;
        case PROP_HAS_FRAME:
            g_value_set_boolean(value, gtk_button_get_has_frame(self->btn));
            break;
        case PROP_USE_UNDERLINE:
            g_value_set_boolean(value, gtk_button_get_use_underline(self->btn));
            break;
        case PROP_CAN_SHRINK:
            g_value_set_boolean(value, gtk_button_get_can_shrink(self->btn));
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
    }
}

static void add_child(
    GtkBuildable *buildable,
    GtkBuilder *builder,
    GObject *child,
    const char *type
) {
    CustomBtn *self = CUSTOM_BTN(buildable);
    gtk_button_set_child(self->btn, GTK_WIDGET(child));
}

static void buildable_init(GtkBuildableIface *iface) {
    iface->add_child = add_child;
}

static void dispose(GObject *obj) {
    CustomBtn *self = CUSTOM_BTN(obj);

    g_clear_pointer((GtkWidget **)&self->btn, gtk_widget_unparent);

    G_OBJECT_CLASS(custom_btn_parent_class)->dispose(obj);
}

static void custom_btn_init(CustomBtn *self) {
    self->btn = GTK_BUTTON(gtk_button_new());
    gtk_widget_set_layout_manager(GTK_WIDGET(self), gtk_bin_layout_new());
    gtk_widget_set_parent(GTK_WIDGET(self->btn), GTK_WIDGET(self));
}

static void custom_btn_class_init(CustomBtnClass *klass) {
    GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    obj_class->dispose = dispose;
    obj_class->set_property = set_property;
    obj_class->get_property = get_property;

    props[PROP_LABEL] = g_param_spec_string(
        "label",
        NULL,
        NULL,
        NULL,
        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS
    );

    props[PROP_ICON_NAME] = g_param_spec_string(
        "icon-name",
        NULL,
        NULL,
        NULL,
        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS
    );

    props[PROP_HAS_FRAME] = g_param_spec_boolean(
        "has-frame",
        NULL,
        NULL,
        TRUE,
        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS
    );

    props[PROP_CAN_SHRINK] = g_param_spec_boolean(
        "can-shrink",
        NULL,
        NULL,
        FALSE,
        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY
    );

    props[PROP_USE_UNDERLINE] = g_param_spec_boolean(
        "use-underline",
        NULL,
        NULL,
        FALSE,
        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY
    );

    g_object_class_install_properties(obj_class, N_PROPS, props);
}

This seems completely unnecessary for something that used to be as simple as

struct _PkBtn {
    GtkButton parent_instance;

    // custom properties
};

You would basically be rewriting the button with the exact same code as the original button just to extend it. And you would have to do this for all widgets that you want to extend functionality to.

So is this right? For every widget that I want to extend is it preferred to just create a wrapper GtkWidget and hold a reference to the widget you want to extend, then forward all properties to the reference or is there a better way to go about this?

The question I have is: What is the goal you want to achieve by extending an widget?

For your example: What kind of properties would you need to extend Gtk.Button with?

I am building a ui kit. So just like stated in the original question I need to add custom properties to widgets to extend functionality. The end user is going to be building with xml so widgets need to be able to use the original properties in <property name=””> so if we are talking about composition then it basically requires rewriting most of the property system and just fowarding the values back to the original widget.

Not every class in GTK can be derived: a lot of them are marked as “final”, because the type system cannot distinguish between a derived type that just adds properties from a derived type that overrides virtual functions or signals.

You can write your own UI toolkit by augmenting GTK; platform libraries like libadwaita and libgranite do that. If you’re trying to write a UI toolkit that wraps GTK, then you’re on your own: GTK is not meant to be used as a “UI toolkit backend”.

You can write your own UI toolkit by augmenting GTK;

what exactly do you mean by this? Also are you saying that some widgets can be extended and some can’t? If so can you give an example of a widget that can be safely extended and one that can’t. Are they marked in the source code? Because it would be an awful lot easier to just extend a button to add a couple properties than rewrite basically the entire functionality of the button and forward props. At that point I may as well just write my own button that extends GtkWidget.

Are you writing new widgets, or are you just making wrappers around all GTK widgets?

Correct. In the API reference you’ll see the difference between types that can be derived, like GtkButton and types that can’t be derived—i.e. “final”—like GtkLabel.

It really doesn’t matter what you think is more “convenient”, if your goal is to extend random GTK widgets. GTK provides you with types, and if a type cannot be derived then your only option is to compose it inside your own widget. The overarching design of GTK is to make it possible to compose complex UI without derivation of leaf widgets. Very, very few applications need to derive GtkButton itself: they will use a button inside a more complex layout, and embed the logic for handling button properties and signals inside that layout.

So your saying technically I can extend the GtkButton but not the GtkLabel. But it is recommended to just compose your own widgets for all by holding a reference and setting properties to that reference.

Yes, that’s the preferred approach.

It is possible to derive a new class from a label.

Here is an example that I programmed a long time ago. It is a label that can be edited with the help of a double click.

/*
 * Fr 31. Mai 14:35:38 CEST 2024 
 * /home/holger-2/programmierung/gtk-prog/240531-editable-label 
 * Name: 		 class-editable-label.c
 * Description: creates an editable label that is on a
 * Double click responds.
 */ 
 
#include"class-editable-label.h"

struct _EditableLabel
{
  GtkWidget parent_instance;
  GtkWidget *editable;
};
 
struct _EditableLabelClass
{
  GtkWidgetClass parent_class;
};
 
G_DEFINE_TYPE (EditableLabel, editable_label, GTK_TYPE_WIDGET)

void editable_label_insert_text(EditableLabel *self,char *text)
{
 int position;
 gtk_editable_delete_text(GTK_EDITABLE(self->editable),0,-1);
 gtk_editable_insert_text(GTK_EDITABLE(self->editable),text,-1,&position);
}

void cb_gesture_pressed ( GtkGestureClick* self, gint n_press, gdouble x, gdouble y, gpointer user_data)
{
    if (n_press == 2)
    {
        gtk_editable_set_editable (GTK_EDITABLE (EDITABLE_LABEL(user_data)->editable),TRUE);
	gtk_editable_label_start_editing(GTK_EDITABLE_LABEL(EDITABLE_LABEL(user_data)->editable));
    }
    else
	gtk_editable_set_editable (GTK_EDITABLE (EDITABLE_LABEL(user_data)->editable),FALSE);
}

static gboolean on_key_pressed(GtkEventControllerKey *controller, 
                           guint keyval,
                            guint keycode,
                            GdkModifierType state,
                            gpointer user_data)
{
   // filter out Enter and TAB 
   if(keycode == 36  || keycode == 23)
   {
   	gtk_editable_label_stop_editing (GTK_EDITABLE_LABEL(EDITABLE_LABEL(user_data)->editable),TRUE);
    	gtk_editable_set_editable (GTK_EDITABLE (EDITABLE_LABEL(user_data)->editable),FALSE);
   }	

    return GDK_EVENT_PROPAGATE;
}

static void	
editable_label_dispose (GObject *gobject)
{
        EditableLabel *self = EDITABLE_LABEL (gobject);
	g_clear_pointer(&self->editable, gtk_widget_unparent);	
	G_OBJECT_CLASS (editable_label_parent_class)->dispose (gobject);

}

static void
editable_label_init(EditableLabel *self)
{
	self->editable = gtk_editable_label_new("");
	GtkGesture *gesture = gtk_gesture_click_new ();
        g_signal_connect (gesture, "pressed", G_CALLBACK (cb_gesture_pressed), self);
        gtk_widget_add_controller (GTK_WIDGET(self), GTK_EVENT_CONTROLLER (gesture));
	// filter out Enter and TAB
        GtkEventController*  keyevent = gtk_event_controller_key_new ();
        gtk_widget_add_controller(GTK_WIDGET(self->editable),keyevent);
        g_signal_connect_after(keyevent,"key-pressed",G_CALLBACK(on_key_pressed),self);

	gtk_editable_set_editable (GTK_EDITABLE(self->editable),FALSE);
	gtk_widget_set_parent(GTK_WIDGET(self->editable), GTK_WIDGET(self));
	// LayoutManager may by changed !!
        GtkLayoutManager *center_layout;
	center_layout = gtk_widget_get_layout_manager (GTK_WIDGET(self));
	gtk_center_layout_set_start_widget (GTK_CENTER_LAYOUT (center_layout),self->editable);
}

static void
editable_label_class_init (EditableLabelClass *class)
{
  G_OBJECT_CLASS(class)->dispose = editable_label_dispose;
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
 
  gtk_widget_class_set_layout_manager_type(widget_class,GTK_TYPE_CENTER_LAYOUT); 
}
 
GtkWidget *
editable_label_new (char *text)
{
  EditableLabel *self;
 
  self = g_object_new (EDITABLE_TYPE_LABEL,NULL);
  if (text != NULL)
  {	  
   int position;
   gtk_editable_insert_text(GTK_EDITABLE(self->editable),text,-1,&position);
  }
  return GTK_WIDGET (self);
 }
 

Have fun trying.

Greeting Holger

I appreciate the response but this really doesn’t address the question. The code you posted is basically the exact same thing as I posted.