// // MPExpressionView.m // MathPad // // Created by Kim Wittenburg on 17.04.14. // Copyright (c) 2014 Kim Wittenburg. All rights reserved. // #import "MPExpressionView.h" #import "MPExpressionStorage.h" #import "MPExpressionLayout.h" #import "MPFunctionLayout.h" #import "MPRangePath.h" #import "NSIndexPath+MPAdditions.h" #import "MPSumFunction.h" @interface MPExpressionView () @property (nonatomic, weak) NSButton *functionsButton; @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) { if ([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; self.needsDisplay = YES; } - (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 indexOfElementAtSymbolLocation:locationInTarget offset:&locationInElement]; id targetElement; // There is only a target element if the selection is not the last location in an expression if (targetElementIndex < targetExpression.numberOfElements) { targetElement = [targetExpression elementAtIndex:targetElementIndex]; } if (!selectWords && !extendingSelection && (locationInElement == 0 || locationInTarget == targetExpression.length)) { // First or last index in an element or expression // Last element in the expression if (locationInTarget == targetExpression.length) { // 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 locationOfElementAtIndex:functionPath.lastIndex]; 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.length) { if (selectWords) { locationInTarget = [targetExpression locationOfElementAtIndex:targetElementIndex+1]; } 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 indexOfElementAtSymbolLocation:locationInTarget 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 locationOfElementAtIndex:functionPath.lastIndex]; return [functionPath indexPathByReplacingLastIndexWithIndex:functionLocationInExpression]; } else { targetExpressionPath = [targetExpressionPath indexPathByReplacingLastIndexWithIndex:newChildIndex]; targetExpression = [self.expressionStorage elementAtIndexPath:targetExpressionPath]; return [targetExpressionPath indexPathByAddingIndex:targetExpression.length]; } } // 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.length]; } } } else if (locationInTarget > 0) { if (selectWords) { if (locationInElement == 0) { targetElementIndex--; } locationInTarget = [targetExpression locationOfElementAtIndex:targetElementIndex]; } 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 locationOfElementAtIndex:anchorIndex]; } if (commonPath.length < newSelectionPath.length-1) { newIndex = [closestCommonAncestor locationOfElementAtIndex:newIndex]; } 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] initWithElements:@[@"12345", [[MPSumFunction alloc] init], [[MPSumFunction alloc] init]]]; expressionStorage.expressionView = self; _expressionStorage = expressionStorage; NSBundle *frameworkBundle = [NSBundle bundleForClass:[self class]]; NSImage *image = [frameworkBundle imageForResource:@"FunctionsButtonDisclosure"]; [image setName:@"FunctionsButtonDisclosure"]; // Setup the Functions Button NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect]; 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; self.functionsButton = button; [self addSubview:self.functionsButton]; // Setup Selection self.selection = [[MPRangePath alloc] initWithRange:NSMakeRange(0, 0)]; self.caretBlinkRate = 1.0; [self restartCaretTimer]; } #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; } #pragma mark Actions - (void)showFunctions:(id)sender { NSViewController *controller = [[NSViewController alloc] initWithNibName:nil bundle:nil]; controller.view = [[NSView alloc] init]; NSPopover *popover = [[NSPopover alloc] init]; popover.contentSize = NSMakeSize(100.0, 100.0); popover.contentViewController = controller; popover.animates = YES; popover.behavior = NSPopoverBehaviorSemitransient; [popover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge]; } #pragma mark NSView Stuff - (BOOL)acceptsFirstResponder { return YES; } - (BOOL)canBecomeKeyView { return YES; } - (BOOL)isFlipped { return NO; } - (BOOL)isOpaque { return YES; } - (void)setFrame:(NSRect)frameRect { [self setNeedsLayout:YES]; [super setFrame:frameRect]; } - (void)layout { NSSize buttonSize = [self.functionsButton fittingSize]; self.functionsButton.frame = NSMakeRect(self.bounds.size.width - buttonSize.width - 10, (self.bounds.size.height - buttonSize.height) / 2, buttonSize.width, buttonSize.height); [super layout]; } - (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; NSMutableCharacterSet *allowedCharacters = [NSMutableCharacterSet alphanumericCharacterSet]; [allowedCharacters addCharactersInString:@"+-*= "]; if (characters.length == 1 && [characters stringByTrimmingCharactersInSet:allowedCharacters].length == 0) { MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; [targetExpression replaceSymbolsInRange:self.selection.rangeAtLastIndex withElements:@[characters]]; self.selection = MPMakeRangePath([self.selection.location indexPathByIncrementingLastIndex], 0); } else { [self interpretKeyEvents:@[theEvent]]; } } - (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.length], 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)selectAll:(id)sender { NSIndexPath *location = [NSIndexPath indexPathWithIndex:0]; self.selection = MPMakeRangePath(location, self.expressionStorage.length); } - (void)deleteBackward:(id)sender { MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]]; if (self.selection.length > 0) { [targetExpression replaceSymbolsInRange:self.selection.rangeAtLastIndex withElements:@[]]; self.selection = MPMakeRangePath(self.selection.location, 0); } else if (self.selection.location.lastIndex > 0) { [targetExpression replaceSymbolsInRange:NSMakeRange(self.selection.location.lastIndex-1, 1) withElements:@[]]; self.selection = MPMakeRangePath([self.selection.location indexPathByDecrementingLastIndex], 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; // Draw the selection if (self.caretVisible || self.selection.length > 0) { if (self.selection.length == 0) { [[NSColor blackColor] set]; } else { [[NSColor selectedTextBackgroundColor] set]; } NSAffineTransform *transform = [NSAffineTransform transform]; [transform translateXBy:expressionOrigin.x yBy:expressionOrigin.y]; [transform concat]; NSRectFill([self selectionRect]); [transform invert]; [transform concat]; } // Draw the expression [[NSColor textColor] set]; [self.expressionStorage.rootLayout drawAtPoint:expressionOrigin]; } @end