OS X のメニューバーに常駐するメニューエクストラを作る

# OS X のメニューバー

![Capture 2015-07-23 23.30.00.png](https://qiita-image-store.s3.amazonaws.com/0/3603/3cdd9ca0-9986-e0d8-7455-7cdc00c40505.png)

OS X のメニューバーには数多くのメニュー項目が並んでいます。特にデスクトップの右側にはシステム標準のものからサードパーティのアプリまで、人によっては数多くのメニューが林立していることでしょう。本稿ではこれら「右側メニュー」の話とその作り方について解説します。


# メニューエクストラとステータスメニュー

これらの「右側メニュー」はメニューエクストラまたはステータスメニューと呼ばれるものです。呼び方が2通りあるのは、そこには2種類のメニューが存在するからです。もっとも簡単な見分け方は、`⌘`（コマンドキー）を押しながらメニューをドラッグした時にメニューを移動または削除ができるものがメニューエクストラで、そうでないのがステータスメニューです。システム標準の音量などはメニューエクストラの代表例です。

![Capture 2015-07-23 23.38.17.png](https://qiita-image-store.s3.amazonaws.com/0/3603/241898f1-3149-73da-7aed-8f303224ee54.png)

これらメニューエクストラはSystemUIServerというプロセスが .menu プラグインをロードして動作する仕組みです。右側メニューをリフレッシュしたい場合にはSystemUIServerをキルするとよいでしょう。

```
$ killall SystemUIServer
```

右側メニューにはメニューエクストラとステータスメニューの2種類があると説明しましたが、一般の開発者にAPIが解放されているのはステータスメニューの方です。NSStatusBarとNSStatusItemは普通に利用できるものなので、世の中にはこれを実装したアプリが多数存在しています。DropboxやEvernoteはその代表例です。

![Capture 2015-07-23 23.38.12.png](https://qiita-image-store.s3.amazonaws.com/0/3603/85a7b566-bdf3-cd4e-d5d4-4c0a60fd513c.png)

その昔、ステータスメニューのチュートリアルとして[Cocoa はやっぱり！](http://sitearo.com/cocoa/0900_general/ccyp_StatusBar.pdf)の解説記事にはお世話になりました。

ステータスメニューはメニューエストラとは違いアプリが起動中でないと存在することができないので、そこでよくあるのは本体アプリとは別にDockに出現しないヘルパープログラム（おそらくLSUIElementか何かをInfo.plistに定義してある）を裏に常駐させるパターンです。Dropboxはおそらく本体アプリそのものが裏に常駐、Evernoteはヘルパープログラムが別途常駐しているものと思われます。

一方メニューエクストラはNSMenuExtraというAPIになりますが、これはプライベートAPIとなるため通常は利用できません。このことがメニューエクストラの実装例が少ない理由なのかと思われます。そんな中でも少なからず、[MenuMeters](http://www.ragingmenace.com/software/menumeters/)や[ASM](http://en.osdn.jp/projects/sfnet_x-asm/downloads/asm-src/2.0.1/asm2.0.1.src.tgz/), [AtomicBeef](http://osx.iusethis.com/app/atomicbeef)は貴重なオープンソースのメニューエクストラの実装例でした。

## ステータスメニューを実装する前に（余談）

これは余談ですが、各アプリが問答無用でステータスメニューを追加してしまっていることにより、あるいは意味もなくステータスメニュー型の常駐アプリが氾濫していることにより、Macユーザーのデスクトップにはもう十分なメニュースペースが残っていません。ステータスメニューを実装する際にはそれを非表示にすることができるオプションを環境設定などに用意しておくべきです。

なおメニューエクストラであればユーザーが任意でメニューを削除することが可能です。

![Capture 2015-07-24 00.39.21.png](https://qiita-image-store.s3.amazonaws.com/0/3603/05610dd4-3bc8-5cc6-1c71-f946754b7aa8.png)
_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をインポートしていればこれは不要なので、まずこのファイルは削除します。そして各ファイルの構造体の宣言を書き換えます。

```objc:修正前
- (id)accessibilityHitTest:(struct CGPoint)arg1;
```
```objc:修正後
- (id)accessibilityHitTest:(CGPoint)arg1;
```

他の箇所も同様に対応します。

ついでにインポートディレクティブはCocoaに変えておきます。

```objc:NSMenuExtra.h
#import <Cocoa/Cocoa.h>
```

## Xcode プロジェクトを作成する

![Capture 2015-07-24 00.12.22.png](https://qiita-image-store.s3.amazonaws.com/0/3603/570d3b97-1a6e-9f81-0677-90d4772db8cf.png)

OS XのFrameworks & LibraryからBundleテンプレートを選びます。次にBundle Extensionは`menu`に変更します。


## ヘッダーファイルとフレームワークの参照を追加する

class-dumpで抽出した各ヘッダーファイルとフレームワークの参照をプロジェクトに追加します。

* NSMenuExtra.h
* NSMenuExtraView.h
* SystemUIPlugin.framework
* Cocoa.framework


## メニューエクストラビューを実装する

メニューバー上に描画されるビューを実装します。

```objc:MyExtraView.h
#import <Cocoa/Cocoa.h>
#import "NSMenuExtraView.h"

@interface MyExtraView : NSMenuExtraView
@end
```
```objc:MyExtraView.m
#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のサブクラスを作って実装します。

```objc:MyExtra.h
#import <Cocoa/Cocoa.h>
#import "NSMenuExtra.h"

@interface MyExtra : NSMenuExtra
@end
```

```objc:MyExtra.m
#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です。

![Capture 2015-07-24 00.19.48.png](https://qiita-image-store.s3.amazonaws.com/0/3603/8b39f924-bc3a-4954-0696-f31f94ab08cb.png)


## ビルドして実行

![Capture 2015-07-24 00.25.51.png](https://qiita-image-store.s3.amazonaws.com/0/3603/3853cf84-77e5-1b98-69aa-ee87bb5914e8.png)
ビルドすると`.menu`拡張子のバンドルファイルが出来上がります。これをFinderでダブルクリックするとメニューエクストラを起動します。ただこれだけではすぐに反映されないので、先に示した方法でSystemUIServerをキルしてリロードします。

![Capture 2015-07-24 00.26.53.png](https://qiita-image-store.s3.amazonaws.com/0/3603/f298549d-fc44-01cb-65ef-b5acb6384000.png)

サンプルコード準備中