-
-
Notifications
You must be signed in to change notification settings - Fork 690
Expand file tree
/
Copy pathMMVimView.m
More file actions
1093 lines (910 loc) · 38.4 KB
/
MMVimView.m
File metadata and controls
1093 lines (910 loc) · 38.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* vi:set ts=8 sts=4 sw=4 ft=objc:
*
* VIM - Vi IMproved by Bram Moolenaar
* MacVim GUI port by Bjorn Winckler
*
* Do ":help uganda" in Vim to read copying and usage conditions.
* Do ":help credits" in Vim to see a list of people who contributed.
* See README.txt for an overview of the Vim source code.
*/
/*
* MMVimView
*
* A view class with a tabline, scrollbars, and a text view. The tabline may
* appear at the top of the view in which case it fills up the view from left
* to right edge. Any number of scrollbars may appear adjacent to all other
* edges of the view (there may be more than one scrollbar per edge and
* scrollbars may also be placed on the left edge of the view). The rest of
* the view is filled by the text view.
*/
#import "Miscellaneous.h"
#import "MMCoreTextView.h"
#import "MMTextView.h"
#import "MMVimController.h"
#import "MMVimView.h"
#import "MMWindowController.h"
#import <PSMTabBarControl/PSMTabBarControl.h>
// Scroller type; these must match SBAR_* in gui.h
enum {
MMScrollerTypeLeft = 0,
MMScrollerTypeRight,
MMScrollerTypeBottom
};
// TODO: Move!
@interface MMScroller : NSScroller {
int32_t identifier;
int type;
NSRange range;
}
- (id)initWithIdentifier:(int32_t)ident type:(int)type;
- (int32_t)scrollerId;
- (int)type;
- (NSRange)range;
- (void)setRange:(NSRange)newRange;
@end
@interface MMVimView (Private)
- (BOOL)bottomScrollbarVisible;
- (BOOL)leftScrollbarVisible;
- (BOOL)rightScrollbarVisible;
- (void)placeScrollbars;
- (NSUInteger)representedIndexOfTabViewItem:(NSTabViewItem *)tvi;
- (MMScroller *)scrollbarForIdentifier:(int32_t)ident index:(unsigned *)idx;
- (NSSize)vimViewSizeForTextViewSize:(NSSize)textViewSize;
- (NSRect)textViewRectForVimViewSize:(NSSize)contentSize;
- (NSTabView *)tabView;
- (void)frameSizeMayHaveChanged:(BOOL)keepGUISize;
@end
// This is an informal protocol implemented by MMWindowController (maybe it
// shold be a formal protocol, but ...).
@interface NSWindowController (MMVimViewDelegate)
- (void)liveResizeWillStart;
- (void)liveResizeDidEnd;
@end
@implementation MMVimView
- (MMVimView *)initWithFrame:(NSRect)frame
vimController:(MMVimController *)controller
{
if (!(self = [super initWithFrame:frame]))
return nil;
vimController = controller;
scrollbars = [[NSMutableArray alloc] init];
// Only the tabline is autoresized, all other subview placement is done in
// frameSizeMayHaveChanged.
[self setAutoresizesSubviews:YES];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSInteger renderer = [ud integerForKey:MMRendererKey];
ASLogInfo(@"Use renderer=%ld", renderer);
if (MMRendererCoreText == renderer) {
// HACK! 'textView' has type MMTextView, but MMCoreTextView is not
// derived from MMTextView.
textView = (MMTextView *)[[MMCoreTextView alloc] initWithFrame:frame];
} else {
// Use Cocoa text system for text rendering.
textView = [[MMTextView alloc] initWithFrame:frame];
}
// Allow control of text view inset via MMTextInset* user defaults.
int left = [ud integerForKey:MMTextInsetLeftKey];
int top = [ud integerForKey:MMTextInsetTopKey];
[textView setTextContainerInset:NSMakeSize(left, top)];
[textView setAutoresizingMask:NSViewNotSizable];
[self addSubview:textView];
// Create the tab view (which is never visible, but the tab bar control
// needs it to function).
tabView = [[NSTabView alloc] initWithFrame:NSZeroRect];
// Create the tab bar control (which is responsible for actually
// drawing the tabline and tabs).
NSRect tabFrame = { { 0, frame.size.height - kPSMTabBarControlHeight },
{ frame.size.width, kPSMTabBarControlHeight } };
tabBarControl = [[PSMTabBarControl alloc] initWithFrame:tabFrame];
[tabView setDelegate:tabBarControl];
[tabBarControl setTabView:tabView];
[tabBarControl setDelegate:self];
[tabBarControl setHidden:YES];
if (shouldUseYosemiteTabBarStyle() || shouldUseMojaveTabBarStyle()) {
CGFloat screenWidth = [[NSScreen mainScreen] frame].size.width;
int tabMaxWidth = [ud integerForKey:MMTabMaxWidthKey];
if (tabMaxWidth == 0)
tabMaxWidth = screenWidth;
int tabOptimumWidth = [ud integerForKey:MMTabOptimumWidthKey];
if (tabOptimumWidth == 0)
tabOptimumWidth = screenWidth;
NSString* tabStyleName = shouldUseMojaveTabBarStyle() ? @"Mojave" : @"Yosemite";
[tabBarControl setStyleNamed:tabStyleName];
[tabBarControl setCellMinWidth:[ud integerForKey:MMTabMinWidthKey]];
[tabBarControl setCellMaxWidth:tabMaxWidth];
[tabBarControl setCellOptimumWidth:tabOptimumWidth];
} else {
[tabBarControl setCellMinWidth:[ud integerForKey:MMTabMinWidthKey]];
[tabBarControl setCellMaxWidth:[ud integerForKey:MMTabMaxWidthKey]];
[tabBarControl setCellOptimumWidth:
[ud integerForKey:MMTabOptimumWidthKey]];
}
[tabBarControl setShowAddTabButton:[ud boolForKey:MMShowAddTabButtonKey]];
[[tabBarControl addTabButton] setTarget:self];
[[tabBarControl addTabButton] setAction:@selector(addNewTab:)];
[tabBarControl setAllowsDragBetweenWindows:NO];
[tabBarControl registerForDraggedTypes:
[NSArray arrayWithObject:NSFilenamesPboardType]];
[tabBarControl setAutoresizingMask:NSViewWidthSizable|NSViewMinYMargin];
//[tabBarControl setPartnerView:textView];
// tab bar resizing only works if awakeFromNib is called (that's where
// the NSViewFrameDidChangeNotification callback is installed). Sounds like
// a PSMTabBarControl bug, let's live with it for now.
[tabBarControl awakeFromNib];
[self addSubview:tabBarControl];
return self;
}
- (void)dealloc
{
ASLogDebug(@"");
[tabBarControl release]; tabBarControl = nil;
[tabView release]; tabView = nil;
[scrollbars release]; scrollbars = nil;
// HACK! The text storage is the principal owner of the text system, but we
// keep only a reference to the text view, so release the text storage
// first (unless we are using the CoreText renderer).
if ([textView isKindOfClass:[MMTextView class]])
[[textView textStorage] release];
[textView release]; textView = nil;
[super dealloc];
}
- (BOOL)isOpaque
{
return textView.defaultBackgroundColor.alphaComponent == 1;
}
- (void)drawRect:(NSRect)rect
{
// On Leopard, we want to have a textured window background for nice
// looking tabs. However, the textured window background looks really
// weird behind the window resize throbber, so emulate the look of an
// NSScrollView in the bottom right corner.
if (![[self window] showsResizeIndicator]
|| !([[self window] styleMask] & NSWindowStyleMaskTexturedBackground))
return;
// This should not be reachable in 10.7 or above and is deprecated code.
// See documentation for showsResizeIndicator and placeScrollbars: comments.
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_7)
int sw = [NSScroller scrollerWidthForControlSize:NSControlSizeRegular scrollerStyle:NSScrollerStyleLegacy];
#else
int sw = [NSScroller scrollerWidth];
#endif
// add .5 to the pixel locations to put the lines on a pixel boundary.
// the top and right edges of the rect will be outside of the bounds rect
// and clipped away.
NSRect sizerRect = NSMakeRect([self bounds].size.width - sw + .5, -.5,
sw, sw);
//NSBezierPath* path = [NSBezierPath bezierPath];
NSBezierPath* path = [NSBezierPath bezierPathWithRect:sizerRect];
// On Tiger, we have color #E8E8E8 behind the resize throbber
// (which is windowBackgroundColor on untextured windows or controlColor in
// general). Terminal.app on Leopard has #FFFFFF background and #D9D9D9 as
// stroke. The colors below are #FFFFFF and #D4D4D4, which is close enough
// for me.
[[NSColor controlBackgroundColor] set];
[path fill];
[[NSColor secondarySelectedControlColor] set];
[path stroke];
if ([self leftScrollbarVisible]) {
// If the left scrollbar is visible there is an empty square under it.
// Fill it in just like on the right hand corner. The half pixel
// offset ensures the outline goes on the top and right side of the
// square; the left and bottom parts of the outline are clipped.
sizerRect = NSMakeRect(-.5,-.5,sw,sw);
path = [NSBezierPath bezierPathWithRect:sizerRect];
[[NSColor controlBackgroundColor] set];
[path fill];
[[NSColor secondarySelectedControlColor] set];
[path stroke];
}
}
- (MMTextView *)textView
{
return textView;
}
- (PSMTabBarControl *)tabBarControl
{
return tabBarControl;
}
- (void)cleanup
{
vimController = nil;
// NOTE! There is a bug in PSMTabBarControl in that it retains the delegate
// so reset the delegate here, otherwise the delegate may never get
// released.
[tabView setDelegate:nil];
[tabBarControl setDelegate:nil];
[tabBarControl setTabView:nil];
[[self window] setDelegate:nil];
// NOTE! There is another bug in PSMTabBarControl where the control is not
// removed as an observer, so remove it here (failing to remove an observer
// may lead to very strange bugs).
[[NSNotificationCenter defaultCenter] removeObserver:tabBarControl];
[tabBarControl removeFromSuperviewWithoutNeedingDisplay];
[textView removeFromSuperviewWithoutNeedingDisplay];
unsigned i, count = [scrollbars count];
for (i = 0; i < count; ++i) {
MMScroller *sb = [scrollbars objectAtIndex:i];
[sb removeFromSuperviewWithoutNeedingDisplay];
}
[tabView removeAllTabViewItems];
}
- (NSSize)desiredSize
{
return [self vimViewSizeForTextViewSize:[textView desiredSize]];
}
- (NSSize)minSize
{
return [self vimViewSizeForTextViewSize:[textView minSize]];
}
- (NSSize)constrainRows:(int *)r columns:(int *)c toSize:(NSSize)size
{
NSSize textViewSize = [self textViewRectForVimViewSize:size].size;
textViewSize = [textView constrainRows:r columns:c toSize:textViewSize];
return [self vimViewSizeForTextViewSize:textViewSize];
}
- (void)setDesiredRows:(int)r columns:(int)c
{
[textView setMaxRows:r columns:c];
}
- (IBAction)addNewTab:(id)sender
{
[vimController sendMessage:AddNewTabMsgID data:nil];
}
- (void)updateTabsWithData:(NSData *)data
{
const void *p = [data bytes];
const void *end = p + [data length];
int tabIdx = 0;
// HACK! Current tab is first in the message. This way it is not
// necessary to guess which tab should be the selected one (this can be
// problematic for instance when new tabs are created).
int curtabIdx = *((int*)p); p += sizeof(int);
NSArray *tabViewItems = [[self tabBarControl] representedTabViewItems];
while (p < end) {
NSTabViewItem *tvi = nil;
//int wincount = *((int*)p); p += sizeof(int);
int infoCount = *((int*)p); p += sizeof(int);
unsigned i;
for (i = 0; i < infoCount; ++i) {
int length = *((int*)p); p += sizeof(int);
if (length <= 0)
continue;
NSString *val = [[NSString alloc]
initWithBytes:(void*)p length:length
encoding:NSUTF8StringEncoding];
p += length;
switch (i) {
case MMTabLabel:
// Set the label of the tab, adding a new tab when needed.
tvi = [[self tabView] numberOfTabViewItems] <= tabIdx
? [self addNewTabViewItem]
: [tabViewItems objectAtIndex:tabIdx];
[tvi setLabel:val];
++tabIdx;
break;
case MMTabToolTip:
if (tvi)
[[self tabBarControl] setToolTip:val
forTabViewItem:tvi];
break;
default:
ASLogWarn(@"Unknown tab info for index: %d", i);
}
[val release];
}
}
// Remove unused tabs from the NSTabView. Note that when a tab is closed
// the NSTabView will automatically select another tab, but we want Vim to
// take care of which tab to select so set the vimTaskSelectedTab flag to
// prevent the tab selection message to be passed on to the VimTask.
vimTaskSelectedTab = YES;
int i, count = [[self tabView] numberOfTabViewItems];
for (i = count-1; i >= tabIdx; --i) {
id tvi = [tabViewItems objectAtIndex:i];
[[self tabView] removeTabViewItem:tvi];
}
vimTaskSelectedTab = NO;
[self selectTabWithIndex:curtabIdx];
}
- (void)selectTabWithIndex:(int)idx
{
NSArray *tabViewItems = [[self tabBarControl] representedTabViewItems];
if (idx < 0 || idx >= [tabViewItems count]) {
ASLogWarn(@"No tab with index %d exists.", idx);
return;
}
// Do not try to select a tab if already selected.
NSTabViewItem *tvi = [tabViewItems objectAtIndex:idx];
if (tvi != [[self tabView] selectedTabViewItem]) {
vimTaskSelectedTab = YES;
[[self tabView] selectTabViewItem:tvi];
vimTaskSelectedTab = NO;
// We might need to change the scrollbars that are visible.
self.pendingPlaceScrollbars = YES;
}
}
- (NSTabViewItem *)addNewTabViewItem
{
// NOTE! A newly created tab is not by selected by default; Vim decides
// which tab should be selected at all times. However, the AppKit will
// automatically select the first tab added to a tab view.
// The documentation claims initWithIdentifier can be given a nil identifier, but the API itself
// is decorated such that doing so produces a warning, so the tab count is used as identifier.
NSInteger identifier = [[self tabView] numberOfTabViewItems];
NSTabViewItem *tvi = [[NSTabViewItem alloc] initWithIdentifier:[NSNumber numberWithInt:identifier]];
// NOTE: If this is the first tab it will be automatically selected.
vimTaskSelectedTab = YES;
[[self tabView] addTabViewItem:tvi];
vimTaskSelectedTab = NO;
[tvi autorelease];
return tvi;
}
- (void)createScrollbarWithIdentifier:(int32_t)ident type:(int)type
{
MMScroller *scroller = [[MMScroller alloc] initWithIdentifier:ident
type:type];
[scroller setTarget:self];
[scroller setAction:@selector(scroll:)];
[self addSubview:scroller];
[scrollbars addObject:scroller];
[scroller release];
self.pendingPlaceScrollbars = YES;
}
- (BOOL)destroyScrollbarWithIdentifier:(int32_t)ident
{
unsigned idx = 0;
MMScroller *scroller = [self scrollbarForIdentifier:ident index:&idx];
if (!scroller) return NO;
[scroller removeFromSuperview];
[scrollbars removeObjectAtIndex:idx];
self.pendingPlaceScrollbars = YES;
// If a visible scroller was removed then the vim view must resize. This
// is handled by the window controller (the vim view never resizes itself).
return ![scroller isHidden];
}
- (BOOL)showScrollbarWithIdentifier:(int32_t)ident state:(BOOL)visible
{
MMScroller *scroller = [self scrollbarForIdentifier:ident index:NULL];
if (!scroller) return NO;
BOOL wasVisible = ![scroller isHidden];
[scroller setHidden:!visible];
self.pendingPlaceScrollbars = YES;
// If a scroller was hidden or shown then the vim view must resize. This
// is handled by the window controller (the vim view never resizes itself).
return wasVisible != visible;
}
- (void)setScrollbarThumbValue:(float)val proportion:(float)prop
identifier:(int32_t)ident
{
MMScroller *scroller = [self scrollbarForIdentifier:ident index:NULL];
[scroller setDoubleValue:val];
[scroller setKnobProportion:prop];
[scroller setEnabled:prop != 1.f];
}
- (void)scroll:(id)sender
{
NSMutableData *data = [NSMutableData data];
int32_t ident = [(MMScroller*)sender scrollerId];
int hitPart = [sender hitPart];
float value = [sender floatValue];
[data appendBytes:&ident length:sizeof(int32_t)];
[data appendBytes:&hitPart length:sizeof(int)];
[data appendBytes:&value length:sizeof(float)];
[vimController sendMessage:ScrollbarEventMsgID data:data];
}
- (void)setScrollbarPosition:(int)pos length:(int)len identifier:(int32_t)ident
{
MMScroller *scroller = [self scrollbarForIdentifier:ident index:NULL];
NSRange range = NSMakeRange(pos, len);
if (!NSEqualRanges(range, [scroller range])) {
[scroller setRange:range];
// This could be sent because a text window was created or closed, so
// we might need to update which scrollbars are visible.
}
self.pendingPlaceScrollbars = YES;
}
- (void)finishPlaceScrollbars
{
if (self.pendingPlaceScrollbars) {
self.pendingPlaceScrollbars = NO;
[self placeScrollbars];
}
}
- (void)setDefaultColorsBackground:(NSColor *)back foreground:(NSColor *)fore
{
[textView setDefaultColorsBackground:back foreground:fore];
CALayer *backedLayer = [self layer];
if (backedLayer) {
// This only happens in 10.14+, where everything is layer-backed by
// default. Since textView draws itself as a separate layer, we don't
// want this layer to draw anything. This is especially important with
// 'transparency' where there's alpha blending and we don't want this
// layer to be in the way and double-blending things.
[backedLayer setBackgroundColor:CGColorGetConstantColor(kCGColorClear)];
}
for (NSUInteger i = 0, count = [scrollbars count]; i < count; ++i) {
MMScroller *sb = [scrollbars objectAtIndex:i];
[sb setNeedsDisplay:YES];
}
[self setNeedsDisplay:YES];
}
// -- PSMTabBarControl delegate ----------------------------------------------
- (BOOL)tabView:(NSTabView *)theTabView shouldSelectTabViewItem:
(NSTabViewItem *)tabViewItem
{
// NOTE: It would be reasonable to think that 'shouldSelect...' implies
// that this message only gets sent when the user clicks the tab.
// Unfortunately it is not so, which is why we need the
// 'vimTaskSelectedTab' flag.
//
// HACK! The selection message should not be propagated to Vim if Vim
// selected the tab (e.g. as opposed the user clicking the tab). The
// delegate method has no way of knowing who initiated the selection so a
// flag is set when Vim initiated the selection.
if (!vimTaskSelectedTab) {
// Propagate the selection message to Vim.
NSUInteger idx = [self representedIndexOfTabViewItem:tabViewItem];
if (NSNotFound != idx) {
int i = (int)idx; // HACK! Never more than MAXINT tabs?!
NSData *data = [NSData dataWithBytes:&i length:sizeof(int)];
[vimController sendMessage:SelectTabMsgID data:data];
}
}
// Unless Vim selected the tab, return NO, and let Vim decide if the tab
// should get selected or not.
return vimTaskSelectedTab;
}
- (BOOL)tabView:(NSTabView *)theTabView shouldCloseTabViewItem:
(NSTabViewItem *)tabViewItem
{
// HACK! This method is only called when the user clicks the close button
// on the tab. Instead of letting the tab bar close the tab, we return NO
// and pass a message on to Vim to let it handle the closing.
NSUInteger idx = [self representedIndexOfTabViewItem:tabViewItem];
int i = (int)idx; // HACK! Never more than MAXINT tabs?!
NSData *data = [NSData dataWithBytes:&i length:sizeof(int)];
[vimController sendMessage:CloseTabMsgID data:data];
return NO;
}
- (void)tabView:(NSTabView *)theTabView didDragTabViewItem:
(NSTabViewItem *)tabViewItem toIndex:(int)idx
{
NSMutableData *data = [NSMutableData data];
[data appendBytes:&idx length:sizeof(int)];
[vimController sendMessage:DraggedTabMsgID data:data];
}
- (NSDragOperation)tabBarControl:(PSMTabBarControl *)theTabBarControl
draggingEntered:(id <NSDraggingInfo>)sender
forTabAtIndex:(NSUInteger)tabIndex
{
NSPasteboard *pb = [sender draggingPasteboard];
return [[pb types] containsObject:NSFilenamesPboardType]
? NSDragOperationCopy
: NSDragOperationNone;
}
- (BOOL)tabBarControl:(PSMTabBarControl *)theTabBarControl
performDragOperation:(id <NSDraggingInfo>)sender
forTabAtIndex:(NSUInteger)tabIndex
{
NSPasteboard *pb = [sender draggingPasteboard];
if ([[pb types] containsObject:NSFilenamesPboardType]) {
NSArray *filenames = [pb propertyListForType:NSFilenamesPboardType];
if ([filenames count] == 0)
return NO;
if (tabIndex != NSNotFound) {
// If dropping on a specific tab, only open one file
[vimController file:[filenames objectAtIndex:0]
draggedToTabAtIndex:tabIndex];
} else {
// Files were dropped on empty part of tab bar; open them all
[vimController filesDraggedToTabBar:filenames];
}
return YES;
} else {
return NO;
}
}
// -- NSView customization ---------------------------------------------------
- (void)viewWillStartLiveResize
{
id windowController = [[self window] windowController];
[windowController liveResizeWillStart];
[super viewWillStartLiveResize];
}
- (void)viewDidEndLiveResize
{
id windowController = [[self window] windowController];
[windowController liveResizeDidEnd];
[super viewDidEndLiveResize];
}
- (void)setFrameSize:(NSSize)size
{
// NOTE: Instead of only acting when a frame was resized, we do some
// updating each time a frame may be resized. (At the moment, if we only
// respond to actual frame changes then typing ":set lines=1000" twice in a
// row will result in the vim view holding more rows than the can fit
// inside the window.)
[super setFrameSize:size];
[self frameSizeMayHaveChanged:NO];
}
- (void)setFrameSizeKeepGUISize:(NSSize)size
{
// NOTE: Instead of only acting when a frame was resized, we do some
// updating each time a frame may be resized. (At the moment, if we only
// respond to actual frame changes then typing ":set lines=1000" twice in a
// row will result in the vim view holding more rows than the can fit
// inside the window.)
[super setFrameSize:size];
[self frameSizeMayHaveChanged:YES];
}
- (void)setFrame:(NSRect)frame
{
// See comment in setFrameSize: above.
[super setFrame:frame];
[self frameSizeMayHaveChanged:NO];
}
@end // MMVimView
@implementation MMVimView (Private)
- (BOOL)bottomScrollbarVisible
{
unsigned i, count = [scrollbars count];
for (i = 0; i < count; ++i) {
MMScroller *scroller = [scrollbars objectAtIndex:i];
if ([scroller type] == MMScrollerTypeBottom && ![scroller isHidden])
return YES;
}
return NO;
}
- (BOOL)leftScrollbarVisible
{
unsigned i, count = [scrollbars count];
for (i = 0; i < count; ++i) {
MMScroller *scroller = [scrollbars objectAtIndex:i];
if ([scroller type] == MMScrollerTypeLeft && ![scroller isHidden])
return YES;
}
return NO;
}
- (BOOL)rightScrollbarVisible
{
unsigned i, count = [scrollbars count];
for (i = 0; i < count; ++i) {
MMScroller *scroller = [scrollbars objectAtIndex:i];
if ([scroller type] == MMScrollerTypeRight && ![scroller isHidden])
return YES;
}
return NO;
}
- (void)placeScrollbars
{
NSRect textViewFrame = [textView frame];
BOOL leftSbVisible = NO;
BOOL rightSbVisible = NO;
BOOL botSbVisible = NO;
// HACK! Find the lowest left&right vertical scrollbars This hack
// continues further down.
unsigned lowestLeftSbIdx = (unsigned)-1;
unsigned lowestRightSbIdx = (unsigned)-1;
unsigned rowMaxLeft = 0, rowMaxRight = 0;
unsigned i, count = [scrollbars count];
for (i = 0; i < count; ++i) {
MMScroller *scroller = [scrollbars objectAtIndex:i];
if (![scroller isHidden]) {
NSRange range = [scroller range];
if ([scroller type] == MMScrollerTypeLeft
&& range.location >= rowMaxLeft) {
rowMaxLeft = range.location;
lowestLeftSbIdx = i;
leftSbVisible = YES;
} else if ([scroller type] == MMScrollerTypeRight
&& range.location >= rowMaxRight) {
rowMaxRight = range.location;
lowestRightSbIdx = i;
rightSbVisible = YES;
} else if ([scroller type] == MMScrollerTypeBottom) {
botSbVisible = YES;
}
}
}
// Place the scrollbars.
for (i = 0; i < count; ++i) {
MMScroller *scroller = [scrollbars objectAtIndex:i];
if ([scroller isHidden])
continue;
NSRect rect;
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_7)
CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:NSControlSizeRegular scrollerStyle:NSScrollerStyleLegacy];
#else
CGFloat scrollerWidth = [NSScroller scrollerWidth];
#endif
if ([scroller type] == MMScrollerTypeBottom) {
rect = [textView rectForColumnsInRange:[scroller range]];
rect.size.height = scrollerWidth;
if (leftSbVisible)
rect.origin.x += scrollerWidth;
// HACK! Make sure the horizontal scrollbar covers the text view
// all the way to the right, otherwise it looks ugly when the user
// drags the window to resize.
float w = NSMaxX(textViewFrame) - NSMaxX(rect);
if (w > 0)
rect.size.width += w;
// Make sure scrollbar rect is bounded by the text view frame.
// Also leave some room for the resize indicator on the right in
// case there is no right scrollbar.
if (rect.origin.x < textViewFrame.origin.x)
rect.origin.x = textViewFrame.origin.x;
else if (rect.origin.x > NSMaxX(textViewFrame))
rect.origin.x = NSMaxX(textViewFrame);
if (NSMaxX(rect) > NSMaxX(textViewFrame))
rect.size.width -= NSMaxX(rect) - NSMaxX(textViewFrame);
if (!rightSbVisible)
rect.size.width -= scrollerWidth;
if (rect.size.width < 0)
rect.size.width = 0;
} else {
rect = [textView rectForRowsInRange:[scroller range]];
// Adjust for the fact that text layout is flipped.
rect.origin.y = NSMaxY(textViewFrame) - rect.origin.y
- rect.size.height;
rect.size.width = scrollerWidth;
if ([scroller type] == MMScrollerTypeRight)
rect.origin.x = NSMaxX(textViewFrame);
// HACK! Make sure the lowest vertical scrollbar covers the text
// view all the way to the bottom. This is done because Vim only
// makes the scrollbar cover the (vim-)window it is associated with
// and this means there is always an empty gap in the scrollbar
// region next to the command line.
// TODO! Find a nicer way to do this.
if (i == lowestLeftSbIdx || i == lowestRightSbIdx) {
float h = rect.origin.y + rect.size.height
- textViewFrame.origin.y;
if (rect.size.height < h) {
rect.origin.y = textViewFrame.origin.y;
rect.size.height = h;
}
}
// Vertical scrollers must not cover the resize box in the
// bottom-right corner of the window.
if ([[self window] showsResizeIndicator] // Note: This is deprecated as of 10.7, see below comment.
&& rect.origin.y < scrollerWidth) {
rect.size.height -= scrollerWidth - rect.origin.y;
rect.origin.y = scrollerWidth;
}
// Make sure scrollbar rect is bounded by the text view frame.
if (rect.origin.y < textViewFrame.origin.y) {
rect.size.height -= textViewFrame.origin.y - rect.origin.y;
rect.origin.y = textViewFrame.origin.y;
} else if (rect.origin.y > NSMaxY(textViewFrame))
rect.origin.y = NSMaxY(textViewFrame);
if (NSMaxY(rect) > NSMaxY(textViewFrame))
rect.size.height -= NSMaxY(rect) - NSMaxY(textViewFrame);
if (rect.size.height < 0)
rect.size.height = 0;
}
NSRect oldRect = [scroller frame];
if (!NSEqualRects(oldRect, rect)) {
[scroller setFrame:rect];
// Clear behind the old scroller frame, or parts of the old
// scroller might still be visible after setFrame:.
[[[self window] contentView] setNeedsDisplayInRect:oldRect];
[scroller setNeedsDisplay:YES];
}
}
if (NSAppKitVersionNumber < NSAppKitVersionNumber10_7) {
// HACK: If there is no bottom or right scrollbar the resize indicator will
// cover the bottom-right corner of the text view so tell NSWindow not to
// draw it in this situation.
//
// Note: This API is ignored from 10.7 onward and is now deprecated. This
// should be removed if we want to drop support for 10.6.
[[self window] setShowsResizeIndicator:(rightSbVisible||botSbVisible)];
}
}
- (NSUInteger)representedIndexOfTabViewItem:(NSTabViewItem *)tvi
{
NSArray *tabViewItems = [[self tabBarControl] representedTabViewItems];
return [tabViewItems indexOfObject:tvi];
}
- (MMScroller *)scrollbarForIdentifier:(int32_t)ident index:(unsigned *)idx
{
unsigned i, count = [scrollbars count];
for (i = 0; i < count; ++i) {
MMScroller *scroller = [scrollbars objectAtIndex:i];
if ([scroller scrollerId] == ident) {
if (idx) *idx = i;
return scroller;
}
}
return nil;
}
- (NSSize)vimViewSizeForTextViewSize:(NSSize)textViewSize
{
NSSize size = textViewSize;
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_7)
CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:NSControlSizeRegular scrollerStyle:NSScrollerStyleLegacy];
#else
CGFloat scrollerWidth = [NSScroller scrollerWidth];
#endif
if (![[self tabBarControl] isHidden])
size.height += [[self tabBarControl] frame].size.height;
if ([self bottomScrollbarVisible])
size.height += scrollerWidth;
if ([self leftScrollbarVisible])
size.width += scrollerWidth;
if ([self rightScrollbarVisible])
size.width += scrollerWidth;
return size;
}
- (NSRect)textViewRectForVimViewSize:(NSSize)contentSize
{
NSRect rect = { {0, 0}, {contentSize.width, contentSize.height} };
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_7)
CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:NSControlSizeRegular scrollerStyle:NSScrollerStyleLegacy];
#else
CGFloat scrollerWidth = [NSScroller scrollerWidth];
#endif
if (![[self tabBarControl] isHidden])
rect.size.height -= [[self tabBarControl] frame].size.height;
if ([self bottomScrollbarVisible]) {
rect.size.height -= scrollerWidth;
rect.origin.y += scrollerWidth;
}
if ([self leftScrollbarVisible]) {
rect.size.width -= scrollerWidth;
rect.origin.x += scrollerWidth;
}
if ([self rightScrollbarVisible])
rect.size.width -= scrollerWidth;
return rect;
}
- (NSTabView *)tabView
{
return tabView;
}
- (void)frameSizeMayHaveChanged:(BOOL)keepGUISize
{
// NOTE: Whenever a call is made that may have changed the frame size we
// take the opportunity to make sure all subviews are in place and that the
// (rows,columns) are constrained to lie inside the new frame. We not only
// do this when the frame really has changed since it is possible to modify
// the number of (rows,columns) without changing the frame size.
// Give all superfluous space to the text view. It might be smaller or
// larger than it wants to be, but this is needed during live resizing.
NSRect textViewRect = [self textViewRectForVimViewSize:[self frame].size];
[textView setFrame:textViewRect];
// Immediately place the scrollbars instead of deferring till later here.
// Deferral ended up causing some bugs, in particular when in <10.14
// CoreText renderer where [NSAnimationContext beginGrouping] is used to
// bundle state changes together and the deferred placeScrollbars would get
// the wrong data to use. An alternative would be to check for that and only
// call finishPlaceScrollbars once we call [NSAnimationContext endGrouping]
// but that makes the code mode complicated. Just do it here and the
// performance is fine as this gets called occasionally only
// (pendingPlaceScrollbars is mostly for the case if we are adding a lot of
// scrollbars at once we want to only call placeScrollbars once instead of
// doing it N times).
self.pendingPlaceScrollbars = NO;
[self placeScrollbars];
// It is possible that the current number of (rows,columns) is too big or
// too small to fit the new frame. If so, notify Vim that the text
// dimensions should change, but don't actually change the number of
// (rows,columns). These numbers may only change when Vim initiates the
// change (as opposed to the user dragging the window resizer, for
// example).
//
// Note that the message sent to Vim depends on whether we're in
// a live resize or not -- this is necessary to avoid the window jittering
// when the user drags to resize.
int constrained[2];
NSSize textViewSize = [textView frame].size;
[textView constrainRows:&constrained[0] columns:&constrained[1]
toSize:textViewSize];
int rows, cols;
[textView getMaxRows:&rows columns:&cols];
if (constrained[0] != rows || constrained[1] != cols) {
NSData *data = [NSData dataWithBytes:constrained length:2*sizeof(int)];
int msgid = [self inLiveResize] ? LiveResizeMsgID
: (keepGUISize ? SetTextDimensionsNoResizeWindowMsgID : SetTextDimensionsMsgID);
ASLogDebug(@"Notify Vim that text dimensions changed from %dx%d to "
"%dx%d (%s)", cols, rows, constrained[1], constrained[0],
MMVimMsgIDStrings[msgid]);
if (msgid != LiveResizeMsgID || !self.pendingLiveResize) {
// Live resize messages can be sent really rapidly, especailly if
// it's from double clicking the window border (to indicate filling
// all the way to that side to the window manager). We want to rate
// limit sending live resize one at a time, or the IPC will get
// swamped which causes slowdowns and some messages will also be dropped.
// As a result we basically discard all live resize messages if one
// is already going on. liveResizeDidEnd: will perform a final clean
// up resizing.
self.pendingLiveResize = (msgid == LiveResizeMsgID);
[vimController sendMessageNow:msgid data:data timeout:1];
}
// We only want to set the window title if this resize came from
// a live-resize, not (for example) setting 'columns' or 'lines'.
if ([self inLiveResize]) {
[[self window] setTitle:[NSString stringWithFormat:@"%dx%d",
constrained[1], constrained[0]]];
}
}
}
@end // MMVimView (Private)