- MMTextStorage ensures that all glyphs have the same width (or twice that, for
[MacVim/jjgod.git] / MMTextStorage.m
blob4b174fc4b468ecc56f42a2ede4535ca3012baffd
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11 #import "MMTextStorage.h"
12 #import "MMAppController.h"
14 // If 0 DRAW_TRANSP flag will be ignored.  Setting it to 1 causes the cursor
15 // background to be drawn in white.
16 #define HEED_DRAW_TRANSP 0
18 #define DRAW_TRANSP               0x01    /* draw with transparant bg */
19 #define DRAW_BOLD                 0x02    /* draw bold text */
20 #define DRAW_UNDERL               0x04    /* draw underline text */
21 #define DRAW_UNDERC               0x08    /* draw undercurl text */
22 #define DRAW_ITALIC               0x10    /* draw italic text */
27 @interface MMTextStorage (Private)
28 - (void)lazyResize;
29 @end
33 @implementation MMTextStorage
35 - (id)init
37     if ((self = [super init])) {
38         attribString = [[NSMutableAttributedString alloc] initWithString:@""];
39         // NOTE!  It does not matter which font is set here, Vim will set its
40         // own font on startup anyway.  Just set some bogus values.
41         font = [[NSFont userFixedPitchFontOfSize:0] retain];
42         cellSize.height = [font pointSize];
43         cellSize.width = [font defaultLineHeightForFont];
44     }
46     return self;
49 - (void)dealloc
51     //NSLog(@"%@ %s", [self className], _cmd);
53     [emptyRowString release];
54     [font release];
55     [defaultBackgroundColor release];
56     [attribString release];
57     [super dealloc];
60 - (NSString *)string
62     //NSLog(@"%s : attribString=%@", _cmd, attribString);
63     return [attribString string];
66 - (NSDictionary *)attributesAtIndex:(unsigned)index
67                      effectiveRange:(NSRangePointer)range
69     //NSLog(@"%s", _cmd);
70     if (index>=[attribString length]) {
71         //NSLog(@"%sWARNING: index (%d) out of bounds", _cmd, index);
72         if (range) {
73             *range = NSMakeRange(NSNotFound, 0);
74         }
75         return [NSDictionary dictionary];
76     }
78     return [attribString attributesAtIndex:index effectiveRange:range];
81 - (void)replaceCharactersInRange:(NSRange)range
82                       withString:(NSString *)string
84     //NSLog(@"replaceCharactersInRange:(%d,%d) withString:%@", range.location,
85     //        range.length, string);
86     NSLog(@"WARNING: calling %s on MMTextStorage is unsupported", _cmd);
87     //[attribString replaceCharactersInRange:range withString:string];
90 - (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range
92     // NOTE!  This method must be implemented since the text system calls it
93     // constantly to 'fix attributes', apply font substitution, etc.
94 #if 0
95     [attribString setAttributes:attributes range:range];
96 #else
97     // HACK! If the font attribute is being modified, then ensure that the new
98     // font has a fixed advancement which is either the same as the current
99     // font or twice that, depending on whether it is a 'wide' character that
100     // is being fixed or not.  This code really only works if 'range' has
101     // length 1 or 2.
102     NSFont *newFont = [attributes objectForKey:NSFontAttributeName];
103     if (newFont) {
104         float adv = cellSize.width;
105         if ([attribString length] > range.location+1) {
106             // If the first char is followed by zero-width space, then it is a
107             // 'wide' character, so double the advancement.
108             NSString *string = [attribString string];
109             if ([string characterAtIndex:range.location+1] == 0x200b)
110                 adv += adv;
111         }
113         // Create a new font which has the 'fixed advance attribute' set.
114         NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
115             [NSNumber numberWithFloat:adv], NSFontFixedAdvanceAttribute, nil];
116         NSFontDescriptor *desc = [newFont fontDescriptor];
117         desc = [desc fontDescriptorByAddingAttributes:dict];
118         newFont = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
120         // Now modify the 'attributes' dictionary to hold the new font.
121         NSMutableDictionary *newAttr = [NSMutableDictionary
122             dictionaryWithDictionary:attributes];
123         [newAttr setObject:newFont forKey:NSFontAttributeName];
125         [attribString setAttributes:newAttr range:range];
126     } else {
127         [attribString setAttributes:attributes range:range];
128     }
129 #endif
132 - (int)maxRows
134     return maxRows;
137 - (int)maxColumns
139     return maxColumns;
142 - (void)getMaxRows:(int*)rows columns:(int*)cols
144     if (rows) *rows = maxRows;
145     if (cols) *cols = maxColumns;
148 - (void)setMaxRows:(int)rows columns:(int)cols
150     // NOTE: Just remember the new values, the actual resizing is done lazily.
151     maxRows = rows;
152     maxColumns = cols;
155 - (void)replaceString:(NSString*)string atRow:(int)row column:(int)col
156         withFlags:(int)flags foregroundColor:(NSColor*)fg
157         backgroundColor:(NSColor*)bg
159     //NSLog(@"replaceString:atRow:%d column:%d withFlags:%d", row, col, flags);
160     [self lazyResize];
162     if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
163             || col+[string length] > maxColumns) {
164         //NSLog(@"[%s] WARNING : out of range, row=%d (%d) col=%d (%d) "
165         //        "length=%d (%d)", _cmd, row, maxRows, col, maxColumns,
166         //        [string length], [attribString length]);
167         return;
168     }
170     if (!(fg && bg)) {
171         NSLog(@"[%s] WARNING: background or foreground color not specified",
172                 _cmd);
173         return;
174     }
176     NSRange range = NSMakeRange(col+row*(maxColumns+1), [string length]);
177     [attribString replaceCharactersInRange:range withString:string];
179     NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
180             font, NSFontAttributeName,
181 #if !HEED_DRAW_TRANSP
182             bg, NSBackgroundColorAttributeName,
183 #endif
184             fg, NSForegroundColorAttributeName, nil];
185     [attribString setAttributes:attributes range:range];
187 #if HEED_DRAW_TRANSP
188     if ( !(flags & DRAW_TRANSP) ) {
189         [attribString addAttribute:NSBackgroundColorAttributeName value:bg
190                 range:range];
191     }
192 #endif
194     // TODO: cache bold font and apply in setAttributes:range:
195     if (flags & DRAW_BOLD) {
196         [attribString applyFontTraits:NSBoldFontMask range:range];
197     }
199     // TODO: cache italic font and apply in setAttributes:range:
200     if (flags & DRAW_ITALIC) {
201         [attribString applyFontTraits:NSItalicFontMask range:range];
202     }
204     if (flags & DRAW_UNDERL) {
205         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
206                 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
207         [attribString addAttribute:NSUnderlineStyleAttributeName
208                 value:value range:range];
209     }
211     // TODO: figure out how do draw proper undercurls
212     if (flags & DRAW_UNDERC) {
213         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
214                 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
215         [attribString addAttribute:NSUnderlineStyleAttributeName
216                 value:value range:range];
217     }
219     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
220            range:range changeInLength:0];
224  * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
225  * of the scroll region.
226  */
227 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
228               scrollBottom:(int)bottom left:(int)left right:(int)right
229                      color:(NSColor *)color
231     //NSLog(@"deleteLinesFromRow:%d lineCount:%d", row, count);
232     [self lazyResize];
234     if (row < 0 || row+count > maxRows) {
235         //NSLog(@"[%s] WARNING : out of range, row=%d (%d) count=%d", _cmd, row,
236         //        maxRows, count);
237         return;
238     }
240     int total = 1 + bottom - row;
241     int move = total - count;
242     int width = right - left + 1;
243     NSRange destRange = { row*(maxColumns+1) + left, width };
244     NSRange srcRange = { (row+count)*(maxColumns+1) + left, width };
245     int i;
247     for (i = 0; i < move; ++i) {
248         NSAttributedString *srcString = [attribString
249                 attributedSubstringFromRange:srcRange];
250         [attribString replaceCharactersInRange:destRange
251                           withAttributedString:srcString];
252         [self edited:(NSTextStorageEditedCharacters
253                 | NSTextStorageEditedAttributes)
254                 range:destRange changeInLength:0];
255         destRange.location += maxColumns+1;
256         srcRange.location += maxColumns+1;
257     }
259     for (i = 0; i < count; ++i) {
260         NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
261                 font, NSFontAttributeName,
262                 color, NSForegroundColorAttributeName,
263                 color, NSBackgroundColorAttributeName, nil];
264         [attribString setAttributes:attribs range:destRange];
265         [self edited:NSTextStorageEditedAttributes range:destRange
266                 changeInLength:0];
267         destRange.location += maxColumns+1;
268     }
272  * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
273  * of the scroll region.
274  */
275 - (void)insertLinesAtRow:(int)row lineCount:(int)count
276             scrollBottom:(int)bottom left:(int)left right:(int)right
277                    color:(NSColor *)color
279     //NSLog(@"insertLinesAtRow:%d lineCount:%d", row, count);
280     [self lazyResize];
282     if (row < 0 || row+count > maxRows) {
283         //NSLog(@"[%s] WARNING : out of range, row=%d (%d) count=%d", _cmd, row,
284         //        maxRows, count);
285         return;
286     }
288     int total = 1 + bottom - row;
289     int move = total - count;
290     int width = right - left + 1;
291     NSRange destRange = { bottom*(maxColumns+1) + left, width };
292     NSRange srcRange = { (row+move-1)*(maxColumns+1) + left, width };
293     int i;
295     for (i = 0; i < move; ++i) {
296         NSAttributedString *srcString = [attribString
297                 attributedSubstringFromRange:srcRange];
298         [attribString replaceCharactersInRange:destRange
299                           withAttributedString:srcString];
300         [self edited:(NSTextStorageEditedCharacters
301                 | NSTextStorageEditedAttributes)
302                 range:destRange changeInLength:0];
303         destRange.location -= maxColumns+1;
304         srcRange.location -= maxColumns+1;
305     }
307     for (i = 0; i < count; ++i) {
308         NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
309                 font, NSFontAttributeName,
310                 color, NSForegroundColorAttributeName,
311                 color, NSBackgroundColorAttributeName, nil];
312         [attribString setAttributes:attribs range:destRange];
313         [self edited:NSTextStorageEditedAttributes range:destRange
314                 changeInLength:0];
315         destRange.location -= maxColumns+1;
316     }
319 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
320                    column:(int)col2 color:(NSColor *)color
322     //NSLog(@"clearBlockFromRow:%d column:%d toRow:%d column:%d", row1, col1,
323     //        row2, col2);
324     [self lazyResize];
326     if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns) {
327         //NSLog(@"[%s] WARNING : out of range, row1=%d row2=%d (%d) col1=%d "
328         //        "col2=%d (%d)", _cmd, row1, row2, maxRows, col1, col2,
329         //        maxColumns);
330         return;
331     }
333     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
334             font, NSFontAttributeName,
335             color, NSForegroundColorAttributeName,
336             color, NSBackgroundColorAttributeName, nil];
338     NSRange range = { row1*(maxColumns+1) + col1, col2-col1+1 };
340     int r;
341     for (r=row1; r<=row2; ++r) {
342         [attribString setAttributes:attribs range:range];
343         [self edited:NSTextStorageEditedAttributes range:range
344                 changeInLength:0];
345         range.location += maxColumns+1;
346     }
349 - (void)clearAllWithColor:(NSColor *)color
351     //NSLog(@"%s%@", _cmd, color);
353     NSRange range = { 0, [attribString length] };
354     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
355             font, NSFontAttributeName,
356             color, NSForegroundColorAttributeName,
357             color, NSBackgroundColorAttributeName, nil];
358     [attribString setAttributes:attribs range:range];
359     [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
362 - (void)setDefaultColorsBackground:(NSColor *)bgColor
363                         foreground:(NSColor *)fgColor
365     // NOTE: Foreground color is ignored.
366     [defaultBackgroundColor release];
367     defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
370 - (void)setFont:(NSFont*)newFont
372     if (newFont && font != newFont) {
373         [font release];
375         // NOTE! When setting a new font we make sure that the advancement of
376         // each glyph is fixed. 
378         float em = [newFont widthOfString:@"m"];
379         float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
380                 floatForKey:MMCellWidthMultiplierKey];
382         cellSize.width = em * cellWidthMultiplier;
384         NSDictionary *dict = [NSDictionary
385             dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
386                           forKey:NSFontFixedAdvanceAttribute];
387         NSFontDescriptor *desc = [newFont fontDescriptor];
388         desc = [desc fontDescriptorByAddingAttributes:dict];
390         font = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
391         [font retain];
393         NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
394         cellSize.height = lm ? [lm defaultLineHeightForFont:font]
395                              : [font defaultLineHeightForFont];
396     }
399 - (NSFont*)font
401     return font;
404 - (NSSize)size
406     return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
409 - (NSSize)cellSize
411     return cellSize;
414 - (NSRect)rectForRowsInRange:(NSRange)range
416     NSRect rect = { 0, 0, 0, 0 };
417     unsigned start = range.location > maxRows ? maxRows : range.location;
418     unsigned length = range.length;
420     if (start+length > maxRows)
421         length = maxRows - start;
423     rect.origin.y = cellSize.height * start;
424     rect.size.height = cellSize.height * length;
426     return rect;
429 - (NSRect)rectForColumnsInRange:(NSRange)range
431     NSRect rect = { 0, 0, 0, 0 };
432     unsigned start = range.location > maxColumns ? maxColumns : range.location;
433     unsigned length = range.length;
435     if (start+length > maxColumns)
436         length = maxColumns - start;
438     rect.origin.x = cellSize.width * start;
439     rect.size.width = cellSize.width * length;
441     return rect;
444 - (unsigned)offsetFromRow:(int)row column:(int)col
446     // Ensure the offset returned is valid.
447     // This code also works if maxRows and/or maxColumns is 0.
448     if (row >= maxRows) row = maxRows-1;
449     if (row < 0) row = 0;
450     if (col >= maxColumns) col = maxColumns-1;
451     if (col < 0) col = 0;
453     return (unsigned)(col + row*(maxColumns+1));
456 - (BOOL)resizeToFitSize:(NSSize)size
458     int rows = maxRows, cols = maxColumns;
460     [self fitToSize:size rows:&rows columns:&cols];
461     if (rows != maxRows || cols != maxColumns) {
462         [self setMaxRows:rows columns:cols];
463         return YES;
464     }
466     // Return NO only if dimensions did not change.
467     return NO;
470 - (NSSize)fitToSize:(NSSize)size
472     return [self fitToSize:size rows:NULL columns:NULL];
475 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
477     NSSize curSize = [self size];
478     NSSize fitSize = curSize;
479     int fitRows = maxRows;
480     int fitCols = maxColumns;
482     if (size.height < curSize.height) {
483         // Remove lines until the height of the text storage fits inside
484         // 'size'.  However, always make sure there are at least 3 lines in the
485         // text storage.  (Why 3? It seem Vim never allows less than 3 lines.)
486         //
487         // TODO: No need to search since line height is fixed, just calculate
488         // the new height.
489         int rowCount = maxRows;
490         int rowsToRemove;
491         for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
492             float height = cellSize.height*rowCount;
494             if (height <= size.height) {
495                 fitSize.height = height;
496                 break;
497             }
499             --rowCount;
500         }
502         fitRows -= rowsToRemove;
503     } else if (size.height > curSize.height) {
504         float fh = cellSize.height;
505         if (fh < 1.0f) fh = 1.0f;
507         fitRows = floor(size.height/fh);
508         fitSize.height = fh*fitRows;
509     }
511     if (size.width != curSize.width) {
512         float fw = cellSize.width;
513         if (fw < 1.0f) fw = 1.0f;
515         fitCols = floor(size.width/fw);
516         fitSize.width = fw*fitCols;
517     }
519     if (rows) *rows = fitRows;
520     if (columns) *columns = fitCols;
522     return fitSize;
525 @end // MMTextStorage
530 @implementation MMTextStorage (Private)
531 - (void)lazyResize
533     int i;
535     // Do nothing if the dimensions are already right.
536     if (actualRows == maxRows && actualColumns == maxColumns)
537         return;
539     NSRange oldRange = NSMakeRange(0, actualRows*(actualColumns+1));
541     actualRows = maxRows;
542     actualColumns = maxColumns;
544     NSDictionary *dict;
545     if (defaultBackgroundColor) {
546         dict = [NSDictionary dictionaryWithObjectsAndKeys:
547                 font, NSFontAttributeName,
548                 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
549     } else {
550         dict = [NSDictionary dictionaryWithObjectsAndKeys:
551                 font, NSFontAttributeName, nil];
552     }
553             
554     NSMutableString *rowString = [NSMutableString string];
555     for (i = 0; i < maxColumns; ++i) {
556         [rowString appendString:@" "];
557     }
558     [rowString appendString:@"\n"];
560     [emptyRowString release];
561     emptyRowString = [[NSAttributedString alloc] initWithString:rowString
562                                                      attributes:dict];
564     [attribString release];
565     attribString = [[NSMutableAttributedString alloc] init];
566     for (i=0; i<maxRows; ++i) {
567         [attribString appendAttributedString:emptyRowString];
568     }
570     NSRange fullRange = NSMakeRange(0, [attribString length]);
571     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
572            range:oldRange changeInLength:fullRange.length-oldRange.length];
575 @end // MMTextStorage (Private)