views:

952

answers:

5

I am writing a text editor for Mac OS X. I need to display hidden characters in an NSTextView (such as spaces, tabs, and special characters). I have spent a lot of time searching for how to do this but so far I have not found an answer. If anyone could point me in the right direction I would be grateful.

+2  A: 

Have a look at the NSLayoutManager class. Your NSTextView will have a layout manager associated with it, and the layout manager is responsible for associating a character (space, tab, etc.) with a glyph (the image of that character drawn on the screen).

In your case, you would probably be most interested in the replaceGlyphAtIndex:withGlyph: method, which would allow you to replace individual glyphs.

e.James
A: 

What I have done is override the method below in an NSLayoutManager subclass.

- (void)drawGlyphsForGlyphRange:(NSRange)range atPoint:(NSPoint)origin
{
    [super drawGlyphsForGlyphRange:range atPoint:origin];

    for (int i = range.location; i != range.location + range.length; i++)
    {
     // test each character in this range
     // if appropriate replace it with -replaceGlyphAtIndex:withGlyph:
    }
}

I loop through the index of each character. The problem I now have is how to determine what character is found at each location. Should I use an NSLayoutManager method or ask the NSTextView itself? Are indices into the former the same as in the latter?

I can get an individual glyph with -glyphAtIndex: but I cannot figure out how to determine what character it corresponds to.

titaniumdecoy
A: 

I solved the problem of converting between NSGlyphs and the corresponding unichar in the NSTextView. The code below works beautifully and replaces spaces with bullets for visible text:

- (void)drawGlyphsForGlyphRange:(NSRange)range atPoint:(NSPoint)origin
{
    NSFont *font = [[CURRENT_TEXT_VIEW typingAttributes]
                       objectForKey:NSFontAttributeName];

    NSGlyph bullet = [font glyphWithName:@"bullet"];

    for (int i = range.location; i != range.location + range.length; i++)
    {
     unsigned charIndex = [self characterIndexForGlyphAtIndex:i];

     unichar c =[[[self textStorage] string] characterAtIndex:charIndex];

     if (c == ' ')
      [self replaceGlyphAtIndex:charIndex withGlyph:bullet];
    }

    [super drawGlyphsForGlyphRange:range atPoint:origin];
}
titaniumdecoy
+1  A: 

Perhaps -[NSLayoutManager setShowsControlCharacters:] and/or -[NSLayoutManager setShowsInvisibleCharacters:] will do what you want.

Ned Holbrook
+2  A: 

I wrote a text editor a few years back - here's some meaningless code that should get you looking in (hopefully) the right direction (this is an NSLayoutManager subclass btw - and yes I know it's leaking like the proverbial kitchen sink):

- (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)containerOrigin
{
    if ([[[[MJDocumentController sharedDocumentController] currentDocument] editor] showInvisibles])
    {
     //init glyphs
     unichar crlf = 0x00B6; 
     NSString *CRLF = [[NSString alloc] initWithCharacters:&crlf length:1];
     unichar space = 0x00B7;
     NSString *SPACE = [[NSString alloc] initWithCharacters:&space length:1];
     unichar tab = 0x2192; 
     NSString *TAB = [[NSString alloc] initWithCharacters:&tab length:1];

     NSString *docContents = [[self textStorage] string];
     NSString *glyph;
     NSPoint glyphPoint;
     NSRect glyphRect;
     NSDictionary *attr = [[NSDictionary alloc] initWithObjectsAndKeys:[NSUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:@"invisiblesColor"]], NSForegroundColorAttributeName, nil];

     //loop thru current range, drawing glyphs
     int i;
     for (i = glyphRange.location; i < NSMaxRange(glyphRange); i++)
     {
      glyph = @"";

      //look for special chars
      switch ([docContents characterAtIndex:i])
      {
       //space
       case ' ':
        glyph = SPACE;
        break;

       //tab
       case '\t':
        glyph = TAB;
        break;

       //eol
       case 0x2028:
       case 0x2029:
       case '\n':
       case '\r':
        glyph = CRLF;
        break;

       //do nothing
       default:
        glyph = @"";
        break;     
      }

      //should we draw?
      if ([glyph length])
      {
       glyphPoint = [self locationForGlyphAtIndex:i];
       glyphRect = [self lineFragmentRectForGlyphAtIndex:i effectiveRange:NULL];
       glyphPoint.x += glyphRect.origin.x;
       glyphPoint.y = glyphRect.origin.y;
       [glyph drawAtPoint:glyphPoint withAttributes:attr];
      }
     }
    }

    [super drawGlyphsForGlyphRange:glyphRange atPoint:containerOrigin];
}