Использование NSNumberFormatter в NSPopover

Есть ли способ получить NSNumberFormatter (или, предположительно, любой другой NSFormatter) для работы в NSPopover?

Значение NSTextField в popover привязано к объекту NSViewController, представленному как объект. Когда в поле вводится недопустимый номер (например, "asdf" ), лист, в котором указано недопустимое значение, представлен в NSWindow, который содержит NSView, который представил popover.

Как только вы нажмете 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

Я предполагаю, что поскольку временное окно popover исчезло после отображения листа, а также текущий редактор и любой объект, который может ответить на селектор.

Настройка поведения popover для NSPopoverBehaviorSemitransient несколько помогает, но если popover получает отклонение с недопустимым значением в текстовом поле, исключение все равно бросается.

В этот момент все, о чем я могу думать, чтобы избежать этой проблемы, - это ручная проверка числовых значений. Тьфу.

Обновление 1

Как показал Брайан Вебстер, это фундаментальная проблема с AppKit.

Поскольку мои требования к проверке были довольно простыми (только положительные целые числа), обходным путем было выполнить ручную проверку в объекте KVC, который использовался как представленный объект в NSViewController, отображаемый NSPopover. Поскольку 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

Я установил тестовый проект, чтобы увидеть, могу ли я воспроизвести его, и я получил такое же поведение. Вот что представляет собой последовательность событий:

  • Когда вы нажимаете enter в текстовом поле, это вызывает привязку, которая пытается подтвердить значение в поле с помощью NSNumberFormatter.
  • При сбое система привязок представляет объект NSError через цепочку ответчиков. Это пузырится до NSApplication, что представляет ошибку в виде листа в окне.
  • Внешний вид листа вызывает закрытие popover, что, в свою очередь, снова вызывает одно и то же обязательство, которое пытается отобразить другую ошибку. Однако, поскольку в окне уже отображается лист, вторая ошибка никогда не отображается. Если вы измените параметры привязки и включите "Всегда присутствующие модальные оповещения приложения" (что будет отображать ошибку в отдельном окне вместо листа), вы увидите два отдельных окна предупреждений.

Я думаю, что эта ошибка-внутри-ошибка, которая бросает AppKit для цикла, и где-то в дороге, когда он пытается запутаться с редактором полей (что NSTextView в трассировке стека), он заканчивается обмен сообщениями теперь освобожденный NSTextField.

Лучшим обходным решением, которое я нашел, является реализация -willPresentError: в подклассе NSViewController, который я использую для управления popover, например:

- (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. Поскольку popover исчезнет, ​​когда ошибка будет отображаться в любом случае, это не должно иметь никакого вредного эффекта, предполагая, что вы создаете popover с нуля каждый раз, когда он отображается, а не повторно используете его.

Кроме того, поскольку кнопки "ОК" и "Отменить изменение" больше не имеют смысла, когда поле, к которому они обращаются, исчезло, я удаляю attempter восстановления системы привязки из ошибки, прежде чем передавать его в AppKit для отображения, Таким образом, он просто говорит: "Значение X недействительно" с кнопкой "ОК", которая ничего не делает, кроме как отклонить окно ошибки.

Обратите внимание, что это работает только в том случае, если на привязке включено "Всегда присутствующие модальные оповещения приложения". В противном случае метод willPresentError:, по-видимому, не будет вызван AppKit, если он будет отображать ошибку в виде листа, по крайней мере, не на контроллере представления. Возможно, вы сможете вставить логику где-то еще в цепочку ответчиков, хотя, например. контроллер главного окна, если вы хотите сохранить поведение листа.

Я оставлю это вам, чтобы решить, является ли это более или менее уродливым, чем проверка значений вручную.:)

Ответ 2

Сначала установите делегат 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. Возможно, вам придется протестировать его с помощью более поздних ОС.

Ответ 3

Такая же проблема возникает при ошибках проверки, возникающих в объектах модели Core Data. Другим подходом является замена модального диалога, предоставленного системой, и представление ошибки с использованием popover внутри существующего popover:

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. Простые ограничения автоматического макета обеспечивают, чтобы походка ошибок была соответствующим образом.

Это всего лишь начальное усилие, которое, я уверен, можно улучшить. Например, с логикой, чтобы представить причину ошибки ошибки и вызвать средство восстановления attempter (которое я отказался от... обычных функций отмены, позволяет в любом случае вернуться к исходному значению).