diff --git a/internal/gutil/cast_test.go b/internal/gutil/cast_test.go index a27e9603..4f527120 100644 --- a/internal/gutil/cast_test.go +++ b/internal/gutil/cast_test.go @@ -50,41 +50,3 @@ func TestMustCastPanics(t *testing.T) { }() gutil.MustCast[gio.SimpleAction](str, gio.SimpleActionGLibType()) } - -func TestFillFromUI(t *testing.T) { - const ui = ` - 10 - -` - 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, ``) -} diff --git a/internal/gutil/gutil.go b/internal/gutil/gutil.go index 5e187370..41f4a38a 100644 --- a/internal/gutil/gutil.go +++ b/internal/gutil/gutil.go @@ -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 {