2009年7月12日

プロトコルの @optional

iPhone開発本を読んでいて知ったのだが Objective-C 2.0 ではプロトコルに @optional が追加され、実装が任意のメソッドを定義できるようになっている。

Objective-C 2.0プログラミング言語 > 任意のプロトコル(日本語)

こんな感じ(上記 ADCリファレンス より引用)

@protocol MyProtocol

- (void)requiredMethod;

@optional
- (void)anOptionalMethod;
- (void)anotherOptionalMethod;

@required
- (void)anotherRequiredMethod;

@end


知らなかった。WEBの情報だけでなくたまには本も読むものだ(と思ったりした)。


(参考)
Wikipedia - Objective-C: Protocols
以下、上記サイトから引用。
Objective-C 2.0 added support for marking certain methods in a protocol optional; the compiler will not enforce that such methods are implemented.


iPhoneアプリ開発まっしぐら - iPhoneアプリ開発における 非形式プロトコル (informal protocol) の利用について

# なるほど。非形式なプロトコルを使わないで任意実装メソッドが定義できるようになったのか。

2009年7月11日

ホットキー変更対応(24) - キー設定の変更をシステムへ反映する

前々回まででホットキー設定 UIのひな形ができたが、変更してもシステムへは反映していなかった。今回はその実装を行う。

まず HotkeyTextView で変更があったことをコントローラへ通知する必要がある。今回は自前のターゲット/アクションを用意しよう。

まず HotkeyTextViewへ target と action プロパティを用意する。

HotkeyTextView.h

@interface HotkeyTextView : NSTextView {
:
id _target;
SEL _action;
}
:
@property (retain) id target;
@property (retain) SEL action;

@end



次にホットキーが変更された場合に target に対して action メソッドを呼び出す処理を用意する。これは keyDown: の中がいいだろう。

HotkeyTextView.m
- (void)keyDown:(NSEvent *)theEvent
{
:
// 確定処理
if (_hotkey.modifier != modifier || _hotkey.code == keycode) {
_hotkey.modifier = modifier;
_hotkey.code = keycode;

if (self.target && [self.target respondsToSelector:self.action]) {
[self.target performSelector:self.action withObject:self.hotkey];
}
}

[self redraw];
[self endEdit];
}

前回は同じキーが押された場合の処理が抜けていたのでついでに加えておいた(実際にはホットキーが起動するため、イベントはとれないのだが)。
必要な情報を Hotkey へ格納した後、target / action の存在チェックを行い、メッセージを投げる。引数として Hotkeyを渡しておく。


一方、使う側はこんな感じ。
AppController.m
- (void)awakeFromNib
{
_hotkey_register = [HotkeyRegister sharedRegister];

Hotkey *hotkey;

hotkey = [[[Hotkey alloc] init] autorelease];
:
_text1.hotkey = hotkey;
_text1.target = self; <--追加
_text1.action = @selector(changeHotkey:); <--追加
:


変更時に呼び出される changeHotkey: でシステムへの反映を行う。

- (void)changeHotkey:(id)sender
{
NSLog(@"changeHotkey was called: %@", sender);
[_hotkey_register registHotkey:sender];
}


- [HotkeyRegister registHotkey] は直前のホットキーを取り消した(unregist)後に、新しいキーを登録するように実装してある。

さて実行してみよう。デバッグコンソールで動作を追って見る。
「⌥⌘P」を「⌃⌥⌘⇧Space」へ変更してみよう。

[Session started at 2009-07-11 06:10:45:24 +0900.]
:
2009-07-11 06:10:34.376 Hotkey[1062:10b] changeHotkey was called: <Hotkey: 0x1559a0>
↑ホットキーを変更すると changeHotkey: が呼び出された。
2009-07-11 06:10:34.416 Hotkey[1062:10b] unregistHotkey: <Hotkey: 0x1559a0>
 ↑直前のキーが取り消され
2009-07-11 06:10:34.434 Hotkey[1062:10b] registHotkey: keyid=2, modifier=1b00, code=31, ref=1aefe0,
target=<AppController: 0x151730>, action=keyDown:
 ↑新しいキーが登録された
2009-07-11 06:10:34.464 Hotkey[1062:10b] {(
<Hotkey: 0x155760>,
<Hotkey: 0x1559a0>
)}
2009-07-11 06:10:38.954 Hotkey[1062:10b] keyDown: keycode=31 [⌃⌥⌘⇧Space]
 ↑試しに新しいホットキーを押すとちゃんと反応した。
2009-07-11 06:10:44.979 Hotkey[1062:10b] keyDown: keycode=25 [⌥⌘L]




サンプル:HotKey-4.zip

2009年7月10日

Objective-C 2.0 プロパティのsetter/getterを自前で実装

前回のコードでは setter/getterを自前で実装したいが為にプロパティ宣言は行わなかったが、調べてみるとできることがわかった。

http://journal.mycom.co.jp/column/objc/102/index.html



ダイナミックObjective-C - 102 プロパティ(2) - プロパティの宣言


前回のコード:
HotkeyTextView.h
@interface HotkeyTextView : NSTextView {

BOOL _is_editing;
Hotkey* _hotkey;
}

@property (retain) Hotkey* hotkey;

- (Hotkey*)hotkey;
- (void)setHotkey:(Hotkey*)hotkey;


HotkeyTextView.m
@implementation HotkeyTextView

@synthesize hotkey = _hotkey



AppController.m
- (void)awakeFromNib
{
:
[_text1 setHotkey:hotkey];
:



これをこうする。

HotkeyTextView.h
@property (retain) Hotkey* hotkey;   // プロパティ宣言追加

//- (Hotkey*)hotkey; // コメントアウト
//- (void)setHotkey:(Hotkey*)hotkey; // コメントアウト



HotkeyTextView.m
@implementation HotkeyTextView

// @synthesize hotkey = _hotkey <-- コメントアウト



AppController.m
// [_text1 setHotkey:hotkey];
_text1.hotkey = hotkey; // プロパティでアクセス



つまり @synthesize が setter/getter メソッドを自動生成するので、その宣言をなくすだけで良い。

2009年7月9日

ホットキー変更対応(23) - キー設定UIの実装

さて次はホットキー設定のユーザインターフェイスを実装する。以前の検証で作った NSTextViewのサブクラスを持ってきて修正する。クラス名は HotkeyTextView。

HotkeyTextView.h

@class Hotkey;
@interface HotkeyTextView : NSTextView {

BOOL _is_editing;
Hotkey* _hotkey;
}

- (Hotkey*)hotkey;
- (void)setHotkey:(Hotkey*)hotkey;

@end

setHotkey: は再描画処理を書きたいのでプロパティを使わず自前で実装する。

以下、実装。以前紹介した時とほぼ同じ。大きく違うのは Hotkey クラスを使うところ。
Hotkey.m

まずセッターとゲッター
- (Hotkey*)hotkey
{
return _hotkey;
}

- (void)setHotkey:(Hotkey*)hotkey
{
[hotkey retain];
[_hotkey release];
_hotkey = hotkey;
[self redraw];
}


再描画は - [Hotkey string] で得た文字列を使う。
- (void)redraw
{
[self setString:[_hotkey string]];
}


イベント系:ダブルクリック検出
- (void)mouseDown:(id)theEvent
{
if ([theEvent clickCount] >= 2) {
[self startEdit];
}
}

startEdit でキー入力モードへ移行する。

- (void)startEdit
{
[[self window] makeFirstResponder:self];
[self setSelectable:YES];
_is_editing = YES;
NSRange range = NSMakeRange(0, [[self string] length]);
[self setSelectedRange:range];
}

- (void)endEdit
{
[self setSelectable:NO];
_is_editing = NO;
}


_is_editing フラグで入力モードを管理している。キー入力を受け付けるモードになったら、現在のキー文字列を選択状態(反転表示)にする。


イベント処理:キー入力
- (void)keyDown:(NSEvent *)theEvent
{
if (!_is_editing) {
return;
}

UInt32 modifier = 0;
UInt32 keycode = [theEvent keyCode];

NSUInteger modifier_flags = [theEvent modifierFlags];
if (modifier_flags & NSShiftKeyMask) {
modifier |= shiftKey;
}
if (modifier_flags & NSCommandKeyMask) {
modifier |= cmdKey;
}
if (modifier_flags & NSAlternateKeyMask) {
modifier |= optionKey;
}
if (modifier_flags & NSControlKeyMask) {
modifier |= controlKey;
}

if (!([Hotkey isHotKeyForModifier:modifier])) {
if (keycode == kVK_Escape) {
[self redraw];
[self endEdit];
}
// abort
return;
}

if (![Hotkey isHotKeyForKeyCode:keycode]) {
// abort
return;
}

_hotkey.modifier = modifier;
_hotkey.code = keycode;

[self redraw];
[self endEdit];
}

最初に修飾キーを判定し、その後ホットキーとして受け付けるかどうかのチェックを入れる。それらのチェックが済んだら Hotkeyインスタンスへmodifierとcodeを設定し再描画させる。最後に endEdit を呼んで入力モードを終了する。

ホットキーとして登録できるのはCommand/Option/Controlerのいづれかの修飾キーが押されている場合のみで、そのルールは Hotkey クラスに用意しておく。
Hotkey.m
+ (BOOL)isHotKeyForModifier:(UInt32)modifier
{
if (modifier & (cmdKey | optionKey | controlKey)) {
return YES;
} else {
return NO;
}
}




HotkeyTextViewクラスが実装できたので、これを試すサンプル画面を作ってみる。
InterfaceBuilderで HotkeyTextView を2つ貼付けてみた。


AppControllerでアウトレットを設定し、これらへ Hotkeyを指定する。
AppController.m
- (void)awakeFromNib
{
_hotkey_register = [HotkeyRegister sharedRegister];

Hotkey *hotkey;

hotkey = [[[Hotkey alloc] init] autorelease];
hotkey.code = 0x23; // 'S'
hotkey.modifier = cmdKey | optionKey;
hotkey.target = self;
hotkey.action = @selector(keyDown:);
[_hotkey_register registHotkey:hotkey];
[_text1 setHotkey:hotkey];

hotkey = [[[Hotkey alloc] init] autorelease];
hotkey.code = 0x25; // 'L'
hotkey.modifier = cmdKey | optionKey;
hotkey.target = self;
hotkey.action = @selector(keyDown:);
[_hotkey_register registHotkey:hotkey];
[_text2 setHotkey:hotkey];

}


これでできあがり。動かしてみよう。
まず初期表示。


よし、問題なし。


次にダブルクリックして変更してみる。


大丈夫だ。

とりあえずいいようだ。

サンプル:HotKey-3.zip

- - - -
今回はホットキーを変更しても表示が変わるだけでシステムへの再登録は行っていない。
この辺りは次回以降。

2009年7月8日

ホットキー変更対応(22) - ふたたびキー表示

ホットキー登録ができるようになったので今度は登録キーの表示を行う。以前の検証を思い出しつつ Hotkeyクラスを拡張して表示ロジックを組み込む。動作確認の為、前回のサンプルにキーを表示させてみよう。

まずキーの文字表現から。以前の設計では HotkeyCharConverter なんて大げさな名前のクラスを用意するつもりだったが、ホットキー用のモデルクラス Hotkey を用意することになったので、ここに文字表現の責務を持たせることにする。

Hotkey を次のように拡張する。

Hotkey.h

@class HotkeyRegister;
@interface Hotkey : NSObject {
:
}

- (NSString*)string;
- (BOOL)isHotkey;

@end


- [Hotkey string] で修飾キーを含めた文字表現を取得できる。
例えば Command+Option+P なら @"⌥⌘P" を返す。


実装はこんな感じ。まずマッピング用の構造体配列を定義する。
Hotkey.m
static const struct {
UInt32 keycode;
NSString* description;
NSString* string;
BOOL is_hotkey;
}
keymap[] = {
{ kVK_ANSI_A, @"A" , @"A" , YES },
{ kVK_ANSI_S, @"S" , @"S" , YES },
:


続いて追加メソッドの実装。
- (NSString*)string
{
NSString* key_desc = @"";

if (self.modifier & controlKey) {
key_desc = [key_desc stringByAppendingFormat:@"%C", kControlUnicode];
}
if (self.modifier & optionKey) {
key_desc = [key_desc stringByAppendingFormat:@"%C", kOptionUnicode];
}
if (self.modifier & cmdKey) {
key_desc = [key_desc stringByAppendingFormat:@"%C", kCommandUnicode];
}
if (self.modifier & shiftKey) {
key_desc = [key_desc stringByAppendingFormat:@"%C", kShiftUnicode];
}
key_desc = [key_desc stringByAppendingFormat:@"%@", _keymap[[self indexOfKeymap]].string];

return key_desc;
}


- (BOOL)isHotkey
{
return _keymap[[self indexOfKeymap]].is_hotkey;
}


それとキーマップのインデックスを取得するメソッドを用意。
- (UInt32)indexOfKeymap
{
UInt32 index;
UInt32 max = sizeof(_keymap)/sizeof(_keymap[0]);
for (index=0; index < max; index++) {
if (_keymap[index].keycode == self.code) {
break;
}
}
if (index == max) {
index = 0; // DUMMY
}
return index;
}



さて実行してみよう。今回は押したキーの文字表現をデバッガコンソールへ表示するようにしてみた。
@implementation AppController

- (void)keyDown:(Hotkey*)hotkey
{
NSLog(@"keyDown: keycode=%x [%@]", hotkey.code, [hotkey string]);
}
:



結果はこう。


おお出た。


サンプル:HotKey-2.zip

2009年7月7日

ホットキー変更対応(21) - 実装サンプル

サンプルアプリを作り昨日まで作成したクラスの動作確認を行う。
サンプル:HotKey-1.zip

いつものごとく AppController を用意し、InterfaceBuilderでインスタンス化しておく。そして実行時 Nibファイル(Xib)読み込み後に呼び出される awakeFromNib でホットキーの登録を行う。

AppController.m

- (void)awakeFromNib
{
_hotkey_register = [HotkeyRegister sharedRegister];

Hotkey *hotkey;

hotkey = [[[Hotkey alloc] init] autorelease];
hotkey.code = 0x23; // 'P'
hotkey.modifier = cmdKey | optionKey;
hotkey.target = self;
hotkey.action = @selector(keyDown:);
[_hotkey_register registHotkey:hotkey];

hotkey = [[[Hotkey alloc] init] autorelease];
hotkey.code = 0x25; // 'L'
hotkey.modifier = cmdKey | optionKey;
hotkey.target = self;
hotkey.action = @selector(keyDown:);
[_hotkey_register registHotkey:hotkey];

}


試しに Command + Option + P or L の2つのキーを登録してみた。これらのキーが呼び出されると - [AppController keyDown:] が呼び出される。

- (void)keyDown:(Hotkey*)hotkey
{
NSLog(@"keyDown: keycode=%x", hotkey.code);
}



後始末として NSApplication のデリゲートメソッド - [NSApplication applicationWillTerminate:] で - [HotkeyRegister unregistAll] を呼び出す。

- (void)applicationWillTerminate:(NSNotification *)aNotification
{
[_hotkey_register unregistAll];
}



さて実行してみよう。

アプリケーションが立ち上がったら Command + Option + P または L を押す。

デバッガコンソール
[Session started at 2009-07-07 22:42:07 +0900.]
2009-07-07 22:42:08.584 Hotkey[22009:10b] registHotkey: keyid=0, modifier=900, code=23, ref=13c020,
target=, action=keyDown:
2009-07-07 22:42:08.589 Hotkey[22009:10b] {(

)}
2009-07-07 22:42:08.592 Hotkey[22009:10b] registHotkey: keyid=1, modifier=900, code=25, ref=12cbc0,
target=, action=keyDown:
2009-07-07 22:42:08.663 Hotkey[22009:10b] {(
,

)}
2009-07-07 22:42:11.813 Hotkey[22009:10b] keyDown: keycode=25
2009-07-07 22:42:12.845 Hotkey[22009:10b] keyDown: keycode=23
2009-07-07 22:42:14.238 Hotkey[22009:10b] unregistHotkey:
2009-07-07 22:42:14.243 Hotkey[22009:10b] unregistHotkey:
2009-07-07 22:42:14.245 Hotkey[22009:10b] finished

The Debugger has exited with status 0.


ちゃんと - [AppController keyDown:] が呼び出されているようだ。
アプリケーション終了時の - [HotkeyRegister ungregistHotkey:] も最後に呼び出されている。

2009年7月6日

ホットキー変更対応(20) - HotkeyRegisterの実装

ホットキーをシステムへ登録するクラスの実装に入る。

ヘッダはこんな感じ。
HotkeyRegister.h

@class Hotkey;
@interface HotkeyRegister : NSObject {

NSMutableSet* _hotkey_set;
}

@property (retain) NSMutableSet* hotkey_set;

+ (HotkeyRegister*)sharedRegister;
- (void)unregistAll;

- (BOOL)registHotkey:(Hotkey*)hotkey;
- (BOOL)unregistHotkey:(Hotkey*)hotkey;

@end


NSMutableSet の _hotkey_set に Hotkeyのインスタンスを追加して管理する。ここで - [Hotkey isEqual:] が生きてくる(今回は keycodeと modifier両方で同値性をチェックしている)。

またインスタンスは1つあれば十分なので、Singletonパターンを使い、インスタンスは sharedRegister で取得させる。


次に実装。

ホットキー登録部分は以前紹介したように Carbon APIを使う。
Cocoaの日々 - ホットキー (2008/04)


こんな感じ。
HotkeyRegister.m
#define SC_HOTKEY_SIGNATURE 'schk'

OSStatus hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData);

static HotkeyRegister* _hotkey_register = nil;
static UInt32 _hotkey_id = 0;

@implementation HotkeyRegister
@synthesize hotkey_set = _hotkey_set;

- (void)unregistAll
{
for (Hotkey* hotkey in _hotkey_set) {
[self unregistHotkey:hotkey];
}
[_hotkey_set release];

NSLog(@"finished");
}


+ (HotkeyRegister*)sharedRegister
{
if (!_hotkey_register) {

_hotkey_register = [[HotkeyRegister alloc] init];
_hotkey_register.hotkey_set = [[NSMutableSet alloc] init];
EventTypeSpec eventTypeSpecList[] ={
{ kEventClassKeyboard, kEventHotKeyPressed }
};

InstallApplicationEventHandler(
&hotKeyHandler, GetEventTypeCount(eventTypeSpecList),
eventTypeSpecList, self, NULL);
}

return _hotkey_register;
}


// Hot key handler
OSStatus hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData)
{
EventHotKeyID hotKeyID;
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL,
sizeof(hotKeyID), NULL, &hotKeyID);

if (hotKeyID.signature == SC_HOTKEY_SIGNATURE) {

for (Hotkey* hotkey in _hotkey_register.hotkey_set) {
if (hotKeyID.id == hotkey.keyid) {
[hotkey.target performSelector:hotkey.action withObject:hotkey];
}
}
}
return noErr;
}

- (BOOL)unregistHotkey:(Hotkey*)hotkey
{
OSStatus status = UnregisterEventHotKey(hotkey.ref);

if (status != noErr) {
NSLog(@"UnregisterEventHotKey() was failed : %d", status);
return NO;
}
NSLog(@"unregistHotkey: %@", hotkey);
return YES;
}

- (BOOL)registHotkey:(Hotkey*)hotkey
{
// replace or overwrite ??
if ([_hotkey_set containsObject:hotkey]) {
// same hotkey exists, then replace it
if (![self unregistHotkey:hotkey]) {
// error
return NO;
}
}

EventHotKeyID hotKeyID;
hotKeyID.id = _hotkey_id++;
hotKeyID.signature = SC_HOTKEY_SIGNATURE;
OSStatus status;
EventHotKeyRef hotkeyRef;

status = RegisterEventHotKey(hotkey.code, hotkey.modifier, hotKeyID,
GetApplicationEventTarget(), 0, &hotkeyRef);
hotkey.ref = hotkeyRef;
hotkey.keyid = hotKeyID.id;

if (status != noErr) {
NSLog(@"RegsiterEventHotKey() was failed : %d", status);
return NO;
}

[_hotkey_set addObject:hotkey];

NSLog(@"registHotkey: %@", [hotkey dump]);
NSLog(@"%@", _hotkey_set);


return YES;

}

@end


ホットキー毎にとっておく必要のある情報(idやEventHotKeyRef)は、あらかじめ用意しておいた Hotkeyのプロパティへ保存しておく。id は登録毎に新規に発番するようにしている。
登録後、システムからのコールバックを受けて hotKeyHandler( ) が呼び出される。この処理で _hotkey_set の中に登録されている Hotkey 情報を調べ、id が一致するものがあれば、Hotkey.target へ Hotkey.action のメッセージを投げる( - [NSObject performSelector:withObject: ] を使う)。

その他、後始末用の unregistAll などを用意しておく。

- - - -
ここまで作ったクラスの動作確認を行うため
次回は一度サンプルアプリに組み込んで動作させてみる。