2014-03-05 26 views
12

有什麼辦法讓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 

基本上,你可以通過始終提供有效值來避免表單或對話框出現問題。上面的代碼在分配默認值時會考慮最小值和最大值。該子類還考慮了無字符串或空字符串以及鉗位值。

這讓我感覺不太髒。

回答

11

我建立了一個測試項目,看看我能否重現這一點,並且我得到了同樣的行爲。以下是這似乎是事件的順序:

  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

1

同樣的問題發生在源自核心數據模型對象的驗證錯誤。另一種方法是使用現有的酥料餅內的酥料餅的更換系統提供的模態對話框和呈現錯誤:

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的文本字段。簡單的自動佈局約束可確保錯誤彈出窗口的大小適當。

這只是我確信可以改進的初始努力。例如,用邏輯來呈現錯誤的失敗原因並調用恢復調度器(我放棄了......普通的撤銷功能允許用戶無論如何恢復到原始值)。

2

首先,設置酥料餅委託:

[ 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。您可能需要使用稍後的操作系統進行測試。