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