ホームページ開発ツール>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が取得されてしまいます。
S Shot1
 調べたら、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

 以上を踏まえ、テストアプリを作ってみることにします。仕様は以下の通りとしました。

 XcodeでのPluginビルド
注:以下は、Xcode上でプラグインをデバッグするための、本体側の設定ステップを含みます。
  1. Xcodeで新規プロジェクトを作成(テンプレートダイアログで「App」を選択。Product Nameはここでは「NSDockTilePlugIn3」)
  2. プロジェクトにNew Groupを追加。(名前はここでは「DockTilePlugin3」)
  3. DockTilePlugin3に「New File...」でファイル作成(テンプレートダイアログで「Cocoa Class」を選択。ファイル名はここでは「DockTilePlugIn3」)
  4. DockTilePlugIn3.hの#import〜@endを、以下に置き換え
    #import <Cocoa/Cocoa.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface DockTilePlugIn3 : NSObject  {
    	NSMenu *dockMenu;
    }
    @end
    
  5. 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も同様)
    S Shot1 S Shot2
  6. プロジェクトのPROJECTからNSDockTilePlugIn3を選び、Build Settingsで、Apple Clang - Langauge - Objective-c > Objective-c Automatic Reference CountingをNoに。(注:XojoではARCが使えないのでreleaseメソッドを使用しますが、デフォルトではXcode上でコンパイルエラーになるための対処。)
  7. プロジェクトにターゲットを追加。(TARGETS下部の+ボタンを押し、テンプレートダイアログで「Bundle」を選択。Product Nameはここでは「DockTilePlugin3」)
  8. プロジェクトのTARGETSからDockTilePlugIn3を選び、Build Settingsで、Packaging > Wrapper Extensionをdocktilepluginに。
  9. プロジェクトのTARGETSからDockTilePlugIn3を選び、Build Phasesで、Compile SourcesにDocTilePlugin3.mを追加。
  10. プロジェクトのTARGETSからDockTilePlugIn3を選び、Infoで、Key : Principal class、Value : DockTilePlugIn3を追加。
  11. プロジェクトのTARGETSからNSDockTilePlugIn3を選び、Build Phasesで、上段の+ボタンを押して、New Copy Files Phaseを選び、DestinationをPluginsに。下段の+ボタンを押して、ダイアログからDockTilePlugin3.docktilepluginを選択。
  12. プロジェクトのTARGETSからNSDockTilePlugIn3を選び、Infoで、Key : Dock Tile plugin path、Value : DockTilePlugIn3.docktilepluginを追加。
  13. ビルド
  14. 出来上がったDockTilePlugin3.docktilepluginフォルダーを、PlugInsフォルダーを新規作成して内部に置く。(PlugInsの置き場所は任意)

 Xojoでの実装
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成
  2. 以下をAppにペースト(できなければ、Sub - Endの間をOpenイベントに記述)
    Sub Open() Handles Open
      // ドックメニュー生成
      Dim m As DocMenu = new DocMenu(addressOf MenuAction, addressOf MenuValidation)
    End Sub
    
  3. 以下をAppにペースト
    Protected Sub MenuAction(sender As Ptr)
      // Window1の背景色設定をクリア
      Window1.BackgroundColor=Color.White
      Window1.HasBackgroundColor=false
    End Sub
    
  4. 以下をAppにペースト
    Protected Function MenuValidation(sender As Ptr) As Boolean
      // Window1の現在の背景色設定オプションを返す
      return Window1.HasBackgroundColor
    End Function
    
  5. 以下を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
    
  6. 新規クラスを作成(名前は、ここでは「DocMenu」とした。)
  7. 以下をDocMenuにペースト(できなければ移譲に、名前:ActionDelegate、を追加)
    Private Sub ActionDelegate(sender As Ptr)
    
  8. 以下をDocMenuにペースト(できなければ移譲に、名前:ActionDelegate、を追加)
    Private Function ActionDelegate1(sender As Ptr) As Boolean
    
  9. 以下をDocMenuにペースト
    Public Sub Constructor(action As ActionDelegate, action1 As ActionDelegate1)
      // ApplicationControllerを拡張
      makeClass()
      
      // Window側でActionを受け取るメソッドを登録
      ActionHandler = action
      ActionHandler1 = action1
    End Sub
    
  10. 以下をDocMenuにペースト
    Private Shared Sub actionEvent(id As Ptr, SEL As CString, sender As Ptr)
      // インスタンスメソッドに渡す
      ActionHandler.Invoke(sender)  // クラス生成元でActionを受け取るメソッドを呼び出す
    End Sub
    
  11. 以下を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
    
  12. 以下を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
    
  13. 以下を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
    
  14. 以下をDocMenuにペースト
    Private Shared Function validateMenuItem(id As Ptr, SEL As CString, sender As Ptr) As Boolean
      // インスタンスメソッドに渡す
      return ActionHandler1.Invoke(sender)  // クラス生成元でActionを受け取るメソッドを呼び出す
    End Function
    
  15. 以下をDocMenuにペースト(できなければ共有プロパティに、名前:ACflg、データ型:Boolean、初期値:false、を追加)
    Protected Shared Property ACflg As Boolean = false
    
  16. 以下をDocMenuにペースト(できなければ共有プロパティに、名前:ActionHandler、データ型:ActionDelegate、を追加)
    Private Shared Property ActionHandler As ActionDelegate
    
  17. 以下をDocMenuにペースト(できなければ共有プロパティに、名前:ActionHandler1、データ型:ActionDelegate1、を追加)
    Private Shared Property ActionHandler1 As ActionDelegate1
    
  18. 以下をDocMenuにペースト(できなければ共有プロパティに、名前:TargetInstance、データ型:Ptr、を追加)
    Private Shared Property TargetInstance As Ptr
    
 コードの実装は以上です。引き続き、ファイル関連の処理を実施。
  1. 以下の内容でテキストファイルを作成し、名前を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>
    
  2. プロジェクトに、Info.plistをドラッグ&ドロップ

  3. プロジェクト左ペインのBuild Settings>macOS上で右クリックし、Add to "Build Settings">Build Step>Copy Filesを選択
  4. InspectorのDestinationにContents Folderを指定。
  5. 中央ペインにPlugIns(上記「XcodeでのPluginビルド」で作成したもの)をドラッグ&ドロップ
 ドックにアイコンを登録後に右クリックして表示されるメニューを選択すると、ウィンドウの背景色が変化することを確認しました。
S Shot2

S Shot3
注:いずれもメニュー実行前。

 おわりに

 特に非起動時のドックメニューについては、現状を鑑みると、慎重になった方がいいのかもしれません。

 なお、アプリによっては直前に開いた書類の履歴を表示しますが、これは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]