Skip to content

Commit

Permalink
Add support to customize the invisible character substitutions
Browse files Browse the repository at this point in the history
  • Loading branch information
jasminlapalme committed Sep 21, 2017
1 parent 0fcd416 commit f4793e9
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 53 deletions.
14 changes: 14 additions & 0 deletions MGSFragariaView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions MGSFragariaView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions SMLLayoutManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
114 changes: 61 additions & 53 deletions SMLLayoutManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -53,7 +46,8 @@ static CGFloat SquiggleFunction(CGFloat x) {


@implementation SMLLayoutManager {
NSMutableArray *lineRefs;
NSMutableDictionary *invisibleCharacterSubstitutes;
NSMutableDictionary *lineRefCacheCharactersSubstitute;
}


Expand All @@ -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
Expand Down Expand Up @@ -108,29 +109,19 @@ - (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
// faster that using the NSStringDrawing methods.

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
Expand Down Expand Up @@ -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
14 changes: 14 additions & 0 deletions SMLTextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions SMLTextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit f4793e9

Please sign in to comment.