Avoid window flashing white when zooming
[MacVim/KaoriYa.git] / src / MacVim / MMVimView.m
blobfd477f3be8615199b85f505460889798a3461555
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  * MMVimView
12  *
13  * A view class with a tabline, scrollbars, and a text view.  The tabline may
14  * appear at the top of the view in which case it fills up the view from left
15  * to right edge.  Any number of scrollbars may appear adjacent to all other
16  * edges of the view (there may be more than one scrollbar per edge and
17  * scrollbars may also be placed on the left edge of the view).  The rest of
18  * the view is filled by the text view.
19  */
21 #import "Miscellaneous.h"   // Defines MM_ENABLE_ATSUI
23 #if MM_ENABLE_ATSUI
24 # import "MMAtsuiTextView.h"
25 #else
26 # import "MMCoreTextView.h"
27 #endif
28 #import "MMTextView.h"
29 #import "MMVimController.h"
30 #import "MMVimView.h"
31 #import "MMWindowController.h"
32 #import <PSMTabBarControl/PSMTabBarControl.h>
36 // Scroller type; these must match SBAR_* in gui.h
37 enum {
38     MMScrollerTypeLeft = 0,
39     MMScrollerTypeRight,
40     MMScrollerTypeBottom
44 // TODO:  Move!
45 @interface MMScroller : NSScroller {
46     int32_t identifier;
47     int type;
48     NSRange range;
50 - (id)initWithIdentifier:(int32_t)ident type:(int)type;
51 - (int32_t)scrollerId;
52 - (int)type;
53 - (NSRange)range;
54 - (void)setRange:(NSRange)newRange;
55 @end
58 @interface MMVimView (Private)
59 - (BOOL)bottomScrollbarVisible;
60 - (BOOL)leftScrollbarVisible;
61 - (BOOL)rightScrollbarVisible;
62 - (void)placeScrollbars;
63 - (NSUInteger)representedIndexOfTabViewItem:(NSTabViewItem *)tvi;
64 - (MMScroller *)scrollbarForIdentifier:(int32_t)ident index:(unsigned *)idx;
65 - (NSSize)vimViewSizeForTextViewSize:(NSSize)textViewSize;
66 - (NSRect)textViewRectForVimViewSize:(NSSize)contentSize;
67 - (NSTabView *)tabView;
68 - (void)frameSizeMayHaveChanged;
69 @end
72 // This is an informal protocol implemented by MMWindowController (maybe it
73 // shold be a formal protocol, but ...).
74 @interface NSWindowController (MMVimViewDelegate)
75 - (void)liveResizeWillStart;
76 - (void)liveResizeDidEnd;
77 @end
81 @implementation MMVimView
83 - (MMVimView *)initWithFrame:(NSRect)frame
84                vimController:(MMVimController *)controller
86     if (![super initWithFrame:frame])
87         return nil;
88     
89     vimController = controller;
90     scrollbars = [[NSMutableArray alloc] init];
92     // Only the tabline is autoresized, all other subview placement is done in
93     // frameSizeMayHaveChanged.
94     [self setAutoresizesSubviews:YES];
96     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
97     NSInteger renderer = [ud integerForKey:MMRendererKey];
98     ASLogInfo(@"Use renderer=%d", renderer);
100 #if MM_ENABLE_ATSUI
101     if (MMRendererATSUI == renderer) {
102         // HACK! 'textView' has type MMTextView, but MMAtsuiTextView is not
103         // derived from MMTextView.
104         textView = [[MMAtsuiTextView alloc] initWithFrame:frame];
105     }
106 #else
107     if (MMRendererCoreText == renderer) {
108         // HACK! 'textView' has type MMTextView, but MMCoreTextView is not
109         // derived from MMTextView.
110         textView = [[MMCoreTextView alloc] initWithFrame:frame];
111     }
112 #endif
113     else {
114         // Use Cocoa text system for text rendering.
115         textView = [[MMTextView alloc] initWithFrame:frame];
116     }
118     // Allow control of text view inset via MMTextInset* user defaults.
119     int left = [ud integerForKey:MMTextInsetLeftKey];
120     int top = [ud integerForKey:MMTextInsetTopKey];
121     [textView setTextContainerInset:NSMakeSize(left, top)];
123     [textView setAutoresizingMask:NSViewNotSizable];
124     [self addSubview:textView];
125     
126     // Create the tab view (which is never visible, but the tab bar control
127     // needs it to function).
128     tabView = [[NSTabView alloc] initWithFrame:NSZeroRect];
130     // Create the tab bar control (which is responsible for actually
131     // drawing the tabline and tabs).
132     NSRect tabFrame = { { 0, frame.size.height - 22 },
133                         { frame.size.width, 22 } };
134     tabBarControl = [[PSMTabBarControl alloc] initWithFrame:tabFrame];
136     [tabView setDelegate:tabBarControl];
138     [tabBarControl setTabView:tabView];
139     [tabBarControl setDelegate:self];
140     [tabBarControl setHidden:YES];
142     [tabBarControl setCellMinWidth:[ud integerForKey:MMTabMinWidthKey]];
143     [tabBarControl setCellMaxWidth:[ud integerForKey:MMTabMaxWidthKey]];
144     [tabBarControl setCellOptimumWidth:
145                                      [ud integerForKey:MMTabOptimumWidthKey]];
147     [tabBarControl setShowAddTabButton:[ud boolForKey:MMShowAddTabButtonKey]];
148     [[tabBarControl addTabButton] setTarget:self];
149     [[tabBarControl addTabButton] setAction:@selector(addNewTab:)];
150     [tabBarControl setAllowsDragBetweenWindows:NO];
151     [tabBarControl registerForDraggedTypes:
152                             [NSArray arrayWithObject:NSFilenamesPboardType]];
154     [tabBarControl setAutoresizingMask:NSViewWidthSizable|NSViewMinYMargin];
155     
156     //[tabBarControl setPartnerView:textView];
157     
158     // tab bar resizing only works if awakeFromNib is called (that's where
159     // the NSViewFrameDidChangeNotification callback is installed). Sounds like
160     // a PSMTabBarControl bug, let's live with it for now.
161     [tabBarControl awakeFromNib];
163     [self addSubview:tabBarControl];
165     return self;
168 - (void)dealloc
170     ASLogDebug(@"");
172     [tabBarControl release];  tabBarControl = nil;
173     [tabView release];  tabView = nil;
174     [scrollbars release];  scrollbars = nil;
176     // HACK! The text storage is the principal owner of the text system, but we
177     // keep only a reference to the text view, so release the text storage
178     // first (unless we are using the ATSUI renderer).
179     if ([textView isKindOfClass:[MMTextView class]])
180         [[textView textStorage] release];
182     [textView release];  textView = nil;
184     [super dealloc];
187 - (BOOL)isOpaque
189     return YES;
192 - (void)drawRect:(NSRect)rect
194     if (isDirty) {
195         // Clear the entire view
196         [[textView defaultBackgroundColor] set];
197         NSRectFill([self bounds]);
198         isDirty = NO;
199     }
201     NSRect textViewFrame = [textView frame];
202     if (!NSEqualRects(lastTextViewFrame, textViewFrame)) {
203         // If the text view's frame changes we copy the contents of the old
204         // frame to the origin of the new frame.  The reason for this is that
205         // Vim expects the contents of its view not to change unless Vim
206         // changes it.  (Omitting this code causes the view contents to get
207         // messed up e.g. when the left scrollbar is shown.)
208         NSPoint pt = textViewFrame.origin;
209         CGFloat d = textViewFrame.size.height - lastTextViewFrame.size.height;
210         NSRect r = lastTextViewFrame;
211         if (d >= 0) {
212             // Thew view became larger.  Copy the old view to the top of the
213             // new view.
214             pt.y += d;
215             NSCopyBits(0, r, pt);
217             // Clear the part of the view that has been exposed.
218             r = textViewFrame;
219             r.size.height = d;
220             [[textView defaultBackgroundColor] set];
221             NSRectFill(r);
223             r = textViewFrame;
224             r.size.width -= lastTextViewFrame.size.width;
225             if (r.size.width > 0) {
226                 // The width of the view has grown to the right (i.e. user
227                 // clicked maximize button whilst holding Cmd).
228                 r.origin.x += lastTextViewFrame.size.width;
229                 NSRectFill(r);
230             }
231         } else {
232             // The view became smaller.
233             // TODO: Should copy the top of the old view into the new view, but
234             // this does not work since the view has already been resized and
235             // the top of the old view is lost.  Could perhaps work to cache
236             // the view to an offscreen surface before it resizes and then draw
237             // from that?
238             // As a temporary hack we just clear the view instead.
239             NSRectFill(textViewFrame);
240             // r.origin.y -= d;
241             // r.size.height = textViewFrame.size.height;
242         }
244         lastTextViewFrame = textViewFrame;
245     }
247     // On Leopard, we want to have a textured window background for nice
248     // looking tabs. However, the textured window background looks really
249     // weird behind the window resize throbber, so emulate the look of an
250     // NSScrollView in the bottom right corner.
251     if (![[self window] showsResizeIndicator]  // XXX: make this a flag
252             || !([[self window] styleMask] & NSTexturedBackgroundWindowMask))
253         return;
255     int sw = [NSScroller scrollerWidth];
257     // add .5 to the pixel locations to put the lines on a pixel boundary.
258     // the top and right edges of the rect will be outside of the bounds rect
259     // and clipped away.
260     NSRect sizerRect = NSMakeRect([self bounds].size.width - sw + .5, -.5,
261             sw, sw);
262     //NSBezierPath* path = [NSBezierPath bezierPath];
263     NSBezierPath* path = [NSBezierPath bezierPathWithRect:sizerRect];
265     // On Tiger, we have color #E8E8E8 behind the resize throbber
266     // (which is windowBackgroundColor on untextured windows or controlColor in
267     // general). Terminal.app on Leopard has #FFFFFF background and #D9D9D9 as
268     // stroke. The colors below are #FFFFFF and #D4D4D4, which is close enough
269     // for me.
270     [[NSColor controlBackgroundColor] set];
271     [path fill];
273     [[NSColor secondarySelectedControlColor] set];
274     [path stroke];
276     if ([self leftScrollbarVisible]) {
277         // If the left scrollbar is visible there is an empty square under it.
278         // Fill it in just like on the right hand corner.  The half pixel
279         // offset ensures the outline goes on the top and right side of the
280         // square; the left and bottom parts of the outline are clipped.
281         sizerRect = NSMakeRect(-.5,-.5,sw,sw);
282         path = [NSBezierPath bezierPathWithRect:sizerRect];
283         [[NSColor controlBackgroundColor] set];
284         [path fill];
285         [[NSColor secondarySelectedControlColor] set];
286         [path stroke];
287     }
290 - (MMTextView *)textView
292     return textView;
295 - (PSMTabBarControl *)tabBarControl
297     return tabBarControl;
300 - (void)cleanup
302     vimController = nil;
303     
304     // NOTE! There is a bug in PSMTabBarControl in that it retains the delegate
305     // so reset the delegate here, otherwise the delegate may never get
306     // released.
307     [tabView setDelegate:nil];
308     [tabBarControl setDelegate:nil];
309     [tabBarControl setTabView:nil];
310     [[self window] setDelegate:nil];
312     // NOTE! There is another bug in PSMTabBarControl where the control is not
313     // removed as an observer, so remove it here (failing to remove an observer
314     // may lead to very strange bugs).
315     [[NSNotificationCenter defaultCenter] removeObserver:tabBarControl];
317     [tabBarControl removeFromSuperviewWithoutNeedingDisplay];
318     [textView removeFromSuperviewWithoutNeedingDisplay];
320     unsigned i, count = [scrollbars count];
321     for (i = 0; i < count; ++i) {
322         MMScroller *sb = [scrollbars objectAtIndex:i];
323         [sb removeFromSuperviewWithoutNeedingDisplay];
324     }
326     [tabView removeAllTabViewItems];
329 - (NSSize)desiredSize
331     return [self vimViewSizeForTextViewSize:[textView desiredSize]];
334 - (NSSize)minSize
336     return [self vimViewSizeForTextViewSize:[textView minSize]];
339 - (NSSize)constrainRows:(int *)r columns:(int *)c toSize:(NSSize)size
341     NSSize textViewSize = [self textViewRectForVimViewSize:size].size;
342     textViewSize = [textView constrainRows:r columns:c toSize:textViewSize];
343     return [self vimViewSizeForTextViewSize:textViewSize];
346 - (void)setDesiredRows:(int)r columns:(int)c
348     [textView setMaxRows:r columns:c];
351 - (IBAction)addNewTab:(id)sender
353     [vimController sendMessage:AddNewTabMsgID data:nil];
356 - (void)updateTabsWithData:(NSData *)data
358     const void *p = [data bytes];
359     const void *end = p + [data length];
360     int tabIdx = 0;
362     // HACK!  Current tab is first in the message.  This way it is not
363     // necessary to guess which tab should be the selected one (this can be
364     // problematic for instance when new tabs are created).
365     int curtabIdx = *((int*)p);  p += sizeof(int);
367     NSArray *tabViewItems = [[self tabBarControl] representedTabViewItems];
369     while (p < end) {
370         NSTabViewItem *tvi = nil;
372         //int wincount = *((int*)p);  p += sizeof(int);
373         int infoCount = *((int*)p); p += sizeof(int);
374         unsigned i;
375         for (i = 0; i < infoCount; ++i) {
376             int length = *((int*)p);  p += sizeof(int);
377             if (length <= 0)
378                 continue;
380             NSString *val = [[NSString alloc]
381                     initWithBytes:(void*)p length:length
382                          encoding:NSUTF8StringEncoding];
383             p += length;
385             switch (i) {
386                 case MMTabLabel:
387                     // Set the label of the tab, adding a new tab when needed.
388                     tvi = [[self tabView] numberOfTabViewItems] <= tabIdx
389                             ? [self addNewTabViewItem]
390                             : [tabViewItems objectAtIndex:tabIdx];
391                     [tvi setLabel:val];
392                     ++tabIdx;
393                     break;
394                 case MMTabToolTip:
395                     if (tvi)
396                         [[self tabBarControl] setToolTip:val
397                                           forTabViewItem:tvi];
398                     break;
399                 default:
400                     ASLogWarn(@"Unknown tab info for index: %d", i);
401             }
403             [val release];
404         }
405     }
407     // Remove unused tabs from the NSTabView.  Note that when a tab is closed
408     // the NSTabView will automatically select another tab, but we want Vim to
409     // take care of which tab to select so set the vimTaskSelectedTab flag to
410     // prevent the tab selection message to be passed on to the VimTask.
411     vimTaskSelectedTab = YES;
412     int i, count = [[self tabView] numberOfTabViewItems];
413     for (i = count-1; i >= tabIdx; --i) {
414         id tvi = [tabViewItems objectAtIndex:i];
415         [[self tabView] removeTabViewItem:tvi];
416     }
417     vimTaskSelectedTab = NO;
419     [self selectTabWithIndex:curtabIdx];
422 - (void)selectTabWithIndex:(int)idx
424     NSArray *tabViewItems = [[self tabBarControl] representedTabViewItems];
425     if (idx < 0 || idx >= [tabViewItems count]) {
426         ASLogWarn(@"No tab with index %d exists.", idx);
427         return;
428     }
430     // Do not try to select a tab if already selected.
431     NSTabViewItem *tvi = [tabViewItems objectAtIndex:idx];
432     if (tvi != [[self tabView] selectedTabViewItem]) {
433         vimTaskSelectedTab = YES;
434         [[self tabView] selectTabViewItem:tvi];
435         vimTaskSelectedTab = NO;
437         // We might need to change the scrollbars that are visible.
438         [self placeScrollbars];
439     }
442 - (NSTabViewItem *)addNewTabViewItem
444     // NOTE!  A newly created tab is not by selected by default; Vim decides
445     // which tab should be selected at all times.  However, the AppKit will
446     // automatically select the first tab added to a tab view.
448     NSTabViewItem *tvi = [[NSTabViewItem alloc] initWithIdentifier:nil];
450     // NOTE: If this is the first tab it will be automatically selected.
451     vimTaskSelectedTab = YES;
452     [[self tabView] addTabViewItem:tvi];
453     vimTaskSelectedTab = NO;
455     [tvi autorelease];
457     return tvi;
460 - (void)createScrollbarWithIdentifier:(int32_t)ident type:(int)type
462     MMScroller *scroller = [[MMScroller alloc] initWithIdentifier:ident
463                                                              type:type];
464     [scroller setTarget:self];
465     [scroller setAction:@selector(scroll:)];
467     [self addSubview:scroller];
468     [scrollbars addObject:scroller];
469     [scroller release];
472 - (BOOL)destroyScrollbarWithIdentifier:(int32_t)ident
474     unsigned idx = 0;
475     MMScroller *scroller = [self scrollbarForIdentifier:ident index:&idx];
476     if (!scroller) return NO;
478     [scroller removeFromSuperview];
479     [scrollbars removeObjectAtIndex:idx];
481     // If a visible scroller was removed then the vim view must resize.  This
482     // is handled by the window controller (the vim view never resizes itself).
483     return ![scroller isHidden];
486 - (BOOL)showScrollbarWithIdentifier:(int32_t)ident state:(BOOL)visible
488     MMScroller *scroller = [self scrollbarForIdentifier:ident index:NULL];
489     if (!scroller) return NO;
491     BOOL wasVisible = ![scroller isHidden];
492     [scroller setHidden:!visible];
494     // If a scroller was hidden or shown then the vim view must resize.  This
495     // is handled by the window controller (the vim view never resizes itself).
496     return wasVisible != visible;
499 - (void)setScrollbarThumbValue:(float)val proportion:(float)prop
500                     identifier:(int32_t)ident
502     MMScroller *scroller = [self scrollbarForIdentifier:ident index:NULL];
503 #if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5)
504     [scroller setDoubleValue:val];
505     [scroller setKnobProportion:prop];
506 #else
507     [scroller setFloatValue:val knobProportion:prop];
508 #endif
509     [scroller setEnabled:prop != 1.f];
513 - (void)scroll:(id)sender
515     NSMutableData *data = [NSMutableData data];
516     int32_t ident = [(MMScroller*)sender scrollerId];
517     int hitPart = [sender hitPart];
518     float value = [sender floatValue];
520     [data appendBytes:&ident length:sizeof(int32_t)];
521     [data appendBytes:&hitPart length:sizeof(int)];
522     [data appendBytes:&value length:sizeof(float)];
524     [vimController sendMessage:ScrollbarEventMsgID data:data];
527 - (void)setScrollbarPosition:(int)pos length:(int)len identifier:(int32_t)ident
529     MMScroller *scroller = [self scrollbarForIdentifier:ident index:NULL];
530     NSRange range = NSMakeRange(pos, len);
531     if (!NSEqualRanges(range, [scroller range])) {
532         [scroller setRange:range];
533         // TODO!  Should only do this once per update.
535         // This could be sent because a text window was created or closed, so
536         // we might need to update which scrollbars are visible.
537         [self placeScrollbars];
538     }
541 - (void)setDefaultColorsBackground:(NSColor *)back foreground:(NSColor *)fore
543     [textView setDefaultColorsBackground:back foreground:fore];
547 // -- PSMTabBarControl delegate ----------------------------------------------
550 - (BOOL)tabView:(NSTabView *)theTabView shouldSelectTabViewItem:
551     (NSTabViewItem *)tabViewItem
553     // NOTE: It would be reasonable to think that 'shouldSelect...' implies
554     // that this message only gets sent when the user clicks the tab.
555     // Unfortunately it is not so, which is why we need the
556     // 'vimTaskSelectedTab' flag.
557     //
558     // HACK!  The selection message should not be propagated to Vim if Vim
559     // selected the tab (e.g. as opposed the user clicking the tab).  The
560     // delegate method has no way of knowing who initiated the selection so a
561     // flag is set when Vim initiated the selection.
562     if (!vimTaskSelectedTab) {
563         // Propagate the selection message to Vim.
564         NSUInteger idx = [self representedIndexOfTabViewItem:tabViewItem];
565         if (NSNotFound != idx) {
566             int i = (int)idx;   // HACK! Never more than MAXINT tabs?!
567             NSData *data = [NSData dataWithBytes:&i length:sizeof(int)];
568             [vimController sendMessage:SelectTabMsgID data:data];
569         }
570     }
572     // Unless Vim selected the tab, return NO, and let Vim decide if the tab
573     // should get selected or not.
574     return vimTaskSelectedTab;
577 - (BOOL)tabView:(NSTabView *)theTabView shouldCloseTabViewItem:
578         (NSTabViewItem *)tabViewItem
580     // HACK!  This method is only called when the user clicks the close button
581     // on the tab.  Instead of letting the tab bar close the tab, we return NO
582     // and pass a message on to Vim to let it handle the closing.
583     NSUInteger idx = [self representedIndexOfTabViewItem:tabViewItem];
584     int i = (int)idx;   // HACK! Never more than MAXINT tabs?!
585     NSData *data = [NSData dataWithBytes:&i length:sizeof(int)];
586     [vimController sendMessage:CloseTabMsgID data:data];
588     return NO;
591 - (void)tabView:(NSTabView *)theTabView didDragTabViewItem:
592         (NSTabViewItem *)tabViewItem toIndex:(int)idx
594     NSMutableData *data = [NSMutableData data];
595     [data appendBytes:&idx length:sizeof(int)];
597     [vimController sendMessage:DraggedTabMsgID data:data];
600 - (NSDragOperation)tabBarControl:(PSMTabBarControl *)theTabBarControl
601         draggingEntered:(id <NSDraggingInfo>)sender
602         forTabAtIndex:(NSUInteger)tabIndex
604     NSPasteboard *pb = [sender draggingPasteboard];
605     return [[pb types] containsObject:NSFilenamesPboardType]
606             ? NSDragOperationCopy
607             : NSDragOperationNone;
610 - (BOOL)tabBarControl:(PSMTabBarControl *)theTabBarControl
611         performDragOperation:(id <NSDraggingInfo>)sender
612         forTabAtIndex:(NSUInteger)tabIndex
614     NSPasteboard *pb = [sender draggingPasteboard];
615     if ([[pb types] containsObject:NSFilenamesPboardType]) {
616         NSArray *filenames = [pb propertyListForType:NSFilenamesPboardType];
617         if ([filenames count] == 0)
618             return NO;
619         if (tabIndex != NSNotFound) {
620             // If dropping on a specific tab, only open one file
621             [vimController file:[filenames objectAtIndex:0]
622                 draggedToTabAtIndex:tabIndex];
623         } else {
624             // Files were dropped on empty part of tab bar; open them all
625             [vimController filesDraggedToTabBar:filenames];
626         }
627         return YES;
628     } else {
629         return NO;
630     }
635 // -- NSView customization ---------------------------------------------------
638 - (void)viewWillStartLiveResize
640     id windowController = [[self window] windowController];
641     [windowController liveResizeWillStart];
643     [super viewWillStartLiveResize];
646 - (void)viewDidEndLiveResize
648     id windowController = [[self window] windowController];
649     [windowController liveResizeDidEnd];
651     [super viewDidEndLiveResize];
654 - (void)setFrameSize:(NSSize)size
656     // NOTE: Instead of only acting when a frame was resized, we do some
657     // updating each time a frame may be resized.  (At the moment, if we only
658     // respond to actual frame changes then typing ":set lines=1000" twice in a
659     // row will result in the vim view holding more rows than the can fit
660     // inside the window.)
661     [super setFrameSize:size];
662     [self frameSizeMayHaveChanged];
665 - (void)setFrame:(NSRect)frame
667     // See comment in setFrameSize: above.
668     [super setFrame:frame];
669     [self frameSizeMayHaveChanged];
672 - (void)markDirty
674     // When the view is marked as dirty, the entire background will be cleared
675     // the next time the view is redrawn.
676     isDirty = YES;
679 @end // MMVimView
684 @implementation MMVimView (Private)
686 - (BOOL)bottomScrollbarVisible
688     unsigned i, count = [scrollbars count];
689     for (i = 0; i < count; ++i) {
690         MMScroller *scroller = [scrollbars objectAtIndex:i];
691         if ([scroller type] == MMScrollerTypeBottom && ![scroller isHidden])
692             return YES;
693     }
695     return NO;
698 - (BOOL)leftScrollbarVisible
700     unsigned i, count = [scrollbars count];
701     for (i = 0; i < count; ++i) {
702         MMScroller *scroller = [scrollbars objectAtIndex:i];
703         if ([scroller type] == MMScrollerTypeLeft && ![scroller isHidden])
704             return YES;
705     }
707     return NO;
710 - (BOOL)rightScrollbarVisible
712     unsigned i, count = [scrollbars count];
713     for (i = 0; i < count; ++i) {
714         MMScroller *scroller = [scrollbars objectAtIndex:i];
715         if ([scroller type] == MMScrollerTypeRight && ![scroller isHidden])
716             return YES;
717     }
719     return NO;
722 - (void)placeScrollbars
724     NSRect textViewFrame = [textView frame];
725     BOOL lsbVisible = [self leftScrollbarVisible];
727     // HACK!  Find the lowest left&right vertical scrollbars, as well as the
728     // rightmost horizontal scrollbar.  This hack continues further down.
729     //
730     // TODO!  Can there be no more than one horizontal scrollbar?  If so, the
731     // code can be simplified.
732     unsigned lowestLeftSbIdx = (unsigned)-1;
733     unsigned lowestRightSbIdx = (unsigned)-1;
734     unsigned rightmostSbIdx = (unsigned)-1;
735     unsigned rowMaxLeft = 0, rowMaxRight = 0, colMax = 0;
736     unsigned i, count = [scrollbars count];
737     for (i = 0; i < count; ++i) {
738         MMScroller *scroller = [scrollbars objectAtIndex:i];
739         if (![scroller isHidden]) {
740             NSRange range = [scroller range];
741             if ([scroller type] == MMScrollerTypeLeft
742                     && range.location >= rowMaxLeft) {
743                 rowMaxLeft = range.location;
744                 lowestLeftSbIdx = i;
745             } else if ([scroller type] == MMScrollerTypeRight
746                     && range.location >= rowMaxRight) {
747                 rowMaxRight = range.location;
748                 lowestRightSbIdx = i;
749             } else if ([scroller type] == MMScrollerTypeBottom
750                     && range.location >= colMax) {
751                 colMax = range.location;
752                 rightmostSbIdx = i;
753             }
754         }
755     }
757     // Place the scrollbars.
758     for (i = 0; i < count; ++i) {
759         MMScroller *scroller = [scrollbars objectAtIndex:i];
760         if ([scroller isHidden])
761             continue;
763         NSRect rect;
764         if ([scroller type] == MMScrollerTypeBottom) {
765             rect = [textView rectForColumnsInRange:[scroller range]];
766             rect.size.height = [NSScroller scrollerWidth];
767             if (lsbVisible)
768                 rect.origin.x += [NSScroller scrollerWidth];
770             // HACK!  Make sure the rightmost horizontal scrollbar covers the
771             // text view all the way to the right, otherwise it looks ugly when
772             // the user drags the window to resize.
773             if (i == rightmostSbIdx) {
774                 float w = NSMaxX(textViewFrame) - NSMaxX(rect);
775                 if (w > 0)
776                     rect.size.width += w;
777             }
779             // Make sure scrollbar rect is bounded by the text view frame.
780             if (rect.origin.x < textViewFrame.origin.x)
781                 rect.origin.x = textViewFrame.origin.x;
782             else if (rect.origin.x > NSMaxX(textViewFrame))
783                 rect.origin.x = NSMaxX(textViewFrame);
784             if (NSMaxX(rect) > NSMaxX(textViewFrame))
785                 rect.size.width -= NSMaxX(rect) - NSMaxX(textViewFrame);
786             if (rect.size.width < 0)
787                 rect.size.width = 0;
788         } else {
789             rect = [textView rectForRowsInRange:[scroller range]];
790             // Adjust for the fact that text layout is flipped.
791             rect.origin.y = NSMaxY(textViewFrame) - rect.origin.y
792                     - rect.size.height;
793             rect.size.width = [NSScroller scrollerWidth];
794             if ([scroller type] == MMScrollerTypeRight)
795                 rect.origin.x = NSMaxX(textViewFrame);
797             // HACK!  Make sure the lowest vertical scrollbar covers the text
798             // view all the way to the bottom.  This is done because Vim only
799             // makes the scrollbar cover the (vim-)window it is associated with
800             // and this means there is always an empty gap in the scrollbar
801             // region next to the command line.
802             // TODO!  Find a nicer way to do this.
803             if (i == lowestLeftSbIdx || i == lowestRightSbIdx) {
804                 float h = rect.origin.y + rect.size.height
805                           - textViewFrame.origin.y;
806                 if (rect.size.height < h) {
807                     rect.origin.y = textViewFrame.origin.y;
808                     rect.size.height = h;
809                 }
810             }
812             // Vertical scrollers must not cover the resize box in the
813             // bottom-right corner of the window.
814             if ([[self window] showsResizeIndicator]  // XXX: make this a flag
815                 && rect.origin.y < [NSScroller scrollerWidth]) {
816                 rect.size.height -= [NSScroller scrollerWidth] - rect.origin.y;
817                 rect.origin.y = [NSScroller scrollerWidth];
818             }
820             // Make sure scrollbar rect is bounded by the text view frame.
821             if (rect.origin.y < textViewFrame.origin.y) {
822                 rect.size.height -= textViewFrame.origin.y - rect.origin.y;
823                 rect.origin.y = textViewFrame.origin.y;
824             } else if (rect.origin.y > NSMaxY(textViewFrame))
825                 rect.origin.y = NSMaxY(textViewFrame);
826             if (NSMaxY(rect) > NSMaxY(textViewFrame))
827                 rect.size.height -= NSMaxY(rect) - NSMaxY(textViewFrame);
828             if (rect.size.height < 0)
829                 rect.size.height = 0;
830         }
832         NSRect oldRect = [scroller frame];
833         if (!NSEqualRects(oldRect, rect)) {
834             [scroller setFrame:rect];
835             // Clear behind the old scroller frame, or parts of the old
836             // scroller might still be visible after setFrame:.
837             [[[self window] contentView] setNeedsDisplayInRect:oldRect];
838             [scroller setNeedsDisplay:YES];
839         }
840     }
843 - (NSUInteger)representedIndexOfTabViewItem:(NSTabViewItem *)tvi
845     NSArray *tabViewItems = [[self tabBarControl] representedTabViewItems];
846     return [tabViewItems indexOfObject:tvi];
849 - (MMScroller *)scrollbarForIdentifier:(int32_t)ident index:(unsigned *)idx
851     unsigned i, count = [scrollbars count];
852     for (i = 0; i < count; ++i) {
853         MMScroller *scroller = [scrollbars objectAtIndex:i];
854         if ([scroller scrollerId] == ident) {
855             if (idx) *idx = i;
856             return scroller;
857         }
858     }
860     return nil;
863 - (NSSize)vimViewSizeForTextViewSize:(NSSize)textViewSize
865     NSSize size = textViewSize;
867     if (![[self tabBarControl] isHidden])
868         size.height += [[self tabBarControl] frame].size.height;
870     if ([self bottomScrollbarVisible])
871         size.height += [NSScroller scrollerWidth];
872     if ([self leftScrollbarVisible])
873         size.width += [NSScroller scrollerWidth];
874     if ([self rightScrollbarVisible])
875         size.width += [NSScroller scrollerWidth];
877     return size;
880 - (NSRect)textViewRectForVimViewSize:(NSSize)contentSize
882     NSRect rect = { {0, 0}, {contentSize.width, contentSize.height} };
884     if (![[self tabBarControl] isHidden])
885         rect.size.height -= [[self tabBarControl] frame].size.height;
887     if ([self bottomScrollbarVisible]) {
888         rect.size.height -= [NSScroller scrollerWidth];
889         rect.origin.y += [NSScroller scrollerWidth];
890     }
891     if ([self leftScrollbarVisible]) {
892         rect.size.width -= [NSScroller scrollerWidth];
893         rect.origin.x += [NSScroller scrollerWidth];
894     }
895     if ([self rightScrollbarVisible])
896         rect.size.width -= [NSScroller scrollerWidth];
898     return rect;
901 - (NSTabView *)tabView
903     return tabView;
906 - (void)frameSizeMayHaveChanged
908     // NOTE: Whenever a call is made that may have changed the frame size we
909     // take the opportunity to make sure all subviews are in place and that the
910     // (rows,columns) are constrained to lie inside the new frame.  We not only
911     // do this when the frame really has changed since it is possible to modify
912     // the number of (rows,columns) without changing the frame size.
914     // Give all superfluous space to the text view. It might be smaller or
915     // larger than it wants to be, but this is needed during live resizing.
916     NSRect textViewRect = [self textViewRectForVimViewSize:[self frame].size];
917     [textView setFrame:textViewRect];
919     [self placeScrollbars];
921     // It is possible that the current number of (rows,columns) is too big or
922     // too small to fit the new frame.  If so, notify Vim that the text
923     // dimensions should change, but don't actually change the number of
924     // (rows,columns).  These numbers may only change when Vim initiates the
925     // change (as opposed to the user dragging the window resizer, for
926     // example).
927     //
928     // Note that the message sent to Vim depends on whether we're in
929     // a live resize or not -- this is necessary to avoid the window jittering
930     // when the user drags to resize.
931     int constrained[2];
932     NSSize textViewSize = [textView frame].size;
933     [textView constrainRows:&constrained[0] columns:&constrained[1]
934                      toSize:textViewSize];
936     int rows, cols;
937     [textView getMaxRows:&rows columns:&cols];
939     if (constrained[0] != rows || constrained[1] != cols) {
940         NSData *data = [NSData dataWithBytes:constrained length:2*sizeof(int)];
941         int msgid = [self inLiveResize] ? LiveResizeMsgID
942                                         : SetTextDimensionsMsgID;
944         ASLogDebug(@"Notify Vim that text dimensions changed from %dx%d to "
945                    "%dx%d (%s)", cols, rows, constrained[1], constrained[0],
946                    MessageStrings[msgid]);
948         [vimController sendMessage:msgid data:data];
950         // We only want to set the window title if this resize came from
951         // a live-resize, not (for example) setting 'columns' or 'lines'.
952         if ([self inLiveResize]) {
953             [[self window] setTitle:[NSString stringWithFormat:@"%dx%d",
954                     constrained[1], constrained[0]]];
955         }
956     }
959 @end // MMVimView (Private)
964 @implementation MMScroller
966 - (id)initWithIdentifier:(int32_t)ident type:(int)theType
968     // HACK! NSScroller creates a horizontal scroller if it is init'ed with a
969     // frame whose with exceeds its height; so create a bogus rect and pass it
970     // to initWithFrame.
971     NSRect frame = theType == MMScrollerTypeBottom
972             ? NSMakeRect(0, 0, 1, 0)
973             : NSMakeRect(0, 0, 0, 1);
975     self = [super initWithFrame:frame];
976     if (!self) return nil;
978     identifier = ident;
979     type = theType;
980     [self setHidden:YES];
981     [self setEnabled:YES];
982     [self setAutoresizingMask:NSViewNotSizable];
984     return self;
987 - (int32_t)scrollerId
989     return identifier;
992 - (int)type
994     return type;
997 - (NSRange)range
999     return range;
1002 - (void)setRange:(NSRange)newRange
1004     range = newRange;
1007 - (void)scrollWheel:(NSEvent *)event
1009     // HACK! Pass message on to the text view.
1010     NSView *vimView = [self superview];
1011     if ([vimView isKindOfClass:[MMVimView class]])
1012         [[(MMVimView*)vimView textView] scrollWheel:event];
1015 - (void)mouseDown:(NSEvent *)event
1017     // TODO: This is an ugly way of getting the connection to the backend.
1018     NSConnection *connection = nil;
1019     id wc = [[self window] windowController];
1020     if ([wc isKindOfClass:[MMWindowController class]]) {
1021         MMVimController *vc = [(MMWindowController*)wc vimController];
1022         id proxy = [vc backendProxy];
1023         connection = [(NSDistantObject*)proxy connectionForProxy];
1024     }
1026     // NOTE: The scroller goes into "event tracking mode" when the user clicks
1027     // (and holds) the mouse button.  We have to manually add the backend
1028     // connection to this mode while the mouse button is held, else DO messages
1029     // from Vim will not be processed until the mouse button is released.
1030     [connection addRequestMode:NSEventTrackingRunLoopMode];
1031     [super mouseDown:event];
1032     [connection removeRequestMode:NSEventTrackingRunLoopMode];
1035 @end // MMScroller