有没有办法让NSNumberFormatter(或者可能是任何其他NSFormatter)在NSPopover中工作?
弹出窗口中的NSTextField值绑定到NSViewController的representObject。当在字段中输入无效的数字时(例如,“asdf”),在NSWindow中会显示一个表示该值无效的表格,其中包含呈现弹出窗口的NSView。
单击“确定”后,您将获得以下回溯:
* 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的基本问题。
由于我的验证需求非常简单(只是正整数),因此解决方法是在KVC对象中进行手动验证,该对象用作NSPopover显示的NSViewController中的representObject。由于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
基本上,您可以通过始终提供有效值来避免工作表或对话框出现问题。上面的代码在分配默认值时会考虑最小值和最大值。子类还考虑了nil或空字符串以及钳位值。
这让我感觉不那么脏了。
答案 0 :(得分:11)
我设置了一个测试项目,看看我是否可以重现这个,我也有同样的行为。这似乎是事件序列:
NSNumberFormatter
验证字段中的值。 NSError
对象。这会冒泡到NSApplication
,这会将错误显示为窗口上的工作表。我认为这是错误 - 错误 - 错误导致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:
调用会删除绑定,因此当弹出窗口关闭时,它不会尝试重新验证文本字段。因为无论如何都会在显示错误时弹出窗口消失,所以这不应该有任何不良影响,假设你每次显示时都是从头开始创建弹出框而不是重复使用它。
此外,由于“OK”和“Discard Change”按钮在他们所指的字段消失后不再有意义,我将错误从绑定系统的恢复尝试器中删除,然后再将其传递给AppKit进行显示。这样,它只是说“值X无效”,“OK”按钮除了关闭错误窗口外什么都不做。
请注意,只有在绑定上启用“始终存在应用程序模式警报”时,此方法才有效。否则,willPresentError:
方法似乎不会被AppKit调用,如果它将作为工作表显示错误,至少不会在视图控制器上显示。您可能能够在响应程序链中的其他位置插入逻辑,例如,主窗口的控制器,如果你想保持工作表的行为。
我会留给你判断这是否比手动验证值更难或更难看。 :)
答案 1 :(得分:3)
首先,设置popover委托:
[ 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。您可能需要使用以后的操作系统进行测试。
答案 2 :(得分:1)
源自Core Data模型对象的验证错误也会出现同样的问题。另一种方法是替换系统提供的模式对话框,并使用现有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.validationErrorPopover
只是一个配置了瞬态行为和HUD外观的NSPopover,ZBErrorViewController
是一个普通的NSViewController,添加了一个属性来保存NSError对象,其视图包含绑定到错误localizedDescription
的文本字段。简单的自动布局约束可确保错误弹出窗口的大小适当。
这只是我最初的努力,我确信可以改进。例如,使用逻辑来显示错误的失败原因并调用恢复尝试程序(我放弃了...普通撤消功能允许用户无论如何都要恢复原始值)。