1172 lines
48 KiB
Objective-C
Executable File
1172 lines
48 KiB
Objective-C
Executable File
//
|
|
// MPExpressionView.m
|
|
// MathKit
|
|
//
|
|
// 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 "MPFunctionsViewController.h"
|
|
|
|
#import "MPFunctionLayout.h"
|
|
#import "MPFractionFunction.h"
|
|
#import "MPParenthesisFunction.h"
|
|
#import "MPPowerFunction.h"
|
|
|
|
#import "MPParsedExpression.h"
|
|
#import "MPMathRules.h"
|
|
#import "MPToken.h"
|
|
|
|
#import "MPRangePath.h"
|
|
#import "NSIndexPath+MPAdditions.h"
|
|
|
|
|
|
|
|
@interface MPExpressionView ()
|
|
|
|
@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 (MPSelectionHelper)
|
|
|
|
- (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 (MPSelectionHelper)
|
|
|
|
- (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<MPExpressionElement> 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
|
|
|
|
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<MPExpressionElement> 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
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
@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.rootLayout.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 *functionsButton = self.functionsButton;
|
|
[self addSubview:functionsButton];
|
|
|
|
NSDictionary *variableBindings = NSDictionaryOfVariableBindings(functionsButton);
|
|
|
|
[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 - NSView Configuration
|
|
|
|
|
|
- (BOOL)acceptsFirstResponder
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL)canBecomeKeyView
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL)isOpaque
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void)resetCursorRects
|
|
{
|
|
[self addCursorRect:self.bounds
|
|
cursor:[NSCursor IBeamCursor]];
|
|
}
|
|
|
|
|
|
- (BOOL)becomeFirstResponder
|
|
{
|
|
[self restartCaretTimer];
|
|
return [super becomeFirstResponder];
|
|
}
|
|
|
|
|
|
- (BOOL)resignFirstResponder
|
|
{
|
|
[self.caretTimer invalidate];
|
|
self.caretVisible = NO;
|
|
self.needsDisplay = YES;
|
|
return [super resignFirstResponder];
|
|
}
|
|
|
|
|
|
- (void)setFrame:(NSRect)frameRect
|
|
{
|
|
[self setNeedsLayout:YES];
|
|
[super setFrame:frameRect];
|
|
}
|
|
|
|
|
|
- (NSSize)intrinsicContentSize
|
|
{
|
|
NSSize size = self.expressionStorage.rootLayout.bounds.size;
|
|
size.width += 100;
|
|
return size;
|
|
}
|
|
|
|
|
|
#pragma mark - Properties
|
|
|
|
|
|
- (void)setExpressionStorage:(MPExpressionStorage *)expressionStorage
|
|
{
|
|
_expressionStorage.rootLayout.expressionView = nil;
|
|
_expressionStorage = expressionStorage;;
|
|
_expressionStorage.rootLayout.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;
|
|
NSString *title;
|
|
if (syntaxErrors.count == 1) {
|
|
title = NSLocalizedString(@"1 Syntax Error", nil);
|
|
} else {
|
|
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];
|
|
NSMenuItem *titleItem = [[NSMenuItem alloc] init];
|
|
titleItem.attributedTitle = attributedTitle;
|
|
[self.syntaxErrorsPopUpButton.menu addItem:titleItem];
|
|
|
|
NSUInteger index = 0;
|
|
for (NSError *error in syntaxErrors) {
|
|
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:error.localizedDescription
|
|
action:NULL
|
|
keyEquivalent:@""];
|
|
[self.syntaxErrorsPopUpButton.menu addItem:item];
|
|
index++;
|
|
}
|
|
}
|
|
|
|
|
|
- (void)setMathError:(NSError *)mathError
|
|
{
|
|
_mathError = mathError;
|
|
self.mathErrorTextField.stringValue = mathError != nil ? mathError.localizedDescription : @"";
|
|
}
|
|
|
|
|
|
- (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(toggleFunctionsPopover:);
|
|
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];
|
|
textField.drawsBackground = NO;
|
|
_mathErrorTextField = textField;
|
|
}
|
|
return _mathErrorTextField;
|
|
}
|
|
|
|
|
|
- (void)didSelectError:(NSPopUpButton *)sender
|
|
{
|
|
NSError *error = self.syntaxErrors[sender.indexOfSelectedItem-1];
|
|
NSIndexPath *pathToExpression = error.userInfo[MPPathToExpressionKey];
|
|
NSRange errorRange = [error.userInfo[MPErrorRangeKey] rangeValue];
|
|
pathToExpression = [pathToExpression indexPathByAddingIndex:errorRange.location];
|
|
self.selection = MPMakeRangePath(pathToExpression, errorRange.length);
|
|
}
|
|
|
|
|
|
#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 Key Event Handling
|
|
|
|
|
|
- (void)keyDown:(NSEvent *)theEvent
|
|
{
|
|
NSString *characters = theEvent.characters;
|
|
|
|
if ([characters isEqualToString:@"("]) {
|
|
[self insertParenthesisFunction:nil];
|
|
return;
|
|
}
|
|
if ([characters isEqualToString:@")"]) {
|
|
[self insertClosingParenthesis];
|
|
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 isEqualTo:@"i"]) {
|
|
if (self.selection.location.lastIndex > 0) {
|
|
id<MPExpressionElement> symbol = [self.expressionStorage symbolAtIndex:self.selection.location.lastIndex-1];
|
|
if ([symbol isEqual:@"p"]) {
|
|
self.selection = MPMakeRangePath([self.selection.location indexPathByDecrementingLastIndex], self.selection.length+1);
|
|
characters = @"π";
|
|
}
|
|
}
|
|
}
|
|
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]];
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Actions
|
|
|
|
|
|
- (void)switchToDegrees:(id)sender
|
|
{
|
|
[MPMathRules sharedRules].isUsingDegrees = YES;
|
|
}
|
|
|
|
|
|
- (void)switchToRadians:(id)sender
|
|
{
|
|
[MPMathRules sharedRules].isUsingDegrees = NO;
|
|
}
|
|
|
|
|
|
- (void)switchRadiansDegrees:(id)sender
|
|
{
|
|
[MPMathRules sharedRules].isUsingDegrees = ![MPMathRules sharedRules].isUsingDegrees;
|
|
}
|
|
|
|
|
|
- (IBAction)toggleFunctionsPopover:(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:self.functionsButton
|
|
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 Editing Actions
|
|
|
|
|
|
- (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)insertClosingParenthesis
|
|
{
|
|
if (self.selection.length > 0) {
|
|
[self insertParenthesisFunction:self];
|
|
} else {
|
|
NSIndexPath *currentPath = [[self.selection.location indexPathByRemovingLastIndex] indexPathByRemovingLastIndex];
|
|
while (currentPath.length > 0) {
|
|
id element = [self.expressionStorage elementAtIndexPath:currentPath];
|
|
if ([element isKindOfClass:[MPParenthesisFunction class]]) {
|
|
MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[currentPath indexPathByRemovingLastIndex]];
|
|
NSUInteger selectedIndex = currentPath.lastIndex + 1;
|
|
selectedIndex = [targetExpression convertIndex:selectedIndex
|
|
fromReferenceFrame:MPElementReferenceFrame
|
|
toReferenceFrame:MPSymbolReferenceFrame];
|
|
self.selection = MPMakeRangePath([currentPath indexPathByReplacingLastIndexWithIndex:selectedIndex], 0);
|
|
break;
|
|
} else {
|
|
currentPath = [[currentPath indexPathByRemovingLastIndex] indexPathByRemovingLastIndex];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (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
|
|
{
|
|
if (self.selection.length == 0) {
|
|
MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]];
|
|
NSInteger index = self.selection.location.lastIndex;
|
|
NSRange numeratorSymbols = NSMakeRange(index, 0);
|
|
while (--index >= 0) {
|
|
id<MPExpressionElement> symbol = [targetExpression symbolAtIndex:index];
|
|
if ([symbol isString]) {
|
|
NSUInteger tokenIndex = [targetExpression convertIndex:index
|
|
fromReferenceFrame:MPSymbolReferenceFrame
|
|
toReferenceFrame:MPTokenReferenceFrame];
|
|
id<MPToken> token = [targetExpression tokenAtIndex:tokenIndex];
|
|
if (token.tokenType == MPNumberToken || token.tokenType == MPVariableToken || token.tokenType == MPElementaryFunctionToken) {
|
|
numeratorSymbols.location--;
|
|
numeratorSymbols.length++;
|
|
} else {
|
|
break;
|
|
}
|
|
} else if (numeratorSymbols.length == 0) {
|
|
numeratorSymbols.location--;
|
|
numeratorSymbols.length++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
MPExpression *numeratorExpression = [targetExpression subexpressionWithRange:numeratorSymbols
|
|
referenceFrame:MPSymbolReferenceFrame];
|
|
MPFractionFunction *function = [[MPFractionFunction alloc] init];
|
|
function.numeratorExpression = numeratorExpression;
|
|
[targetExpression replaceItemsInRange:numeratorSymbols referenceFrame:MPSymbolReferenceFrame withElements:@[function]];
|
|
NSUInteger functionElementIndex = [targetExpression convertIndex:numeratorSymbols.location
|
|
fromReferenceFrame:MPSymbolReferenceFrame
|
|
toReferenceFrame:MPElementReferenceFrame];
|
|
NSUInteger selectedSubexpression = numeratorSymbols.length == 0 ? 0 : 1;
|
|
self.selection = MPMakeRangePath([[[self.selection.location indexPathByReplacingLastIndexWithIndex:functionElementIndex] indexPathByAddingIndex:selectedSubexpression] indexPathByAddingIndex:0], 0);
|
|
} else {
|
|
MPExpression *selectedElementsExpression = [self.expressionStorage subexpressionWithRangePath:self.selection
|
|
referenceFrame:MPSymbolReferenceFrame];
|
|
MPFractionFunction *function = [[MPFractionFunction alloc] init];
|
|
function.numeratorExpression = 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], 0);
|
|
}
|
|
}
|
|
|
|
|
|
- (void)insertNewline:(id)sender
|
|
{
|
|
if (self.target && self.action) {
|
|
[self.target performSelector:self.action
|
|
withObject:self
|
|
afterDelay:0.0];
|
|
}
|
|
}
|
|
|
|
|
|
- (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<MPExpressionElement> 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void)delete:(id)sender
|
|
{
|
|
[self.expressionStorage replaceItemsInRangePath:self.selection
|
|
referenceFrame:MPSymbolReferenceFrame
|
|
withElements:@[]];
|
|
self.selection = MPMakeRangePath(self.selection.location, 0);
|
|
}
|
|
|
|
|
|
#pragma mark Selection Actions
|
|
|
|
|
|
- (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);
|
|
}
|
|
|
|
|
|
#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];
|
|
}
|
|
|
|
|
|
#pragma mark - User Interface Validations
|
|
|
|
|
|
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem
|
|
{
|
|
BOOL degrees = [MPMathRules sharedRules].isUsingDegrees;
|
|
if (anItem.action == @selector(switchToRadians:)) {
|
|
id anObject = anItem;
|
|
if ([anObject respondsToSelector:@selector(setState:)]) {
|
|
[anObject setState:degrees?NSOffState:NSOnState];
|
|
}
|
|
return YES;
|
|
} else if (anItem.action == @selector(switchToDegrees:)) {
|
|
id anObject = anItem;
|
|
if ([anObject respondsToSelector:@selector(setState:)]) {
|
|
[anObject setState:degrees?NSOnState:NSOffState];
|
|
}
|
|
return YES;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
@end
|