Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions internal/gutil/cast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,41 +50,3 @@ func TestMustCastPanics(t *testing.T) {
}()
gutil.MustCast[gio.SimpleAction](str, gio.SimpleActionGLibType())
}

func TestFillFromUI(t *testing.T) {
const ui = `<interface>
<object class="GtkAdjustment" id="Adj"><property name="upper">10</property></object>
<object class="GtkAdjustment" id="Other"/>
</interface>`
var dst struct {
Adj *gtk.Adjustment
Renamed *gtk.Adjustment `gtk:"Other"`
Missing *gtk.Adjustment
Skipped *gtk.Adjustment `gtk:"-"`
}
gutil.FillFromUI(&dst, ui)
if dst.Adj == nil || dst.Adj.GetUpper() != 10 || dst.Renamed == nil {
t.Fatalf("FillFromUI did not fill fields: %+v", dst)
}
if dst.Missing != nil || dst.Skipped != nil {
t.Errorf("FillFromUI filled missing/skipped fields: %+v", dst)
}
// The builder is gone; our references must keep the objects alive.
if dst.Adj.GetUpper() != 10 {
t.Error("object did not survive the builder")
}
gutil.UnrefFields(&dst)
if dst.Adj != nil || dst.Renamed != nil {
t.Error("UnrefFields did not clear fields")
}
}

func TestFillFromUIWrongType(t *testing.T) {
defer func() {
if recover() == nil {
t.Error("FillFromUI with a wrong field type did not panic")
}
}()
var dst struct{ Adj *gtk.Label }
gutil.FillFromUI(&dst, `<interface><object class="GtkAdjustment" id="Adj"/></interface>`)
}
156 changes: 0 additions & 156 deletions internal/gutil/gutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,164 +12,8 @@ import (
"codeberg.org/puregotk/puregotk/v4/gobject"
"codeberg.org/puregotk/puregotk/v4/gobject/types"
"codeberg.org/puregotk/puregotk/v4/gtk"
"deedles.dev/trayscale/internal/locale"
)

// FillFromBuilder sets fields in the struct dst using objects looked
// up in builder. If the field has a `gtk` tag, the value of it is
// used as the name to look up in builder, unless the tag is exactly
// `"-"` in which case the field is skipped. If it does not have a
// tag, the field name is used instead. If an object by that name does
// not exist in the builder, it is quietly skipped. If it does exist
// but is the wrong type, this function will panic.
//
// Only exported pointer fields to puregotk wrappers are considered.
// puregotk's casts are unchecked, so a field's type must be registered
// with [RegisterType] for its GType to be checked; filling an
// unregistered type panics.
//
// Each stored object gets its own reference, so dst stays valid after
// builder is released. Release them with [UnrefFields].
//
// For example,
//
// var example struct {
// Label *gtk.Label
// Button *gtk.Button `gtk:"DoneButton"`
// }
// FillFromBuilder(&example, builder)
//
// will cause the Label field to get filled with the object named
// "Label" and the Button field to get filled with the object named
// "DoneButton".
func FillFromBuilder[T any](dst *T, builder *gtk.Builder) {
for fv, name := range builderFields(dst) {
// puregotk's GetObject returns a new reference, which the field
// keeps.
obj := builder.GetObject(name)
if obj == nil {
continue
}

et := fv.Type().Elem()
gtype, ok := typeRegistry[et]
if !ok {
obj.Unref()
panic(fmt.Sprintf("gutil.FillFromBuilder: no GType registered for %v (field for %q)", et, name))
}
if !IsA(obj, gtype()) {
obj.Unref()
panic(fmt.Sprintf("gutil.FillFromBuilder: %q is a %v, want %v", name, TypeName(obj), et))
}

nv := reflect.New(et)
nv.Interface().(gobject.Ptr).SetGoPointer(obj.GoPointer())
fv.Set(nv)
}
}

// UnrefFields drops the references [FillFromBuilder] took for dst's
// fields and clears them.
func UnrefFields[T any](dst *T) {
for fv := range builderFields(dst) {
if fv.IsNil() {
continue
}
p := fv.Interface().(gobject.Ptr).GoPointer()
if p != 0 {
gobject.ObjectNewFromInternalPtr(p).Unref()
}
fv.SetZero()
}
}

func builderFields[T any](dst *T) iter.Seq2[reflect.Value, string] {
return func(yield func(reflect.Value, string) bool) {
v := reflect.ValueOf(dst).Elem()
t := v.Type()
for i := range t.NumField() {
ft := t.Field(i)
if !ft.IsExported() || ft.Type.Kind() != reflect.Pointer || !ft.Type.Implements(ptrType) {
continue
}
name := ft.Name
if tag, ok := ft.Tag.Lookup("gtk"); ok {
if tag == "-" {
continue
}
name = tag
}
if !yield(v.Field(i), name) {
return
}
}
}
}

var ptrType = reflect.TypeFor[gobject.Ptr]()

var typeRegistry = map[reflect.Type]func() types.GType{}

// RegisterType records the GType of the puregotk wrapper T so that
// [FillFromBuilder] can check and fill fields of type *T.
func RegisterType[T any](gtype func() types.GType) {
typeRegistry[reflect.TypeFor[T]()] = gtype
}

func init() {
RegisterType[gtk.Box](gtk.BoxGLibType)
RegisterType[gtk.Button](gtk.ButtonGLibType)
RegisterType[gtk.DropDown](gtk.DropDownGLibType)
RegisterType[gtk.DropTarget](gtk.DropTargetGLibType)
RegisterType[gtk.Image](gtk.ImageGLibType)
RegisterType[gtk.Label](gtk.LabelGLibType)
RegisterType[gtk.ListBox](gtk.ListBoxGLibType)
RegisterType[gtk.MenuButton](gtk.MenuButtonGLibType)
RegisterType[gtk.ScrolledWindow](gtk.ScrolledWindowGLibType)
RegisterType[gtk.SearchBar](gtk.SearchBarGLibType)
RegisterType[gtk.SearchEntry](gtk.SearchEntryGLibType)
RegisterType[gtk.Adjustment](gtk.AdjustmentGLibType)
RegisterType[gtk.Window](gtk.WindowGLibType)
RegisterType[adw.Spinner](adw.SpinnerGLibType)
RegisterType[adw.ViewSwitcherSidebar](adw.ViewSwitcherSidebarGLibType)
RegisterType[gtk.Switch](gtk.SwitchGLibType)
RegisterType[gtk.ToggleButton](gtk.ToggleButtonGLibType)
RegisterType[adw.ActionRow](adw.ActionRowGLibType)
RegisterType[adw.ApplicationWindow](adw.ApplicationWindowGLibType)
RegisterType[adw.ButtonRow](adw.ButtonRowGLibType)
RegisterType[adw.ComboRow](adw.ComboRowGLibType)
RegisterType[adw.EntryRow](adw.EntryRowGLibType)
RegisterType[adw.ExpanderRow](adw.ExpanderRowGLibType)
RegisterType[adw.NavigationSplitView](adw.NavigationSplitViewGLibType)
RegisterType[adw.PreferencesDialog](adw.PreferencesDialogGLibType)
RegisterType[adw.PreferencesGroup](adw.PreferencesGroupGLibType)
RegisterType[adw.PreferencesPage](adw.PreferencesPageGLibType)
RegisterType[adw.SpinRow](adw.SpinRowGLibType)
RegisterType[adw.StatusPage](adw.StatusPageGLibType)
RegisterType[adw.SwitchRow](adw.SwitchRowGLibType)
RegisterType[adw.ToastOverlay](adw.ToastOverlayGLibType)
RegisterType[adw.ViewStack](adw.ViewStackGLibType)
}

// FillFromUI loads the given xml data in the order that it is passed
// and fills into with it in the same way that [FillFromBuilder] does.
// Each xml value should be an entire XML UI definition file. Invalid
// XML is a programming error and panics. The builder is released
// before returning.
func FillFromUI[T any](into *T, xml ...string) {
builder := gtk.NewBuilder()
defer builder.Unref()

builder.SetTranslationDomain(locale.TranslationDomain)
for _, v := range xml {
if _, err := builder.AddFromString(v, -1); err != nil {
panic(err)
}
}

FillFromBuilder(into, builder)
}

// ErrHasCode returns true if and only if err is a [glib.Error] and
// its error code is code.
func ErrHasCode(err error, code int) bool {
Expand Down
Loading