OS X のメニューバー
OS X のメニューバーには数多くのメニュー項目が並んでいます。特にデスクトップの右側にはシステム標準のものからサードパーティのアプリまで、人によっては数多くのメニューが林立していることでしょう。本稿ではこれら「右側メニュー」の話とその作り方について解説します。
メニューエクストラとステータスメニュー
これらの「右側メニュー」はメニューエクストラまたはステータスメニューと呼ばれるものです。呼び方が2通りあるのは、そこには2種類のメニューが存在するからです。もっとも簡単な見分け方は、⌘
(コマンドキー)を押しながらメニューをドラッグした時にメニューを移動または削除ができるものがメニューエクストラで、そうでないのがステータスメニューです。システム標準の音量などはメニューエクストラの代表例です。
これらメニューエクストラはSystemUIServerというプロセスが .menu プラグインをロードして動作する仕組みです。右側メニューをリフレッシュしたい場合にはSystemUIServerをキルするとよいでしょう。
$ killall SystemUIServer
右側メニューにはメニューエクストラとステータスメニューの2種類があると説明しましたが、一般の開発者にAPIが解放されているのはステータスメニューの方です。NSStatusBarとNSStatusItemは普通に利用できるものなので、世の中にはこれを実装したアプリが多数存在しています。DropboxやEvernoteはその代表例です。
その昔、ステータスメニューのチュートリアルとしてCocoa はやっぱり!の解説記事にはお世話になりました。
ステータスメニューはメニューエストラとは違いアプリが起動中でないと存在することができないので、そこでよくあるのは本体アプリとは別にDockに出現しないヘルパープログラム(おそらくLSUIElementか何かをInfo.plistに定義してある)を裏に常駐させるパターンです。Dropboxはおそらく本体アプリそのものが裏に常駐、Evernoteはヘルパープログラムが別途常駐しているものと思われます。
一方メニューエクストラはNSMenuExtraというAPIになりますが、これはプライベートAPIとなるため通常は利用できません。このことがメニューエクストラの実装例が少ない理由なのかと思われます。そんな中でも少なからず、MenuMetersやASM, AtomicBeefは貴重なオープンソースのメニューエクストラの実装例でした。
ステータスメニューを実装する前に(余談)
これは余談ですが、各アプリが問答無用でステータスメニューを追加してしまっていることにより、あるいは意味もなくステータスメニュー型の常駐アプリが氾濫していることにより、Macユーザーのデスクトップにはもう十分なメニュースペースが残っていません。ステータスメニューを実装する際にはそれを非表示にすることができるオプションを環境設定などに用意しておくべきです。
なおメニューエクストラであればユーザーが任意でメニューを削除することが可能です。
Xcodeのようなメインメニューが多いアプリは右側を侵食してしまう
メニューエクストラの実装方法
NSMenuExtraはプライベートAPIであるため、どうにかしてクラスの定義を参照しなければなりません。そのためにclass-dumpを使ってNSMenuExtraのObjective-Cヘッダーを抽出します。この手法はCocoaアプリをハックするための定石となっています。
class-dump を導入する
なければhomebrewなどでインストールしてしまいましょう。
$ brew install class-dump
SystemUIPluginのヘッダーを抽出する
SystemUIPlugin.frameworkはプライベートフレームワークなのでヘッダーファイルがありません。ここでclass-dumpの出番です。
/System/Library/PrivateFrameworks/SystemUIPlugin.framework/Versions/A/SystemUIPlugin
$ class-dump -H /System/Library/PrivateFrameworks/SystemUIPlugin.framework/Versions/A/SystemUIPlugin ~/Desktop/MenuExtraHeaders/
ヘッダーを修正する
一部の構造体はCDStructures.h
に別途定義されますが、Cocoa.hをインポートしていればこれは不要なので、まずこのファイルは削除します。そして各ファイルの構造体の宣言を書き換えます。
- (id)accessibilityHitTest:(struct CGPoint)arg1;
- (id)accessibilityHitTest:(CGPoint)arg1;
他の箇所も同様に対応します。
ついでにインポートディレクティブはCocoaに変えておきます。
# import <Cocoa/Cocoa.h>
Xcode プロジェクトを作成する
OS XのFrameworks & LibraryからBundleテンプレートを選びます。次にBundle Extensionはmenu
に変更します。
ヘッダーファイルとフレームワークの参照を追加する
class-dumpで抽出した各ヘッダーファイルとフレームワークの参照をプロジェクトに追加します。
- NSMenuExtra.h
- NSMenuExtraView.h
- SystemUIPlugin.framework
- Cocoa.framework
メニューエクストラビューを実装する
メニューバー上に描画されるビューを実装します。
# import <Cocoa/Cocoa.h>
# import "NSMenuExtraView.h"
@interface MyExtraView : NSMenuExtraView
@end
# import "MyExtraView.h"
@implementation MyExtraView
- (void)drawRect:(NSRect)rect
{
[[NSColor purpleColor] set];
NSRect aRect = NSInsetRect(rect, 4.0, 4.0);
[[NSBezierPath bezierPathWithOvalInRect:aRect] fill];
}
@end
NSMenuExtraのサブクラスを実装する
NSMenuExtraのサブクラスを作って実装します。
# import <Cocoa/Cocoa.h>
# import "NSMenuExtra.h"
@interface MyExtra : NSMenuExtra
@end
# import "MyExtra.h"
# import "MyExtraView.h"
@interface MyExtra ()
@property (nonatomic) NSMenu *myMenu;
@end
@implementation MyExtra
- (instancetype)initWithBundle:(NSBundle*)bundle
{
self = [super initWithBundle:bundle];
if (self) {
self.view = [[MyExtraView alloc] initWithFrame:self.view.frame menuExtra:self];
self.myMenu = [[NSMenu alloc] initWithTitle:@"menu"];
[self.myMenu setAutoenablesItems:NO];
[self.myMenu addItemWithTitle:@"ドラッグ可能な" action: @selector(action:) keyEquivalent:@"d"];
[self.myMenu addItemWithTitle:@"メニューエクストラ" action:nil keyEquivalent:@"m"];
[self.myMenu addItemWithTitle:@"やったぜ" action:nil keyEquivalent:@"Y"];
NSLog(@"========= LOAD MENU EXTRA =========");
}
return self;
}
- (void)action:(id)sender
{
NSLog(@"Hello Menu Extra !");
}
- (NSMenu*)menu
{
return self.myMenu;
}
@end
プリンシパルクラスを指定する
Info.plistにプリンシパルクラスを指定します。今回はMyExtraです。
ビルドして実行
ビルドすると.menu
拡張子のバンドルファイルが出来上がります。これをFinderでダブルクリックするとメニューエクストラを起動します。ただこれだけではすぐに反映されないので、先に示した方法でSystemUIServerをキルしてリロードします。
サンプルコード準備中