13

NSNumberFormatter (またはおそらく他の NSFormatter) を NSPopover で動作させる方法はありますか?

ポップオーバーの NSTextField の値は、NSViewController の presentedObject にバインドされます。フィールドに無効な数値 (「asdf」など) が入力されると、ポップオーバーを表示した NSView を含む NSWindow に値が無効であることを示すシートが表示されます。

[OK] をクリックするとすぐに、次のバックトレースが表示されます。

* thread #1: tid = 0x4e666a, 0x00007fff931f9097 libobjc.A.dylib`objc_msgSend + 23, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame #0: 0x00007fff931f9097 libobjc.A.dylib`objc_msgSend + 23
frame #1: 0x00007fff8a1fa6c8 AppKit`-[NSTextView(NSSharing) becomeKeyWindow] + 106
frame #2: 0x00007fff8a080941 AppKit`-[NSWindow(NSWindow_Theme) acquireKeyAppearance] + 207
frame #3: 0x00007fff8a0800df AppKit`-[NSWindow becomeKeyWindow] + 1420
frame #4: 0x00007fff8a07f5c6 AppKit`-[NSWindow _changeKeyAndMainLimitedOK:] + 803
frame #5: 0x00007fff8a1a205d AppKit`-[NSWindow _orderOutAndCalcKeyWithCounter:stillVisible:docWindow:] + 1156
frame #6: 0x00007fff8a0876c5 AppKit`-[NSWindow _reallyDoOrderWindow:relativeTo:findKey:forCounter:force:isModal:] + 3123
frame #7: 0x00007fff8a0867f0 AppKit`-[NSWindow _doOrderWindow:relativeTo:findKey:forCounter:force:isModal:] + 786
frame #8: 0x00007fff8a086470 AppKit`-[NSWindow orderWindow:relativeTo:] + 162
frame #9: 0x00007fff8a1a1425 AppKit`__18-[NSWindow _close]_block_invoke + 443
frame #10: 0x00007fff8a1a1230 AppKit`-[NSWindow _close] + 370
frame #11: 0x00007fff8a2d0565 AppKit`__106-[NSApplication(NSErrorPresentation) presentError:modalForWindow:delegate:didPresentSelector:contextInfo:]_block_invoke3221 + 50
frame #12: 0x00007fff8a2d02f7 AppKit`-[NSApplication(NSErrorPresentation) _something:wasPresentedWithResult:soContinue:] + 18
frame #13: 0x00007fff8a28fe9d AppKit`-[NSAlert didEndAlert:returnCode:contextInfo:] + 90
frame #14: 0x00007fff8a28f8c2 AppKit`-[NSWindow endSheet:returnCode:] + 368
frame #15: 0x00007fff8a28f49d AppKit`-[NSAlert buttonPressed:] + 107
frame #16: 0x00007fff8a1543d0 AppKit`-[NSApplication sendAction:to:from:] + 327
frame #17: 0x00007fff8a15424e AppKit`-[NSControl sendAction:to:] + 86
frame #18: 0x00007fff8a1a0d7d AppKit`-[NSCell _sendActionFrom:] + 128
frame #19: 0x00007fff8a1ba715 AppKit`-[NSCell trackMouse:inRect:ofView:untilMouseUp:] + 2316
frame #20: 0x00007fff8a1b9ae7 AppKit`-[NSButtonCell trackMouse:inRect:ofView:untilMouseUp:] + 487
frame #21: 0x00007fff8a1b91fd AppKit`-[NSControl mouseDown:] + 706
frame #22: 0x00007fff8a13ad08 AppKit`-[NSWindow sendEvent:] + 11296
frame #23: 0x00007fff8a0d9744 AppKit`-[NSApplication sendEvent:] + 2021
frame #24: 0x00007fff89f29a29 AppKit`-[NSApplication run] + 646
frame #25: 0x00007fff89f14803 AppKit`NSApplicationMain + 940

クラッシュ時の objc_msgSend のレジスタは次のとおりです。

(lldb) reg read
General Purpose Registers:
   rax = 0x0000610000190740
   rbx = 0x0000610000190740
   rcx = 0x0000000000000080
   rdx = 0x00007fff8a97fd93  "currentEditor"
   rdi = 0x0000610000190740
   rsi = 0x00007fff8a9612bf  "respondsToSelector:"
   rbp = 0x00007fff5fbfeae0
   rsp = 0x00007fff5fbfeab8
    r8 = 0x000000000000002e
    r9 = 0xffff9fffffeb1bbf
   r10 = 0x00007fff8a9612bf  "respondsToSelector:"
   r11 = 0xbaddbe5c3e96bead
   r12 = 0x0000610000053830
   r13 = 0x00007fff931f9080  libobjc.A.dylib`objc_msgSend
   r14 = 0x000060000012a500
   r15 = 0x00007fff931f9080  libobjc.A.dylib`objc_msgSend
   rip = 0x00007fff931f9097  libobjc.A.dylib`objc_msgSend + 23
rflags = 0x0000000000010246
    cs = 0x000000000000002b
    fs = 0x0000000000000000
    gs = 0x00000000c0100000

これは、シートが表示された後に一時的なポップオーバーのウィンドウが消えたためであり、現在のエディターとセレクターに応答できるオブジェクトも消えたためだと思います。

ポップオーバーの動作を NSPopoverBehaviorSemitransient に設定すると多少は効果がありますが、テキスト フィールドに無効な値が入力されてポップオーバーが閉じられた場合でも例外がスローされます。

この時点で、この問題を回避するために考えられるのは、数値を手動で検証することだけです。うん。

更新 1

Brian Webster が以下で発見したように、これは AppKit の根本的な問題です。

私の検証のニーズは非常に単純 (正の整数のみ) だったので、回避策は、NSPopover によって表示される NSViewController でpresentedObject として使用される KVC オブジェクトで手動検証を行うことでした。NSTextFieldは実際には文字列値を使用する必要があるため、-valueForKey: と -setValue:forKey: を使用してスカラー値を変換します。テキスト フィールドのバインドされた値に対して [すぐに検証する] をオンにすると、テキスト フィールドが変更されるたびに検証メソッドが呼び出されます。

(質問する前に、NSValueTransformer は検証プロセスに関与していないため、この作業を実行できません。フィールドにデータが入力されるか、変更が保存されたときにのみ呼び出されます。ユーザーが無効なものを入力したらすぐにフィードバックが必要でした。 data -- NSFormatter が行うように。)

これが私がやったことの要点です:

- (id)valueForKey:(NSString *)key
{
    if ([key isEqualToString:@"property1"]) {
        return [NSString stringWithFormat:@"%zd", _property1];
    }
    else if ([key isEqualToString:@"property2"]) {
        return [NSString stringWithFormat:@"%zd", _property2];
    }
    else {
        return [super valueForKey:key];
    }
}


- (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError
{
    if (! *ioValue) {
        *ioValue = @"0";
    }
    else if ([*ioValue isKindOfClass:[NSString class]]) {
        NSString *inputString = [[(NSString *)*ioValue copy] autorelease];
        inputString = [inputString stringByReplacingOccurrencesOfString:@"," withString:@""];
        NSInteger integerValue = [inputString integerValue];
        if (integerValue < 0) {
            integerValue = -integerValue;
        }
        *ioValue = [NSString stringWithFormat:@"%zd", integerValue];
    }

    return YES;
}

- (void)setValue:(id)value forKey:(NSString *)key
{
    if ([value isKindOfClass:[NSString class]]) {
        if ([key isEqualToString:@"property1"]) {
            _property1 = [value integerValue];
        }
        else if ([key isEqualToString:@"property2"]) {
            _property2 = [value integerValue];
        }
        else {
            [super setValue:value forKey:key];
        }
    }
    else {
        [super setValue:value forKey:key];
    }
}

今、私はシャワーを浴びる必要があります。

更新 2

@PixelCutCompany からの、PaintCode アプリでの処理方法に関するいくつかの役立つヒントに感謝します。

https://twitter.com/PixelCutCompany/status/441695942774104064 https://twitter.com/PixelCutCompany/status/441696198140125184

私はこれを思いついた:

@interface PopupNumberFormatter : NSNumberFormatter

@end

@implementation PopupNumberFormatter

- (BOOL)getObjectValue:(out id *)anObject forString:(NSString *)aString range:(inout NSRange *)rangep error:(out NSError **)error
{
    NSNumber *minimum = [self minimum];
    NSNumber *maximum = [self maximum];

    if (aString == nil || [aString length] == 0) {
        if (minimum) {
            *anObject = minimum;
        }
        else if (maximum) {
            *anObject = maximum;
        }
        else {
            *anObject = [NSNumber numberWithInteger:0];
        }
    }
    else {
    if (! [super getObjectValue:anObject forString:aString range:rangep error:nil]) {
        // if the superclass can't parse the string, assign a reasonable default
        if (minimum) {
            *anObject = minimum;
        }
        else if (maximum) {
            *anObject = maximum;
        }
        else {
            *anObject = [NSNumber numberWithInteger:0];
        }
    }
    else {
        // clamp the parsed value to a minimum and maximum (if set)
        if (minimum && [*anObject compare:minimum] == NSOrderedAscending) {
            *anObject = minimum;
        }
        else if (maximum && [*anObject compare:maximum] == NSOrderedDescending) {
            *anObject = maximum;
        }
    }
    }

    return YES;
}

@end

基本的に、常に有効な値を指定することで、シートまたはダイアログの問題を回避できます。上記のコードは、デフォルト値を割り当てるときに最小値と最大値を考慮します。サブクラスは、クランプ値だけでなく、nil または空の文字列も考慮します。

これにより、汚れをあまり感じなくなります。

4

3 に答える 3

1

コア データ モデル オブジェクトに起因する検証エラーでも同じ問題が発生します。もう 1 つの方法は、システム提供のモーダル ダイアログを置き換え、既存のポップオーバー内でポップオーバーを使用してエラーを表示することです。

ポップオーバーに表示されるエラーの例

-[NSResponder presentError: modalForWindow: delegate: didPresentSelector: contextInfo:]これは、メインのポップオーバーのコンテンツ ビュー コントローラーでオーバーライドすることで実行できます。防弾とは言いませんが、次の例では、エラーが発生した場所でエラー ポップオーバーを表示するのに非常にうまく機能しています。

- (void)presentError:(NSError *)error modalForWindow:(NSWindow *)window delegate:(id)delegate didPresentSelector:(SEL)didPresentSelector contextInfo:(void *)contextInfo {

self.validationErrorPopover.contentViewController = [[ZBErrorViewController alloc] initWithError:error];

NSView *sourceView;
if ([self.view.window.firstResponder isKindOfClass:[NSText class]]) // i.e., current field editor
    sourceView = (NSText*)self.view.window.firstResponder;
else
    sourceView = self.view;

[self.validationErrorPopover showRelativeToRect:[self.view convertRect:sourceView.bounds fromView:sourceView] ofView:self.view preferredEdge:NSMaxYEdge];
}

上記の例でself.validationErrorPopoverは、 は一時的な動作と HUD の外観で構成された単なるZBErrorViewControllerNSPopover であり、NSError オブジェクトを保持するプロパティが追加された通常の NSViewController であり、そのビューにはエラーの にバインドされたテキスト フィールドが含まれていますlocalizedDescription。シンプルな自動レイアウト制約により、エラー ポップオーバーが適切なサイズになるようにします。

これは、改善できると確信している最初の取り組みにすぎません。たとえば、エラーの失敗の理由を提示し、回復の試みを呼び出すロジックを使用します (私はあきらめました... 通常の元に戻す機能により、ユーザーはとにかく元の値に戻すことができます)。

于 2014-06-25T19:24:22.463 に答える