// // MPExpressionView.m // MathPad // // Created by Kim Wittenburg on 17.04.14. // Copyright (c) 2014 Kim Wittenburg. All rights reserved. // #import "MPExpressionView.h" #import "MPExpression.h" #import "MPExpressionElement.h" #import "MPParsedExpression.h" #import "MPPowerFunction.h" #import "MPParenthesisFunction.h" #import "MPFractionFunction.h" #import "MPRangePath.h" #import "MPMathRules.h" #import "MPExpressionStorage.h" #import "MPExpressionLayout.h" #import "MPFunctionLayout.h" #import "MPFunctionsViewController.h" #import "NSIndexPath+MPAdditions.h" @interface MPExpressionView () @property (nonatomic, strong) NSButton *radiansDegreesButton; @property (nonatomic, strong) NSButton *functionsButton; @property (nonatomic, strong) NSPopover *functionsPopover; @property (nonatomic, strong) MPFunctionsViewController *functionsViewController; @property (nonatomic, strong) NSPopUpButton *syntaxErrorsPopUpButton; @property (nonatomic, strong) NSTextField *mathErrorTextField; @property (nonatomic, strong) NSTimer *caretTimer; @property (nonatomic) NSTimeInterval caretBlinkRate; @property (nonatomic) BOOL caretVisible; @property (nonatomic, getter = isSelectionModifyingStart) BOOL selectionModifyingStart; @property (nonatomic, strong) NSIndexPath *mouseAnchor; @end @interface MPExpressionView (MPDrawing) - (NSPoint)expressionOrigin; @end @interface MPExpressionView (MPSelection) - (void)restartCaretTimer; - (void)updateCaret:(NSTimer *)timer; - (NSRect)selectionRect; - (NSIndexPath *)selectionToTheRightOf:(NSIndexPath *)selectionPath byExtendingSelection:(BOOL)extendingSelection selectWords:(BOOL)selectWords; - (NSIndexPath *)selectionToTheLeftOf:(NSIndexPath *)selectionPath byExtendingSelection:(BOOL)extendingSelection selectWords:(BOOL)selectWords; - (MPRangePath *)rangePathEnclosingAnchorPath:(NSIndexPath *)anchor newSelectionPath:(NSIndexPath *)newSelection; @end @implementation MPExpressionView (MPDrawing) - (NSPoint)expressionOrigin { NSRect expressionBounds = [self.expressionStorage.rootLayout bounds]; CGFloat y = (self.bounds.size.height - expressionBounds.size.height) / 2 + fabs(expressionBounds.origin.y); return NSMakePoint(10, y); } @end @implementation MPExpressionView (MPSelection) - (void)restartCaretTimer { if (self.caretTimer && [self.caretTimer isValid]) { [self.caretTimer invalidate]; } self.caretTimer = [NSTimer scheduledTimerWithTimeInterval:self.caretBlinkRate/2 target:self selector:@selector(updateCaret:) userInfo:nil repeats:YES]; self.caretVisible = NO; [self updateCaret:self.caretTimer]; } - (void)updateCaret:(NSTimer *)timer { self.caretVisible = !self.caretVisible; NSRect updatedRect = self.selectionRect; NSPoint expressionOrigin = self.expressionOrigin; updatedRect.origin.x += expressionOrigin.x; updatedRect.origin.y += expressionOrigin.y; [self setNeedsDisplayInRect:updatedRect]; } - (NSRect)selectionRect { NSRect selectionRect = [self.expressionStorage.rootLayout boundingRectForRangePath:self.selection]; if (self.selection.length == 0) { selectionRect.size.width = 1; } return selectionRect; } - (NSIndexPath *)selectionToTheRightOf:(NSIndexPath *)selectionPath byExtendingSelection:(BOOL)extendingSelection selectWords:(BOOL)selectWords { NSIndexPath *targetExpressionPath = [selectionPath indexPathByRemovingLastIndex]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; NSUInteger locationInTarget = selectionPath.lastIndex; NSUInteger locationInElement; NSUInteger targetElementIndex = [targetExpression convertIndex:locationInTarget fromReferenceFrame:MPSymbolReferenceFrame toReferenceFrame:MPElementReferenceFrame offset:&locationInElement]; id targetElement; // There is only a target element if the selection is not the last location in an expression if (targetElementIndex < targetExpression.countElements) { targetElement = [targetExpression elementAtIndex:targetElementIndex]; } if (!selectWords && !extendingSelection && (locationInElement == 0 || locationInTarget == targetExpression.countSymbols)) { // First or last index in an element or expression // Last element in the expression if (locationInTarget == targetExpression.countSymbols) { // The selection is inside a function and should proceed if (selectionPath.length > 1) { NSIndexPath *functionPath = [[selectionPath indexPathByRemovingLastIndex] indexPathByRemovingLastIndex]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:functionPath]; NSUInteger currentChildIndex = [selectionPath indexPathByRemovingLastIndex].lastIndex; NSUInteger newChildIndex = [functionLayout indexOfChildAfterChildAtIndex:currentChildIndex]; // The function is to be exited if (newChildIndex == NSNotFound) { targetExpression = [self.expressionStorage elementAtIndexPath:[functionPath indexPathByRemovingLastIndex]]; NSUInteger functionLocationInExpression = [targetExpression convertIndex:functionPath.lastIndex fromReferenceFrame:MPElementReferenceFrame toReferenceFrame:MPSymbolReferenceFrame]; return [functionPath indexPathByReplacingLastIndexWithIndex:functionLocationInExpression+1]; } else { return [[targetExpressionPath indexPathByReplacingLastIndexWithIndex:newChildIndex] indexPathByAddingIndex:0]; } } // else the selection does not change // First Element } else { if ([targetElement isString]) { locationInTarget++; } else { NSIndexPath *targetFunctionPath = [selectionPath indexPathByReplacingLastIndexWithIndex:targetElementIndex]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:targetFunctionPath]; NSUInteger leadingChildIndex = [functionLayout indexOfLeadingChild]; return [[targetFunctionPath indexPathByAddingIndex:leadingChildIndex] indexPathByAddingIndex:0]; } } } else if (locationInTarget < targetExpression.countSymbols) { if (selectWords) { locationInTarget = [targetExpression convertIndex:targetElementIndex+1 fromReferenceFrame:MPElementReferenceFrame toReferenceFrame:MPSymbolReferenceFrame]; } else { locationInTarget++; } } return [selectionPath indexPathByReplacingLastIndexWithIndex:locationInTarget]; } - (NSIndexPath *)selectionToTheLeftOf:(NSIndexPath *)selectionPath byExtendingSelection:(BOOL)extendingSelection selectWords:(BOOL)selectWords { NSIndexPath *targetExpressionPath = [selectionPath indexPathByRemovingLastIndex]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; NSUInteger locationInTarget = selectionPath.lastIndex; NSUInteger locationInElement; NSUInteger targetElementIndex = [targetExpression convertIndex:locationInTarget fromReferenceFrame:MPSymbolReferenceFrame toReferenceFrame:MPElementReferenceFrame offset:&locationInElement]; NSUInteger previousElementIndex = targetElementIndex - (locationInElement == 0 ? 1 : 0); id previousElement; if (locationInTarget > 0) { previousElement = [targetExpression elementAtIndex:previousElementIndex]; } if (!selectWords && !extendingSelection && locationInElement == 0) { // First element in expression if (locationInTarget == 0) { if (selectionPath.length > 1) { NSIndexPath *functionPath = [[selectionPath indexPathByRemovingLastIndex] indexPathByRemovingLastIndex]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:functionPath]; NSUInteger currentChildIndex = [selectionPath indexPathByRemovingLastIndex].lastIndex; NSUInteger newChildIndex = [functionLayout indexOfChildBeforeChildAtIndex:currentChildIndex]; // The function is to be exited if (newChildIndex == NSNotFound) { targetExpression = [self.expressionStorage elementAtIndexPath:[functionPath indexPathByRemovingLastIndex]]; NSUInteger functionLocationInExpression = [targetExpression convertIndex:functionPath.lastIndex fromReferenceFrame:MPElementReferenceFrame toReferenceFrame:MPSymbolReferenceFrame]; return [functionPath indexPathByReplacingLastIndexWithIndex:functionLocationInExpression]; } else { targetExpressionPath = [targetExpressionPath indexPathByReplacingLastIndexWithIndex:newChildIndex]; targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; return [targetExpressionPath indexPathByAddingIndex:targetExpression.countSymbols]; } } // else the selection does not change // Just } else { if ([previousElement isString]) { locationInTarget--; } else { NSIndexPath *targetFunctionPath = [selectionPath indexPathByReplacingLastIndexWithIndex:previousElementIndex]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:targetFunctionPath]; NSUInteger trailingChildIndex = [functionLayout indexOfTrailingChild]; targetExpressionPath = [targetFunctionPath indexPathByAddingIndex:trailingChildIndex]; targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; return [targetExpressionPath indexPathByAddingIndex:targetExpression.countSymbols]; } } } else if (locationInTarget > 0) { if (selectWords) { if (locationInElement == 0) { targetElementIndex--; } locationInTarget = [targetExpression convertIndex:targetElementIndex fromReferenceFrame:MPElementReferenceFrame toReferenceFrame:MPSymbolReferenceFrame]; } else { locationInTarget--; } } return [selectionPath indexPathByReplacingLastIndexWithIndex:locationInTarget]; } - (MPRangePath *)rangePathEnclosingAnchorPath:(NSIndexPath *)anchorPath newSelectionPath:(NSIndexPath *)newSelectionPath { if ([anchorPath isEqual:newSelectionPath]) { return MPMakeRangePath(anchorPath, 0); } NSIndexPath *commonPath = [anchorPath commonIndexPathWith:newSelectionPath]; if (commonPath.length == anchorPath.length-1 && commonPath.length == newSelectionPath.length-1) { // The two paths point to different locations in the same expression NSUInteger anchorIndex = [anchorPath indexAtPosition:commonPath.length]; NSUInteger newIndex = [newSelectionPath indexAtPosition:commonPath.length]; NSUInteger minIndex = MIN(anchorIndex, newIndex); NSUInteger length = MAX(anchorIndex, newIndex) - minIndex; return MPMakeRangePath([commonPath indexPathByAddingIndex:minIndex], length); } else { if ((commonPath.length & 1) == 1) { commonPath = [commonPath indexPathByRemovingLastIndex]; } MPExpression *closestCommonAncestor = [self.expressionStorage elementAtIndexPath:commonPath]; NSUInteger anchorIndex = [anchorPath indexAtPosition:commonPath.length]; NSUInteger newIndex = [newSelectionPath indexAtPosition:commonPath.length]; if (commonPath.length < anchorPath.length-1) { anchorIndex = [closestCommonAncestor convertIndex:anchorIndex fromReferenceFrame:MPElementReferenceFrame toReferenceFrame:MPSymbolReferenceFrame]; } if (commonPath.length < newSelectionPath.length-1) { newIndex = [closestCommonAncestor convertIndex:newIndex fromReferenceFrame:MPElementReferenceFrame toReferenceFrame:MPSymbolReferenceFrame]; } NSUInteger minIndex = MIN(anchorIndex, newIndex); if (commonPath.length < anchorPath.length-1 && anchorIndex != minIndex) { anchorIndex++; } else if (commonPath.length < newSelectionPath.length-1 && newIndex != minIndex) { newIndex++; } NSUInteger length = MAX(anchorIndex, newIndex) - minIndex; if (anchorIndex == newIndex) { length++; } MPRangePath *newSelection = MPMakeRangePath([commonPath indexPathByAddingIndex:minIndex], length); return newSelection; } } @end @implementation MPExpressionView #pragma mark Creation Methods - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { [self initializeExpressionView]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self initializeExpressionView]; } return self; } - (void)initializeExpressionView { // Setup the Expression Storage MPExpressionStorage *expressionStorage = [[MPExpressionStorage alloc] init]; expressionStorage.expressionView = self; _expressionStorage = expressionStorage; [self initializeButtons]; [self initializeErrorsViews]; self.selection = [[MPRangePath alloc] initWithRange:NSMakeRange(0, 0)]; self.caretBlinkRate = 1.0; [self restartCaretTimer]; } - (void)initializeButtons { NSButton *radiansDegreesButton = self.radiansDegreesButton; [self addSubview:radiansDegreesButton]; NSButton *functionsButton = self.functionsButton; [self addSubview:functionsButton]; NSDictionary *variableBindings = NSDictionaryOfVariableBindings(radiansDegreesButton, functionsButton); [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[radiansDegreesButton]" options:0 metrics:nil views:variableBindings]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-10-[radiansDegreesButton]" options:0 metrics:nil views:variableBindings]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[functionsButton]-10-|" options:0 metrics:nil views:variableBindings]]; [self addConstraint:[NSLayoutConstraint constraintWithItem:functionsButton attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; } - (void)initializeErrorsViews { NSPopUpButton *syntaxErrors = self.syntaxErrorsPopUpButton; NSTextField *mathError = self.mathErrorTextField; [self addSubview:syntaxErrors]; [self addSubview:mathError]; NSDictionary *variableBindings = NSDictionaryOfVariableBindings(syntaxErrors, mathError); [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[syntaxErrors]" options:0 metrics:nil views:variableBindings]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[syntaxErrors]-10-|" options:0 metrics:nil views:variableBindings]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[mathError]" options:0 metrics:nil views:variableBindings]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[mathError]-10-|" options:0 metrics:nil views:variableBindings]]; } #pragma mark Properties - (void)setExpressionStorage:(MPExpressionStorage *)expressionStorage { _expressionStorage.expressionView = nil; _expressionStorage = expressionStorage;; _expressionStorage.expressionView = self; [self invalidateIntrinsicContentSize]; } - (void)setSelection:(MPRangePath *)selection { _selection = selection; [self restartCaretTimer]; self.needsDisplay = YES; } - (void)setSyntaxErrors:(NSArray *)syntaxErrors { _syntaxErrors = syntaxErrors; [self.syntaxErrorsPopUpButton removeAllItems]; if (syntaxErrors.count == 0) { self.syntaxErrorsPopUpButton.hidden = YES; return; } self.syntaxErrorsPopUpButton.hidden = NO; [self.syntaxErrorsPopUpButton addItemWithTitle:@""]; if (syntaxErrors.count == 1) { NSString *title = NSLocalizedString(@"1 Syntax Error", nil); NSDictionary *attributes = @{NSForegroundColorAttributeName: [NSColor redColor], NSFontAttributeName: [NSFont boldSystemFontOfSize:12.0]}; NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:attributes]; [self.syntaxErrorsPopUpButton itemAtIndex:0].attributedTitle = attributedTitle; } else { NSString *title = [NSString stringWithFormat:NSLocalizedString(@"%ld Syntax Errors", nil), syntaxErrors.count]; NSDictionary *attributes = @{NSForegroundColorAttributeName: [NSColor redColor], NSFontAttributeName: [NSFont boldSystemFontOfSize:12.0]}; NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:attributes]; [self.syntaxErrorsPopUpButton itemAtIndex:0].attributedTitle = attributedTitle; } NSUInteger index = 0; for (NSError *error in syntaxErrors) { [self.syntaxErrorsPopUpButton addItemWithTitle:error.localizedDescription]; index++; } } - (void)setMathError:(NSError *)mathError { _mathError = mathError; self.mathErrorTextField.stringValue = mathError != nil ? mathError.localizedDescription : @""; } - (NSButton *)radiansDegreesButton { if (!_radiansDegreesButton) { NSButton *radiansDegreesButton = [[NSButton alloc] initWithFrame:NSZeroRect]; radiansDegreesButton.translatesAutoresizingMaskIntoConstraints = NO; radiansDegreesButton.buttonType = NSMomentaryPushInButton; radiansDegreesButton.bordered = YES; radiansDegreesButton.bezelStyle = NSRoundedBezelStyle; radiansDegreesButton.imagePosition = NSNoImage; radiansDegreesButton.alignment = NSCenterTextAlignment; radiansDegreesButton.title = NSLocalizedString([MPMathRules sharedRules].isUsingDegrees ? @"Deg" : @"Rad", nil); radiansDegreesButton.font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSRegularControlSize]]; radiansDegreesButton.target = self; radiansDegreesButton.action = @selector(switchRadiansDegrees:); _radiansDegreesButton = radiansDegreesButton; } return _radiansDegreesButton; } - (NSButton *)functionsButton { if (!_functionsButton) { NSBundle *frameworkBundle = [NSBundle bundleForClass:[self class]]; NSImage *image = [frameworkBundle imageForResource:@"FunctionsButtonDisclosure"]; [image setName:@"FunctionsButtonDisclosure"]; NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect]; button.translatesAutoresizingMaskIntoConstraints = NO; button.target = self; button.action = @selector(showFunctions:); button.buttonType = NSMomentaryChangeButton; button.bezelStyle = NSShadowlessSquareBezelStyle; button.bordered = NO; NSFont *font = [NSFont fontWithName:@"Times New Roman" size:25.0]; NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:@"Σ" attributes:@{NSFontAttributeName: font, NSForegroundColorAttributeName: [NSColor colorWithWhite:.61 alpha:1]}]; button.attributedTitle = attributedTitle; button.imagePosition = NSImageLeft; button.image = image; _functionsButton = button; } return _functionsButton; } - (NSPopUpButton *)syntaxErrorsPopUpButton { if (!_syntaxErrorsPopUpButton) { NSPopUpButton *button = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 100, 26) pullsDown:YES]; button.translatesAutoresizingMaskIntoConstraints = NO; [button.cell setArrowPosition:NSPopUpArrowAtBottom]; button.bezelStyle = NSRecessedBezelStyle; button.buttonType = NSPushOnPushOffButton; button.showsBorderOnlyWhileMouseInside = YES; button.font = [NSFont boldSystemFontOfSize:12.0]; button.hidden = YES; button.target = self; button.action = @selector(didSelectError:); _syntaxErrorsPopUpButton = button; } return _syntaxErrorsPopUpButton; } - (NSTextField *)mathErrorTextField { if (!_mathErrorTextField) { NSTextField *textField = [[NSTextField alloc] init]; textField.translatesAutoresizingMaskIntoConstraints = NO; textField.bordered = NO; textField.selectable = NO; textField.editable = NO; textField.textColor = [NSColor redColor]; textField.font = [NSFont boldSystemFontOfSize:12.0]; _mathErrorTextField = textField; } return _mathErrorTextField; } - (void)didSelectError:(NSPopUpButton *)sender { NSError *error = self.syntaxErrors[sender.indexOfSelectedItem-1]; NSIndexPath *pathToExpression = error.userInfo[MPPathToExpressionKey]; pathToExpression = [pathToExpression indexPathByAddingIndex:[error.userInfo[MPErrorIndexKey] integerValue]]; self.selection = MPMakeRangePath(pathToExpression, 0); } #pragma mark Actions - (void)switchRadiansDegrees:(id)sender { [MPMathRules sharedRules].isUsingDegrees = ![MPMathRules sharedRules].isUsingDegrees; self.radiansDegreesButton.title = NSLocalizedString([MPMathRules sharedRules].isUsingDegrees ? @"Deg" : @"Rad", nil); } - (void)showFunctions:(id)sender { if (self.functionsPopover == nil || self.functionsViewController == nil) { self.functionsViewController = [[MPFunctionsViewController alloc] init]; self.functionsViewController.target = self; self.functionsViewController.action = @selector(insertFunction:); self.functionsPopover = [[NSPopover alloc] init]; self.functionsPopover.contentViewController = self.functionsViewController; self.functionsPopover.animates = YES; self.functionsPopover.behavior = NSPopoverBehaviorSemitransient; } if (self.functionsPopover.isShown) { [self.functionsPopover close]; } else { [self.functionsPopover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge]; } } - (void)insertFunction:(MPFunction *)function { [self.functionsPopover close]; [self.expressionStorage replaceItemsInRangePath:self.selection referenceFrame:MPSymbolReferenceFrame withElements:@[function]]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; NSUInteger functionElementIndex = [targetExpression convertIndex:self.selection.location.lastIndex fromReferenceFrame:MPSymbolReferenceFrame toReferenceFrame:MPElementReferenceFrame]; NSIndexPath *functionPath = [self.selection.location indexPathByReplacingLastIndexWithIndex:functionElementIndex]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:functionPath]; self.selection = MPMakeRangePath([[functionPath indexPathByAddingIndex:functionLayout.indexOfLeadingChild] indexPathByAddingIndex:0], 0); } #pragma mark NSView Stuff - (BOOL)acceptsFirstResponder { return YES; } - (BOOL)canBecomeKeyView { return YES; } - (BOOL)becomeFirstResponder { [self restartCaretTimer]; return [super becomeFirstResponder]; } - (BOOL)resignFirstResponder { [self.caretTimer invalidate]; self.caretVisible = NO; self.needsDisplay = YES; return [super resignFirstResponder]; } - (BOOL)isFlipped { return NO; } - (BOOL)isOpaque { return YES; } - (void)setFrame:(NSRect)frameRect { [self setNeedsLayout:YES]; [super setFrame:frameRect]; } - (NSSize)intrinsicContentSize { NSSize size = self.expressionStorage.rootLayout.bounds.size; size.width += 100; return size; } - (void)resetCursorRects { [self addCursorRect:self.bounds cursor:[NSCursor IBeamCursor]]; } #pragma mark Key Event Handling - (void)keyDown:(NSEvent *)theEvent { NSString *characters = theEvent.characters; if ([characters isEqualToString:@"("]) { [self insertParenthesisFunction:nil]; return; } if (theEvent.keyCode == 10) { [self insertPowerFunction]; return; } if ([characters isEqualToString:@"/"]) { [self insertFractionFunction]; return; } NSString *decimalSeparator = [NSRegularExpression escapedPatternForString:[[NSLocale currentLocale] objectForKey:NSLocaleDecimalSeparator]]; NSMutableCharacterSet *allowedCharacters = [NSMutableCharacterSet alphanumericCharacterSet]; [allowedCharacters addCharactersInString:[NSString stringWithFormat:@"+-*= !%@", decimalSeparator]]; if (characters.length == 1 && [characters stringByTrimmingCharactersInSet:allowedCharacters].length == 0) { if ([characters isEqualToString:@"*"]) { characters = @"⋅"; } [self.expressionStorage replaceItemsInRangePath:self.selection referenceFrame:MPSymbolReferenceFrame withElements:@[characters]]; self.selection = MPMakeRangePath([self.selection.location indexPathByIncrementingLastIndex], 0); } else { [self interpretKeyEvents:@[theEvent]]; } } - (void)insertParenthesisFunction:(id)sender { MPExpression *selectedElementsExpression = [self.expressionStorage subexpressionWithRangePath:self.selection referenceFrame:MPSymbolReferenceFrame]; MPParenthesisFunction *function = [[MPParenthesisFunction alloc] init]; function.expression = selectedElementsExpression; [self.expressionStorage replaceItemsInRangePath:self.selection referenceFrame:MPSymbolReferenceFrame withElements:@[function]]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; NSUInteger functionElementIndex = [targetExpression convertIndex:self.selection.location.lastIndex fromReferenceFrame:MPSymbolReferenceFrame toReferenceFrame:MPElementReferenceFrame]; NSIndexPath *functionPath = [self.selection.location indexPathByReplacingLastIndexWithIndex:functionElementIndex]; self.selection = MPMakeRangePath([[functionPath indexPathByAddingIndex:0] indexPathByAddingIndex:0], self.selection.length); } - (void)insertPowerFunction { MPExpression *selectedElementsExpression = [self.expressionStorage subexpressionWithRangePath:self.selection referenceFrame:MPSymbolReferenceFrame]; MPPowerFunction *function = [[MPPowerFunction alloc] init]; function.exponentExpression = selectedElementsExpression; [self.expressionStorage replaceItemsInRangePath:self.selection referenceFrame:MPSymbolReferenceFrame withElements:@[function]]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; NSUInteger functionElementIndex = [targetExpression convertIndex:self.selection.location.lastIndex fromReferenceFrame:MPSymbolReferenceFrame toReferenceFrame:MPElementReferenceFrame]; NSIndexPath *functionPath = [self.selection.location indexPathByReplacingLastIndexWithIndex:functionElementIndex]; self.selection = MPMakeRangePath([[functionPath indexPathByAddingIndex:0] indexPathByAddingIndex:0], self.selection.length); } - (void)insertFractionFunction { MPExpression *selectedElementsExpression = [self.expressionStorage subexpressionWithRangePath:self.selection referenceFrame:MPSymbolReferenceFrame]; MPFractionFunction *function = [[MPFractionFunction alloc] init]; function.nominatorExpression = selectedElementsExpression; [self.expressionStorage replaceItemsInRangePath:self.selection referenceFrame:MPSymbolReferenceFrame withElements:@[function]]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; NSUInteger functionElementIndex = [targetExpression convertIndex:self.selection.location.lastIndex fromReferenceFrame:MPSymbolReferenceFrame toReferenceFrame:MPElementReferenceFrame]; NSIndexPath *functionPath = [self.selection.location indexPathByReplacingLastIndexWithIndex:functionElementIndex]; self.selection = MPMakeRangePath([[functionPath indexPathByAddingIndex:1] indexPathByAddingIndex:0], 0); } - (void)insertNewline:(id)sender { if (self.target && self.action) { [self.target performSelector:self.action withObject:self afterDelay:0.0]; } } - (void)moveRight:(id)sender { if (self.selection.length > 0) { self.selection = MPMakeRangePath(self.selection.maxRangePath, 0); } else { NSIndexPath *newSelectionLocation = [self selectionToTheRightOf:self.selection.location byExtendingSelection:NO selectWords:NO]; self.selection = MPMakeRangePath(newSelectionLocation, 0); } } - (void)moveLeft:(id)sender { if (self.selection.length > 0) { self.selection = MPMakeRangePath(self.selection.location, 0); } else { NSIndexPath *newSelectionLocation = [self selectionToTheLeftOf:self.selection.location byExtendingSelection:NO selectWords:NO]; self.selection = MPMakeRangePath(newSelectionLocation, 0); } } - (void)moveWordRight:(id)sender { NSIndexPath *location = self.selection.maxRangePath; NSIndexPath *newSelectionLocation = [self selectionToTheRightOf:location byExtendingSelection:NO selectWords:YES]; self.selection = MPMakeRangePath(newSelectionLocation, 0); } - (void)moveWordLeft:(id)sender { NSIndexPath *location = self.selection.location; NSIndexPath *newSelectionLocation = [self selectionToTheLeftOf:location byExtendingSelection:NO selectWords:YES]; self.selection = MPMakeRangePath(newSelectionLocation, 0); } - (void)moveToBeginningOfLine:(id)sender { self.selection = MPMakeRangePath([self.selection.location indexPathByReplacingLastIndexWithIndex:0], 0); } - (void)moveToEndOfLine:(id)sender { MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; self.selection = MPMakeRangePath([self.selection.location indexPathByReplacingLastIndexWithIndex:targetExpression.countSymbols], 0); } - (void)moveLeftAndModifySelection:(id)sender { if (self.selection.length == 0) { self.selectionModifyingStart = YES; } NSIndexPath *location = self.selection.location; NSIndexPath *maxLocation = self.selection.maxRangePath; if (self.selectionModifyingStart) { location = [self selectionToTheLeftOf:location byExtendingSelection:YES selectWords:NO]; } else { maxLocation = [self selectionToTheLeftOf:maxLocation byExtendingSelection:YES selectWords:NO]; } self.selection = [self rangePathEnclosingAnchorPath:maxLocation newSelectionPath:location]; } - (void)moveRightAndModifySelection:(id)sender { if (self.selection.length == 0) { self.selectionModifyingStart = NO; } NSIndexPath *location = self.selection.location; NSIndexPath *maxLocation = self.selection.maxRangePath; if (self.selectionModifyingStart) { location = [self selectionToTheRightOf:location byExtendingSelection:YES selectWords:NO]; } else { maxLocation = [self selectionToTheRightOf:maxLocation byExtendingSelection:YES selectWords:NO]; } self.selection = MPMakeRangePath(location, maxLocation.lastIndex-location.lastIndex); } - (void)moveWordRightAndModifySelection:(id)sender { if (self.selection.length == 0) { self.selectionModifyingStart = NO; } NSIndexPath *location = self.selection.location; NSIndexPath *maxLocation = self.selection.maxRangePath; if (self.selectionModifyingStart) { location = [self selectionToTheRightOf:location byExtendingSelection:YES selectWords:YES]; if (location.lastIndex > maxLocation.lastIndex) { location = [location indexPathByReplacingLastIndexWithIndex:maxLocation.lastIndex]; } } else { maxLocation = [self selectionToTheRightOf:maxLocation byExtendingSelection:YES selectWords:YES]; } self.selection = MPMakeRangePath(location, maxLocation.lastIndex-location.lastIndex); } - (void)moveWordLeftAndModifySelection:(id)sender { if (self.selection.length == 0) { self.selectionModifyingStart = YES; } NSIndexPath *location = self.selection.location; NSIndexPath *maxLocation = self.selection.maxRangePath; if (self.selectionModifyingStart) { location = [self selectionToTheLeftOf:location byExtendingSelection:YES selectWords:YES]; } else { maxLocation = [self selectionToTheLeftOf:maxLocation byExtendingSelection:YES selectWords:YES]; if (maxLocation.lastIndex < location.lastIndex) { maxLocation = [maxLocation indexPathByReplacingLastIndexWithIndex:location.lastIndex]; } } self.selection = MPMakeRangePath(location, maxLocation.lastIndex-location.lastIndex); } - (void)moveUp:(id)sender { NSIndexPath *targetExpressionPath = [self.selection.location indexPathByRemovingLastIndex]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; if (targetExpression.parent != nil) { NSIndexPath *functionPath = [targetExpressionPath indexPathByRemovingLastIndex]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:functionPath]; NSUInteger newChildIndex = [functionLayout indexOfChildAboveChildAtIndex:targetExpressionPath.lastIndex]; self.selection = MPMakeRangePath([[targetExpressionPath indexPathByReplacingLastIndexWithIndex:newChildIndex] indexPathByAddingIndex:0], 0); } } - (void)moveDown:(id)sender { NSIndexPath *targetExpressionPath = [self.selection.location indexPathByRemovingLastIndex]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; if (targetExpression.parent != nil) { NSIndexPath *functionPath = [targetExpressionPath indexPathByRemovingLastIndex]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:functionPath]; NSUInteger newChildIndex = [functionLayout indexOfChildBelowChildAtIndex:targetExpressionPath.lastIndex]; self.selection = MPMakeRangePath([[targetExpressionPath indexPathByReplacingLastIndexWithIndex:newChildIndex] indexPathByAddingIndex:0], 0); } } - (void)selectAll:(id)sender { NSIndexPath *location = [NSIndexPath indexPathWithIndex:0]; self.selection = MPMakeRangePath(location, self.expressionStorage.countSymbols); } - (void)deleteBackward:(id)sender { if (self.selection.length > 0) { [self.expressionStorage replaceItemsInRangePath:self.selection referenceFrame:MPSymbolReferenceFrame withElements:@[]]; self.selection = MPMakeRangePath(self.selection.location, 0); } else if (self.selection.location.lastIndex > 0) { MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; id elementToDelete = [targetExpression elementAtIndex:[targetExpression convertIndex:self.selection.location.lastIndex-1 fromReferenceFrame:MPSymbolReferenceFrame toReferenceFrame:MPElementReferenceFrame]]; if ([elementToDelete isFunction]) { self.selection = MPMakeRangePath(self.selection.location.indexPathByDecrementingLastIndex, 1); } else { [targetExpression replaceItemsInRange:NSMakeRange(self.selection.location.lastIndex-1, 1) referenceFrame:MPSymbolReferenceFrame withElements:@[]]; self.selection = MPMakeRangePath([self.selection.location indexPathByDecrementingLastIndex], 0); } } else { NSIndexPath *targetExpressionPath = [self.selection.location indexPathByRemovingLastIndex]; MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; if (targetExpression.parent != nil) { NSIndexPath *functionPath = [targetExpressionPath indexPathByRemovingLastIndex]; MPFunction *function = [self.expressionStorage elementAtIndexPath:functionPath]; MPFunctionLayout *functionLayout = (MPFunctionLayout *)[self.expressionStorage.rootLayout childLayoutAtIndexPath:functionPath]; NSIndexSet *remainingIndexes = [functionLayout indexesOfRemainingChildren]; NSMutableArray *remainder = [[NSMutableArray alloc] init]; for (NSUInteger index = remainingIndexes.firstIndex; index <= remainingIndexes.lastIndex; index = [remainingIndexes indexGreaterThanIndex:index]) { MPExpression *expression = [function childAtIndex:index]; [remainder addObjectsFromArray:[expression allItemsInReferenceFrame:MPElementReferenceFrame]]; } NSIndexPath *newTargetExpressionPath = [functionPath indexPathByRemovingLastIndex]; MPExpression *newTargetExpression = [self.expressionStorage elementAtIndexPath:newTargetExpressionPath]; NSUInteger newSelectionElementIndex = [newTargetExpression convertIndex:functionPath.lastIndex fromReferenceFrame:MPElementReferenceFrame toReferenceFrame:MPSymbolReferenceFrame]; NSIndexPath *newSelectionLocation = [functionPath indexPathByReplacingLastIndexWithIndex:newSelectionElementIndex]; [self.expressionStorage replaceItemsInRangePath:MPMakeRangePath(newSelectionLocation, 1) referenceFrame:MPSymbolReferenceFrame withElements:remainder]; self.selection = MPMakeRangePath(newSelectionLocation, 0); } } } #pragma mark Mouse Event Handling - (void)mouseDown:(NSEvent *)theEvent { NSPoint pointInView = [self convertPoint:theEvent.locationInWindow fromView:nil]; NSPoint expressionOrigin = self.expressionOrigin; pointInView.x -= expressionOrigin.x; pointInView.y -= expressionOrigin.y; NSIndexPath *selectionPath = [self.expressionStorage.rootLayout indexPathForMousePoint:pointInView]; self.mouseAnchor = selectionPath; self.selection = MPMakeRangePath(selectionPath, 0); } - (void)mouseDragged:(NSEvent *)theEvent { NSPoint pointInView = [self convertPoint:theEvent.locationInWindow fromView:nil]; NSPoint expressionOrigin = self.expressionOrigin; pointInView.x -= expressionOrigin.x; pointInView.y -= expressionOrigin.y; NSIndexPath *mouseSelectionPath = [self.expressionStorage.rootLayout indexPathForMousePoint:pointInView]; self.selection = [self rangePathEnclosingAnchorPath:self.mouseAnchor newSelectionPath:mouseSelectionPath]; } #pragma mark Drawing Methods - (void)drawRect:(NSRect)dirtyRect { // Draw the background [super drawRect:dirtyRect]; [[NSColor whiteColor] set]; NSRectFill(dirtyRect); // Calculate the position of the expression (probably also forces layout of the expression the first time) NSPoint expressionOrigin = self.expressionOrigin; NSAffineTransform *transform = [NSAffineTransform transform]; [transform translateXBy:expressionOrigin.x yBy:expressionOrigin.y]; [transform concat]; // Draw the selection if (self.caretVisible || self.selection.length > 0) { if (self.selection.length == 0) { [[NSColor blackColor] set]; } else { [[NSColor selectedTextBackgroundColor] set]; } NSRectFill([self selectionRect]); } [transform invert]; [transform concat]; // Draw the expression [[NSColor textColor] set]; [self.expressionStorage.rootLayout drawAtPoint:expressionOrigin]; } @end