Controllerと たわむれる12

前回のつづき


QLPreviewPanelのDelegate第一弾

第一弾です。長くなります。

QLPreviewPanelのDelegateにはControllersXXXViewController達を使うのですが、ほとんど同じことをやります。こういうときはクラス全部にコピペせずに共通するスーパークラスをでっち上げるのが正解です。元々のスーパークラスがすべてNSViewControllerですからNSViewControllerのサブクラスを共通のスーパークラスに仕立てます。

では、適当にControllersXXXViewControllerの宣言部または実装部のクラス名を選択します。
で、コンテクストメニューまたは「編集」メニューから「リファクタリング...」を選択します。


スーパークラスを作成」を選択し、スーパークラス名を「ControllersViewController」にします。
多少警告が出ますがとりあえず無視して「プレビュー」「適用」。


#importプリプロセッサの位置が変ですので直しておきます。
他のControllersXXXViewControllerのスーパークラスもControllersViewControllerに変更します。ControllersViewController.hをインクルードするのを忘れずに。


で、ControllersViewController。

ControllersViewController.h

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

@interface ControllersViewController : NSViewController <QLPreviewPanelDelegate>

- (NSView *)previewTragetView;

@end

これもインクルードファイルがおかしいので直しておきます。
それとQLPreviewPanelDelegateプロトコルへの準拠を宣言します。
共通するメソッドは -previewTragetView だけ宣言しておきます。このメソッドは実際にPreviewされるアイテムが表示されているビューを返すメソッドです。当然サブクラスでオーバーライドされる事が前提です。


ControllersViewController.mにいく前に、NSViewControllerの話。
NSViewControllerはNSResponderを親に持ってますが、通常はレスポンダーチェーンに含まれません。今回はそれだと困るのでレスポンダーチェーンに割り込むようにします。


ControllersViewController.m

#import "ControllersViewController.h"

@implementation ControllersViewController
- (void)dealloc
{
    [[self view] removeObserver:self forKeyPath:@"nextResponder"];
    
    [super dealloc];
}
- (void)loadView
{
    [super loadView];
    
    [[self view] addObserver:self
                  forKeyPath:@"nextResponder"
                     options:0
                     context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if(![keyPath isEqualToString:@"nextResponder"]) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        return;
    }
    
    id nextResponder = [object nextResponder];
    if([self isEqual:nextResponder]) return;
    
    [self setNextResponder:nextResponder];
    [object setNextResponder:self];
    
    if([object window]) {
        [[object window] makeFirstResponder:[self previewTragetView]];
    }
}
- (void)keyDown:(NSEvent *)theEvent
{
    if([theEvent isARepeat]) return [super keyDown:theEvent];
    
#define kSPACE_KEY    49
    unsigned short code = [theEvent keyCode];
    switch(code) {
        case kSPACE_KEY:
            [NSApp sendAction:@selector(togglePreviewPanel:) to:nil from:nil];
            return;
    }
    
    [super keyDown:theEvent];
}
- (NSView *)previewTragetView
{ 
    return nil;
}
- (BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event
{
    if([event type] != NSKeyDown) return NO;
    
    NSView *previewTragetView = [self previewTragetView];
    if(!previewTragetView) return NO;
    
    [previewTragetView keyDown:event];
    
    return YES;
}
@end

ちょっと長いです。順番に。
まず、最初の3つのメソッドはレスポンダーチェーンに割り込むためのメソッドです。

  1. 自身のviewのnextResponderをKVOで監視する
  2. 自身のviewのnextResponderが変更されたらviewと新しいnextResponderの間に自身を割り込ませる

という方法を採っています。
既にnextResponderが自身であればもちろん割り込みません。ここでさらに割り込むと無限ループになりますので注意してください。
あと、ついでに、previewTragetViewをfirstResponderにしてます。


レスポンダーチェーンに割り込んだので-keyDown:メソッドが呼ばれるようになります。これを使ってスペースバーでのプレビューの開始を行います。
直接ControllersWindowControllerにメソッドを投げずにAction-Targetを使用してFirstResponderに委譲しています。これで、クラス依存が無くなりますし、もしターゲットが変わってもこのメソッドを変更する必要は無くなります。


-previewTragetViewメソッドはデフォルトではnilを返します。


QLPreviewPanelの最初のDelegateメソッドは -previewPanel:handleEvent:です。
このメソッドでQLPreviewPanelが処理しなかったイベントを処理する機会が与えられます。
処理出来なかった場合はNOを処理出来た場合はYESを返します。NOを返した場合はビープ音が発せられます。
今回はkeyDownイベントのみを全部ごっそりpreviewTragetViewに処理させます。previewTragetViewが処理出来なければおそらくpreviewTragetViewがビープ音を発するでしょう。



次回は第2弾。


つづき Controllerと たわむれる12-2 - masakihの日記