Cocoa 在NSPover中使用NSNumberFormatter

Cocoa 在NSPover中使用NSNumberFormatter,cocoa,nspopover,nsformatter,paintcode,Cocoa,Nspopover,Nsformatter,Paintcode,有没有办法让NSNumberFormatter(或者其他NSFormatter)在NSPover中工作 popover中NSTextField的值绑定到NSViewController的representedObject。当在字段中输入无效数字(例如,“asdf”)时,表示该值无效的工作表将显示在包含显示popover的NSView的NSWindow中 单击“确定”后,您将得到以下回溯: * thread #1: tid = 0x4e666a, 0x00007fff931f9097 libobj

有没有办法让NSNumberFormatter(或者其他NSFormatter)在NSPover中工作

popover中NSTextField的值绑定到NSViewController的representedObject。当在字段中输入无效数字(例如,“asdf”)时,表示该值无效的工作表将显示在包含显示popover的NSView的NSWindow中

单击“确定”后,您将得到以下回溯:

* 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
我猜这是因为瞬态popover的窗口在工作表显示后消失了,当前编辑器和任何可以响应选择器的对象也消失了

将POPOVERBEHAVIORSEMITTRANSIENT设置为POPOVERBEHAVIORSEMITTRANSIENT会有所帮助,但如果在文本字段中使用无效值解除popover,则仍会引发异常

此时,我所能想到的避免这个问题的方法就是手动验证数值。恶心

更新1

正如Brian Webster在下面发现的,这是AppKit的一个基本问题

由于我的验证需求非常简单(仅为正整数),因此解决方法是在KVC对象中进行手动验证,该对象用作NSPover显示的NSViewController中的representedObject。由于NSTextField确实希望使用字符串值,-valueForKey:和-setValue:forKey:用于转换标量值。当为文本字段中的绑定值启用“立即验证”时,只要文本字段发生更改,就会调用验证方法

(在您询问之前,NSValueTransformer无法完成这项工作,因为它不参与验证过程。它只在填充字段或保存更改时才会被调用。我希望在用户输入一些无效数据后立即得到反馈,就像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];
    }
}
基本上,您可以通过始终提供有效值来避免工作表或对话框的问题。上述代码在指定默认值时会考虑最小值和最大值。该子类还考虑了nil或空字符串以及钳制值


这让我感觉不那么脏。

我设置了一个测试项目,看看是否可以复制这个,我得到了同样的行为。以下是事件的顺序:

  • 当您在文本字段中按enter键时,将触发绑定,该绑定尝试通过
    NSNumberFormatter
    验证字段中的值
  • 当它失败时,绑定系统通过响应器链显示一个
    NSError
    对象。这会导致出现
    NSApplication
    ,从而在窗口中将错误显示为一张工作表
  • 工作表的出现触发关闭popover,这反过来又触发相同的绑定,该绑定尝试显示另一个错误。但是,由于窗口上已经显示了一个工作表,因此第二个错误永远不会显示。如果更改绑定选项并启用“始终显示应用程序模式警报”(这将在单独的窗口而不是工作表中显示错误),则会显示两个单独的警报窗口
  • 我认为是这个错误引发了AppKit的循环,当它试图弄乱字段编辑器(这是堆栈跟踪中的
    NSTextView
    )时,它最终会发送消息给现在解除分配的
    NSTextField

    我发现的最佳解决方法是在我用来控制popover的
    NSViewController
    子类中实现
    -willPresentError:
    ,如下所示:

    - (NSError *)willPresentError:(NSError *)error
    {
        NSMutableDictionary* userInfo = [[error userInfo] mutableCopy];
    
        [self.numberTextField unbind:@"value"];
        [userInfo setValue:nil forKey:NSRecoveryAttempterErrorKey];
        [userInfo setValue:nil forKey:NSLocalizedRecoveryOptionsErrorKey];
        return [NSError errorWithDomain:[error domain] code:[error code] userInfo:userInfo];
    }
    
    unbind:
    调用将删除绑定,以便在popover关闭时不会尝试重新验证文本字段。由于弹出框在显示错误时将消失,因此假设每次显示时都是从头开始创建弹出框,而不是重复使用,则不会产生任何不良影响

    此外,由于“OK”和“Discard Change”按钮在它们所指的字段消失时不再有意义,因此我将从错误中删除绑定系统的恢复尝试器,然后将其传递给AppKit进行显示。这样,它只会用一个“OK”按钮来表示“X值无效”,该按钮只会关闭错误窗口

    请注意,只有在绑定上启用“始终显示应用程序模式警报”时,此选项才有效。否则,
    willPresentError:
    方法似乎不会被AppKit调用,如果它将错误显示为工作表,至少不会在视图控制器上。但是,如果希望保持工作表行为,您可以将逻辑插入响应程序链中的其他位置,例如主窗口的控制器


    我将留给您来决定这是否比手动验证值更糟糕。:)

    源于核心数据模型对象的验证错误也会出现同样的问题。另一种方法是替换系统提供的模式对话框,并在现有popover中使用popover显示错误:

    这可以通过在主popover的内容视图控制器中重写
    -[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.validationErrorPover
    只是一个配置了瞬态行为和HUD外观的NSPover,
    zErrorViewController
    是一个普通的NSViewController,添加了一个属性来保存NSError对象,其视图包含绑定到错误的
    localizedDescription
    的文本字段。简单自动布局约束
    - (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];
    }
    
    [ popover setDelegate: myDelegate];
    
    - ( BOOL) popoverShouldClose: ( NSPopover*) popover {
        if( ![[[[ popover contentViewController] view] window] makeFirstResponder: popover]) {
            return NO;
        }
    
    /*  // Using commitEditing also solves the problem. However if user chooses
        // "Discard Changes" during immediate validation, the commitEditing returns YES,
        // and the result of discarding is not visible, because popover is closed.
        if( ![[ popover contentViewController] commitEditing])  {
            return NO;
        }
    */
        // return YES or NO depending on other considerations you may have
        return YES;
    }