Added Keyboard Selection Support Added Mouse Selection Support Added Keyboard Editing Support Corrected Some Bugs Abstracted the Layout System further Added Functions Button (test)
391 lines
12 KiB
Objective-C
391 lines
12 KiB
Objective-C
//
|
|
// MPExpressionView.m
|
|
// MathPad
|
|
//
|
|
// Created by Kim Wittenburg on 17.04.14.
|
|
// Copyright (c) 2014 Kim Wittenburg. All rights reserved.
|
|
//
|
|
|
|
#warning X-Origin is not working yet
|
|
|
|
#import "MPExpressionView.h"
|
|
#import "MPExpressionStorage.h"
|
|
#import "MPExpressionLayout.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;
|
|
|
|
@end
|
|
|
|
@interface MPExpressionView (MPDrawing)
|
|
|
|
- (NSPoint)expressionOrigin;
|
|
|
|
@end
|
|
|
|
@interface MPExpressionView (MPSelection)
|
|
- (void)restartCaretTimer;
|
|
- (void)updateCaret:(NSTimer *)timer;
|
|
|
|
- (NSRect)selectionRect;
|
|
|
|
- (void)selectRangePathWithLocation:(NSIndexPath *)location length:(NSUInteger)length;
|
|
@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(self.bounds.origin.x, self.bounds.origin.y + 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;
|
|
}
|
|
|
|
- (void)selectRangePathWithLocation:(NSIndexPath *)location length:(NSUInteger)length
|
|
{
|
|
MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[location indexPathByRemovingLastIndex]];
|
|
NSUInteger locationIndex = location.lastIndex;
|
|
if (locationIndex > targetExpression.length) {
|
|
locationIndex = targetExpression.length;
|
|
}
|
|
NSUInteger lastSelectedIndex = location.lastIndex + length;
|
|
if (lastSelectedIndex > targetExpression.length) {
|
|
lastSelectedIndex = targetExpression.length;
|
|
}
|
|
self.selection = MPMakeRangePath([location indexPathByReplacingLastIndexWithIndex:locationIndex],lastSelectedIndex - locationIndex);
|
|
}
|
|
|
|
@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
|
|
{
|
|
MPExpressionStorage *expressionStorage = [[MPExpressionStorage alloc] initWithElements:@[@"12345", [[MPSumFunction alloc] init], [[MPSumFunction alloc] init]]];
|
|
expressionStorage.expressionView = self;
|
|
_expressionStorage = expressionStorage;
|
|
NSRect frame = NSMakeRect(10, 10, 500, 500);
|
|
NSButton *button = [[NSButton alloc] initWithFrame:frame];
|
|
button.buttonType = NSSwitchButton;
|
|
[button setTitle:@"Functions"];
|
|
self.functionsButton = button;
|
|
[self addSubview:self.functionsButton];
|
|
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 NSView Stuff
|
|
- (BOOL)acceptsFirstResponder
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)canBecomeKeyView
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlipped
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)isOpaque
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)layout
|
|
{
|
|
NSSize buttonSize = [self.functionsButton fittingSize];
|
|
self.functionsButton.frame = NSMakeRect(self.bounds.size.width - buttonSize.width,
|
|
(self.bounds.size.height - buttonSize.height) / 2,
|
|
buttonSize.width,
|
|
buttonSize.height);
|
|
[super layout];
|
|
}
|
|
|
|
- (NSSize)intrinsicContentSize
|
|
{
|
|
// return NSMakeSize(500, 500);
|
|
// return self.bounds.size;
|
|
return self.expressionStorage.rootLayout.bounds.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.length == 1 && [characters stringByTrimmingCharactersInSet:[NSCharacterSet decimalDigitCharacterSet]].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)moveRight:(id)sender
|
|
{
|
|
if (self.selection.length > 0) {
|
|
self.selection = MPMakeRangePath(self.selection.maxRangePath, 0);
|
|
} else {
|
|
NSIndexPath *newSelectionLocation = [self.selection.location indexPathByIncrementingLastIndex];
|
|
[self selectRangePathWithLocation:newSelectionLocation
|
|
length:0];
|
|
}
|
|
}
|
|
|
|
- (void)moveLeft:(id)sender
|
|
{
|
|
if (self.selection.length > 0) {
|
|
self.selection = MPMakeRangePath(self.selection.location, 0);
|
|
[self selectRangePathWithLocation:self.selection.location length:0];
|
|
} else {
|
|
NSUInteger selectionIndex = self.selection.location.lastIndex;
|
|
if (selectionIndex > 0) {
|
|
--selectionIndex;
|
|
}
|
|
NSIndexPath *newSelectionLocation = [self.selection.location indexPathByReplacingLastIndexWithIndex:selectionIndex];
|
|
[self selectRangePathWithLocation:newSelectionLocation
|
|
length:0];
|
|
}
|
|
}
|
|
|
|
- (void)moveWordRight:(id)sender
|
|
{
|
|
NSIndexPath *location;
|
|
if (self.selection.length > 0) {
|
|
location = self.selection.maxRangePath;
|
|
} else {
|
|
location = self.selection.location;
|
|
}
|
|
MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[location indexPathByRemovingLastIndex]];
|
|
NSUInteger locationInTarget = NSMaxRange(self.selection.rangeAtLastIndex);
|
|
NSUInteger offset;
|
|
NSUInteger elementIndex = [targetExpression indexOfElementAtSymbolLocation:locationInTarget
|
|
offset:&offset];
|
|
NSUInteger newLocation = locationInTarget + [targetExpression elementAtIndex:elementIndex].length - offset;
|
|
[self selectRangePathWithLocation:[location indexPathByReplacingLastIndexWithIndex:newLocation]
|
|
length:0];
|
|
}
|
|
|
|
- (void)moveWordLeft:(id)sender
|
|
{
|
|
MPExpression *targetExpression = [self.expressionStorage elementAtIndexPath:[self.selection.location indexPathByRemovingLastIndex]];
|
|
NSUInteger offset;
|
|
NSUInteger elementIndex = [targetExpression indexOfElementAtSymbolLocation:self.selection.location.lastIndex offset:&offset];
|
|
NSUInteger newLocation = self.selection.location.lastIndex;
|
|
if (offset > 0) {
|
|
newLocation -= newLocation - offset > 0 ? offset : newLocation;
|
|
} else if (newLocation > 0) {
|
|
newLocation -= [targetExpression elementAtIndex:elementIndex-1].length;
|
|
}
|
|
self.selection = MPMakeRangePath([self.selection.location indexPathByReplacingLastIndexWithIndex:newLocation], 0);
|
|
}
|
|
|
|
- (void)moveToBeginningOfLine:(id)sender
|
|
{
|
|
self.selection = MPMakeRangePath([self.selection.location indexPathByReplacingLastIndexWithIndex:0], 0);
|
|
}
|
|
|
|
- (void)moveToEndOfLine:(id)sender
|
|
{
|
|
|
|
}
|
|
|
|
- (void)moveLeftAndModifySelection:(id)sender
|
|
{
|
|
if (self.selection.length == 0) {
|
|
self.selectionModifyingStart = YES;
|
|
}
|
|
NSUInteger start = self.selection.location.lastIndex;
|
|
NSUInteger length = self.selection.length;
|
|
if (self.selectionModifyingStart) {
|
|
if (start > 0) {
|
|
--start;
|
|
++length;
|
|
}
|
|
} else {
|
|
--length;
|
|
}
|
|
self.selection = MPMakeRangePath([self.selection.location indexPathByReplacingLastIndexWithIndex:start], length);
|
|
}
|
|
|
|
- (void)moveRightAndModifySelection:(id)sender
|
|
{
|
|
if (self.selection.length == 0) {
|
|
self.selectionModifyingStart = NO;
|
|
}
|
|
NSUInteger start = self.selection.location.lastIndex;
|
|
NSUInteger length = self.selection.length;
|
|
if (self.selectionModifyingStart) {
|
|
++start;
|
|
--length;
|
|
} else {
|
|
++length;
|
|
}
|
|
[self selectRangePathWithLocation:[self.selection.location indexPathByReplacingLastIndexWithIndex:start]
|
|
length:length];
|
|
}
|
|
|
|
- (void)moveWordRightAndModifySelection:(id)sender
|
|
{
|
|
|
|
}
|
|
|
|
- (void)moveWordLeftAndModifySelection:(id)sender
|
|
{
|
|
|
|
}
|
|
|
|
- (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.selection = MPMakeRangePath(selectionPath, 0);
|
|
}
|
|
|
|
#pragma mark Drawing Methods
|
|
- (void)drawRect:(NSRect)dirtyRect
|
|
{
|
|
// Draw the background
|
|
[super drawRect:dirtyRect];
|
|
[[NSColor whiteColor] set];
|
|
NSRectFill(self.bounds);
|
|
|
|
// 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
|