From f4793e9ddb263516c3946ea11943111609a851a2 Mon Sep 17 00:00:00 2001 From: Jasmin Lapalme Date: Thu, 21 Sep 2017 10:39:40 -0400 Subject: [PATCH] Add support to customize the invisible character substitutions --- MGSFragariaView.h | 14 ++++++ MGSFragariaView.m | 14 ++++++ SMLLayoutManager.h | 14 ++++++ SMLLayoutManager.m | 114 ++++++++++++++++++++++++--------------------- SMLTextView.h | 14 ++++++ SMLTextView.m | 14 ++++++ 6 files changed, 131 insertions(+), 53 deletions(-) diff --git a/MGSFragariaView.h b/MGSFragariaView.h index 6a3f1fec..b7b2ec8a 100644 --- a/MGSFragariaView.h +++ b/MGSFragariaView.h @@ -310,6 +310,20 @@ /** Specifies the colour to render invisible characters in the text view.*/ @property (nonatomic, assign, nonnull) NSColor *textInvisibleCharactersColour; +/** + * Clears the current substitutes for invisible characters + **/ +- (void)clearInvisibleCharacterSubstitutes; + +/** + * Remove the substitute for invisible character `character` + **/ +- (void)removeSubstituteForInvisibleCharacter:(unichar)character; + +/** + * Add a substitute `substitute` for invisible character `character` + **/ +- (void)addSubstitute:(NSString * _Nonnull)substitute forInvisibleCharacter:(unichar)character; #pragma mark - Configuring Text Appearance /// @name Configuring Text Appearance diff --git a/MGSFragariaView.m b/MGSFragariaView.m index 67d85892..08115f42 100644 --- a/MGSFragariaView.m +++ b/MGSFragariaView.m @@ -794,6 +794,20 @@ - (NSColor *)textInvisibleCharactersColour return self.textView.textInvisibleCharactersColour; } +- (void)clearInvisibleCharacterSubstitutes +{ + [self.textView clearInvisibleCharacterSubstitutes]; +} + +- (void)removeSubstituteForInvisibleCharacter:(unichar)character +{ + [self.textView removeSubstituteForInvisibleCharacter:character]; +} + +- (void)addSubstitute:(NSString * _Nonnull)substitute forInvisibleCharacter:(unichar)character +{ + [self.textView addSubstitute:substitute forInvisibleCharacter:character]; +} #pragma mark - Configuring Text Appearance diff --git a/SMLLayoutManager.h b/SMLLayoutManager.h index 6fb01c56..21b6a1d4 100644 --- a/SMLLayoutManager.h +++ b/SMLLayoutManager.h @@ -57,5 +57,19 @@ enum { **/ @property BOOL showsInvisibleCharacters; +/** + * Clears the current substitutes for invisible characters + **/ +- (void)clearInvisibleCharacterSubstitutes; + +/** + * Remove the substitute for invisible character `character` + **/ +- (void)removeSubstituteForInvisibleCharacter:(unichar)character; + +/** + * Add a substitute `substitute` for invisible character `character` + **/ +- (void)addSubstitute:(NSString*)substitute forInvisibleCharacter:(unichar)character; @end diff --git a/SMLLayoutManager.m b/SMLLayoutManager.m index 0199f0a6..22c72c2d 100644 --- a/SMLLayoutManager.m +++ b/SMLLayoutManager.m @@ -29,13 +29,6 @@ #endif -typedef enum : NSUInteger { - kTabLine = 0, - kSpaceLine = 1, - kNewLineLine = 2 -} MGSLineCacheIndex; - - #define kSMLSquiggleAmplitude (3.0) #define kSMLSquigglePeriod (6.0) #define kSMLSquigglePhase (-1.5) @@ -53,7 +46,8 @@ static CGFloat SquiggleFunction(CGFloat x) { @implementation SMLLayoutManager { - NSMutableArray *lineRefs; + NSMutableDictionary *invisibleCharacterSubstitutes; + NSMutableDictionary *lineRefCacheCharactersSubstitute; } @@ -71,7 +65,14 @@ - (id)init if (self) { _invisibleCharactersFont = [NSFont fontWithName:@"Menlo" size:11]; _invisibleCharactersColour = [NSColor orangeColor]; - + + // Default invisible character substitutes + invisibleCharacterSubstitutes = [NSMutableDictionary dictionary]; + invisibleCharacterSubstitutes[@('\t')] = @"\u21E2"; + invisibleCharacterSubstitutes[@('\r')] = @"\u00B6"; + invisibleCharacterSubstitutes[@('\n')] = @"\u00B6"; + invisibleCharacterSubstitutes[@(' ')] = @"\u22C5"; + [self resetAttributesAndGlyphs]; [self setAllowsNonContiguousLayout:YES]; // Setting this to YES sometimes causes "an extra toolbar" and other graphical glitches to sometimes appear in the text view when one sets a temporary attribute, reported as ID #5832329 to Apple @@ -108,18 +109,11 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)containerOr // we may not have any glyphs generated at this stage for (NSInteger idx = glyphRange.location; idx < lengthToRedraw; idx++) { unichar characterToCheck = [completeString characterAtIndex:idx]; - NSUInteger lineRefIndex = 0; - - if (characterToCheck == '\t') { - lineRefIndex = kTabLine; - } else if (characterToCheck == ' ') { - lineRefIndex = kSpaceLine; - } else if (characterToCheck == '\n' || characterToCheck == '\r') { - lineRefIndex = kNewLineLine; - } else { - continue; - } - + + CTLineRef line = (__bridge CTLineRef)lineRefCacheCharactersSubstitute[@(characterToCheck)]; + if (line == nil) + continue; + // http://lists.apple.com/archives/cocoa-dev/2012/Sep/msg00531.html // // Draw profiling indicated that the CoreText approach on 10.8 is an order of magnitude @@ -127,10 +121,7 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)containerOr pointToDrawAt = [self locationForGlyphAtIndex:idx]; glyphFragment = [self lineFragmentRectForGlyphAtIndex:idx effectiveRange:NULL]; - - // get our text line object - CTLineRef line = (__bridge CTLineRef)[lineRefs objectAtIndex:lineRefIndex]; - + pointToDrawAt.x += glyphFragment.origin.x; /* Some control glyphs have zero size (newlines), and if they are @@ -283,48 +274,65 @@ - (void)drawUnderlineForGlyphRange:(NSRange)glyphRange [bp stroke]; } +#pragma mark - Invisible character -#pragma mark - Class extension +/** + * -clearInvisibleCharacterSubstitutes + **/ +- (void)clearInvisibleCharacterSubstitutes +{ + [invisibleCharacterSubstitutes removeAllObjects]; + [self resetAttributesAndGlyphs]; + [[self firstTextView] setNeedsDisplay:YES]; +} + +/** + * -removeSubstituteForInvisibleCharacter: + **/ +- (void)removeSubstituteForInvisibleCharacter:(unichar)character +{ + [invisibleCharacterSubstitutes removeObjectForKey:@(character)]; + [self resetAttributesAndGlyphs]; + [[self firstTextView] setNeedsDisplay:YES]; +} +/** + * -addSubstitute:forInvisibleCharacter: + **/ +- (void)addSubstitute:(NSString*)substitute forInvisibleCharacter:(unichar)character +{ + invisibleCharacterSubstitutes[@(character)] = substitute; + [self resetAttributesAndGlyphs]; + [[self firstTextView] setNeedsDisplay:YES]; +} + +#pragma mark - Class extension /* - * - resetAttributesAndGlyphs + * - _addSubstitute:forCharacter: */ -- (void)resetAttributesAndGlyphs +- (void)_addLineRefSubstitute:(NSString*)substitute forCharacter:(unichar)character { NSDictionary *defAttributes; - NSString *tabCharacter; - NSString *newLineCharacter; - NSString *spaceCharacter; - // assemble our default attributes defAttributes = @{NSFontAttributeName: self.invisibleCharactersFont, NSForegroundColorAttributeName: self.invisibleCharactersColour}; - // define substitute characters for whitespace chars - tabCharacter = @"\u21E2"; - newLineCharacter = @"\u00B6"; - spaceCharacter = @"\u22C5"; - - // all CFTypes can be added to NS collections - // http://www.mikeash.com/pyblog/friday-qa-2010-01-22-toll-free-bridging-internals.html - lineRefs = [NSMutableArray arrayWithCapacity:kNewLineLine+1]; - - NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:tabCharacter attributes:defAttributes]; + NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:substitute attributes:defAttributes]; CTLineRef textLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attrString); - [lineRefs addObject:(__bridge id)textLine]; // kTabLine - CFRelease(textLine); - - attrString = [[NSAttributedString alloc] initWithString:spaceCharacter attributes:defAttributes]; - textLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attrString); - [lineRefs addObject:(__bridge id)textLine]; // kSpaceLine - CFRelease(textLine); - - attrString = [[NSAttributedString alloc] initWithString:newLineCharacter attributes:defAttributes]; - textLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attrString); - [lineRefs addObject:(__bridge id)textLine]; // kNewLineLine + lineRefCacheCharactersSubstitute[@(character)] = (__bridge id)textLine; CFRelease(textLine); } +/* + * - resetAttributesAndGlyphs + */ +- (void)resetAttributesAndGlyphs +{ + lineRefCacheCharactersSubstitute = [NSMutableDictionary dictionary]; + [invisibleCharacterSubstitutes enumerateKeysAndObjectsUsingBlock:^(NSNumber* key, NSString* obj, BOOL* stop) { + [self _addLineRefSubstitute:obj forCharacter:key.unsignedShortValue]; + }]; +} @end diff --git a/SMLTextView.h b/SMLTextView.h index 70af0b16..d2859044 100644 --- a/SMLTextView.h +++ b/SMLTextView.h @@ -209,6 +209,20 @@ /** Specifies the colour to render invisible characters in the text view.*/ @property (nonatomic, assign) NSColor *textInvisibleCharactersColour; +/** + * Clears the current substitutes for invisible characters + **/ +- (void)clearInvisibleCharacterSubstitutes; + +/** + * Remove the substitute for invisible character `character` + **/ +- (void)removeSubstituteForInvisibleCharacter:(unichar)character; + +/** + * Add a substitute `substitute` for invisible character `character` + **/ +- (void)addSubstitute:(NSString *)substitute forInvisibleCharacter:(unichar)character; #pragma mark - Configuring Text Appearance /// @name Configuring Text Appearance diff --git a/SMLTextView.m b/SMLTextView.m index 94c7c020..4beb49a8 100644 --- a/SMLTextView.m +++ b/SMLTextView.m @@ -128,6 +128,20 @@ - (BOOL)showsInvisibleCharacters return self.layoutManager.showsInvisibleCharacters; } +- (void)clearInvisibleCharacterSubstitutes +{ + [self.layoutManager clearInvisibleCharacterSubstitutes]; +} + +- (void)removeSubstituteForInvisibleCharacter:(unichar)character +{ + [self.layoutManager removeSubstituteForInvisibleCharacter:character]; +} + +- (void)addSubstitute:(NSString * _Nonnull)substitute forInvisibleCharacter:(unichar)character +{ + [self.layoutManager addSubstitute:substitute forInvisibleCharacter:character]; +} /* * @property lineSpacing