cocoa - 在 NSPopover 中使用 NSNumberFormatter

标签 cocoa nspopover nsformatter paintcode

有没有办法让 NSNumberFormatter(或者可能是任何其他 NSFormatter)在 NSPopover 中工作?

弹出窗口中 NSTextField 的值绑定(bind)到 NSViewController 的representedObject。当在字段中输入无效数字(例如“asdf”)时,包含显示弹出窗口的 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

我猜测这是因为 transient 弹出窗口的窗口在显示工作表后消失了,当前编辑器和任何可以响应选择器的对象也消失了。

将弹出窗口行为设置为 NSPopoverBehaviorSemitransient 会有所帮助,但如果弹出窗口因文本字段中的值无效而被关闭,则仍会引发异常。

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

更新1

正如 Brian Webster 在下面发现的那样,这是 AppKit 的一个根本问题。

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

(在你问之前,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

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

这让我感觉不那么脏了。

最佳答案

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

  1. 当您在文本字段中按 Enter 键时,会触发绑定(bind),该绑定(bind)会尝试通过 NSNumberFormatter 验证字段中的值。
  2. 当失败时,绑定(bind)系统会通过响应者链呈现一个 NSError 对象。这会冒泡到 NSApplication,后者将错误以表格形式显示在窗口上。
  3. 工作表的出现会触发弹出窗口关闭,从而再次触发相同的绑定(bind),从而尝试显示另一个错误。但是,由于窗口上已经显示了一张工作表,因此第二个错误永远不会显示。如果您更改绑定(bind)选项并启用“始终显示应用程序模式警报”(这将在单独的窗口而不是工作表中显示错误),您将看到显示两个单独的警报窗口。

我认为正是这个错误中的错误导致 AppKit 陷入循环,并且当它试图干扰字段编辑器时(即堆栈跟踪中的 NSTextView) ),它最终会向现在已释放的 NSTextField 发送消息。

我发现的最佳解决方法是在我用来控制弹出窗口的 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: 调用会删除绑定(bind),以便在弹出窗口关闭时不会尝试重新验证文本字段。由于无论如何显示错误时弹出窗口都会消失,因此假设您每次显示弹出窗口时都从头开始创建弹出窗口并且不重复使用它,那么这应该不会产生任何不良影响。

此外,由于“确定”和“放弃更改”按钮在它们所引用的字段消失时不再有多大意义,因此我将绑定(bind)系统的恢复尝试器从错误中删除,然后再将其传递给 AppKit 进行显示。这样,它只是显示“值 X 无效”,并带有“确定”按钮,该按钮除了关闭错误窗口之外什么也不做。

请注意,只有在绑定(bind)上启用“始终显示应用程序模式警报”时,这才有效。否则,如果 AppKit 要将错误显示为工作表(至少不会在 View Controller 上),则 AppKit 似乎不会调用 willPresentError: 方法。不过,您也许可以将逻辑插入响应者链中的其他位置,例如主窗口的 Controller ,如果您想保留工作表行为。

我将让您决定这是否比手动验证值更难看。 :)

关于cocoa - 在 NSPopover 中使用 NSNumberFormatter,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22211672/

相关文章:

Swift macOS 弹出窗口检测更改暗模式

macos - NSAlert 高于 NSPopover

objective-c - macOS - 尝试在 NSTextField 上使用自定义 NSFormatter 惨败

cocoa - 如何将自定义项添加到 Cocoa 中 NSTextField 的上下文菜单中?

objective-c - Mac OSX 10.9.4 中的应用程序图标在运行时不会更改

objective-c - 如何控制NSNumberFormatter的科学风格?

objective-c - 如何将 C String 转换为 NSString?

cocoa - 如何以编程方式创建 NSPopover

objective-c - 带有数字格式化程序的 NSTextField 不接受仅填充字符