FFI Help wrapping ObjC

Moderators: LCMark, LCfraser

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

FFI Help wrapping ObjC

Post by PaulDaMacMan » Tue Apr 17, 2018 5:50 pm

I got this code, seems like it should work, tried a bunch of various combinations, different methods, etc.

Code: Select all

private foreign handler objC_NSURLinitURLWithString(in pURLString as ObjcId) returns ObjcId binds to "objc:NSURL.-URLWithString:"
private foreign handler objC_AVMIDIPlayer(in pFileURL as ObjcId, in pSoundFontURL as optional ObjcId, out pNSError as optional ObjcId ) returns ObjcId binds to "objc:AVMIDIPlayer.-initWithContentsOfURL:soundBankURL:error:"
private foreign handler objC_AVMIDIPlayerPrepare(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-prepareToPlay:"
private foreign handler objC_AVMIDIPlayerPlay(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-play:"
private foreign handler objC_AVMIDIPlayerStop(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-stop:"

--------------------- Public handler:

public handler MakeAVMIDIPlayerInstance(in pFileURL as String, in pSoundFontURL as String) returns ObjcId
	unsafe
		variable tAVMIDIPlayerInstance as ObjcId
		variable tFileNSURL as ObjcId
		variable tSoundFontNSURL as ObjcId
		variable tOutError as ObjcId
		--- need NSURL obj to pass to AVMIDIplayer
		put objC_NSURLinitURLwithString(StringToNSString(tFileNSURL)) into tFileNSURL
		put objC_NSURLinitURLwithString(StringToNSString(pSoundFontURL)) into tSoundFontNSURL
		put objC_AVMIDIPlayer(tFileURLNSString,tSoundFontURLNSString,) into tAVMIDIPlayerInstance
		--- objC_AVMIDIPlayerPrepare(tAVPlayerInstance)
		objC_AVMIDIPlayerPlay(tAVMIDIPlayerInstance)\
	end unsafe
end handler
No matter what I do I get execution error at line 16 which is called from

Code: Select all

put objC_AVMIDIPlayer(tFileURLNSString,tSoundFontURLNSString,) into tAVMIDIPlayerInstance
line 16 is :

Code: Select all

private foreign handler objC_AVMIDIPlayer(in pFileURL as ObjcId, in pSoundFontURL as optional ObjcId, out pNSError as optional ObjcId ) returns ObjcId binds to "objc:AVMIDIPlayer.-initWithContentsOfURL:soundBankURL:error:"
A little help would be much appreciated. T.I.A.

livecodeali
Livecode Staff Member
Livecode Staff Member
Posts: 168
Joined: Thu Apr 18, 2013 2:48 pm

Re: FFI Help wrapping ObjC

Post by livecodeali » Tue Apr 17, 2018 5:59 pm

You need to alloc an instance of AVMIDIPlayer and call initWithContentsOfURL on the instance returned from alloc. initWithContentsOfURL is an instance method (- rather than + in objective-c terminology) so it needs to be called on an instance of the class, which should be the first parameter.

(I think) The majority of objc-c classes will need to be done this way - first calling alloc (a class method) that returns a retained instance, on which the init method can be called.

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Wed Apr 18, 2018 9:01 am

livecodeali wrote:
Tue Apr 17, 2018 5:59 pm
You need to alloc an instance of AVMIDIPlayer and call initWithContentsOfURL on the instance returned from alloc. initWithContentsOfURL is an instance method (- rather than + in objective-c terminology) so it needs to be called on an instance of the class, which should be the first parameter.
Ah yes it is a - (not a +) so fixing up the bindings I should do something like this:

Code: Select all

private variable mAVMIDIPlayerInstance as ObjcRetainedId
private foreign handler objC_NSURLinitURLWithString(in pURLString as ObjcId) returns ObjcId binds to "objc:NSURL.+URLWithString:"
private foreign handler objC_AVMIDIPlayerAlloc() returns ObjcRetainedId binds to "objc:AVMIDIPlayer.+alloc"
private foreign handler objC_AVMIDIPlayer(in pAVMIDIPlayerInstance as ObjcRetainedId, in pFileNSURL as ObjcId, in pSoundFontNSURL as optional ObjcId, out pNSError as optional ObjcId) returns ObjcRetainedId binds to "objc:AVMIDIPlayer.-initWithContentsOfURL:soundBankURL:error:"
private foreign handler objC_AVMIDIPlayerPrepare(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-prepareToPlay:"
private foreign handler objC_AVMIDIPlayerPlay(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-play:"
private foreign handler objC_AVMIDIPlayerStop(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-stop:"
(I think) The majority of objc-c classes will need to be done this way - first calling alloc (a class method) that returns a retained instance, on which the init method can be called.
Hmm but what about + class methods like this:

Code: Select all

"objc:NSURL.+URLWithString:"
"Creates and returns an NSURL object initialized with a provided URL string."
That makes it sounds like allocation is done for you.

Sorry if these are OOP newb questions/mistakes, I'm trying to grok a lot of different things lately, and trying not to let show that I don't really know what I'm doing, I might be moving into areas outside of my league. I just didn't think that creating an object and passing it two NSURLs would be all that difficult. More ObjC FFI examples to look at would probably help (as would some CompSci courses probably.)
Anyway I do seem to be getting somewhere with it because I'm getting the LC Engine to crash a lot lately :-D

livecodeali
Livecode Staff Member
Livecode Staff Member
Posts: 168
Joined: Thu Apr 18, 2013 2:48 pm

Re: FFI Help wrapping ObjC

Post by livecodeali » Sat Apr 21, 2018 10:02 pm

Yes,

Code: Select all

"objc:NSURL.+URLWithString:"
is a good example of one that doesn't require a call to alloc.

I did caveat my statement with both 'I think' and 'the majority' (i.e. > 50%) :wink: it's just a sense I get from my use of it and is by no means a statement of fact!

I can't remember if I've posted elsewhere but probably helps to keep a list of 'things we have around that use obj-c bindings':

Extending LiveCode docs:
https://github.com/livecode/livecode-id ... bjective-c

iOS Button, Mac button & field
https://github.com/livecode/livecode/bl ... button.lcb
https://github.com/livecode/livecode/bl ... button.lcb
https://github.com/livecode/livecode/bl ... tfield.lcb

iOS field (WIP)
https://github.com/livecode/livecode/pull/6139/files

Objc binding tests
https://github.com/livecode/livecode/bl ... p-objc.lcb

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Tue Apr 24, 2018 7:31 pm

livecodeali wrote:
Sat Apr 21, 2018 10:02 pm
Yes,

Code: Select all

"objc:NSURL.+URLWithString:"
is a good example of one that doesn't require a call to alloc.

I did caveat my statement with both 'I think' and 'the majority' (i.e. > 50%) :wink: it's just a sense I get from my use of it and is by no means a statement of fact!

I can't remember if I've posted elsewhere but probably helps to keep a list of 'things we have around that use obj-c bindings':

Extending LiveCode docs:
https://github.com/livecode/livecode-id ... bjective-c

iOS Button, Mac button & field
https://github.com/livecode/livecode/bl ... button.lcb
https://github.com/livecode/livecode/bl ... button.lcb
https://github.com/livecode/livecode/bl ... tfield.lcb

iOS field (WIP)
https://github.com/livecode/livecode/pull/6139/files

Objc binding tests
https://github.com/livecode/livecode/bl ... p-objc.lcb
Thanks for the aggregated list of important LCB/ObjC reference stuff. Particularly interop-objc.lcb tests ( the others I already knew of but good to have them together). It's difficult to debug because 9/10 times it just reports "unable to bind" and the lcb line. Things seem to work better when you go up the inheritance hierarchy and use superclass or NSObject methods like setValue:forKey: instead of using the class' instance methods directly, at least that was the case for the thing I was messing with yesterday.

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Wed Apr 25, 2018 2:48 am

Maybe add this lib to the list, though it could probably be better (error checking :-D ). I'm slowly plugging away at wrapping AppKit things:
https://github.com/PaulMcClernan/LiveCo ... macOSTools

bn
VIP Livecode Opensource Backer
VIP Livecode Opensource Backer
Posts: 3233
Joined: Sun Jan 07, 2007 9:12 pm
Location: Bochum, Germany

Re: FFI Help wrapping ObjC

Post by bn » Wed Apr 25, 2018 6:57 am

Paul,

thank you for posting this. This is really cool, instructive and useful.

Kind regards
Bernd

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Wed Apr 25, 2018 10:58 am

Cool, thanks bn, and you're welcome. I thank Shaosean for getting me started working on that lib.

OK well this thread was originally about trying to wrap AVMIDIPlayer, and after fixing issues with object allocation and NSURL construction, etc. I've come to realize that when this thing DOES "work" it crashes the engine:

Code: Select all

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x0000000000000003

VM Regions Near 0x3:
--> 
    __TEXT                 000000010cd89000-000000010d501000 [ 7648K] r-x/rwx SM=COW  /Applications/LiveCode Community 9.0.0.app/Contents/MacOS/LiveCode-Community

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   libobjc.A.dylib               	0x00007fff8d64afb0 objc_assign_strongCast_non_gc(objc_object*, objc_object**) + 0
1   libAVFAudio.dylib             	0x00007fff86be093d -[AVMIDIPlayer initWithContentsOfURL:soundBankURL:error:] + 63
2   com.runrev.livecode           	0x000000010d07f48c ffi_call_unix64 + 76
3   com.runrev.livecode           	0x000000010d07fceb ffi_closure_unix64 + 1827
I see what looks like AVMIDIPlayer getting loaded in the crash log there. :?

livecodeali
Livecode Staff Member
Livecode Staff Member
Posts: 168
Joined: Thu Apr 18, 2013 2:48 pm

Re: FFI Help wrapping ObjC

Post by livecodeali » Fri Apr 27, 2018 1:09 pm

Ah! Sorry I totally missed a bunch of things. ObjRetainedId should be used only in the places where the call will increase the ref count- in this case the alloc call. Also you should use ObjcObject everywhere except in the actual foreign handler declarations. So:

Code: Select all

private variable mAVMIDIPlayerInstance as ObjObject
private foreign handler objC_NSURLinitURLWithString(in pURLString as ObjcId) returns ObjcId binds to "objc:NSURL.+URLWithString:"
private foreign handler objC_AVMIDIPlayerAlloc() returns ObjcRetainedId binds to "objc:AVMIDIPlayer.+alloc"
private foreign handler objC_AVMIDIPlayer(in pAVMIDIPlayerInstance as ObjcId, in pFileNSURL as ObjcId, in pSoundFontNSURL as optional ObjcId, out pNSError as optional ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-initWithContentsOfURL:soundBankURL:error:"
private foreign handler objC_AVMIDIPlayerPrepare(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-prepareToPlay:"
private foreign handler objC_AVMIDIPlayerPlay(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-play:"
private foreign handler objC_AVMIDIPlayerStop(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-stop:"

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Fri Apr 27, 2018 5:46 pm

livecodeali wrote:
Fri Apr 27, 2018 1:09 pm
Ah! Sorry I totally missed a bunch of things. ObjRetainedId should be used only in the places where the call will increase the ref count- in this case the alloc call. Also you should use ObjcObject everywhere except in the actual foreign handler declarations. So:

Code: Select all

private variable mAVMIDIPlayerInstance as ObjObject
private foreign handler objC_NSURLinitURLWithString(in pURLString as ObjcId) returns ObjcId binds to "objc:NSURL.+URLWithString:"
private foreign handler objC_AVMIDIPlayerAlloc() returns ObjcRetainedId binds to "objc:AVMIDIPlayer.+alloc"
private foreign handler objC_AVMIDIPlayer(in pAVMIDIPlayerInstance as ObjcId, in pFileNSURL as ObjcId, in pSoundFontNSURL as optional ObjcId, out pNSError as optional ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-initWithContentsOfURL:soundBankURL:error:"
private foreign handler objC_AVMIDIPlayerPrepare(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-prepareToPlay:"
private foreign handler objC_AVMIDIPlayerPlay(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-play:"
private foreign handler objC_AVMIDIPlayerStop(in pAVPlayerRef as ObjcId) returns ObjcId binds to "objc:AVMIDIPlayer.-stop:"
Made those changes, still no joy :-( The engine still crashes when I call the objC_AVMIDIPlayer FFI
Application Specific Information:
objc_msgSend() selector name: release

I'll post the whole thing later. I'm thinking it's threading or callback related but AFAIK a callback handler is not required and AVMIDIPlayer is supposed to be simple high-level stuff.

livecodeali
Livecode Staff Member
Livecode Staff Member
Posts: 168
Joined: Thu Apr 18, 2013 2:48 pm

Re: FFI Help wrapping ObjC

Post by livecodeali » Fri Apr 27, 2018 10:39 pm

Hmm, that suggests something is being over-released, which means it is likely being marked as retained when it shouldn't be... I'm sure it will be something simple!

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Sat Apr 28, 2018 1:56 am

livecodeali wrote:
Fri Apr 27, 2018 10:39 pm
Hmm, that suggests something is being over-released, which means it is likely being marked as retained when it shouldn't be... I'm sure it will be something simple!
:cry: :? :( :o :) :D :D :D :D :D :D :D

HAPPY HAPPY JOY JOY!!!

After much trial and error I got it working, not even sure what exactly did it, but I think it was actually something NOT being retained, passed the ObjcRetainedId back again on the init method and started to get further, worked out another problem with prepare & play and Viola!!! Cheesy DOS Music goodness!!!

Here's the working code:

Code: Select all

library community.macavmidiplay.paulmcclernan

use com.livecode.foreign
use com.livecode.objc
use com.livecode.widget
use com.livecode.canvas
use com.livecode.engine
use com.livecode.library.widgetutils

metadata version is "1.0.0"
metadata author is "Paul McClernan"
metadata title is "mac AVMIDIPlayer library"
metadata svgicon is "M66.43,52.07c-0.71,0.32-1.48,0.56-2.31,0.78c-2.35,0.65-5.44,0.04-5.44,0.04l-8.52,9.56c0,0,11.03,11.58,13.36,13.92 s4.02,6.19,4.02,6.19l8.77-19.76C76.31,62.8,67.18,58.92,66.43,52.07z M56.35,86.86c-2.83-2.4-16.4-15.09-16.4-15.09L21.22,90.12c0,0,3.25,1.93,5.41,1.86c5.6-0.17,10.48-3.77,15.46-3.77 c4.58,0,8.99,3.52,15.56,3.77c1.55,0.06,2.88-0.35,4.41-1.24c0.7-0.41,2.02-1.54,2.02-1.54S59.18,89.26,56.35,86.86z M52.45,18.42c5.85-5.85,5.42-15.63,5.42-15.63s-8.65,0.97-13.34,6.82c-5.22,6.51-5.07,13.39-4.94,14.51 C44.49,24.13,48.71,22.16,52.45,18.42z M18.38,48.03c-3.59,0.27-3.23,4.28-3.21,4.45l0.03,0.32l-0.23,0.23l-5.54,5.89l-5.85-5.95l8.91,24.95l18.58-18.16l-8.8-9.83 C22.28,49.93,19.79,47.92,18.38,48.03z M9.98,39.65c0,0,1.4-2.6,5.19-5.27c2.15-1.51,4.69-2.88,7.45-3.52c1.75-0.41,3.72-0.62,5.86-0.62 c3.63,0,9.12,1.35,9.12,1.35l-10.85,4.24c-1.77,0.8-2.58,2.62-2.85,3.38l14.4,13.75c3.71-3.62,6.78-6.15,9.99-9.57 c-0.51-4.23,0.89-8.11,3.22-10.81c4.73-5.48,13.47-2.78,13.47-2.78l-3.55,3.05c0,0-4.13,3.16-3.01,6.79 c0.27,0.87,0.84,2.34,2.44,3.17c0.93,0.48,4.09,0.37,5.6-0.95C68.48,40.1,70.45,38,70.45,38s0.88-1.54,1.8-2.43 c1.11-1.08,3.56-2.91,3.56-2.91s-4.82-5.38-8.14-6.83c-2.91-1.27-6.15-1.89-9.71-1.91c-7.4-0.03-12.83,4.65-16.7,4.35 c-3.19-0.25-9.47-4.38-14.6-4.35c-7.22,0.05-13.04,2.44-17.47,7.33C5.44,35.37,4,45.11,4,45.11l1.89-1.93c0,0,2.98-0.11,3.34-0.41 c1.48-1.24,1.31-2.33,1.31-2.33L9.98,39.65z M18.15,46.11c1.68-0.19,4.88,2.09,4.88,2.09l9.22,10.03l4.11-3.86L21.75,39.42l0.13-0.52c0.03-0.14,0.88-3.49,3.9-4.84 l5.18-2.02c-1.95-0.07-5.6-0.11-7.97,0.42c-3.26,0.74-8.73,4.32-10.78,7.04c0.06,0.17,0.09,0.34,0.1,0.53 c0.08,1.49-1.41,3.45-2.02,3.98c-0.73,0.63-2.55,1.03-3.63,1.21l-3.94,3.94l6.59,6.6l3.85-3.86c-0.04-1.6,0.45-3.4,1.78-4.57 C15.67,46.71,16.48,46.3,18.15,46.11z M70.95,39.93c-0.62,0.44-4.18,6.07-9.58,4.98c-2.9-0.58-4.65-3.5-4.92-5.3c-0.6-4.07,1.8-6.16,3.56-7.69 c0.59-0.52,1.6-1.31,1.6-1.31s-6.16-0.73-9.16,3.5c-3.98,5.61-1.98,9.83-1.98,9.83S13.22,80.66,12.03,81.82 c-0.09,0.56-0.5,3.71,0.93,5.31c1.56,1.74,3.06,2.79,5.34,2.24c1.3-0.32,39.44-38.44,39.44-38.44s3.65,0.56,5.67,0.07 C68.3,49.8,70.71,46.32,70.95,39.93z M65.52,84.81c-0.14,0.85-0.52,1.48-1.2,1.99c-0.5,0.41-1.12,0.62-1.88,0.62c-2.8,0-6.23-3.51-6.24-3.52L41.82,70.25 l6.44-6.55c0,0,13.44,13.7,14.11,14.36C63.03,78.73,65.79,83.22,65.52,84.81z"

private foreign handler objC_NSURLURLWithString(in pURLString as ObjcId) returns ObjcId binds to "objc:NSURL.+URLWithString:"
private foreign handler objC_NSErrorAlloc() returns ObjcRetainedId binds to "objc:NSError.+alloc"
-- private foreign handler objC_AVMIDIPlayerAlloc() returns ObjcId binds to "objc:AVMIDIPlayer.+alloc"
private foreign handler objC_AVMIDIPlayerAlloc() returns ObjcRetainedId binds to "objc:AVMIDIPlayer.+alloc"
private foreign handler objC_AVMIDIPlayer(in pAVMIDIPlayerInstance as ObjcRetainedId, in pFileNSURL as ObjcId, in pSoundFontNSURL as ObjcId, out pNSError as ObjcId) returns ObjcRetainedId binds to "objc:AVMIDIPlayer.-initWithContentsOfURL:soundBankURL:error:"
private foreign handler objC_AVMIDIPlayerPrepare(in pAVPlayerRef as ObjcRetainedId) returns nothing binds to "objc:AVMIDIPlayer.-prepareToPlay"
private foreign handler objC_AVMIDIPlayerPlay(in pAVPlayerRef as ObjcRetainedId, in pAVPlayDone as optional pointer) returns nothing binds to "objc:AVMIDIPlayer.-play:"
private foreign handler objC_AVMIDIPlayerStop(in pAVPlayerRef as ObjcRetainedId) returns nothing binds to "objc:AVMIDIPlayer.-stop:"

--------------------- Public handler:

-- private variable mAVMIDIPlayerInstance as ObjcObject
private variable mAVMIDIPlayerInstance as ObjcRetainedId

public handler macAVMIDIPlayer(in pFileURL as String, in pSoundFontURL as String) returns nothing
		--- need NSURL objects to pass to AVMIDIplayer
		variable tFileNSURL as ObjcObject
		variable tSoundFontNSURL as ObjcObject
		--- need NSError object to pass to AVMIDIplayer
		variable tNSError as optional ObjcObject

		variable tAVMIDIPlayerCompletionHandler as optional pointer
	unsafe
		put objC_NSURLURLwithString(StringToNSString(pFileURL)) into tFileNSURL
		put objC_NSURLURLwithString(StringToNSString(pSoundFontURL)) into tSoundFontNSURL
		-- put objC_NSErrorAlloc() into tNSError

    	put objC_AVMIDIPlayerAlloc() into mAVMIDIPlayerInstance
		put objC_AVMIDIPlayer(mAVMIDIPlayerInstance, tFileNSURL ,tSoundFontNSURL, tNSError) into mAVMIDIPlayerInstance
		objC_AVMIDIPlayerPrepare(mAVMIDIPlayerInstance)
		objC_AVMIDIPlayerPlay(mAVMIDIPlayerInstance, tAVMIDIPlayerCompletionHandler)
	end unsafe

end handler

end library

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Sat Apr 28, 2018 2:03 am

I put it up on github along with the test stack, a MIDI file and a small (cheesy) General MIDI sound font:
https://github.com/PaulMcClernan/LCB_macAVMidiPlayer

PaulDaMacMan
Posts: 190
Joined: Wed Apr 24, 2013 4:53 pm

Re: FFI Help wrapping ObjC

Post by PaulDaMacMan » Sat Apr 28, 2018 3:33 pm

At least part of the problem was that passing a valid URL to NSURL object that doesn't actually connect to a local file is not an error but it seems it would crash the LC engine when the NSURL was passed to the AVMIDIPlayer object, making things harder to diagnose. I didn't realize what was going on until I started moving files around.

bn
VIP Livecode Opensource Backer
VIP Livecode Opensource Backer
Posts: 3233
Joined: Sun Jan 07, 2007 9:12 pm
Location: Bochum, Germany

Re: FFI Help wrapping ObjC

Post by bn » Sat Apr 28, 2018 8:48 pm

Hi Paul,

I installed the library and the .mid files and .sf2 files

Works like a charm once I understood that not all .mid files play with all sound files.

Nice to have "boing" back...
It feels like a miracle, kind of, to me.
And I imagine you felt the same.

I am _almost_ tempted to look into ObjC and the MacOS APIs, but just almost :)

Congratulations

Kind regards
Bernd

Post Reply

Return to “LiveCode Builder”