在NSPopover中使用NSNumberFormatter

问题描述:

有什么办法让NSNumberFormatter(或者其他NSFormatter)在NSPopover中工作?在NSPopover中使用NSNumberFormatter

弹出窗口中NSTextField的值绑定到NSViewController的representObject。当在字段中输入无效号码(例如,“asdf”)时,表示该值无效的表单会显示在NSWindow中,该表单包含显示弹出窗口的NSView。

只要你单击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

布赖恩韦伯斯特以下发现的,这是与AppKit的一个基本问题。

由于我的验证需求非常简单(只是正整数),所以解决方法是在用作NSPopover显示的NSViewController中的RepresentObject的KVC对象中进行手动验证。由于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]; 
    } 
} 

现在我需要洗个澡。

更新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 

基本上,你可以通过始终提供有效值来避免表单或对话框出现问题。上面的代码在分配默认值时会考虑最小值和最大值。该子类还考虑了无字符串或空字符串以及钳位值。

这让我感觉不太脏。

我建立了一个测试项目,看看我能否重现这一点,并且我得到了同样的行为。以下是这似乎是事件的顺序:

  1. 当你点击在文本字段中输入,触发结合,它试图通过NSNumberFormatter以验证在字段中的值。
  2. 失败时,绑定系统通过响应者链呈现一个NSError对象。这会冒泡到NSApplication,该错误会在窗口中显示为表单。
  3. 工作表的外观触发了关闭的弹出窗口,这又会再次触发相同的绑定,这会尝试显示另一个错误。但是,由于窗口中已经显示了一张表,所以第二个错误不会显示。如果您更改了绑定选项并启用了“始终显示应用程序模式警报”(它将在单独的窗口而不是表单中显示错误),您将看到两个单独的警报窗口显示。

我认为这是这个错误中,一个错误抛出了AppKit为一个循环,和地方的道路时它试图乱用字段编辑器(这是在堆栈跟踪NSTextView),它结束现在解除分配NSTextField

我已经找到了最好的解决方法是实施-willPresentError:NSViewController子类我使用控制酥料饼,像这样:

- (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关闭时,尝试重新验证文本字段。由于无论如何都会显示错误,所以弹出消失将消失,假设您每次显示弹出窗口时都从头开始创建弹出窗口,而不是重新使用它,这应该不会有任何不良影响。另外,由于当它们所指的字段已经消失时,“确定”和“放弃更改”按钮不再有什么意义,我将该绑定系统的恢复尝试者从错误中移除,然后将其传递给AppKit显示。这样,它只是说“X值无效”,并带有一个“确定”按钮,该按钮除了关闭错误窗口外什么也不做。

请注意,这仅适用于绑定上启用了“总是呈现应用程序模式警报”的情况。否则,willPresentError:方法似乎不会被AppKit调用,如果它将显示错误作为工作表,至少不会在视图控制器上显示。不过,您可能可以将逻辑插入响应者链中的其他地方。主窗口的控制器,如果你想保持工作表的行为。

我会让你决定这是否比手动验证值要差不多。 :)

+0

哦,是的,我肯定会将此分类为AppKit错误。当我有机会时,我会用雷达提交我的示例项目。 –

+0

请将错误报告放在OpenRadar上,让我知道这个数字是什么:我绝对想要这样做(并感谢您花时间创建示例项目!) – chockenberry

同样的问题发生在源自核心数据模型对象的验证错误。另一种方法是使用现有的酥料饼内的酥料饼的更换系统提供的模态对话框和呈现错误:

example of an error presented in a 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.validationErrorPopover只是一个NSPopover配置了短暂的行为和HUD外观, ZBErrorViewController是一个普通的NSViewController,增加了一个属性来保存NSError对象,其视图包含一个绑定到错误localizedDescription的文本字段。简单的自动布局约束可确保错误弹出窗口的大小适当。

这只是我确信可以改进的初始努力。例如,用逻辑来呈现错误的失败原因并调用恢复调度器(我放弃了......普通的撤销功能允许用户无论如何恢复到原始值)。

首先,设置酥料饼委托:

[ popover setDelegate: myDelegate]; 

在委托实施popoverShouldClose:方法等如下。这个想法是,“立即验证”控制将拒绝退出其第一响应者状态,直到用户提供有效值。

- (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; 
} 

这对我的作品在OS X 10.8酥料饼的行为NSPopoverBehaviorSemitransient和NSPopoverBehaviorTransient。您可能需要使用稍后的操作系统进行测试。