2014-03-05 4 views
12

NSNumberFormatter (또는 아마도 다른 NSFormatter)를 NSPopover에서 사용할 수있는 방법이 있습니까?NSPopover에서 NSNumberFormatter 사용

popover의 NSTextField 값은 NSViewController의 representObject에 바인딩됩니다. 잘못된 숫자가 필드에 입력되면 (예 : "asdf") 값이 유효하지 않다는 것을 알리는 시트가 NSWindow에 표시됩니다. NSWindow에는 popover를 표시 한 NSView가 포함되어 있습니다.

는 는

는 즉시 확인, 다음 역 추적 가져 오기를 클릭로 : objc_msgSend의 충돌시

* 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 

레지스터이다 : 나는 추측하고있어

(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의 창이 사라지고 현재 편집기와 선택기에 응답 할 수있는 객체도 표시됩니다.

Popover 동작을 NSPopoverBehaviorSemitransient로 설정하면 다소 도움이되지만 텍스트 필드에서 잘못된 값으로 Popover가 해제되면 예외가 여전히 발생합니다.

이 시점에서 나는이 문제를 피하기 위해 숫자 값의 수동 검증을 생각할 수 있습니다. 왝.

업데이트 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]; 
    } 
} 

지금 나는 샤워를해야합니다. 그들은 PaintCode 응용 프로그램에서 작업을 수행하는 방법에 대한 @PixelCutCompany에서 유용한 힌트 몇 가지에

업데이트 2

감사합니다 :

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 또는 빈 문자열을 고려합니다.

이렇게하면 기분이 훨씬 덜럽습니다.

답변

11

필자는이 프로젝트를 재현 할 수 있는지 테스트 프로젝트를 설정하고 동일한 유형의 동작을 얻었습니다. 사용자가 텍스트 필드에 입력 쳤을 때

  1. , 즉 바인딩을 트리거의 NSNumberFormatter를 통해 필드의 값을 검증하기 위해 시도 : 여기에 이벤트의 순서로 나타납니다거야.
  2. 바인딩 시스템은 응답 체인을 통해 NSError 개체를 제공합니다. 이 오류는 NSApplication으로 바뀌며 오류가 창에 시트로 표시됩니다.
  3. 시트의 모양에 따라 팝 오버가 닫히고, 동일한 바인딩이 다시 트리거되어 다른 오류가 표시됩니다. 그러나 이미 창에 시트가 표시되어 있으므로 두 번째 오류가 표시되지 않습니다. 바인딩 옵션을 변경하고 "응용 프로그램 모달 경고 항상 표시"(시트가 아닌 별도의 창에 오류가 표시됨)를 활성화하면 두 개의 별도 경고 창이 표시됩니다.

내가 그것을 필드 편집기로 엉망 (즉 스택 추적에서 NSTextView있어)하려고 할 때 루프에 대한 AppKit의를 throw이 오류 내-AN-오류, 그리고 어딘가에 도로 아래로 생각, 그것은 끝 현재 할당 해제 된 메시징 NSTextField. 그것은 '아무튼 있도록

- (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: 호출이 바인딩을 제거합니다

내가 찾은 가장 좋은 해결 방법은 내가의 팝 오버를 제어과 같이하기 위해 사용하고있어 NSViewController 하위 클래스에서 -willPresentError:을 구현하는 것입니다 popover가 닫힐 때 텍스트 필드의 유효성을 다시 검사하지 않습니다. 어쨌든 오류가 표시되면 popover가 사라지기 때문에 표시 할 때마다 처음부터 popover를 생성하고 재사용하지 않는다고 가정하면 아무런 부작용이 없어야합니다.

또한 "OK"및 "Discard Change"버튼이 참조하는 필드가 사라지면 더 이상 의미가 없으므로 AppKit에 전달하기 전에 오류로부터 바인딩 시스템의 복구 attempter를 제거합니다. 디스플레이. 이렇게하면 "값 X가 유효하지 않습니다"라는 메시지가 나타나고 "확인"버튼 만 누르면 오류 창이 사라집니다.

바인딩에서 항상 "응용 프로그램 모달 경고 표시"가 활성화 된 경우에만 작동합니다. 그렇지 않으면 willPresentError: 메서드는 오류를 시트로 표시하려고 할 때 AppKit에서 호출하지 않는 것처럼 보입니다. 적어도보기 컨트롤러에는 표시되지 않습니다. 예를 들어 응답 체인에 로직을 삽입 할 수 있습니다. 시트 동작을 유지하려면 메인 윈도우의 컨트롤러.

수동으로 값을 확인하는 것보다 다소 추한 지 여부를 결정하는 데 남겨 두겠습니다. :)

+0

오, 그래, 분명히 이것을 AppKit 버그로 분류 할 것입니다. 기회가되면 레이더에서 샘플 프로젝트를 제출할 것입니다. –

+0

버그 보고서를 OpenRadar에 올려 놓고 그 번호를 알려주십시오 : 저는 이것을 확실히 버리고 싶습니다. (그리고 샘플 프로젝트를 만들 시간을내어 주셔서 감사합니다!) – chockenberry

1

핵심 데이터 모델 개체에서 비롯된 유효성 검사 오류와 동일한 문제가 발생합니다.또 다른 방법은 기존의 팝 오버 내에서 팝 오버를 사용하여 오류를 시스템 제공 모달 대화 상자를 교체하고 제시하는 것입니다

example of an error presented in a popover

이 메인 팝 오버의 내용 뷰 컨트롤러에 -[NSResponder presentError: modalForWindow: delegate: didPresentSelector: contextInfo:]을 재정의 할 수 있습니다. , self.validationErrorPopover 그냥 NSPopover이 과도 행동과 HUD 모양으로 구성되어, 위의 예에서

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

: 나는 방탄이다하지만 오류가 발생한 경우 다음과 같은 오류 팝 오버를 제시 꽤 좋은 일을 말을하지 않습니다 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; 
} 

이 팝 오버 행동 NSPopoverBehaviorSemitransient 및 NSPopoverBehaviorTransient와 OS X 10.8에서 나를 위해 작동합니다. 나중에 OS로 테스트해야 할 수도 있습니다.