LoginSignup
76
76

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-07-23

OS X のメニューバー

Capture 2015-07-23 23.30.00.png

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

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

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

Capture 2015-07-23 23.38.17.png

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

$ killall SystemUIServer

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

Capture 2015-07-23 23.38.12.png

その昔、ステータスメニューのチュートリアルとしてCocoa はやっぱり!の解説記事にはお世話になりました。

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

一方メニューエクストラはNSMenuExtraというAPIになりますが、これはプライベートAPIとなるため通常は利用できません。このことがメニューエクストラの実装例が少ない理由なのかと思われます。そんな中でも少なからず、MenuMetersASM, AtomicBeefは貴重なオープンソースのメニューエクストラの実装例でした。

ステータスメニューを実装する前に(余談)

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

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

Capture 2015-07-24 00.39.21.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をインポートしていればこれは不要なので、まずこのファイルは削除します。そして各ファイルの構造体の宣言を書き換えます。

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

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

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

NSMenuExtra.h
#import <Cocoa/Cocoa.h>

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

Capture 2015-07-24 00.12.22.png

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

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

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

  • NSMenuExtra.h
  • NSMenuExtraView.h
  • SystemUIPlugin.framework
  • Cocoa.framework

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

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

MyExtraView.h
#import <Cocoa/Cocoa.h>
#import "NSMenuExtraView.h"

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

MyExtra.h
#import <Cocoa/Cocoa.h>
#import "NSMenuExtra.h"

@interface MyExtra : NSMenuExtra
@end
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

ビルドして実行

Capture 2015-07-24 00.25.51.png
ビルドすると.menu拡張子のバンドルファイルが出来上がります。これをFinderでダブルクリックするとメニューエクストラを起動します。ただこれだけではすぐに反映されないので、先に示した方法でSystemUIServerをキルしてリロードします。

Capture 2015-07-24 00.26.53.png

サンプルコード準備中

76
76
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
76