Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Higher level subclassing API #207

Merged
merged 2 commits into from
Aug 26, 2023
Merged
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
46 changes: 23 additions & 23 deletions macos/_examples/subclass/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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"))

Expand Down
10 changes: 5 additions & 5 deletions objc/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand All @@ -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")
}
Expand All @@ -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)

Expand Down
8 changes: 4 additions & 4 deletions objc/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}

Expand All @@ -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()
}

Expand Down
33 changes: 33 additions & 0 deletions objc/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ package objc
// const char* Object_Description(void* ptr);
import "C"
import (
"reflect"
"unsafe"
)

Expand Down Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions objc/userclass.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
80 changes: 80 additions & 0 deletions objc/userclass_test.go
Original file line number Diff line number Diff line change
@@ -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"),
)
})
}
Loading