From f4a356090547a13e2d8295e0d39da666ff517a81 Mon Sep 17 00:00:00 2001 From: DEMAREY Christophe Date: Tue, 16 Apr 2024 14:50:21 +0200 Subject: [PATCH] add an API to easily build Mac Os menus --- src/ObjectiveC-Cocoa/CocoaMenu.class.st | 102 +++++++++---- src/ObjectiveC-Cocoa/CocoaMenuItem.class.st | 33 +++-- .../CocoaMenuSeparator.class.st | 17 +++ src/ObjectiveC-Cocoa/CocoaMenuTarget.class.st | 5 +- .../CocoaServicesMenu.class.st | 42 ++++++ src/ObjectiveC-Cocoa/String.extension.st | 14 +- src/ObjectiveC/ObjCLibrary.class.st | 135 ++++++++++++++++++ 7 files changed, 307 insertions(+), 41 deletions(-) create mode 100644 src/ObjectiveC-Cocoa/CocoaMenuSeparator.class.st create mode 100644 src/ObjectiveC-Cocoa/CocoaServicesMenu.class.st diff --git a/src/ObjectiveC-Cocoa/CocoaMenu.class.st b/src/ObjectiveC-Cocoa/CocoaMenu.class.st index 4a20c42..b2256c6 100644 --- a/src/ObjectiveC-Cocoa/CocoaMenu.class.st +++ b/src/ObjectiveC-Cocoa/CocoaMenu.class.st @@ -1,11 +1,15 @@ +" +I represent an item of a Cocoa Menu. +I can hold items or submenus. +" Class { #name : #CocoaMenu, #superclass : #Object, #instVars : [ 'nsMenu', + 'items', 'title', 'nsTitle', - 'items', 'menuItem' ], #classVars : [ @@ -14,15 +18,23 @@ Class { #category : #'ObjectiveC-Cocoa-Menus' } +{ #category : #'instance creation' } +CocoaMenu class >> with: aNSMenu [ + + ^ self new + setMenu: aNSMenu; + yourself +] + { #category : #adding } -CocoaMenu >> addItemWithTitle: aString action: aFullBlockClosure [ +CocoaMenu >> addItemWithTitle: aString action: aBlock [ - ^ self addItemWithTitle: aString action: aFullBlockClosure shortcut: '' + ^ self addItemWithTitle: aString action: aBlock shortcut: '' ] { #category : #adding } CocoaMenu >> addItemWithTitle: aTitle action: actionBlock shortcut: shortcutString [ - + items add: (CocoaMenuItem new title: aTitle; action: actionBlock; @@ -31,60 +43,96 @@ CocoaMenu >> addItemWithTitle: aTitle action: actionBlock shortcut: shortcutStri ] { #category : #adding } -CocoaMenu >> addSubmenu: aTitle with: builderBlock [ - - | m | - m := self class new. - m title: aTitle. - builderBlock value: m. - items add: m. - ^ m +CocoaMenu >> addSeparator [ + + items add: CocoaMenuSeparator new ] { #category : #adding } -CocoaMenu >> addToMenu: aCocoaMenu [ +CocoaMenu >> addServicesMenu [ + + items add: CocoaServicesMenu new +] + +{ #category : #adding } +CocoaMenu >> addSubmenu: aTitle with: builderBlock [ + | menu | + menu := self class new. + menu title: aTitle. + builderBlock value: menu. + items add: menu. + ^ menu +] + +{ #category : #private } +CocoaMenu >> addToMenu: aCocoaMenu [ + + self buildNSMenu. - - menuItem := aCocoaMenu nsMenu addItemWithTitle: nsTitle action: ObjCObject nil keyEquivalent:'' asNSString. + menuItem := aCocoaMenu nsMenu + addItemWithTitle: nsTitle + action: ObjCObject nil + keyEquivalent: '' asNSString. aCocoaMenu nsMenu setSubmenu: nsMenu forItem: menuItem. ] -{ #category : #building } +{ #category : #configuring } +CocoaMenu >> beMainMenu [ + + self buildNSMenu. + #NSApplication inObjC sharedApplication setMainMenu: nsMenu. + MainMenu := self +] + +{ #category : #private } CocoaMenu >> buildNSMenu [ + - nsTitle := title asNSString. + nsTitle := (title ifNil: [ '' ]) asNSString. nsMenu := #NSMenu inObjC alloc initWithTitle: nsTitle. - - items do: [ :i | i addToMenu: self ]. - + items do: [ :item | item addToMenu: self ]. ^ nsMenu ] { #category : #initialization } CocoaMenu >> initialize [ - + super initialize. items := OrderedCollection new ] { #category : #accessing } -CocoaMenu >> nsMenu [ +CocoaMenu >> nsMenu [ + ^ nsMenu ] -{ #category : #initialization } -CocoaMenu >> setAsMainMenu [ +{ #category : #printing } +CocoaMenu >> printOn: aStream [ + aStream << self class name << '(' << (title ifNil: [ '' ]) << ')' +] + +{ #category : #removing } +CocoaMenu >> removeAllItems [ + + nsMenu removeAllItems. - self buildNSMenu. - #NSApplication inObjC sharedApplication setMainMenu: nsMenu. + self flag: 'TODO: release ObjC menu items'. + items := OrderedCollection new. + +] - MainMenu := self +{ #category : #private } +CocoaMenu >> setMenu: aNSMenu [ + + nsMenu := aNSMenu ] { #category : #accessing } CocoaMenu >> title: aString [ + title := aString ] diff --git a/src/ObjectiveC-Cocoa/CocoaMenuItem.class.st b/src/ObjectiveC-Cocoa/CocoaMenuItem.class.st index dfbe42d..13f1165 100644 --- a/src/ObjectiveC-Cocoa/CocoaMenuItem.class.st +++ b/src/ObjectiveC-Cocoa/CocoaMenuItem.class.st @@ -1,10 +1,14 @@ +" +I represent an item of a Cocoa menu. +I can hold menu items or submenus. +" Class { #name : #CocoaMenuItem, #superclass : #Object, #instVars : [ - 'title', 'action', 'shortcut', + 'title', 'target', 'nsTitle', 'nsShortcut', @@ -14,31 +18,40 @@ Class { } { #category : #accessing } -CocoaMenuItem >> action: aFullBlockClosure [ - action := aFullBlockClosure +CocoaMenuItem >> action: aBlock [ + + action := aBlock ] { #category : #adding } -CocoaMenuItem >> addToMenu: aCocoaMenu [ - - target := CocoaMenuTarget new block: action; yourself. - ObjCProxyClass newFor: target. +CocoaMenuItem >> addToMenu: aCocoaMenu [ + nsTitle := title asNSString. nsShortcut := shortcut asNSString. - - menuItem := aCocoaMenu nsMenu addItemWithTitle: nsTitle action: #execute asObjCSelector keyEquivalent: nsShortcut. + + menuItem := aCocoaMenu nsMenu + addItemWithTitle: nsTitle + action: #execute asObjCSelector + keyEquivalent: nsShortcut. + + target := CocoaMenuTarget new + block: action; + yourself. + ObjCProxyClass newFor: target. menuItem setTarget: target. - menuItem setEnabled: true. + menuItem setEnabled: true ] { #category : #accessing } CocoaMenuItem >> shortcut: aString [ + shortcut := aString ] { #category : #accessing } CocoaMenuItem >> title: aString [ + title := aString ] diff --git a/src/ObjectiveC-Cocoa/CocoaMenuSeparator.class.st b/src/ObjectiveC-Cocoa/CocoaMenuSeparator.class.st new file mode 100644 index 0000000..5d59bf7 --- /dev/null +++ b/src/ObjectiveC-Cocoa/CocoaMenuSeparator.class.st @@ -0,0 +1,17 @@ +" +A Cocoa menu separator. I'm also a menu item. +" +Class { + #name : #CocoaMenuSeparator, + #superclass : #Object, + #category : #'ObjectiveC-Cocoa-Menus' +} + +{ #category : #adding } +CocoaMenuSeparator >> addToMenu: aCocoaMenu [ + + + | separator | + separator := #NSMenuItem inObjC separatorItem. + aCocoaMenu nsMenu addItem: separator +] diff --git a/src/ObjectiveC-Cocoa/CocoaMenuTarget.class.st b/src/ObjectiveC-Cocoa/CocoaMenuTarget.class.st index 80eb8bf..09408ca 100644 --- a/src/ObjectiveC-Cocoa/CocoaMenuTarget.class.st +++ b/src/ObjectiveC-Cocoa/CocoaMenuTarget.class.st @@ -1,3 +1,6 @@ +" +I represent a Cocoa menu target, i.e. the ObjectiveC object that will receive the callbacks. +" Class { #name : #CocoaMenuTarget, #superclass : #Object, @@ -20,7 +23,7 @@ CocoaMenuTarget >> block: anObject [ block := anObject ] -{ #category : #accessing } +{ #category : #execution } CocoaMenuTarget >> execute [ diff --git a/src/ObjectiveC-Cocoa/CocoaServicesMenu.class.st b/src/ObjectiveC-Cocoa/CocoaServicesMenu.class.st new file mode 100644 index 0000000..0057b1a --- /dev/null +++ b/src/ObjectiveC-Cocoa/CocoaServicesMenu.class.st @@ -0,0 +1,42 @@ +" +I represent the Mac os ""Services"" menu. +You can use me to add a ""Services"" entry to your app menu. +" +Class { + #name : #CocoaServicesMenu, + #superclass : #CocoaMenu, + #category : #'ObjectiveC-Cocoa-Menus' +} + +{ #category : #configuring } +CocoaServicesMenu >> beServicesMenu [ + + + #NSApplication inObjC sharedApplication setServicesMenu: nsMenu +] + +{ #category : #private } +CocoaServicesMenu >> buildNSMenu [ + + nsMenu := self servicesMenu. + nsMenu isNull + ifTrue: [ + super buildNSMenu. + self beServicesMenu ] + ifFalse: [ nsTitle := title asNSString ]. + ^ nsMenu +] + +{ #category : #initialization } +CocoaServicesMenu >> initialize [ + + super initialize. + title := 'Services' +] + +{ #category : #accessing } +CocoaServicesMenu >> servicesMenu [ + "Get the Application services menu if set or null" + + ^ #NSApplication inObjC sharedApplication servicesMenu +] diff --git a/src/ObjectiveC-Cocoa/String.extension.st b/src/ObjectiveC-Cocoa/String.extension.st index 6408c82..b6156b7 100644 --- a/src/ObjectiveC-Cocoa/String.extension.st +++ b/src/ObjectiveC-Cocoa/String.extension.st @@ -2,7 +2,15 @@ Extension { #name : #String } { #category : #'*ObjectiveC-Cocoa' } String >> asNSString [ - ^ self - ifNotEmpty: [ #NSString inObjC alloc initWithUTF8String: self ] - ifEmpty: [ #NSString inObjC string ] + + + | encoded param | + encoded := self utf8Encoded. + param := ByteArray new: encoded size + 1. + param pinInMemory. + + LibC memCopy: encoded to: param size: encoded size. + param at: encoded size + 1 put: 0. + + ^ #NSString inObjC alloc initWithUTF8String: param ] diff --git a/src/ObjectiveC/ObjCLibrary.class.st b/src/ObjectiveC/ObjCLibrary.class.st index d7dc732..b6307f4 100644 --- a/src/ObjectiveC/ObjCLibrary.class.st +++ b/src/ObjectiveC/ObjCLibrary.class.st @@ -61,16 +61,69 @@ ObjCLibrary >> calloutAPIClass [ ^ TFCalloutAPI ] +{ #category : #'objc - creating' } +ObjCLibrary >> class_addMethodClass: cls selector: name implementation: imp signature: types [ + + ^ self ffiCall: #(int class_addMethod(void* cls, void* name, void* imp, const char *types)) +] + { #category : #'accessing platform' } ObjCLibrary >> ffiBindingOf: aName [ ^ self class ffiBindingOf: aName ] +{ #category : #'objc - accessing' } +ObjCLibrary >> lookupClass: aString [ + + ^ self ffiCall: #(void* objc_lookUpClass(char *aString)) +] + +{ #category : #'objc - accessing' } +ObjCLibrary >> lookupSelector: aString [ + + ^ self ffiCall: #(void* sel_registerName(const char *aString)) +] + { #category : #'accessing platform' } ObjCLibrary >> macLibraryName [ ^ 'libobjc.dylib' ] +{ #category : #'objc - converting' } +ObjCLibrary >> nsStringOf: aString [ + + | class encoded param | + + encoded := aString utf8Encoded. + param := ByteArray new: encoded size + 1. + param pinInMemory. + + LibC memCopy: encoded to: param size: encoded size. + param at: encoded size + 1 put: 0. + + class := self lookupClass: 'NSString'. + ^ self sendMessageNamed: 'stringWithUTF8String:' to: class with: param +] + +{ #category : #'objc - creating' } +ObjCLibrary >> objc_allocateClassPairSuperclass: superclass name: name extraBytes: extraBytes [ + + ^ self ffiCall: #(void* objc_allocateClassPair(void* superclass, const char *name, size_t extraBytes)) +] + +{ #category : #'objc - creating' } +ObjCLibrary >> objc_registerClassPair: cls [ + "Registers a class that was allocated using objc_allocateClassPair" + + self ffiCall: #(void objc_registerClassPair(void* cls)) +] + +{ #category : #'objc - releasing' } +ObjCLibrary >> release: aObjCObject [ + + self sendMessageNamed: 'release' to: aObjCObject +] + { #category : #accessing } ObjCLibrary >> runner [ @@ -82,3 +135,85 @@ ObjCLibrary >> runner [ ^ runner ] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessage: sel to: cls [ + + ^ self ffiCall: #(void* objc_msgSend(void* cls, void* sel)) +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessage: sel to: rcv with: aParam [ + + ^ self ffiCall: #(void* objc_msgSend(void* rcv, void* sel, void* aParam)) +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessage: sel to: rcv with: aParam1 with: aParam2 [ + + ^ self ffiCall: #(void* objc_msgSend(void* rcv, void* sel, void* aParam1, void* aParam2)) +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessage: sel to: rcv with: aParam1 with: aParam2 with: aParam3 [ + + ^ self ffiCall: #(void* objc_msgSend(void* rcv, void* sel, void* aParam1, void* aParam2, void* aParam3)) +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessage: sel to: rcv withInteger: aParam [ + + ^ self ffiCall: #(void* objc_msgSend(void* rcv, void* sel, int aParam)) +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessageNamed: selectorName to: cls [ + + ^ self sendMessage: (self lookupSelector: selectorName) to: cls +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessageNamed: selectorName to: rcv with: aParam [ + | sel | + + sel := self lookupSelector: selectorName. + ^ self sendMessage: sel to: rcv with: aParam +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessageNamed: selectorName to: rcv with: aParam1 with: aParam2 [ + | sel | + + sel := self lookupSelector: selectorName. + ^ self sendMessage: sel to: rcv with: aParam1 with: aParam2 +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessageNamed: selectorName to: rcv with: aParam1 with: aParam2 with: aParam3 [ + | sel | + + sel := self lookupSelector: selectorName. + ^ self sendMessage: sel to: rcv with: aParam1 with: aParam2 with: aParam3 +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessageNamed: selectorName to: rcv withInteger: aParam [ + | sel | + + sel := self lookupSelector: selectorName. + ^ self sendMessage: sel to: rcv withInteger: aParam +] + +{ #category : #'objc - message sending' } +ObjCLibrary >> sendMessageNamed: selectorName toClassNamed: className [ + + ^ self sendMessage: (self lookupSelector: selectorName) to: (self lookupClass: className) +] + +{ #category : #'objc - accessing' } +ObjCLibrary >> sharedApplication [ + + ^ self + sendMessageNamed: 'sharedApplication' + toClassNamed: 'NSApplication' +]