diff --git a/macos/_examples/subclass/main.go b/macos/_examples/subclass/main.go index ff650de6..84ee956f 100644 --- a/macos/_examples/subclass/main.go +++ b/macos/_examples/subclass/main.go @@ -8,44 +8,44 @@ import ( "github.com/progrium/macdriver/objc" ) -type TestView struct { - objc.Object +type CustomView struct { + appkit.View `objc:"NSView"` } -func (v TestView) acceptsFirstResponder() bool { +func (v CustomView) AcceptsFirstResponder() bool { return true } -func (v TestView) keyDown(event appkit.Event) { +func (v CustomView) KeyDown(event appkit.Event) { log.Println("Keydown:", v.Class().Name(), event.Class().Name()) } -func (v TestView) dividerThickness() float64 { +type CustomSplitView struct { + appkit.SplitView `objc:"NSSplitView"` +} + +func (v CustomSplitView) DividerThickness() float64 { return 10.0 } -func (v TestView) dividerColor() appkit.Color { +func (v CustomSplitView) DividerColor() appkit.Color { return appkit.Color_BlackColor() } func main() { log.Println("Program started.") - // Create a SplitView subclass using AllocateClass - SplitViewClass := objc.AllocateClass(objc.GetClass("NSSplitView"), "TestSplitView", 0) - objc.AddMethod(SplitViewClass, objc.Sel("acceptsFirstResponder"), (TestView).acceptsFirstResponder) - objc.AddMethod(SplitViewClass, objc.Sel("keyDown:"), (TestView).keyDown) - - // Implement these methods for the dividerThickness and dividerColor properties on the subclass - objc.AddMethod(SplitViewClass, objc.Sel("dividerThickness"), (TestView).dividerThickness) - objc.AddMethod(SplitViewClass, objc.Sel("dividerColor"), (TestView).dividerColor) - - objc.RegisterClass(SplitViewClass) + CustomViewClass := objc.NewClass[CustomView]( + objc.Sel("acceptsFirstResponder"), + objc.Sel("keyDown:"), + ) + objc.RegisterClass(CustomViewClass) - ViewClass := objc.AllocateClass(objc.GetClass("NSView"), "TestView", 0) - objc.AddMethod(ViewClass, objc.Sel("acceptsFirstResponder"), (TestView).acceptsFirstResponder) - objc.AddMethod(ViewClass, objc.Sel("keyDown:"), (TestView).keyDown) - objc.RegisterClass(ViewClass) + CustomSplitViewClass := objc.NewClass[CustomSplitView]( + objc.Sel("dividerThickness"), + objc.Sel("dividerColor"), + ) + objc.RegisterClass(CustomSplitViewClass) app := appkit.Application_SharedApplication() @@ -64,11 +64,11 @@ func main() { win.SetTitle("Hello world") win.SetLevel(appkit.MainMenuWindowLevel + 2) - view := appkit.SplitViewFrom(SplitViewClass.CreateInstance(0).Ptr()).InitWithFrame(frame) + view := CustomSplitViewClass.New().InitWithFrame(frame) view.SetVertical(true) - neatView := appkit.ViewFrom(ViewClass.CreateInstance(0).Ptr()).InitWithFrame(rectOf(0, 0, 150, 99)) - coolView := appkit.ViewFrom(ViewClass.CreateInstance(0).Ptr()).InitWithFrame(rectOf(10, 0, 150, 99)) + neatView := CustomViewClass.New().InitWithFrame(rectOf(0, 0, 150, 99)) + coolView := CustomViewClass.New().InitWithFrame(rectOf(10, 0, 150, 99)) neatView.AddSubview(appkit.NewLabel("NEAT")) coolView.AddSubview(appkit.NewLabel("COOL")) diff --git a/objc/call.go b/objc/call.go index 62a5d638..388d9a09 100644 --- a/objc/call.go +++ b/objc/call.go @@ -79,7 +79,7 @@ func directPointer(t reflect.Type) bool { // AddMethod adds an instance method using a Go function. // The first argument of the Go function should be the object instance, // the second argument should be the method selector. -func AddMethod(class Class, sel Selector, f any) bool { +func AddMethod(class IClass, sel Selector, f any) bool { rf := reflect.ValueOf(f) typeEncoding := _getMethodTypeEncoding(rf.Type(), false) @@ -95,7 +95,7 @@ func AddMethod(class Class, sel Selector, f any) bool { // ReplaceMethod replaces an instance method using a Go function. // The first argument of the Go function should be the object instance, // the second argument should be the method selector. -func ReplaceMethod(class Class, sel Selector, f any) { +func ReplaceMethod(class IClass, sel Selector, f any) { rf := reflect.ValueOf(f) typeEncoding := _getMethodTypeEncoding(rf.Type(), false) @@ -110,12 +110,12 @@ func ReplaceMethod(class Class, sel Selector, f any) { // AddClassMethod adds a class method using a Go function. // The first argument of the Go function should be the class, // the second argument should be the method selector. -func AddClassMethod(class Class, sel Selector, f any) bool { +func AddClassMethod(class IClass, sel Selector, f any) bool { rf := reflect.ValueOf(f) typeEncoding := _getMethodTypeEncoding(rf.Type(), true) imp, handle := wrapGoFuncAsMethodIMP(rf) - metaClass := class.Class() + metaClass := class.MetaClass() if metaClass.Ptr() == nil { panic("no meta class") } @@ -129,7 +129,7 @@ func AddClassMethod(class Class, sel Selector, f any) bool { // ReplaceClassMethod replaces a class method using a Go function. // The first argument of the Go function should be the class, // the second argument should be the method selector. -func ReplaceClassMethod(class Class, sel Selector, f any) { +func ReplaceClassMethod(class IClass, sel Selector, f any) { rf := reflect.ValueOf(f) typeEncoding := _getMethodTypeEncoding(rf.Type(), true) diff --git a/objc/class.go b/objc/class.go index 0c02187b..f9e43881 100644 --- a/objc/class.go +++ b/objc/class.go @@ -42,7 +42,7 @@ type IClass interface { Name() string SetVersion(version int) Version() int - Class() Class + MetaClass() Class SuperClass() Class RespondsToSelector(sel Selector) bool AddMethod(sel Selector, imp IMP, types string) bool @@ -90,14 +90,14 @@ func AllocateClass(superClass Class, name string, extraBytes uint) Class { // Registers a class that was allocated using [AllocateClass] [Full Topic] // // [Full Topic]: https://developer.apple.com/documentation/objectivec/1418603-objc_registerclasspair?language=objc -func RegisterClass(class Class) { +func RegisterClass(class IClass) { C.Objc_RegisterClassPair(class.Ptr()) } // Destroys a class and its associated metaclass. [Full Topic] // // [Full Topic]: https://developer.apple.com/documentation/objectivec/1418912-objc_disposeclasspair?language=objc -func DisposeClass(class Class) { +func DisposeClass(class IClass) { C.Objc_DisposeClassPair(class.Ptr()) } @@ -112,7 +112,7 @@ func (c Class) Name() string { return name } -func (c Class) Class() Class { +func (c Class) MetaClass() Class { return ObjectFrom(c.ptr).Class() } diff --git a/objc/object.go b/objc/object.go index b8f9a2ce..9319427f 100644 --- a/objc/object.go +++ b/objc/object.go @@ -28,6 +28,7 @@ package objc // const char* Object_Description(void* ptr); import "C" import ( + "reflect" "unsafe" ) @@ -70,6 +71,38 @@ func Ptr(o Handle) unsafe.Pointer { return o.Ptr() } +func setPtr(obj any, ptr unsafe.Pointer) { + if o, ok := obj.(*Object); ok { + o.ptr = ptr + } else { + if objPtr := findObjectPointer(reflect.ValueOf(obj)); objPtr != nil { + objPtr.ptr = ptr + } else { + panic("unable to find embedded object") + } + } +} + +func findObjectPointer(v reflect.Value) *Object { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + t := v.Type() + for i := 0; i < v.NumField(); i++ { + if t.Field(i).Type == reflect.TypeOf(Object{}) { + ptr := v.Field(i).Addr().Interface().(*Object) + return ptr + } + if t.Field(i).Anonymous { + f := findObjectPointer(v.Field(i)) + if f != nil { + return f + } + } + } + return nil +} + // The root class of most Objective-C class hierarchies, from which subclasses inherit a basic interface to the runtime system and the ability to behave as Objective-C objects. [Full Topic] // // [Full Topic]: https://developer.apple.com/documentation/objectivec/nsobject?language=objc diff --git a/objc/userclass.go b/objc/userclass.go new file mode 100644 index 00000000..788c342b --- /dev/null +++ b/objc/userclass.go @@ -0,0 +1,47 @@ +package objc + +import "reflect" + +// UserClass is a generic wrapper around Class returned by NewClass. +type UserClass[T IObject] struct { + Class +} + +// New creates an instance of the class then calls init and autorelease before returning. +func (c UserClass[T]) New() T { + var o T + oo := c.CreateInstance(0).PerformSelector(Sel("init")) + setPtr(&o, oo.Ptr()) + o.Autorelease() + return o +} + +// NewClass will allocate a new class using the name of the type passed +// and NSObject as the superclass unless the passed type has a struct tag +// with the key "objc" specifying the name of the superclass to use. +// The returned class will still need to be registered. +func NewClass[T IObject](selectors ...Selector) UserClass[T] { + var o T + typ := reflect.TypeOf(o) + var super string + for i := 0; i < typ.NumField(); i++ { + super = typ.Field(i).Tag.Get("objc") + if super != "" { + break + } + } + if super == "" { + super = "NSObject" + } + cls := AllocateClass(GetClass(super), typ.Name(), 0) + for _, sel := range selectors { + m, ok := typ.MethodByName(selectorToGoName(sel.Name())) + if !ok { + panic("allocating class from struct without method for selector: " + sel.Name()) + } + AddMethod(cls, sel, m.Func.Interface()) + } + return UserClass[T]{ + Class: cls, + } +} diff --git a/objc/userclass_test.go b/objc/userclass_test.go new file mode 100644 index 00000000..0ce06ce5 --- /dev/null +++ b/objc/userclass_test.go @@ -0,0 +1,80 @@ +package objc + +import ( + "testing" + + "github.com/progrium/macdriver/internal/assert" +) + +// uses NSObject as super without struct tag, regardless of embedded type. +type FooObject struct { + Object +} + +func (f FooObject) MethodForFoo() string { + return "foo" +} + +// uses FooObject from struct tag as super, which should be registered first. +type BarObject struct { + FooObject `objc:"FooObject"` +} + +func (b BarObject) MethodForBar() string { + return "bar" +} + +func TestNewClass(t *testing.T) { + FooObjectClass := NewClass[FooObject]( + Sel("methodForFoo"), + ) + RegisterClass(FooObjectClass) + + BarObjectClass := NewClass[BarObject]( + Sel("methodForBar"), + ) + RegisterClass(BarObjectClass) + + // as per our rules, New returned objects are autoreleased + WithAutoreleasePool(func() { + foo := FooObjectClass.New() + // foo go method + if foo.MethodForFoo() != "foo" { + t.Fatal("unexpected return") + } + // foo objc method + fooRet := foo.PerformSelector(Sel("methodForFoo")) + if ToGoString(fooRet.Ptr()) != "foo" { + t.Fatal("unexpected return") + } + + bar := BarObjectClass.New() + // bar go method + if bar.MethodForBar() != "bar" { + t.Fatal("unexpected return") + } + // bar objc method + barRet := bar.PerformSelector(Sel("methodForBar")) + if ToGoString(barRet.Ptr()) != "bar" { + t.Fatal("unexpected return") + } + // as well as foo go method + if bar.MethodForFoo() != "foo" { + t.Fatal("unexpected return") + } + // and foo objc method + barRet = bar.PerformSelector(Sel("methodForFoo")) + if ToGoString(barRet.Ptr()) != "foo" { + t.Fatal("unexpected return") + } + }) + +} + +func TestNewClassNoMethod(t *testing.T) { + assert.Panics(t, func() { + NewClass[FooObject]( + Sel("noMethodLikeThisOnFooObject"), + ) + }) +}