From 4a3ea0cede7057efffbf5fad923e312f3e3f6349 Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Sun, 31 Aug 2014 15:41:17 +0200 Subject: [PATCH] Improved Model Added Keyboard Selection Support Added Mouse Selection Support Added Keyboard Editing Support Corrected Some Bugs Abstracted the Layout System further Added Functions Button (test) --- MathPad.xcodeproj/project.pbxproj | 10 +- MathPad/Base.lproj/MPDocument.xib | 30 +-- MathPad/MPExpression.h | 54 ++++- MathPad/MPExpression.m | 95 ++++---- MathPad/MPExpressionLayout.h | 2 +- MathPad/MPExpressionLayout.m | 196 +++++++++++++---- MathPad/MPExpressionStorage.h | 9 +- MathPad/MPExpressionStorage.m | 47 +--- MathPad/MPExpressionView.h | 3 +- MathPad/MPExpressionView.m | 347 +++++++++++++++++++++++++++--- MathPad/MPFunction.h | 1 + MathPad/MPFunction.m | 10 +- MathPad/MPFunctionLayout.h | 5 +- MathPad/MPFunctionLayout.m | 49 ++--- MathPad/MPLayout.h | 29 ++- MathPad/MPLayout.m | 95 ++++++-- MathPad/MPRangePath.h | 2 + MathPad/MPSumFunction.h | 6 +- MathPad/MPSumFunction.m | 3 +- MathPad/MPSumFunctionLayout.m | 121 +++++++++-- MathPad/MathPad-Info.plist | 2 + MathPad/NSIndexPath+MPAdditions.h | 7 + MathPad/NSIndexPath+MPAdditions.m | 24 +++ 23 files changed, 885 insertions(+), 262 deletions(-) diff --git a/MathPad.xcodeproj/project.pbxproj b/MathPad.xcodeproj/project.pbxproj index 1e341c5..25003a0 100644 --- a/MathPad.xcodeproj/project.pbxproj +++ b/MathPad.xcodeproj/project.pbxproj @@ -27,12 +27,13 @@ 3BBBA35E1903FD3600824E74 /* MPRangePath.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87E35C1900933200259938 /* MPRangePath.m */; }; 3BBBA35F1903FD3600824E74 /* MPRangePath.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B87E35C1900933200259938 /* MPRangePath.m */; }; 3BBBA3951905704200824E74 /* MPRangeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BBBA3941905704200824E74 /* MPRangeTests.m */; }; + 3BC4660B19B2425A0033F13A /* MPDocument.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BF9978318DE623E009CF6C4 /* MPDocument.xib */; }; + 3BC4661419B245C60033F13A /* Fonts in Resources */ = {isa = PBXBuildFile; fileRef = 3BC4661319B245C60033F13A /* Fonts */; }; 3BF9976F18DE623E009CF6C4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BF9976E18DE623E009CF6C4 /* Cocoa.framework */; }; 3BF9977918DE623E009CF6C4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3BF9977718DE623E009CF6C4 /* InfoPlist.strings */; }; 3BF9977B18DE623E009CF6C4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BF9977A18DE623E009CF6C4 /* main.m */; }; 3BF9977F18DE623E009CF6C4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 3BF9977D18DE623E009CF6C4 /* Credits.rtf */; }; 3BF9978218DE623E009CF6C4 /* MPDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BF9978118DE623E009CF6C4 /* MPDocument.m */; }; - 3BF9978518DE623E009CF6C4 /* MPDocument.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BF9978318DE623E009CF6C4 /* MPDocument.xib */; }; 3BF9978818DE623E009CF6C4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3BF9978618DE623E009CF6C4 /* MainMenu.xib */; }; 3BF9978A18DE623E009CF6C4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3BF9978918DE623E009CF6C4 /* Images.xcassets */; }; 3BF9979118DE623E009CF6C4 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BF9979018DE623E009CF6C4 /* XCTest.framework */; }; @@ -80,6 +81,7 @@ 3BBBA3591903EA9B00824E74 /* MPModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPModel.h; sourceTree = ""; }; 3BBBA38419047FC900824E74 /* MPView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPView.h; sourceTree = ""; }; 3BBBA3941905704200824E74 /* MPRangeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPRangeTests.m; sourceTree = ""; }; + 3BC4661319B245C60033F13A /* Fonts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fonts; sourceTree = ""; }; 3BF9976B18DE623E009CF6C4 /* MathPad.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MathPad.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3BF9976E18DE623E009CF6C4 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 3BF9977118DE623E009CF6C4 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -179,6 +181,8 @@ children = ( 3BF9978318DE623E009CF6C4 /* MPDocument.xib */, 3BF9978618DE623E009CF6C4 /* MainMenu.xib */, + 3BF9978918DE623E009CF6C4 /* Images.xcassets */, + 3BC4661319B245C60033F13A /* Fonts */, ); name = Resources; sourceTree = ""; @@ -268,7 +272,6 @@ 3B87E351190082BB00259938 /* View */, 3B87E352190082C000259938 /* Controller */, 3B87E353190082E200259938 /* Resources */, - 3BF9978918DE623E009CF6C4 /* Images.xcassets */, 3BF9977518DE623E009CF6C4 /* Supporting Files */, ); path = MathPad; @@ -384,7 +387,8 @@ files = ( 3BF9977918DE623E009CF6C4 /* InfoPlist.strings in Resources */, 3BF9978A18DE623E009CF6C4 /* Images.xcassets in Resources */, - 3BF9978518DE623E009CF6C4 /* MPDocument.xib in Resources */, + 3BC4660B19B2425A0033F13A /* MPDocument.xib in Resources */, + 3BC4661419B245C60033F13A /* Fonts in Resources */, 3BF9977F18DE623E009CF6C4 /* Credits.rtf in Resources */, 3BF9978818DE623E009CF6C4 /* MainMenu.xib in Resources */, ); diff --git a/MathPad/Base.lproj/MPDocument.xib b/MathPad/Base.lproj/MPDocument.xib index 0359d18..0a691eb 100644 --- a/MathPad/Base.lproj/MPDocument.xib +++ b/MathPad/Base.lproj/MPDocument.xib @@ -14,37 +14,23 @@ - - + - + - + - + - - - - - - - + + + + diff --git a/MathPad/MPExpression.h b/MathPad/MPExpression.h index 7f062ea..8a9f9e9 100644 --- a/MathPad/MPExpression.h +++ b/MathPad/MPExpression.h @@ -216,6 +216,32 @@ - (NSUInteger)indexOfElement:(id)element; +/*! + @method indexOfElementAtSymbolLocation:offset + @brief Calculates the index of the element the specified location points + to. + + @discussion The @c location is in the length reference frame whereas the + returned value is an element index. This method converts from + the former to the latter. + + If the location exceeds the receiver's bounds a @c + NSRangeException will be raised. + + @param location + The location of which you want the corresponding element index. + + @param offset + An output parameter that gets set to the offst into the symbol + whose index is returned. If location for example points to the + symbol @c '2' in the string element @c '123' the offset @c would + be set to @c 1. + + @return The index of the element the location points to. + */ +- (NSUInteger)indexOfElementAtSymbolLocation:(NSUInteger)location offset:(out NSUInteger *)offset; + + /*! @method replaceSymbolsInRange:withElements: @brief Replaces the elements in the given range with the contents of the @@ -242,7 +268,6 @@ */ - (void)replaceSymbolsInRange:(NSRange)range withElements:(NSArray *)elements; -// TODO: - (NSUInteger)indexOfElementAtSymbolLocation:(NSUInteger)location; #warning Evaluating must possibly return error - (double)doubleValue; // Evaluates Expression @@ -304,6 +329,18 @@ #pragma mark Working With the Expression Tree +/*! + @method rootExpression + @brief Returns the root expression from the receiver's expression tree. + + @discussion The root expression is the ultimate parent of all expressions and + functions in the expression tree. + + @return The root expression from the receiver's expression tree. + */ +- (MPExpression *)rootExpression; + + /*! @method elementAtIndexPath: @brief Returns the element at the specified index path. @@ -511,6 +548,19 @@ */ - (NSArray *)elements; -// TODO: - (NSMutableArray *)mutableElements; + +/*! + @method mutableElements + @brief Returns a proxy mutable array object that responds to all methods + defined by @c NSMutableArray. + + @discussion Mutations on the proxy object also change the receiver. The proxy + object does not respond to coding methods. Copying the proxy + object will not duplicate it. + + @return A proxy object that responds to all methods defined by @c + NSMutableArray. + */ +// - (NSMutableArray *)mutableElements; @end diff --git a/MathPad/MPExpression.m b/MathPad/MPExpression.m index 5320328..cc22c52 100644 --- a/MathPad/MPExpression.m +++ b/MathPad/MPExpression.m @@ -20,13 +20,10 @@ @interface MPExpression (MPExpressionPrivate) - (NSUInteger)lengthOfElements:(NSArray *)elements; -- (NSUInteger)indexOfElementAtLocation:(NSUInteger)location; - (void)validateElements:(NSArray *)elements; - (BOOL)splitElementsAtLocation:(NSUInteger)location insertionIndex:(out NSUInteger *)insertionIndex; -- (NSUInteger)calculateSplitOffsetForSplitLocation:(NSUInteger)location - inElementAtIndex:(out NSUInteger *)elementIndex; @end @@ -41,13 +38,6 @@ return length; } -- (NSUInteger)indexOfElementAtLocation:(NSUInteger)location -{ - NSUInteger index = 0; - [self calculateSplitOffsetForSplitLocation:location inElementAtIndex:&index]; - return index; -} - - (void)validateElements:(NSArray *)elements { for (id element in elements) { @@ -66,18 +56,14 @@ *insertionIndex = 0; return NO; } - NSUInteger splitElementIndex; - NSUInteger splitOffset = [self calculateSplitOffsetForSplitLocation:location - inElementAtIndex:&splitElementIndex]; - id splitElement = self.elements[splitElementIndex]; - if (splitOffset == splitElement.length) { - splitOffset = 0; - splitElementIndex++; - } + + NSUInteger splitOffset; + NSUInteger splitElementIndex = [self indexOfElementAtSymbolLocation:location + offset:&splitOffset]; if (splitOffset != 0) { - NSString *stringElement = (NSString *)splitElement; - NSString *leftPart = [stringElement substringToIndex:splitOffset]; - NSString *rightPart = [stringElement substringFromIndex:splitOffset]; + NSString *splitElement = (NSString *)self.elements[splitElementIndex]; + NSString *leftPart = [splitElement substringToIndex:splitOffset]; + NSString *rightPart = [splitElement substringFromIndex:splitOffset]; [self.elements replaceObjectsInRange:NSMakeRange(splitElementIndex, 1) withObjectsFromArray:@[leftPart, rightPart]]; ++splitElementIndex; @@ -86,24 +72,6 @@ return splitOffset != 0; } -- (NSUInteger)calculateSplitOffsetForSplitLocation:(NSUInteger)location - inElementAtIndex:(out NSUInteger *)elementIndex -{ - NSUInteger length = 0; - NSUInteger index = 0; - NSUInteger elementLength = 0; - for (id element in self.elements) { - elementLength = element.length; - length += elementLength; - if (length >= location) { - break; - } - ++index; - } - *elementIndex = index; - return elementLength - (length - location); -} - @end @implementation MPExpression { @@ -215,6 +183,41 @@ return [self.elements indexOfObject:element]; } +- (NSUInteger)indexOfElementAtSymbolLocation:(NSUInteger)location + offset:(out NSUInteger *)offset +{ + if (location == 0) { + if (offset != NULL) { + *offset = 0; + } + return 0; + } + + // Calculating elementIndex and splitOffset + NSUInteger totalLength = 0; + NSUInteger elementIndex = 0; + NSUInteger elementLength = 0; + for (id element in self.elements) { + elementLength = element.length; + totalLength += elementLength; + if (totalLength >= location) { + break; + } + ++elementIndex; + } + NSUInteger splitOffset = elementLength - (totalLength - location); + + id element = self.elements[elementIndex]; + if (splitOffset == element.length) { + splitOffset = 0; + elementIndex++; + } + if (offset != NULL) { + *offset = splitOffset; + } + return elementIndex; +} + - (void)replaceSymbolsInRange:(NSRange)range withElements:(NSArray *)elements { @@ -276,7 +279,7 @@ } #pragma mark Basic NSObject Methods - +/* - (BOOL)isEqual:(id)object { if (self == object) { @@ -295,7 +298,7 @@ { return [self.elements isEqualToArray:anExpression.elements]; } - +*/ - (NSString *)description { #warning Bad Implementation @@ -359,6 +362,14 @@ @implementation MPExpression (MPExpressionExtension) #pragma mark Working With the Expression Tree +- (MPExpression *)rootExpression +{ + if (self.parent == nil) { + return self; + } + return [self.parent rootExpression]; +} + - (id)elementAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.length == 0) { @@ -369,7 +380,7 @@ return element; } if ([element isFunction]) { - return [(MPFunction *)element elementAtIndexPath:[indexPath indexPathByRemovingLastIndex]]; + return [(MPFunction *)element elementAtIndexPath:[indexPath indexPathByRemovingFirstIndex]]; } return nil; } diff --git a/MathPad/MPExpressionLayout.h b/MathPad/MPExpressionLayout.h index 6cc7162..decb929 100644 --- a/MathPad/MPExpressionLayout.h +++ b/MathPad/MPExpressionLayout.h @@ -12,7 +12,7 @@ @interface MPExpressionLayout : MPLayout -- (instancetype)initRootLayoutWithExpressionStorage:(MPExpressionStorage *)expressionStorage; +- (instancetype)initRootLayoutWithExpression:(MPExpression *)expression; @property (readonly, nonatomic, weak) MPExpression *expression; diff --git a/MathPad/MPExpressionLayout.m b/MathPad/MPExpressionLayout.m index cbccff5..baa4e9d 100644 --- a/MathPad/MPExpressionLayout.m +++ b/MathPad/MPExpressionLayout.m @@ -9,6 +9,10 @@ #import "MPExpressionLayout.h" #import "MPFunctionLayout.h" +#import "NSIndexPath+MPAdditions.h" + +#define kMPEmptyBoxWidth (self.usesSmallSize ? 2.0 : 3.0) + @interface MPExpressionLayout (MPLineGeneration) - (CTLineRef)lineForElementAtIndex:(NSUInteger)index; @@ -34,9 +38,8 @@ - (CTLineRef)createLineForString:(NSString *)aString { - NSAttributedString *text = [[NSAttributedString alloc] initWithString:aString - attributes:@{NSFontAttributeName: [NSFont fontWithName:@"Lucida Grande" size:18.0]}]; + attributes:@{NSFontAttributeName: self.font}]; CFAttributedStringRef attributedString = CFBridgingRetain(text); CTLineRef line = CTLineCreateWithAttributedString(attributedString); CFRelease(attributedString); // TODO: Is this release appropriate? @@ -48,49 +51,38 @@ @implementation MPExpressionLayout # pragma mark Creation Methods -- (instancetype)initRootLayoutWithExpressionStorage:(MPExpressionStorage *)expressionStorage +- (instancetype)initRootLayoutWithExpression:(MPExpression *)expression { self = [super init]; if (self) { - _expressionStorage = expressionStorage; - _expression = expressionStorage; + _expression = expression; } return self; } -- (instancetype)initWithPath:(NSIndexPath *)path - parent:(MPLayout *)parent +- (instancetype)initWithElementAtPath:(NSIndexPath *)path + inRootExpression:(MPExpression *)rootExpression + parent:(MPLayout *)parent { - self = [super initWithPath:path - parent:parent]; + self = [super initWithElementAtPath:path + inRootExpression:rootExpression + parent:parent]; if (self) { - _expression = [parent.expressionStorage elementAtIndexPath:path]; + _expression = [rootExpression elementAtIndexPath:path]; } return self; } -#pragma mark Properties -@synthesize expressionStorage = _expressionStorage; -- (MPExpressionStorage *)expressionStorage -{ - if (_expressionStorage) { - return _expressionStorage; - } - return self.parent.expressionStorage; -} - -//- (MPExpression *)expression -//{ -// return [self.expressionStorage elementAtIndexPath:self.path]; -//} - #pragma mark Cache Methods - (MPLayout *)childLayoutAtIndex:(NSUInteger)index { id cachedObject = [self cachableObjectForIndex:index generator:^id{ NSIndexPath *indexPath = [self.expression.indexPath indexPathByAddingIndex:index]; MPFunctionLayout *layout = [MPFunctionLayout functionLayoutForFunctionAtIndexPath:indexPath + inRootExpression:self.expression.rootExpression parent:self]; + layout.flipped = self.flipped; + layout.usesSmallSize = self.usesSmallSize; return layout; }]; if ([cachedObject isKindOfClass:[MPLayout class]]) { @@ -99,51 +91,165 @@ return nil; } -- (NSSize)sizeForElementAtIndex:(NSUInteger)index +- (NSRect)boundsOfElementAtIndex:(NSUInteger)index { id symbol = [self.expression elementAtIndex:index]; if ([symbol isString]) { CTLineRef line = [self lineForElementAtIndex:index]; CFRetain(line); - CGRect bounds = CTLineGetBoundsWithOptions(line, /*kCTLineBoundsUseOpticalBounds*/ 0); + CGRect bounds = CTLineGetBoundsWithOptions(line, 0); CFRelease(line); - return bounds.size; + return bounds; } else { - return [self childLayoutAtIndex:index].size; + return [self childLayoutAtIndex:index].bounds; } } #pragma mark Drawing Methods -- (NSSize)generateSize +- (NSRect)generateBounds { - CGFloat width = 0, height = 0; - for (NSUInteger index = 0; index < self.expression.numberOfElements; index++) { - NSSize elementSize = [self sizeForElementAtIndex:index]; - width += elementSize.width; - height = MAX(height, elementSize.height); + if (self.expression.numberOfElements == 0) { + return NSMakeRect(0, [self.font descender], kMPEmptyBoxWidth, self.fontSize); } - return NSMakeSize(width, height); + CGFloat x = 0, y = 0, width = 0, height = 0; + for (NSUInteger index = 0; index < self.expression.numberOfElements; index++) { + NSRect elementBounds = [self boundsOfElementAtIndex:index]; + width += elementBounds.size.width; + height = MAX(height, elementBounds.size.height); + y = MIN(y, elementBounds.origin.y); + } + return NSMakeRect(x, y, width, height); } -- (void)drawAtPoint:(NSPoint)point +- (NSRect)boundingRectForRange:(NSRange)range +{ + NSUInteger startOffset; + NSUInteger startElementIndex = [self.expression indexOfElementAtSymbolLocation:range.location + offset:&startOffset]; + // Calculate x position + CGFloat x = 0, width = 0; + for (NSUInteger index = 0; index < startElementIndex; index++) { + x += [self boundsOfElementAtIndex:index].size.width; + } + + if (startOffset > 0) { + CTLineRef line = [self lineForElementAtIndex:startElementIndex]; + CFRetain(line); + CGFloat xOffset = CTLineGetOffsetForStringIndex(line, startOffset, NULL); + x += xOffset; + width += CTLineGetBoundsWithOptions(line, 0).size.width - xOffset; + CFRelease(line); + } else if (startElementIndex < self.expression.numberOfElements) { // Otherwise the selection is after the last symbol + width += [self boundsOfElementAtIndex:startElementIndex].size.width; + } + + // If we search the caret position we are done + if (range.length == 0) { + return NSMakeRect(x, self.bounds.origin.y, 0, self.bounds.size.height); + } + + NSUInteger endOffset; + NSUInteger endElementIndex = [self.expression indexOfElementAtSymbolLocation:NSMaxRange(range) + offset:&endOffset]; + + // Selection is inside of one string element + if (startElementIndex == endElementIndex) { + CTLineRef line = [self lineForElementAtIndex:endElementIndex]; + CFRetain(line); + CGFloat xStart = CTLineGetOffsetForStringIndex(line, startOffset, NULL); + CGFloat xEnd = CTLineGetOffsetForStringIndex(line, endOffset, NULL); + width = xEnd - xStart; + CFRelease(line); + return NSMakeRect(x, self.bounds.origin.y, width, self.bounds.size.height); + } + + // Calculate width + for (NSUInteger index = startElementIndex + 1; index < endElementIndex; index++) { + width += [self boundsOfElementAtIndex:index].size.width; + } + if (endOffset > 0) { + CTLineRef line = [self lineForElementAtIndex:endElementIndex]; + CFRetain(line); + width += CTLineGetOffsetForStringIndex(line, endOffset, NULL); + CFRelease(line); + } + return NSMakeRect(x, self.bounds.origin.y, width, self.bounds.size.height); +} + +- (NSPoint)offsetOfChildLayoutAtIndex:(NSUInteger)index +{ + CGFloat x = 0; + for (NSUInteger i = 0; i < index; i++) { + x += [self boundsOfElementAtIndex:i].size.width; + } + return NSMakePoint(x, 0); +} + +- (NSIndexPath *)indexPathForMousePoint:(NSPoint)point +{ + NSUInteger currentPosition = 0; + for (NSUInteger index = 0; index < self.expression.numberOfElements; index++) { + NSRect elementBounds = [self boundsOfElementAtIndex:index]; + NSPoint elementOffset = [self offsetOfChildLayoutAtIndex:index]; + elementBounds.origin.x += elementOffset.x; + elementBounds.origin.y += elementOffset.y; + + id element = [self.expression elementAtIndex:index]; + if (NSMouseInRect(point, elementBounds, self.flipped)) { + if ([element isString]) { + CTLineRef line = [self lineForElementAtIndex:index]; + CFRetain(line); + CFIndex localIndex = CTLineGetStringIndexForPosition(line, point); + CFRelease(line); + return [NSIndexPath indexPathWithIndex:currentPosition+localIndex]; + } else { + NSPoint pointInFunction = NSMakePoint(point.x - elementOffset.x, point.y + elementOffset.y); + NSIndexPath *subPath = [[self childLayoutAtIndex:index] indexPathForMousePoint:pointInFunction]; + if (subPath.length == 1) { + // A single index is used to communicate back wether the + // selection should be before or after the function. + // A 0 means before, a 1 means after. + return [NSIndexPath indexPathWithIndex:currentPosition + [subPath indexAtPosition:0]]; + } else { + return [subPath indexPathByPreceedingIndex:index]; + } + } + } + currentPosition += element.length; + } + if (point.x < self.bounds.size.width / 2) { + return [NSIndexPath indexPathWithIndex:0]; + } else { + return [NSIndexPath indexPathWithIndex:self.expression.length]; + } +} + +- (void)draw { // Get the current context CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; CGContextSaveGState(context); + if (self.expression.numberOfElements == 0) { + + CGContextRestoreGState(context); + NSBezierPath *path = [NSBezierPath bezierPathWithRect:NSMakeRect(0, 0 + self.font.descender, kMPEmptyBoxWidth, self.fontSize)]; + path.lineWidth = 0.5; + [path stroke]; + return; + } + // Set the text matrix CGContextSetTextMatrix(context, CGAffineTransformIdentity); // Track the x position - CGFloat x = point.x; - + CGFloat x = 0; + for (NSUInteger index = 0; index < self.expression.numberOfElements; index++) { // The current element id element = [self.expression elementAtIndex:index]; - NSSize elementSize = [self sizeForElementAtIndex:index]; - CGFloat dy = (self.size.height - elementSize.height) / 2; - CGFloat y = point.y + dy; + NSRect elementBounds = [self boundsOfElementAtIndex:index]; if ([element isString]) { // Get the line to draw @@ -151,7 +257,7 @@ CFRetain(line); // Move to the appropriate position - CGContextSetTextPosition(context, x, y); + CGContextSetTextPosition(context, x, 0); // Perform the drawing CTLineDraw(line, context); @@ -160,9 +266,9 @@ } else { // Let the child layout draw itself MPLayout *layout = [self childLayoutAtIndex:index]; - [layout drawAtPoint:NSMakePoint(x, y)]; + [layout drawAtPoint:NSMakePoint(x, 0)]; } - x += elementSize.width; + x += elementBounds.size.width; } CGContextRestoreGState(context); } diff --git a/MathPad/MPExpressionStorage.h b/MathPad/MPExpressionStorage.h index c186c0e..7ad5fd6 100644 --- a/MathPad/MPExpressionStorage.h +++ b/MathPad/MPExpressionStorage.h @@ -12,14 +12,9 @@ @interface MPExpressionStorage : MPExpression -- (instancetype)initWithExpressionView:(MPExpressionView *)expressionView - elements:(NSArray *)elements; +- (instancetype)initWithElements:(NSArray *)elements; -@property (readonly, nonatomic, weak) MPExpressionView *expressionView; +@property (nonatomic, weak) MPExpressionView *expressionView; // Do not set @property (nonatomic, strong) MPExpressionLayout *rootLayout; -- (NSLayoutManager *)layoutManager; -- (NSTextContainer *)textContainer; -- (NSTextStorage *)textStorage; - @end diff --git a/MathPad/MPExpressionStorage.m b/MathPad/MPExpressionStorage.m index 62ef2fa..0a771f5 100644 --- a/MathPad/MPExpressionStorage.m +++ b/MathPad/MPExpressionStorage.m @@ -20,53 +20,20 @@ @implementation MPExpressionStorage -- (instancetype)initWithExpressionView:(MPExpressionView *)expressionView elements:(NSArray *)elements -{ - self = [self initWithElements:elements]; - if (self) { - _expressionView = expressionView; - } - return self; -} - - (instancetype)initWithElements:(NSArray *)elements { self = [super initWithElements:elements]; if (self) { - _rootLayout = [[MPExpressionLayout alloc] initRootLayoutWithExpressionStorage:self]; + _rootLayout = [[MPExpressionLayout alloc] initRootLayoutWithExpression:self]; } return self; } -- (NSLayoutManager *)layoutManager +- (void)setExpressionView:(MPExpressionView *)expressionView { - [self ensureTextSystemObjects]; - return _layoutManager; -} - -- (NSTextContainer *)textContainer -{ - [self ensureTextSystemObjects]; - return _textContainer; -} - -- (NSTextStorage *)textStorage -{ - [self ensureTextSystemObjects]; - return _textStorage; -} - -- (void)ensureTextSystemObjects -{ - if (_layoutManager == nil || _textContainer == nil || _textStorage == nil) { - _textStorage = [[NSTextStorage alloc] init]; - NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)]; - NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; - [layoutManager addTextContainer:textContainer]; - [_textStorage addLayoutManager:layoutManager]; - _textContainer = textContainer; - _layoutManager = layoutManager; - } + _expressionView = expressionView; + self.rootLayout = [[MPExpressionLayout alloc] initRootLayoutWithExpression:self]; + self.rootLayout.flipped = expressionView.isFlipped; } - (void)didChangeElementsInRangePath:(MPRangePath *)rangePath @@ -76,11 +43,13 @@ return; } MPLayout *current = self.rootLayout; - for (NSUInteger index = 1; index < rangePath.location.length-1; index++) { + for (NSUInteger position = 0; position < rangePath.location.length-1; position++) { + NSUInteger index = [rangePath.location indexAtPosition:position]; current = [current childLayoutAtIndex:index]; } [current clearCacheInRange:rangePath.rangeAtLastIndex replacementLength:replacementLength]; + [self.expressionView invalidateIntrinsicContentSize]; self.expressionView.needsDisplay = YES; } diff --git a/MathPad/MPExpressionView.h b/MathPad/MPExpressionView.h index e1a3b51..8102634 100644 --- a/MathPad/MPExpressionView.h +++ b/MathPad/MPExpressionView.h @@ -23,7 +23,6 @@ @property (readonly, nonatomic, strong) MPExpressionStorage *expressionStorage; @property (nonatomic, getter = isEditable) BOOL editable; -//@property (nonatomic, strong) MPRangePath *selection; -@property (nonatomic, strong) NSIndexPath *caretLocation; +@property (nonatomic, strong) MPRangePath *selection; @end diff --git a/MathPad/MPExpressionView.m b/MathPad/MPExpressionView.m index 60cc613..6a40209 100644 --- a/MathPad/MPExpressionView.m +++ b/MathPad/MPExpressionView.m @@ -6,34 +6,109 @@ // 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 (MPCursor) +@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 - -- (instancetype)init -{ - self = [super init]; - if (self) { - [self initializeObjects]; - } - return self; -} - - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { - [self initializeObjects]; + [self initializeExpressionView]; } return self; } @@ -42,25 +117,44 @@ { self = [super initWithCoder:aDecoder]; if (self) { - [self initializeObjects]; + [self initializeExpressionView]; } return self; } -- (void)initializeObjects +- (void)initializeExpressionView { - MPExpressionStorage *expressionStorage = [[MPExpressionStorage alloc] initWithExpressionView:self elements:@[@"12345", [[MPSumFunction alloc] init], [[MPSumFunction alloc] init]]]; + MPExpressionStorage *expressionStorage = [[MPExpressionStorage alloc] initWithElements:@[@"12345", [[MPSumFunction alloc] init], [[MPSumFunction alloc] init]]]; + expressionStorage.expressionView = self; _expressionStorage = expressionStorage; - _caretLocation = [[NSIndexPath alloc] initWithIndex:0]; + 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 - -- (BOOL)isFlipped +- (void)setExpressionStorage:(MPExpressionStorage *)expressionStorage { - return NO; + _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; @@ -71,29 +165,226 @@ return YES; } -#pragma mark Event Handling +- (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 { - [self interpretKeyEvents:@[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 { - [self.expressionStorage deleteElementsInRange:NSMakeRange(0, 1)]; + 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); - [[NSColor blackColor] set]; - NSSize expressionSize = [self.expressionStorage.rootLayout size]; - CGFloat y = (self.bounds.size.height - expressionSize.height) / 2; - NSPoint point = NSMakePoint(self.bounds.origin.x, self.bounds.origin.y + y); - [self.expressionStorage.rootLayout drawAtPoint:point]; + + // 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 diff --git a/MathPad/MPFunction.h b/MathPad/MPFunction.h index 4394886..bba4431 100644 --- a/MathPad/MPFunction.h +++ b/MathPad/MPFunction.h @@ -22,6 +22,7 @@ #pragma mark Working With the Expression Tree @property (nonatomic, weak) MPExpression *parent; // Documentation: Do not set +- (MPExpression *)rootExpression; - (NSIndexPath *)indexPath; - (NSUInteger)numberOfChildren; // Override diff --git a/MathPad/MPFunction.m b/MathPad/MPFunction.m index 3e66ebe..bd3b02d 100644 --- a/MathPad/MPFunction.m +++ b/MathPad/MPFunction.m @@ -24,6 +24,11 @@ } #pragma mark Working With the Expression Tree +- (MPExpression *)rootExpression +{ + return [self.parent rootExpression]; +} + - (NSIndexPath *)indexPath { NSUInteger selfIndex = [self.parent indexOfElement:self]; @@ -60,7 +65,7 @@ { NSUInteger index = 0; for (; index < [self numberOfChildren]; index++) { - if ([[self childAtIndex:index] isEqualToExpression:child]) { + if ([self childAtIndex:index] == child) { return index; } } @@ -106,6 +111,7 @@ } #pragma mark Working With Functions +/* - (BOOL)isEqual:(id)object { if (self == object) { @@ -123,7 +129,7 @@ - (BOOL)isEqualToFunction:(MPFunction *)aFunction { return [aFunction isMemberOfClass:[MPFunction class]] && [self isMemberOfClass:[MPFunction class]]; -} +}*/ - (NSString *)description { diff --git a/MathPad/MPFunctionLayout.h b/MathPad/MPFunctionLayout.h index 2cd6818..769246d 100644 --- a/MathPad/MPFunctionLayout.h +++ b/MathPad/MPFunctionLayout.h @@ -12,6 +12,7 @@ @interface MPFunctionLayout : MPLayout + (MPFunctionLayout *)functionLayoutForFunctionAtIndexPath:(NSIndexPath *)path + inRootExpression:(MPExpression *)rootExpression parent:(MPExpressionLayout *)parent; @property (readonly, nonatomic, weak) MPFunction *function; @@ -31,7 +32,9 @@ // } #pragma mark Size and Drawing Methods -- (NSSize)generateSize; // To be implemented +- (NSPoint)offsetOfChildLayoutAtIndex:(NSUInteger)index; // To be implemented +- (NSIndexPath *)indexPathForMousePoint:(NSPoint)point; // To be implemented +- (NSRect)generateBounds; // To be implemented - (void)drawAtPoint:(NSPoint)point; // To be implemented @end \ No newline at end of file diff --git a/MathPad/MPFunctionLayout.m b/MathPad/MPFunctionLayout.m index dd9c0ac..75d6d9d 100644 --- a/MathPad/MPFunctionLayout.m +++ b/MathPad/MPFunctionLayout.m @@ -17,33 +17,34 @@ #pragma mark Creation Methods + (MPFunctionLayout *)functionLayoutForFunctionAtIndexPath:(NSIndexPath *)path + inRootExpression:(MPExpression *)rootExpression parent:(MPExpressionLayout *)parent { - MPFunction *function = [parent.expressionStorage elementAtIndexPath:path]; + MPFunction *function = [rootExpression elementAtIndexPath:path]; Class class = [function class]; if (class == [MPSumFunction class]) { - return [[MPSumFunctionLayout alloc] initWithPath:path parent:parent]; + return [[MPSumFunctionLayout alloc] initWithElementAtPath:path + inRootExpression:rootExpression + parent:parent]; } - return [[self alloc] initWithPath:path parent:parent]; + return [[self alloc] initWithElementAtPath:path + inRootExpression:rootExpression + parent:parent]; } -- (instancetype)initWithPath:(NSIndexPath *)path - parent:(MPLayout *)parent +- (instancetype)initWithElementAtPath:(NSIndexPath *)path + inRootExpression:(MPExpression *)rootExpression + parent:(MPLayout *)parent { - self = [super initWithPath:path - parent:parent]; + self = [super initWithElementAtPath:path + inRootExpression:rootExpression + parent:parent]; if (self) { - _function = [parent.expressionStorage elementAtIndexPath:path]; + _function = [rootExpression elementAtIndexPath:path]; } return self; } -#pragma mark Properties -//- (MPFunction *)function -//{ -// return [self.expressionStorage elementAtIndexPath:self.path]; -//} - #pragma mark Cache Methods - (CTLineRef)lineForPrivateCacheIndex:(NSUInteger)index generator:(CTLineRef (^)())generator @@ -62,26 +63,22 @@ { return [self cachableObjectForIndex:index generator:^id{ NSIndexPath *childPath = [self.function.indexPath indexPathByAddingIndex:index]; - MPExpressionLayout *layout = [[MPExpressionLayout alloc] initWithPath:childPath - parent:self]; + MPExpressionLayout *layout = [[MPExpressionLayout alloc] initWithElementAtPath:childPath + inRootExpression:self.function.rootExpression + parent:self]; + layout.flipped = self.flipped; + layout.usesSmallSize = (index == 0 || index == 1) ? YES : self.usesSmallSize; return layout; }]; } #pragma mark Size and Drawing Methods - -- (NSSize)sizeForChildAtIndex:(NSUInteger)index +- (NSRect)generateBounds { - MPLayout *childLayout = [self childLayoutAtIndex:index]; - return [childLayout size]; + return NSZeroRect; } -- (NSSize)generateSize -{ - return NSZeroSize; -} - -- (void)drawAtPoint:(NSPoint)point +- (void)draw { } diff --git a/MathPad/MPLayout.h b/MathPad/MPLayout.h index ed9f063..a341e74 100644 --- a/MathPad/MPLayout.h +++ b/MathPad/MPLayout.h @@ -15,11 +15,17 @@ #pragma mark Creation Methods - (instancetype)init; -- (instancetype)initWithPath:(NSIndexPath *)path - parent:(MPLayout *)parent; +- (instancetype)initWithElementAtPath:(NSIndexPath *)path + inRootExpression:(MPExpression *)rootExpression + parent:(MPLayout *)parent; -#pragma mark Text System Objects -@property (readonly, nonatomic, weak) MPExpressionStorage *expressionStorage; +#pragma mark Properties +- (NSFont *)font; +- (CGFloat)fontSize; +- (NSFont *)normalFont; +- (CGFloat)normalFontSize; +- (NSFont *)smallFont; +- (CGFloat)smallFontSize; #pragma mark Cache Tree @property (readonly, nonatomic, weak) MPLayout *parent; @@ -35,14 +41,21 @@ - (void)invalidate; #pragma mark Calculation and Drawing Methods -// TODO: Implement Small Size -// @property (nonatomic) BOOL usesSmallSize; -- (NSSize)size; +@property (nonatomic, getter = isFlipped) BOOL flipped; +@property (nonatomic) BOOL usesSmallSize; +- (NSRect)bounds; + +- (NSRect)boundingRectForRangePath:(MPRangePath *)rangePath; /* if rangePath.length is 0 the returned rect will have a width of 0 */ + - (void)drawAtPoint:(NSPoint)point; @end @interface MPLayout (MPSubclassImplement) - (MPLayout *)childLayoutAtIndex:(NSUInteger)index; // To be implemented -- (NSSize)generateSize; // To be implemented +- (NSRect)generateBounds; // To be implemented +- (NSRect)boundingRectForRange:(NSRange)range; // To be implemented, use rangePath instead, this one has wrong origin +- (NSPoint)offsetOfChildLayoutAtIndex:(NSUInteger)index; +- (NSIndexPath *)indexPathForMousePoint:(NSPoint)point; +- (void)draw; // To be implemented @end \ No newline at end of file diff --git a/MathPad/MPLayout.m b/MathPad/MPLayout.m index 0c49c79..22768e8 100644 --- a/MathPad/MPLayout.m +++ b/MathPad/MPLayout.m @@ -7,6 +7,9 @@ // #import "MPLayout.h" +#import "MPRangePath.h" + +#import "NSIndexPath+MPAdditions.h" @interface MPLayout () @@ -18,7 +21,7 @@ @implementation MPLayout { NSMutableArray *_cache; - NSSize _cachedSize; + NSRect _cachedBounds; } #pragma mark Creation Methods @@ -27,13 +30,14 @@ self = [super init]; if (self) { _cache = [[NSMutableArray alloc] init]; - _cachedSize = NSZeroSize; + _cachedBounds = NSZeroRect; } return self; } -- (id)initWithPath:(NSIndexPath *)path - parent:(MPLayout *)parent +- (instancetype)initWithElementAtPath:(NSIndexPath *)path + inRootExpression:(MPExpression *)rootExpression + parent:(MPLayout *)parent { self = [self init]; if (self) { @@ -42,10 +46,37 @@ return self; } -#pragma Text System Objects -- (MPExpressionStorage *)expressionStorage +#pragma mark Properties +- (NSFont *)font { - return self.parent.expressionStorage; + return self.usesSmallSize ? self.smallFont : self.normalFont; +} + +- (CGFloat)fontSize +{ + return self.usesSmallSize ? self.smallFontSize : self.normalFontSize; +} + +- (NSFont *)normalFont +{ + return [NSFont fontWithName:@"CMU Serif" + size:self.fontSize]; +} + +- (CGFloat)normalFontSize +{ + return 18.0; +} + +- (NSFont *)smallFont +{ + return [NSFont fontWithName:@"CMU Serif" + size:self.smallFontSize]; +} + +- (CGFloat)smallFontSize +{ + return 12.0; } #pragma mark Cache Tree @@ -92,22 +123,42 @@ - (void)invalidate { - _cachedSize = NSZeroSize; + _cachedBounds = NSZeroRect; [self.parent invalidate]; } #pragma mark Calculation and Drawing Methods -- (NSSize)size +- (NSRect)bounds { - if (NSEqualSizes(_cachedSize, NSZeroSize)) { - _cachedSize = [self generateSize]; + if (NSEqualRects(_cachedBounds, NSZeroRect)) { + _cachedBounds = [self generateBounds]; } - return _cachedSize; + return _cachedBounds; +} + +- (NSRect)boundingRectForRangePath:(MPRangePath *)rangePath +{ + if (rangePath.location.length == 1) { + return [self boundingRectForRange:rangePath.rangeAtLastIndex]; + } + NSUInteger nextIndex = [rangePath.location indexAtPosition:0]; + NSIndexPath *newLocation = [rangePath.location indexPathByRemovingFirstIndex]; + MPRangePath *newRangePath = [[MPRangePath alloc] initWithLocation:newLocation length:rangePath.length]; + NSRect bounds = [[self childLayoutAtIndex:nextIndex] boundingRectForRangePath:newRangePath]; + NSPoint offset = [self offsetOfChildLayoutAtIndex:nextIndex]; + bounds.origin = NSMakePoint(bounds.origin.x + offset.x, bounds.origin.y + offset.y); + return bounds; } - (void)drawAtPoint:(NSPoint)point { - + NSAffineTransform *transform = [NSAffineTransform transform]; + [transform translateXBy:point.x + yBy:point.y]; + [transform concat]; + [self draw]; + [transform invert]; + [transform concat]; } @end @@ -119,14 +170,24 @@ return nil; } -- (NSSize)sizeForChildAtIndex:(NSUInteger)index +- (NSRect)generateBounds { - return NSZeroSize; + return NSZeroRect; } -- (NSSize)generateSize +- (NSRect)boundingRectForRange:(NSRange)range { - return NSZeroSize; + return NSZeroRect; +} + +- (NSPoint)offsetOfChildLayoutAtIndex:(NSUInteger)index +{ + return NSZeroPoint; +} + +- (void)draw +{ + } @end diff --git a/MathPad/MPRangePath.h b/MathPad/MPRangePath.h index be32207..ad1ed38 100644 --- a/MathPad/MPRangePath.h +++ b/MathPad/MPRangePath.h @@ -10,6 +10,8 @@ #import "MPExpression.h" +#define MPMakeRangePath(loc, len) [MPRangePath rangePathWithLocation:(loc) length:(len)] + @class MPRangePath, MPExpression; @interface MPRangePath : NSObject diff --git a/MathPad/MPSumFunction.h b/MathPad/MPSumFunction.h index 2f4e964..03a165c 100644 --- a/MathPad/MPSumFunction.h +++ b/MathPad/MPSumFunction.h @@ -13,8 +13,8 @@ @interface MPSumFunction : MPFunction -@property (nonatomic, strong) MPExpression *startExpression; -@property (nonatomic, strong) MPExpression *targetExpression; -@property (nonatomic, strong) MPExpression *sumExpression; +@property (nonatomic, strong) MPExpression *startExpression; // Index 0 +@property (nonatomic, strong) MPExpression *targetExpression; // Index 1 +@property (nonatomic, strong) MPExpression *sumExpression; // Index 2 @end diff --git a/MathPad/MPSumFunction.m b/MathPad/MPSumFunction.m index 4e9822f..e267d2c 100644 --- a/MathPad/MPSumFunction.m +++ b/MathPad/MPSumFunction.m @@ -104,6 +104,7 @@ } #pragma mark Working With Functions +/* - (BOOL)isEqualToFunction:(MPFunction *)aFunction { if (![aFunction isKindOfClass:[MPSumFunction class]]) { @@ -117,7 +118,7 @@ return NO; } return [self.sumExpression isEqualToExpression:sumFunction.sumExpression]; -} +}*/ - (NSString *)description { diff --git a/MathPad/MPSumFunctionLayout.m b/MathPad/MPSumFunctionLayout.m index 14e2aa9..887f2b7 100644 --- a/MathPad/MPSumFunctionLayout.m +++ b/MathPad/MPSumFunctionLayout.m @@ -9,6 +9,13 @@ #import "MPSumFunctionLayout.h" #import "MPSumFunction.h" +#import "NSIndexPath+MPAdditions.h" + +#define kSumFunctionStartExpressionOffset 3 +#define kSumFunctionTargetExpressionOffset 0 +#define kSumFunctionSumExpressionOffset 3 +#define kSumFunctionTrailingOffset 5 + @implementation MPSumFunctionLayout - (MPSumFunction *)sumFunction @@ -21,7 +28,7 @@ CTLineRef line = [self lineForPrivateCacheIndex:0 generator:^CTLineRef{ NSAttributedString *text = [[NSAttributedString alloc] initWithString:@"∑" - attributes:@{NSFontAttributeName: [NSFont fontWithName:@"Lucida Grande" size:18.0]}]; + attributes:@{NSFontAttributeName: self.font}]; CFAttributedStringRef attributedString = CFBridgingRetain(text); CTLineRef line = CTLineCreateWithAttributedString(attributedString); CFRelease(attributedString); // TODO: Is this release appropriate @@ -30,31 +37,119 @@ return line; } -- (NSSize)generateSize +- (NSRect)localLineBounds { - CTLineRef line = [self line]; - CFRetain(line); - CGSize size = CTLineGetBoundsWithOptions(line, /*kCTLineBoundsUseOpticalBounds*/0).size; - CFRelease(line); - return size; + NSRect lineBounds = CTLineGetBoundsWithOptions(self.line, 0); + CGFloat width = MAX(MAX([self childLayoutAtIndex:0].bounds.size.width, [self childLayoutAtIndex:1].bounds.size.width), lineBounds.size.width); + CGFloat xPosition = (width - lineBounds.size.width) / 2; + return NSMakeRect(xPosition, lineBounds.origin.y, lineBounds.size.width, lineBounds.size.height); } -- (void)drawAtPoint:(NSPoint)point +- (NSPoint)offsetOfChildLayoutAtIndex:(NSUInteger)index +{ + NSRect childBounds = [self childLayoutAtIndex:index].bounds; + NSRect localLineBounds = [self localLineBounds]; + NSPoint offset; + if (index == 0) { + // Start Expression + offset.x = localLineBounds.origin.x + localLineBounds.size.width / 2 - childBounds.size.width / 2; + offset.y = -kSumFunctionStartExpressionOffset - childBounds.size.height; + } else if (index == 1) { + // Target Expression + offset.x = localLineBounds.origin.x + localLineBounds.size.width / 2 - childBounds.size.width / 2; + offset.y = localLineBounds.size.height + kSumFunctionTargetExpressionOffset; + } else { + // Sum Expression + MPLayout *startExpressionLayout = [self childLayoutAtIndex:0]; + MPLayout *targetExpressionLayout = [self childLayoutAtIndex:1]; + CGFloat sumWidth = MAX(MAX(localLineBounds.origin.x + localLineBounds.size.width, startExpressionLayout.bounds.size.width), targetExpressionLayout.bounds.size.width); + offset.x = sumWidth + kSumFunctionSumExpressionOffset; + offset.y = 0; + } + return offset; +} + +- (NSIndexPath *)indexPathForMousePoint:(NSPoint)point +{ + // A single index is used to communicate back wether the + // selection should be before or after the function. + // A 0 means before, a 1 means after. + for (NSUInteger index = 0; index < self.function.numberOfChildren; index++) { + MPLayout *childLayout = [self childLayoutAtIndex:index]; + NSRect childBounds = childLayout.bounds; + NSPoint childOffset = [self offsetOfChildLayoutAtIndex:index]; + childBounds.origin.x += childOffset.x; + childBounds.origin.y += childOffset.y; + if (NSMouseInRect(point, childBounds, self.flipped)) { + NSPoint pointInChild = NSMakePoint(point.x + childOffset.x, point.y + childOffset.y); + NSIndexPath *subPath = [childLayout indexPathForMousePoint:pointInChild]; + return [subPath indexPathByPreceedingIndex:index]; + } + } + if (point.x < CTLineGetBoundsWithOptions(self.line, 0).size.width / 2) { + return [NSIndexPath indexPathWithIndex:0]; + } else { + return [NSIndexPath indexPathWithIndex:1]; + } +} + +- (NSRect)generateBounds +{ + NSRect lineBounds = CTLineGetBoundsWithOptions(self.line, 0); + NSRect startExpressionBounds = [self childLayoutAtIndex:0].bounds; + NSRect targetExpressionBounds = [self childLayoutAtIndex:1].bounds; + NSRect sumExpressionBounds = [self childLayoutAtIndex:2].bounds; + NSRect bounds = lineBounds; + + bounds.size.width = MAX(lineBounds.size.width, startExpressionBounds.size.width); + bounds.size.height += startExpressionBounds.size.height + kSumFunctionStartExpressionOffset; + + bounds.size.width = MAX(bounds.size.width, targetExpressionBounds.size.width); + bounds.size.height += targetExpressionBounds.size.height + kSumFunctionTargetExpressionOffset; + + bounds.size.width += kSumFunctionSumExpressionOffset + sumExpressionBounds.size.width; + bounds.size.height = MAX(bounds.size.height, sumExpressionBounds.size.height); + + bounds.origin.y -= targetExpressionBounds.size.height + kSumFunctionStartExpressionOffset; + bounds.size.width += kSumFunctionTrailingOffset; + return bounds; +} + +- (void)draw { // Get the current context CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; - + // Set the text matrix CGContextSetTextMatrix(context, CGAffineTransformIdentity); - // Get the line of text + // Draw the sum symbol CTLineRef line = [self line]; CFRetain(line); - - // Draw the line - CGContextSetTextPosition(context, point.x, point.y); + NSRect localLineBounds = [self localLineBounds]; + CGContextSetTextPosition(context, localLineBounds.origin.x, 0); CTLineDraw(line, context); + // Draw the start function + MPLayout *startExpressionLayout = [self childLayoutAtIndex:0]; + NSPoint startExpressionLocation = NSMakePoint(localLineBounds.origin.x + localLineBounds.size.width / 2, 0); + startExpressionLocation.x -= startExpressionLayout.bounds.size.width / 2; + startExpressionLocation.y -= startExpressionLayout.bounds.size.height + kSumFunctionStartExpressionOffset; + [startExpressionLayout drawAtPoint:startExpressionLocation]; + + // Draw the target function + MPLayout *targetExpressionLayout = [self childLayoutAtIndex:1]; + NSPoint targetExpressionLocation = NSMakePoint(localLineBounds.origin.x + localLineBounds.size.width / 2, localLineBounds.size.height); + targetExpressionLocation.x -= targetExpressionLayout.bounds.size.width / 2; + targetExpressionLocation.y += kSumFunctionTargetExpressionOffset; + [targetExpressionLayout drawAtPoint:targetExpressionLocation]; + + // Draw the sum function + MPLayout *sumExpressionLayout = [self childLayoutAtIndex:2]; + CGFloat sumWidth = MAX(MAX(localLineBounds.origin.x + localLineBounds.size.width, startExpressionLayout.bounds.size.width), targetExpressionLayout.bounds.size.width); + sumWidth += kSumFunctionSumExpressionOffset; + [sumExpressionLayout drawAtPoint:NSMakePoint(sumWidth, 0)]; + CFRelease(line); } diff --git a/MathPad/MathPad-Info.plist b/MathPad/MathPad-Info.plist index 595b7f7..d26f8e5 100644 --- a/MathPad/MathPad-Info.plist +++ b/MathPad/MathPad-Info.plist @@ -51,6 +51,8 @@ Copyright © 2014 Kim Wittenburg. All rights reserved. NSMainNibFile MainMenu + ATSApplicationFontsPath + Fonts NSPrincipalClass NSApplication diff --git a/MathPad/NSIndexPath+MPAdditions.h b/MathPad/NSIndexPath+MPAdditions.h index 0d6979c..56b8c8e 100644 --- a/MathPad/NSIndexPath+MPAdditions.h +++ b/MathPad/NSIndexPath+MPAdditions.h @@ -10,6 +10,10 @@ @interface NSIndexPath (MPAdditions) +- (NSUInteger)lastIndex; + +- (NSIndexPath *)indexPathByReplacingLastIndexWithIndex:(NSUInteger)index; + /*! @method indexPathByRemovingFirstIndex @brief Provides an index path with the indexes in the receiving index path, excluding the first one. @@ -27,4 +31,7 @@ */ - (NSIndexPath *)indexPathByPreceedingIndex:(NSUInteger)index; +- (NSIndexPath *)indexPathByIncrementingLastIndex; +- (NSIndexPath *)indexPathByDecrementingLastIndex; + @end diff --git a/MathPad/NSIndexPath+MPAdditions.m b/MathPad/NSIndexPath+MPAdditions.m index 8bf2767..ee58bce 100644 --- a/MathPad/NSIndexPath+MPAdditions.m +++ b/MathPad/NSIndexPath+MPAdditions.m @@ -10,6 +10,16 @@ @implementation NSIndexPath (MPAdditions) +- (NSUInteger)lastIndex +{ + return [self indexAtPosition:self.length-1]; +} + +- (NSIndexPath *)indexPathByReplacingLastIndexWithIndex:(NSUInteger)index +{ + return [[self indexPathByRemovingLastIndex] indexPathByAddingIndex:index]; +} + - (NSIndexPath *)indexPathByRemovingFirstIndex { if (self.length <= 1) { @@ -34,4 +44,18 @@ return [[NSIndexPath alloc] initWithIndexes:newIndexes length:self.length+1]; } +- (NSIndexPath *)indexPathByIncrementingLastIndex +{ + NSUInteger lastIndex = [self lastIndex]; + lastIndex++; + return [[self indexPathByRemovingLastIndex] indexPathByAddingIndex:lastIndex]; +} + +- (NSIndexPath *)indexPathByDecrementingLastIndex +{ + NSUInteger lastIndex = [self lastIndex]; + lastIndex--; + return [[self indexPathByRemovingLastIndex] indexPathByAddingIndex:lastIndex]; +} + @end