2

I need to code an NSTextField that basically acts like the calculator functionality in Spotlight Search. I have had success parsing the actual input expression with NSExpression and calculating the result, but it's the view part I am getting stuck on.

How do I render the answer in the text field as the user types, but not have it a part of the input? Will I likely have to subclass NSTextField?

Just creating an NSFormatter doesn't give me all the functionality I need, it seems. I can get the field to calculate and replace the text with the result at once with the delegate's controlTextDidEndEditing: message but the dynamic sum to the right is round-tripped back to the formatter.

Does anyone know of an example that comes close to what I am trying to do?

I'm pretty new to Cocoa and am having a little trouble knowing which components to involve to accomplish this.

jscs
  • 63,694
  • 13
  • 151
  • 195
Ray Pendergraph
  • 449
  • 5
  • 19
  • 1
    If you want to dynamically evaluate what's being typed as it's being typed (as opposed to after), look into `controlTextDidChange:`: http://stackoverflow.com/a/11488611/277952 – NSGod Mar 04 '16 at 19:56
  • I updated the questions to be more explicit. Mainly my trouble is with displaying the answer in the field (see pic) as the user types without round-tripping that back into the formatter. I can already calculate the expression as the user types, i just want to display the last answer. – Ray Pendergraph Mar 04 '16 at 20:03
  • Edited your title to describe the specific difficulty you're facing, rather than [a generic request for a component](http://meta.stackexchange.com/a/124930/159251). That should broaden the potential solution field. – jscs Mar 04 '16 at 20:16

2 Answers2

2

First, it'll be much simpler (I think) to use an NSTextView instead of an NSTextField, because with NSTextField you get into the “field editor” madness, where the text field uses a text view (the field editor) under the covers when it has focus. We need to use the text view delegate methods to implement this, so let's just use an NSTextView.

The basic idea is that we'll use an attributed string to display the formula in large black characters followed by the answer in smaller gray characters. Any time the user changes the input, we'll delete the old answer and append the new answer.

For a toy implementation, we'll just do everything in the app delegate. We have an outlet connected to the text view in the XIB:

@interface AppDelegate () <NSTextDelegate, NSTextViewDelegate>

@property (weak) IBOutlet NSWindow *window;
@property (strong) IBOutlet NSTextView *textView;

@end

In the XIB, the text view's delegate outlet is connected to the app delegate.

We'll also need to store the length of the answer (so we can delete it before appending the new answer) and the text attributes for the formula and the answer:

@implementation AppDelegate {
    NSUInteger answerLength;
    NSDictionary *formulaAttributes;
    NSDictionary *answerAttributes;
}

- (void)awakeFromNib {
    [super awakeFromNib];

    formulaAttributes = @{
                          NSFontAttributeName: [NSFont systemFontOfSize:32 weight:NSFontWeightLight]
                          };

    answerAttributes = @{
                         NSFontAttributeName: [NSFont systemFontOfSize:24 weight:NSFontWeightLight],
                         NSForegroundColorAttributeName: [NSColor grayColor]
                         };

    [self updateAnswerInTextView];
}

Whenever the contents of the text view changes, we want to remove the old answer and append the new answer. We also need to save the selected range before modifying the text, and restore it after, because the text view puts the insertion point at the end of the text when we modify it. One last thing: the user could paste attributed text into the text view, and we want to remove any attributes on that pasted text. So we reset the attributes of the text after removing the old answer. We do all this in an NSTextDelegate method:

- (void)textDidChange:(NSNotification *)notification {
    [self updateAnswerInTextView];
}

- (void)updateAnswerInTextView {
    NSRange selectedRange = self.textView.selectedRange;
    [self removeAnswerFromTextView];
    [self applyFormulaAttributesToTextView];
    [self appendAnswerToTextView];
    self.textView.selectedRange = selectedRange;
}

Here are the helper methods:

- (void)removeAnswerFromTextView {
    NSTextStorage *storage = self.textView.textStorage;
    [storage replaceCharactersInRange:self.answerRange withString:@""];
    answerLength = 0;
}

- (void)applyFormulaAttributesToTextView {
    NSTextStorage *storage = self.textView.textStorage;
    [storage setAttributes:formulaAttributes range:NSMakeRange(0, storage.length)];
}

- (void)appendAnswerToTextView {
    id answer = [self answer];
    NSString *answerString = [NSString stringWithFormat:@" = %@", answer];
    NSAttributedString *richAnswer = [[NSAttributedString alloc] initWithString:answerString attributes:answerAttributes];
    [self.textView.textStorage appendAttributedString:richAnswer];
    answerLength = answerString.length;
}

- (NSRange)answerRange {
    NSTextStorage *storage = self.textView.textStorage;
    return NSMakeRange(storage.length - answerLength, answerLength);
}

- (id)answer {
    @try {
        NSExpression *expression = [NSExpression expressionWithFormat:self.textView.textStorage.string];
        return [expression expressionValueWithObject:nil context:nil];
    }
    @catch (NSException *exception) {
        return @"???";
    }
}

Note that NSExpression isn't really a good math parser for this, for two reasons:

  • It throws an exception if there's a parsing error, and Objective-C doesn't really clean things up after an exception, so there might be memory leaks or worse.
  • It uses % to indicate a format specifier, which isn't really a great idea in a string entered by the user.

You should look into using the old Objective-C version of DDMathParser or some other library instead.

Anyway, the code above evaluates the user's formula and displays the answer, but it's got a problem: the user can select and modify the answer part of the string. To fix that, we need to implement an NSTextViewDelegate method to restrict the selected range to just the formula part of the text:

- (NSRange)textView:(NSTextView *)textView willChangeSelectionFromCharacterRange:(NSRange)oldSelectedCharRange toCharacterRange:(NSRange)newSelectedCharRange {
    NSUInteger answerStart = textView.textStorage.length - answerLength;
    NSUInteger newSelectedEnd = NSMaxRange(newSelectedCharRange);
    if (newSelectedEnd > answerStart) {
        newSelectedEnd = answerStart;
        NSUInteger newSelectedStart = MIN(newSelectedCharRange.location, newSelectedEnd);
        newSelectedCharRange.location = newSelectedStart;
        newSelectedCharRange.length = newSelectedEnd - newSelectedStart;
    }
    return newSelectedCharRange;
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
0

Likely you can do this with key-value validation. This gives you the opportunity to change user input instead of simply reject it. You should turn on "validate immediately" or so for the text field.

You can recognize the added result text with attributes and replace them on every change.

Just an idea.

Amin Negm-Awad
  • 16,582
  • 3
  • 35
  • 50