ホームページ>開発ツール>Xojo / Real Studio Trial and Error・XcodeとCocoaのDeclareでDockTilePlugInを試す(メニュー編)
Xojo / Real Studio Trial and Error
目次
XcodeとCocoaのDeclareでDockTilePlugInを試す(メニュー編)
はじめに
以下は、Xojo Cocoaビルドについての話題です。
DockTilePlugInの、メニュー関連の処理について調べてみました。
なお検証には、Xojo 2021 Release 2.1とXcode 13.3.1を用いています。(Mac mini 2018 + macOS 12.3.1 Monterey)
方針
前回積み残しとなっていたメニュー関連ですが、手持ちのアプリを調べたところ、起動時にDockTilePlugInが提供するメニュー(以下、ドックメニュー)を表示するアプリはいくつかあったものの、(純正を含めて)非起動時に表示するものはありませんでした。
なぜないのかは定かではありませんが、仕様上は認められている訳ですから、とりあえずやってみることにしました。
ドックメニューもアイコン同様、プラグインとアプリ本体の両方に実装します。プラグインのものは非起動時、アプリ本体のものは起動時に使用されます。
なので、それぞれ別の機能を割り付けることもできます。
作成と表示に関しては、前回のサンプルが参考になります。(実行のフェイズは含まれていないが、通常のメニューと同一。)
さて、メニューの役割として、アプリ本体を起動したい場合もあろうかと思われますが、仕様を眺めてみても、NSDockTileまたはNSDockTilePlugInからアプリ本体を取得することはできないようです。
一般的に自分自身を取得するには、 [NSBundle mainBundle]が作法のようですが、プラグインの場合は、ドックタイルを管理するDock.appが取得されてしまいます。
調べたら、mainBundleではなく、bundleWithIdentifier:でプラグインのIdentifierを指定すると、プラグイン自身が取得できることが分かりました。
(ちなみに、プラグイン内からアプリ本体のIdentifierを指定しても、取得はできなかった。やり方が悪かったのかもしれないが…)
プラグインが取得できれば、3階層上がることで、アプリ本体(パッケージ)を取得することができます。(注:3階層は一般的なプラグイン配置の場合。)
参考サイト(1):objective c - Find parent directory of a path - Stack Overflow
また、起動時にパラメーターを渡したい時は、(他にもあるかもしれないが)以下の方法で対処できます。
プラグイン(Xcode):openApplicationAtURL:configuration:completionHandler:を使う。(パラメーターは、configurationのsetArguments:で指定)
アプリ本体(Xojo):System.CommandLineで取得。(ただし、アプリパスとパラメーターが、半角スペースで区切られたベタな文字列として渡ってくるので、(例えばapp.ExecutableFile.nativePathを併用して)自分で分解する必要がある。)
参考サイト(2):System.CommandLine - General - Xojo Programming Forum
参考サイト(3):macos - - [NSWorkspace OpenApplicazioneTurl: Configurazione: Configurazione: CompletamentoHandler:] Non funziona in Articolo di accesso - ItCodeGeeks.com
アプリ本体(Xojo)へのドックメニューの実装は、例によってObjective-CのランタイムAPIを用います。
必要なapplicationDockMenu:は、NSApplicationDelegateのメソッドですが、プロトコルなので何を指定すればいいのかと思ったら、答えは以下にありました。
参考サイト(4):Dock Menu - General - Xojo Programming Forum
以上を踏まえ、テストアプリを作ってみることにします。仕様は以下の通りとしました。
- プラグイン:ドックメニューは、メインウィンドウの「背景色を指定して起動(シアン/マゼンタ/イエローから選択)」
- プラグイン:メニューが選択されたら、アプリを起動。パラメーター(色名)はコマンドラインで渡す。
- アプリ本体:ドックメニューは、「背景色をクリア」
- アプリ本体:起動時のパラメーター(色名)はコマンドラインから受け取る。
- アプリ本体:アイコン編と同様、info.plistとPluginsフォルダーを追加。
XcodeでのPluginビルド
注:以下は、Xcode上でプラグインをデバッグするための、本体側の設定ステップを含みます。
- Xcodeで新規プロジェクトを作成(テンプレートダイアログで「App」を選択。Product Nameはここでは「NSDockTilePlugIn3」)
- プロジェクトにNew Groupを追加。(名前はここでは「DockTilePlugin3」)
- DockTilePlugin3に「New File...」でファイル作成(テンプレートダイアログで「Cocoa Class」を選択。ファイル名はここでは「DockTilePlugIn3」)
- DockTilePlugIn3.hの#import〜@endを、以下に置き換え
#import <Cocoa/Cocoa.h> NS_ASSUME_NONNULL_BEGIN @interface DockTilePlugIn3 : NSObject
{ NSMenu *dockMenu; } @end - DockTilePlugIn3.mの#import〜@endを、以下に置き換え
#import "DockTilePlugIn3.h" @implementation DockTilePlugIn3 -(void)doAction1:(id)sender { // 自分自身しか取れない(親アプリでさえ取れない)、っぽい。 NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.mycompany.DockTilePlugIn3"]; // Identifierは各自の設定に合わせる NSURL *fileURL = [bundle bundleURL]; NSURL *appURL = [fileURL URLByDeletingLastPathComponent]; // 1階層上に移動 appURL = [appURL URLByDeletingLastPathComponent]; // 1階層上に移動 appURL = [appURL URLByDeletingLastPathComponent]; // 1階層上に移動 // 背景色を文字列として保持 NSString *arg1; NSInteger tag = [sender tag]; switch (tag) { case 1: arg1 = @"Cyan"; break; case 2: arg1 = @"Magenta"; break; case 3: arg1 = @"Yellow"; break; default: arg1 = @"White"; break; } // アプリを起動 NSWorkspaceOpenConfiguration* configuration = [NSWorkspaceOpenConfiguration new]; NSArray *arguments = [NSArray arrayWithObjects:arg1, nil]; [configuration setArguments:arguments]; // 背景色をコマンドラインの引数としてセット NSWorkspace* workspace = NSWorkspace.sharedWorkspace; [workspace openApplicationAtURL:appURL configuration:configuration completionHandler:nil]; [configuration release]; } - (NSMenu *)dockMenu { // Create the menu if (dockMenu == nil) dockMenu = [[NSMenu alloc] init]; else [dockMenu removeAllItems]; // サブメニュー生成 NSMenu *submenu1 = [[NSMenu alloc] init]; NSMenuItem *submenuitem1 = [submenu1 addItemWithTitle:@"シアン" action:@selector(doAction1:) keyEquivalent:@""]; [submenuitem1 setTarget:self]; [submenuitem1 setTag:1]; NSMenuItem *submenuitem2 = [submenu1 addItemWithTitle:@"マゼンタ" action:@selector(doAction1:) keyEquivalent:@""]; [submenuitem2 setTarget:self]; [submenuitem2 setTag:2]; NSMenuItem *submenuitem3 = [submenu1 addItemWithTitle:@"イエロー" action:@selector(doAction1:) keyEquivalent:@""]; [submenuitem3 setTarget:self]; [submenuitem3 setTag:3]; // メニュー生成して、サブメニュー付加 NSMenuItem *menu1 = [[NSMenuItem alloc] initWithTitle:@"背景色を指定して起動" action:nil keyEquivalent:@""]; [menu1 setSubmenu:submenu1]; // ドックメニューに追加 [dockMenu addItem: menu1]; [submenu1 release]; [menu1 release]; return dockMenu; } - (void)setDockTile:(nullable NSDockTile *)dockTile { // 今回は使用せず } @end
以下の用語が示す場所の例
(クリックで拡大。注:画面はNSDockTilePlugIn2のものになっているが、3も同様)
- プロジェクトのPROJECTからNSDockTilePlugIn3を選び、Build Settingsで、Apple Clang - Langauge - Objective-c > Objective-c Automatic Reference CountingをNoに。(注:XojoではARCが使えないのでreleaseメソッドを使用しますが、デフォルトではXcode上でコンパイルエラーになるための対処。)
- プロジェクトにターゲットを追加。(TARGETS下部の+ボタンを押し、テンプレートダイアログで「Bundle」を選択。Product Nameはここでは「DockTilePlugin3」)
- プロジェクトのTARGETSからDockTilePlugIn3を選び、Build Settingsで、Packaging > Wrapper Extensionをdocktilepluginに。
- プロジェクトのTARGETSからDockTilePlugIn3を選び、Build Phasesで、Compile SourcesにDocTilePlugin3.mを追加。
- プロジェクトのTARGETSからDockTilePlugIn3を選び、Infoで、Key : Principal class、Value : DockTilePlugIn3を追加。
- プロジェクトのTARGETSからNSDockTilePlugIn3を選び、Build Phasesで、上段の+ボタンを押して、New Copy Files Phaseを選び、DestinationをPluginsに。下段の+ボタンを押して、ダイアログからDockTilePlugin3.docktilepluginを選択。
- プロジェクトのTARGETSからNSDockTilePlugIn3を選び、Infoで、Key : Dock Tile plugin path、Value : DockTilePlugIn3.docktilepluginを追加。
- ビルド
- 出来上がったDockTilePlugin3.docktilepluginフォルダーを、PlugInsフォルダーを新規作成して内部に置く。(PlugInsの置き場所は任意)
Xojoでの実装
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
コードの実装は以上です。引き続き、ファイル関連の処理を実施。
- Xojoで新規プロジェクトを作成
- 以下をAppにペースト(できなければ、Sub - Endの間をOpenイベントに記述)
Sub Open() Handles Open // ドックメニュー生成 Dim m As DocMenu = new DocMenu(addressOf MenuAction, addressOf MenuValidation) End Sub
- 以下をAppにペースト
Protected Sub MenuAction(sender As Ptr) // Window1の背景色設定をクリア Window1.BackgroundColor=Color.White Window1.HasBackgroundColor=false End Sub
- 以下をAppにペースト
Protected Function MenuValidation(sender As Ptr) As Boolean // Window1の現在の背景色設定オプションを返す return Window1.HasBackgroundColor End Function
- 以下をWindow1にペースト(できなければ、Sub - Endの間をOpenイベントに記述)
Sub Open() Handles Open // コマンドラインの記述とアプリパスを取得 Dim cmline As String = system.commandline Dim appPath As String = app.ExecutableFile.nativePath // コマンドラインの記述とアプリパスが一致したらArgumentsはないので、何もしない if cmline=appPath then return // コマンドラインの記述をアプリパスで分割(2要素目にArgumentsが入る) Dim ary1() As String = System.CommandLine.Split(appPath) // Argumentsをスペースで分割(先頭にスペースが付いているので、1要素目は空白になる) Dim ary2() As String = ary1(1).Split // 最初のArgumentに応じて背景色をセット select case ary2(1) case "Cyan" me.BackgroundColor = Color.Cyan case "Magenta" me.BackgroundColor = Color.Magenta case "Yellow" me.BackgroundColor = Color.Yellow end select // 背景色の変更を有効に me.HasBackgroundColor=true End Sub
- 新規クラスを作成(名前は、ここでは「DocMenu」とした。)
- 以下をDocMenuにペースト(できなければ移譲に、名前:ActionDelegate、を追加)
Private Sub ActionDelegate(sender As Ptr)
- 以下をDocMenuにペースト(できなければ移譲に、名前:ActionDelegate、を追加)
Private Function ActionDelegate1(sender As Ptr) As Boolean
- 以下をDocMenuにペースト
Public Sub Constructor(action As ActionDelegate, action1 As ActionDelegate1) // ApplicationControllerを拡張 makeClass() // Window側でActionを受け取るメソッドを登録 ActionHandler = action ActionHandler1 = action1 End Sub
- 以下をDocMenuにペースト
Private Shared Sub actionEvent(id As Ptr, SEL As CString, sender As Ptr) // インスタンスメソッドに渡す ActionHandler.Invoke(sender) // クラス生成元でActionを受け取るメソッドを呼び出す End Sub
- 以下をDocMenuにペースト
Private Shared Sub makeClass() // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // Declare宣言 Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean // 処理済なら戻る if ACflg then return // ApplicationControllerの取得 Dim AppCntl As Ptr = NSClassFromString("XOJApplicationController") // Delegateの対象となるメソッドを追加(applicationDockMenu:をXojo側で用意したmakeDocMenuメソッドで受け取る。) if not class_addMethod (AppCntl, NSSelectorFromString("applicationDockMenu:"), AddressOf makeDocMenu, "v@:@") then msgBox "error1." return end if // 処理済フラグ・オン ACflg=true End Sub
- 以下をDocMenuにペースト
Private Shared Function makeDocMenu(id As Ptr, SEL As Cstring, sender As Ptr) As Ptr // 文字列を指定してセレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // Menuのインスタンスを作成 Dim dockMenu As Ptr = NSClassFromString("NSMenu") Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr dockMenu = init(alloc(dockMenu)) // MenuItemのインスタンスを作成 Dim dockMenuItem As Ptr = NSClassFromString("NSMenuItem") dockMenuItem = alloc(dockMenuItem) Declare Function initWithTitle Lib "Cocoa" selector "initWithTitle:action:keyEquivalent:" (receiver as Ptr, title As CFStringRef, action As Ptr, key As CFStringRef) As Ptr dockMenuItem = initWithTitle(dockMenuItem, "背景色をクリア", NSSelectorFromString("action:"), "") // MenuItemのTarget設定 Declare Sub setTarget Lib "Cocoa" selector "setTarget:" (receiver as Ptr, target As Ptr) setTarget(dockMenuItem, makeTarget()) // MenuItemをMenuに追加 Declare Sub addItem Lib "Cocoa" selector "addItem:" (receiver as Ptr, item As Ptr) addItem(dockMenu, dockMenuItem) // 解放 Declare Sub release Lib "Cocoa" selector "release" (receiver as Ptr) release(dockMenuItem) return dockMenu End Function
- 以下をDocMenuにペースト
Private Shared Function makeTarget() As Ptr // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // Declare宣言 Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr) Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean // 既にインスタンス作成済なら、それを返す if TargetInstance <> nil then return TargetInstance end if // クラス名をmyTarget、メタクラス名をNSObjectにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myTarget", 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // Tarrgetに送られてきたActionの受け口となるメソッドを追加(action:をXojo側で用意したactionEventメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("action:"), AddressOf actionEvent, "@@:@") then msgBox "error." return nil end if // Delegateの対象となるメソッド(Protocol?)を追加(validateMenuItem:をXojo側で用意したvalidateMenuItemメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("validateMenuItem:"), AddressOf validateMenuItem, "c24@0:8@16") then msgBox "error." return nil end if // 上記で生成したクラスのインスタンスを作成 Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr Dim targetId As Ptr = init(alloc(newClassId)) // インスタンスを保持 TargetInstance = targetId // インスタンスを返す return targetId End Function
- 以下をDocMenuにペースト
Private Shared Function validateMenuItem(id As Ptr, SEL As CString, sender As Ptr) As Boolean // インスタンスメソッドに渡す return ActionHandler1.Invoke(sender) // クラス生成元でActionを受け取るメソッドを呼び出す End Function
- 以下をDocMenuにペースト(できなければ共有プロパティに、名前:ACflg、データ型:Boolean、初期値:false、を追加)
Protected Shared Property ACflg As Boolean = false
- 以下をDocMenuにペースト(できなければ共有プロパティに、名前:ActionHandler、データ型:ActionDelegate、を追加)
Private Shared Property ActionHandler As ActionDelegate
- 以下をDocMenuにペースト(できなければ共有プロパティに、名前:ActionHandler1、データ型:ActionDelegate1、を追加)
Private Shared Property ActionHandler1 As ActionDelegate1
- 以下をDocMenuにペースト(できなければ共有プロパティに、名前:TargetInstance、データ型:Ptr、を追加)
Private Shared Property TargetInstance As Ptr
ドックにアイコンを登録後に右クリックして表示されるメニューを選択すると、ウィンドウの背景色が変化することを確認しました。
- 以下の内容でテキストファイルを作成し、名前をInfo.plistとして保存
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>NSDockTilePlugIn</key> <string>DockTilePlugin3.docktileplugin</string> </dict> </plist>
- プロジェクトに、Info.plistをドラッグ&ドロップ
- プロジェクト左ペインのBuild Settings>macOS上で右クリックし、Add to "Build Settings">Build Step>Copy Filesを選択
- InspectorのDestinationにContents Folderを指定。
- 中央ペインにPlugIns(上記「XcodeでのPluginビルド」で作成したもの)をドラッグ&ドロップ
注:いずれもメニュー実行前。
おわりに
特に非起動時のドックメニューについては、現状を鑑みると、慎重になった方がいいのかもしれません。
なお、アプリによっては直前に開いた書類の履歴を表示しますが、これはXcodeでDocument Appを選択した場合の標準機能と思われます。(実験で確認)
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):objective c - Find parent directory of a path - Stack Overflow
参考サイト(2):System.CommandLine - General - Xojo Programming Forum
参考サイト(3):macos - - [NSWorkspace OpenApplicazioneTurl: Configurazione: Configurazione: CompletamentoHandler:] Non funziona in Articolo di accesso - ItCodeGeeks.com
参考サイト(4):Dock Menu - General - Xojo Programming Forum
更新履歴
2022.05.17 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]