マウスイベントを処理するCocoaアプリケーションにしてみる

minu*1を起動すると、常にメニューバーには水色の水平ラインが表示される。ライン境界にぼかしが入って見た目は優しくなったが、必要性が無い時まで表示されているのは無駄だし、他の作業の邪魔にもなる。やはり普段はその存在を隠して、必要な時だけ瞬時に利用できるというのが目指すところだ。

目指す仕様

  1. 通常は水色の水平ラインは非表示にしておく。
  2. マウスがスクリーン一番上にタッチした時だけ、水色の水平ラインを表示する。
  3. クリックしたら、minuをアクティブにする。水色の水平ラインは表示され続ける。
  4. もう一度クリックしたら、直前のアプリケーションに戻る。水色の水平ラインは非表示にする。
  5. マウスが一旦スクリーン一番上から離れるまで、水色の水平ラインは非表示の状態をキープする。

マウスの出入りを監視する

Cocoa環境でマウスの出入りを監視するためには、NSViewに対してaddTrackingRectの設定が必要だ。以下のように設定することで、スクリーン一番上の水平ライン1px分がマウスの出入りを監視する範囲として設定される。その範囲にマウスが入ったり出たりすると、mouseEnteredやmouseExitedイベントが発生する。(addTrackingRectが設定されていないと、mouseEnteredやmouseExitedイベントが発生することはない。)

  • 画面一番上の幅1pxの水平ラインを監視対象の範囲に設定している。
  • mouseEnteredまたはmouseExitedイベントは、そのビューが所属するウィンドウへ送信するように設定してみた。
/*----------  EventView.h ----------*/
#import <Cocoa/Cocoa.h>


@interface EventView : NSView {

}

@end
/*----------  EventView.m ----------*/
#import "ClearWindow.h"
#import "EventView.h"


@implementation EventView

static float VIEW_HEIGHT = 22;
static float TRACKING_HEIGHT = 1;
static float LINE_WIDTH  = 2;

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

- (void)awakeFromNib {
    //自分の高さに合わせてウィンドウ上限に再配置する
    NSRect frame = [[self window] frame];
    [self setFrame:NSMakeRect(frame.origin.x, 
                              frame.size.height - VIEW_HEIGHT, 
                              frame.size.width, 
                              VIEW_HEIGHT)];
    //マウスの出入り(mouseEntered、mouseExited)を監視する範囲をスクリーントップの1pxに設定
    NSRect bounds = [self bounds];
    NSRect rect = NSMakeRect(bounds.origin.x, 
                              bounds.size.height - TRACKING_HEIGHT, 
                              bounds.size.width, 
                              TRACKING_HEIGHT);
    //addTrackingRect...はinitの中で設定しても無効、awakeFromNibの中で設定する必要あり
    [self addTrackingRect:rect owner:[self window] userData:nil assumeInside:NO];
}

// canBecomeKeyWindowがYESの場合
//   キーウィンドウになる前(アプリケーションがアクティブになる前)の最初のマウスイベントを許可するかどうか
//     NO : アプリケーションをアクティブにするだけで、mouseDownイベントは発生しない
//     YES: アプリケーションがアクティブになり、mouseDownイベントも発生する
// canBecomeKeyWindowがNOの場合は、acceptsFirstMouseが返す値に影響されず、常にYESの状態になる
- (BOOL)acceptsFirstMouse:(NSEvent *)event {
    return NO;
}

- (void)drawRect:(NSRect)rect {
    //グラフィック関数で一番上に水平ラインを描画
    [[[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:1.0] set];
    NSSetFocusRingStyle(NSFocusRingOnly);//このあと描画した図形の周囲にフォーカスリングが表示される
    NSRectFill(NSMakeRect([self bounds].origin.x, 
                          [self bounds].size.height - LINE_WIDTH, 
                          [self bounds].size.width, 
                          LINE_WIDTH));
}

@end

マウスイベント処理を実装する

  • EventViewで発生したマウスイベントは、自身が所属するウィンドウへ送信するようにしたので、ClearWindowでその処理を実装した。
/*----------  ClearWindow.h ----------*/
#import <Cocoa/Cocoa.h>


@interface ClearWindow : NSWindow {
	NSDictionary* activeApp;
}
- (void)storeActiveApp;
- (void)restoreActiveApp;

@end
  • アプリケーションがアクティブでない時のウィンドウ内のクリックについては、mouseDownイベントを発生して欲しくないので、canBecomeKeyWindowをオーバーライドして、YESを返すようにしておいた。
    • 保有するEventViewのacceptsFirstMouseメソッドの実装によっても、その挙動は変わってくるようだ。
/*----------  ClearWindow.m ----------*/
#import "ClearWindow.h"
#import "EventView.h"


@implementation ClearWindow

static NSString*	APP_BUNDLE_ID = @"com.bebekoubou.minu";
static float		MENU_BAR_HEIGHT = 22;

// ウィンドウを初期化して生成
- (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
    NSRect mainScreenFrame = [[NSScreen mainScreen] frame];
    NSRect areaFrame = NSMakeRect(mainScreenFrame.origin.x, 
                                  mainScreenFrame.size.height - MENU_BAR_HEIGHT, 
                                  mainScreenFrame.size.width, 
                                  MENU_BAR_HEIGHT);
    //ウィンドウの初期化(NSBorderlessWindowMaskによってメニューバーを超えた移動が可能になる)
    id win = [super initWithContentRect:areaFrame styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
    //ウィンドウが描画される上下レベル(レイヤーレベル)を設定(メニューバーよりも上のレベルに)
    [win setLevel: NSPopUpMenuWindowLevel];
    //ウィンドウの背景色を透明色に設定
    [win setBackgroundColor: [NSColor clearColor]];
    //ウィンドウの透明度を設定(不透明1.0〜透明0.0)
    [win setAlphaValue:0.0];
    //ウィンドウは不透明でない(つまり、透明である)
    [win setOpaque:NO];
    //ウィンドウの影を表示しない
    [win setHasShadow: NO];
    //ウィンドウをすべてのSpaceで表示する(Spaces対応のため)
    [win setCanBeVisibleOnAllSpaces: YES];	
    return win;
}

- (void)awakeFromNib {
    activeApp = nil;
}

// canBecomeKeyWindowメソッドがYESを返すようにオーバーライドすることで、キーウィンドウ(キー入力を最優先に受け付けるウィンドウ)になれる
// mouseDownイベントについて
//   - YESを返すと、キーウィンドウになるまではmouseDownイベントが発生しない
//    (アプリケーションがアクティブでない時のクリックでは、mouseDownイベントが発生しない)
//   - NOを返すと、常にmouseDownイベントが発生する
//    (アプリケーションがアクティブでない時のクリックでも、mouseDownイベントが発生する)
- (BOOL) canBecomeKeyWindow {
    return YES;
}

// 直前にアクティブだったアプリケーションを保存する
- (void)storeActiveApp {
    id currentApp = [[NSWorkspace sharedWorkspace] activeApplication];
    //@"NSApplicationName"では日本語ローカライズされたアプリケーション名ではlaunchApplication出来ない(例: Stickies、スティッキーズ)
    //NSString* appName = [activeApp valueForKey:@"NSApplicationName"];
    NSString* currentAppBundleID = [currentApp valueForKey:@"NSApplicationBundleIdentifier"];
    if (![currentAppBundleID isEqualToString:APP_BUNDLE_ID]) {
        activeApp = currentApp;
    }
}

// 直前にアクティブだったアプリケーションに戻す
- (void)restoreActiveApp {
    //@"NSApplicationName"では日本語ローカライズされたアプリケーション名ではlaunchApplication出来ない(例: Stickies、スティッキーズ)
    //NSString* appName = [activeApp valueForKey:@"NSApplicationName"];
    //[[NSWorkspace sharedWorkspace] launchApplication:appName];
    NSString* currentAppBundleID = [activeApp valueForKey:@"NSApplicationBundleIdentifier"];
    [[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:currentAppBundleID 
                                                         options:NSWorkspaceLaunchDefault 
                                  additionalEventParamDescriptor:nil 
                                                launchIdentifier:nil];
}

// マウスが入った
- (void)mouseEntered:(NSEvent *)event {
    [self setAlphaValue:0.8];
    [self storeActiveApp];
}

// マウスが出た
- (void)mouseExited:(NSEvent *)event {
    [self setAlphaValue:0.0];
}

// 左マウスボタンを押した
- (void)mouseDown:(NSEvent *)event {
    [self setAlphaValue:0.0];
    [self restoreActiveApp];
}

@end


以上で、かなり普通のアプリケーションらしく使えるようになってきた。自分で使うだけならこれで満足。ちゃんと仕上げるのであれば、さらに以下の作業も必要になりそうだ。

  • アイコンを設定する
  • アプリケーションメニューを設定する(「minuについて」とか)
  • 欲を言えば、アプリケーションの環境設定から水色の水平ラインの透明度なんかをユーザーが設定できるようにしたい。
  • 現状、MVCの役割分担を無視した設計(すべての処理をビューが担っている)なので、上記の作業の前にはクラスやコードの見直しが必要になりそう。
  • ローカライズの意義はほとんどないが、日本語環境にも対応しておきたい。

*1:メニューバーに右寄せで表示されるアイコンメニューが、文字メニューと重なって見えなくなってしまう状況を解消するためのアプリケーション