validateUserInterfaceItem(1)

メニューとツールバーの有効/無効を制御するプロトコルとして”NSUserInterfaceValidations”がある。

これをNSButtonに対して拡張する手法がある。2006年〜2007年頃に書いたコードだ。

@interface MTLButton : NSButton
{
}
- (void) updateEnableState;
@end

@implementation MTLButton

- (id)initWithFrame:(NSRect)frameRect
{
    self = [self initWithFrame:frameRect];

    if(self)
    {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(updateEnableState)
                                                     name:NSWindowDidUpdateNotification
                                                   object:nil];
    }

    return self;
}


- (void) dealloc
{
    [self subviews];

    [[NSNotificationCenter defaultCenter] removeObserver:self];

    [super dealloc];
}

- (id) initWithCoder: (NSCoder*)aDecoder
{
    self = [super initWithCoder: aDecoder];

    if(self != nil)
    {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(updateEnableState)
                                                     name:NSWindowDidUpdateNotification
                                                   object:nil];
    }

    return self;
}

- (void) updateEnableState
{
    // actionが設定されているかをチェック。(target == nil)の場合はfirstResponderだから有効なのでtargetのnilチェックはしない。
    if( [self action] != nil && [self window] != nil && [self isHiddenOrHasHiddenAncestor] == NO)
    {
        id                  theValidator;

        // レスポンダーチェーンをたどって実際にactionを処理するオブジェクトを探す。そのオブジェクトがメニューやボタンのenable/disableの値を決定出来るはず
        theValidator = [NSApp targetForAction:[self action]
                                           to:[self target]
                                         from:self];


        if([self infoForBinding:@"enabled"])
        {
            // enabledにCocoaBindingが設定されている場合は
            // CocoaBindingの設定を優先させるために何もしない。
        }
        else if ((theValidator == nil) || ![theValidator respondsToSelector:[self action]])
        {
            // actionを実行できるレスポンダーが無い場合は、ボタンは押せない
            [self setEnabled:NO];
        }
        else if ([theValidator respondsToSelector:@selector(validateToolbarItem:)])
        {
            // actionを実行できるレスポンダーが"validateToolbarItem:"を実装しているならば従う
            [self setEnabled:
                [theValidator validateToolbarItem:
                    (NSToolbarItem*)self]];
        }
        else if ([theValidator respondsToSelector:@selector(validateUserInterfaceItem:)])
        {
            // actionを実行できるレスポンダーが"validateUserInterfaceItem:"を実装しているならば従う
            [self setEnabled:
                [(id<NSUserInterfaceValidations>)theValidator validateUserInterfaceItem:
                    (id<NSValidatedUserInterfaceItem>)self]];
        }
        else
        {
            // レスポンダーがあるけど、"validateXXXX"系のメソッドが無い場合は、ボタンは押せる。
            [self setEnabled:YES];
        }
    }
}

@end

最近、仕事でiOSでなくCocoaのコードを書く事があり気がついた。 このコードのNotificationを登録する箇所と値が間違っている。

  1. 登録する場所は、 “[MTLButton viewDidMoveToWindow]”で行われるべき。
  2. 登録解除する場所は、”[MTLButton viewWillMoveToWindow:newWindow]”で行われるべき。
  3. Notification登録時にButtonが乗っているWindowでフィルタリングするべき。

そんな訳で、

- (id)initWithFrame:(NSRect)frameRect;
- (void) dealloc;
- (id) initWithCoder: (NSCoder*)aDecoder;

は削除され、以下のコードで置換えるべきだと思うのだが、まだ実装してません。

- (void)viewDidMoveToWindow
{
    [super viewDidMoveToWindow];

    NSWindow* theWindow = [self window];

    if( theWindow != nil )
    {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(updateEnableState)
                                                     name:NSWindowDidUpdateNotification
                                                   object:theWindow];
    }
}

- (void)viewWillMoveToWindow:(NSWindow *)newWindow
{
    [super viewWillMoveToWindow:newWindow];

    NSWindow* theWindow = [self window];

    if( theWindow != nil )
    {
       [[NSNotificationCenter defaultCenter] removeObserver:self
                                                       name:NSWindowDidUpdateNotification
                                                     object:theWindow];
    }

}

ひょっとしたら、windowが放棄される時に”viewWillMoveToWindow:newWindow”が呼ばれないかもしれない。 が、もう寝る。