mirror of
https://github.com/textmate/textmate.git
synced 2026-04-06 03:01:29 -04:00
This view has an active/inactive color or image. The latter is drawn as a pattern color (tiled image).
1185 lines
40 KiB
Plaintext
1185 lines
40 KiB
Plaintext
#import "OakDocumentView.h"
|
||
#import "GutterView.h"
|
||
#import "OTVStatusBar.h"
|
||
#import <document/document.h>
|
||
#import <file/type.h>
|
||
#import <text/ctype.h>
|
||
#import <text/parse.h>
|
||
#import <ns/ns.h>
|
||
#import <oak/debug.h>
|
||
#import <bundles/bundles.h>
|
||
#import <OakFilterList/SymbolChooser.h>
|
||
#import <OakFoundation/NSString Additions.h>
|
||
#import <OakFoundation/NSArray Additions.h>
|
||
#import <OakAppKit/OakAppKit.h>
|
||
#import <OakAppKit/NSColor Additions.h>
|
||
#import <OakAppKit/NSImage Additions.h>
|
||
#import <OakAppKit/OakToolTip.h>
|
||
#import <OakAppKit/OakPasteboard.h>
|
||
#import <OakAppKit/OakPasteboardChooser.h>
|
||
#import <OakAppKit/OakUIConstructionFunctions.h>
|
||
#import <OakAppKit/NSMenuItem Additions.h>
|
||
#import <BundleMenu/BundleMenu.h>
|
||
|
||
OAK_DEBUG_VAR(OakDocumentView);
|
||
|
||
static NSString* const kBookmarksColumnIdentifier = @"bookmarks";
|
||
static NSString* const kFoldingsColumnIdentifier = @"foldings";
|
||
|
||
@interface OakDisableAccessibilityScrollView : NSScrollView
|
||
@end
|
||
|
||
@implementation OakDisableAccessibilityScrollView
|
||
- (BOOL)accessibilityIsIgnored
|
||
{
|
||
return YES;
|
||
}
|
||
@end
|
||
|
||
@interface OakDocumentView () <GutterViewDelegate, GutterViewColumnDataSource, GutterViewColumnDelegate, OTVStatusBarDelegate>
|
||
{
|
||
OBJC_WATCH_LEAKS(OakDocumentView);
|
||
|
||
NSScrollView* gutterScrollView;
|
||
GutterView* gutterView;
|
||
NSColor* gutterDividerColor;
|
||
|
||
OakBackgroundFillView* gutterDividerView;
|
||
OakBackgroundFillView* statusDividerView;
|
||
|
||
NSScrollView* textScrollView;
|
||
OakTextView* textView;
|
||
OTVStatusBar* statusBar;
|
||
document::document_ptr document;
|
||
document::document_t::callback_t* callback;
|
||
|
||
NSMutableArray* topAuxiliaryViews;
|
||
NSMutableArray* bottomAuxiliaryViews;
|
||
|
||
IBOutlet NSPanel* tabSizeSelectorPanel;
|
||
}
|
||
@property (nonatomic, readonly) OTVStatusBar* statusBar;
|
||
@property (nonatomic) NSDictionary* gutterImages;
|
||
@property (nonatomic) NSDictionary* gutterHoverImages;
|
||
@property (nonatomic) NSDictionary* gutterPressedImages;
|
||
@property (nonatomic) SymbolChooser* symbolChooser;
|
||
@property (nonatomic) NSArray* observedKeys;
|
||
- (void)updateStyle;
|
||
@end
|
||
|
||
struct document_view_callback_t : document::document_t::callback_t
|
||
{
|
||
WATCH_LEAKS(document_view_callback_t);
|
||
document_view_callback_t (OakDocumentView* self) : self(self) { }
|
||
void handle_document_event (document::document_ptr document, event_t event)
|
||
{
|
||
if(event == did_change_marks)
|
||
{
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:GVColumnDataSourceDidChange object:self];
|
||
}
|
||
else if(event == did_change_file_type)
|
||
{
|
||
for(auto const& item : bundles::query(bundles::kFieldGrammarScope, document->file_type()))
|
||
self.statusBar.grammarName = [NSString stringWithCxxString:item->name()];
|
||
}
|
||
else if(event == did_change_indent_settings)
|
||
{
|
||
self.statusBar.tabSize = document->buffer().indent().tab_size();
|
||
self.statusBar.softTabs = document->buffer().indent().soft_tabs();
|
||
}
|
||
|
||
if(document->recent_tracking() && document->path() != NULL_STR)
|
||
{
|
||
if(event == did_save || event == did_change_path || (event == did_change_open_status && document->is_open()))
|
||
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:[NSString stringWithCxxString:document->path()]]];
|
||
}
|
||
}
|
||
private:
|
||
__weak OakDocumentView* self;
|
||
};
|
||
|
||
@implementation OakDocumentView
|
||
@synthesize textView, statusBar;
|
||
|
||
- (id)initWithFrame:(NSRect)aRect
|
||
{
|
||
D(DBF_OakDocumentView, bug("%s\n", [NSStringFromRect(aRect) UTF8String]););
|
||
if(self = [super initWithFrame:aRect])
|
||
{
|
||
callback = new document_view_callback_t(self);
|
||
|
||
textView = [[OakTextView alloc] initWithFrame:NSZeroRect];
|
||
textView.autoresizingMask = NSViewWidthSizable|NSViewHeightSizable;
|
||
|
||
textScrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect];
|
||
textScrollView.hasVerticalScroller = YES;
|
||
textScrollView.hasHorizontalScroller = YES;
|
||
textScrollView.autohidesScrollers = YES;
|
||
textScrollView.borderType = NSNoBorder;
|
||
textScrollView.documentView = textView;
|
||
[self addSubview:textScrollView];
|
||
|
||
gutterView = [[GutterView alloc] initWithFrame:NSZeroRect];
|
||
gutterView.partnerView = textView;
|
||
gutterView.delegate = self;
|
||
[gutterView insertColumnWithIdentifier:kBookmarksColumnIdentifier atPosition:0 dataSource:self delegate:self];
|
||
[gutterView insertColumnWithIdentifier:kFoldingsColumnIdentifier atPosition:2 dataSource:self delegate:self];
|
||
|
||
gutterScrollView = [[OakDisableAccessibilityScrollView alloc] initWithFrame:NSZeroRect];
|
||
gutterScrollView.borderType = NSNoBorder;
|
||
gutterScrollView.documentView = gutterView;
|
||
[self addSubview:gutterScrollView];
|
||
|
||
if([[NSUserDefaults standardUserDefaults] boolForKey:@"DocumentView Disable Line Numbers"])
|
||
[gutterView setVisibility:NO forColumnWithIdentifier:GVLineNumbersColumnIdentifier];
|
||
|
||
gutterDividerView = OakCreateVerticalLine(nil);
|
||
[self addSubview:gutterDividerView];
|
||
|
||
statusDividerView = OakCreateHorizontalLine([NSColor colorWithCalibratedWhite:0.500 alpha:1], [NSColor colorWithCalibratedWhite:0.750 alpha:1]);
|
||
[self addSubview:statusDividerView];
|
||
|
||
statusBar = [[OTVStatusBar alloc] initWithFrame:NSZeroRect];
|
||
statusBar.delegate = self;
|
||
statusBar.target = self;
|
||
[self addSubview:statusBar];
|
||
|
||
for(NSView* view in @[ gutterScrollView, gutterView, gutterDividerView, textScrollView, statusDividerView, statusBar ])
|
||
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||
|
||
document::document_ptr doc = document::from_content("", "text.plain"); // file type is only to avoid potential “no grammar” warnings in console
|
||
doc->set_custom_name("null document"); // without a name it grabs an ‘untitled’ token
|
||
[self setDocument:doc];
|
||
|
||
self.observedKeys = @[ @"selectionString", @"tabSize", @"softTabs", @"isMacroRecording"];
|
||
for(NSString* keyPath in self.observedKeys)
|
||
[textView addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionInitial context:NULL];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
+ (BOOL)requiresConstraintBasedLayout
|
||
{
|
||
return YES;
|
||
}
|
||
|
||
- (void)updateConstraints
|
||
{
|
||
[self removeConstraints:[self constraints]];
|
||
[super updateConstraints];
|
||
|
||
NSMutableArray* stackedViews = [NSMutableArray array];
|
||
[stackedViews addObjectsFromArray:topAuxiliaryViews];
|
||
[stackedViews addObject:gutterScrollView];
|
||
[stackedViews addObjectsFromArray:bottomAuxiliaryViews];
|
||
|
||
if(statusBar)
|
||
{
|
||
[stackedViews addObjectsFromArray:@[ statusDividerView, statusBar ]];
|
||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[statusBar(==statusDividerView)]|" options:NSLayoutFormatAlignAllLeft|NSLayoutFormatAlignAllRight metrics:nil views:NSDictionaryOfVariableBindings(statusDividerView, statusBar)]];
|
||
}
|
||
|
||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[gutterScrollView(==gutterView)][gutterDividerView][textScrollView(>=100)]|" options:NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom metrics:nil views:NSDictionaryOfVariableBindings(gutterScrollView, gutterView, gutterDividerView, textScrollView)]];
|
||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[topView]" options:0 metrics:nil views:@{ @"topView" : stackedViews[0] }]];
|
||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[bottomView]|" options:0 metrics:nil views:@{ @"bottomView" : [stackedViews lastObject] }]];
|
||
|
||
for(size_t i = 0; i < [stackedViews count]-1; ++i)
|
||
[self addConstraint:[NSLayoutConstraint constraintWithItem:stackedViews[i] attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:stackedViews[i+1] attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
|
||
|
||
NSArray* array[] = { topAuxiliaryViews, bottomAuxiliaryViews };
|
||
for(NSArray* views : array)
|
||
{
|
||
for(NSView* view in views)
|
||
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)]];
|
||
}
|
||
}
|
||
|
||
- (void)setHideStatusBar:(BOOL)flag
|
||
{
|
||
if(_hideStatusBar == flag)
|
||
return;
|
||
|
||
_hideStatusBar = flag;
|
||
if(_hideStatusBar)
|
||
{
|
||
[statusDividerView removeFromSuperview];
|
||
statusDividerView = nil;
|
||
|
||
[statusBar removeFromSuperview];
|
||
statusBar.delegate = nil;
|
||
statusBar.target = nil;
|
||
statusBar = nil;
|
||
}
|
||
else
|
||
{
|
||
statusDividerView = OakCreateHorizontalLine([NSColor colorWithCalibratedWhite:0.500 alpha:1], [NSColor colorWithCalibratedWhite:0.750 alpha:1]);
|
||
[self addSubview:statusDividerView];
|
||
|
||
statusBar = [[OTVStatusBar alloc] initWithFrame:NSZeroRect];
|
||
statusBar.delegate = self;
|
||
statusBar.target = self;
|
||
[self addSubview:statusBar];
|
||
}
|
||
[self setNeedsUpdateConstraints:YES];
|
||
}
|
||
|
||
- (NSImage*)gutterImage:(NSString*)aName
|
||
{
|
||
if(NSImage* res = [[NSImage imageNamed:aName inSameBundleAsClass:[self class]] copy])
|
||
{
|
||
// We use capHeight instead of x-height since most fonts have the numbers
|
||
// extend to this height, so centering around the x-height would look off
|
||
CGFloat height = [gutterView.lineNumberFont capHeight];
|
||
CGFloat width = [res size].width * height / [res size].height;
|
||
|
||
CGFloat scaleFactor = 1;
|
||
|
||
// Since all images are vector based and don’t contain any spacing to
|
||
// align it, we need to set the individual scaleFactor per image.
|
||
if([aName hasPrefix:@"Bookmark"]) scaleFactor = 1.0;
|
||
if([aName hasPrefix:@"Folding"]) scaleFactor = 1.5;
|
||
if([aName hasPrefix:@"Search"]) scaleFactor = 1.2;
|
||
|
||
[res setSize:NSMakeSize(round(width * scaleFactor), round(height * scaleFactor))];
|
||
|
||
return res;
|
||
}
|
||
NSLog(@"%s no image named ‘%@’", sel_getName(_cmd), aName);
|
||
return nil;
|
||
}
|
||
|
||
- (void)setFont:(NSFont*)newFont
|
||
{
|
||
textView.font = newFont;
|
||
gutterView.lineNumberFont = [NSFont fontWithName:[newFont fontName] size:round(0.8 * [newFont pointSize])];
|
||
|
||
self.gutterImages = @{
|
||
kBookmarksColumnIdentifier : @[ [NSNull null], [self gutterImage:@"Bookmark"], [self gutterImage:@"Search Mark"] ],
|
||
kFoldingsColumnIdentifier : @[ [NSNull null], [self gutterImage:@"Folding Top"], [self gutterImage:@"Folding Collapsed"], [self gutterImage:@"Folding Bottom"] ],
|
||
};
|
||
|
||
self.gutterHoverImages = @{
|
||
kBookmarksColumnIdentifier : @[ [self gutterImage:@"Bookmark Hover Add"], [self gutterImage:@"Bookmark Hover Remove"], [self gutterImage:@"Bookmark Hover Add"] ],
|
||
kFoldingsColumnIdentifier : @[ [NSNull null], [self gutterImage:@"Folding Top Hover"], [self gutterImage:@"Folding Collapsed Hover"], [self gutterImage:@"Folding Bottom Hover"] ],
|
||
};
|
||
|
||
self.gutterPressedImages = @{
|
||
kBookmarksColumnIdentifier : @[ [self gutterImage:@"Bookmark"], [self gutterImage:@"Bookmark"], [self gutterImage:@"Bookmark"] ],
|
||
kFoldingsColumnIdentifier : @[ [NSNull null], [self gutterImage:@"Folding Top Hover"], [self gutterImage:@"Folding Collapsed Hover"], [self gutterImage:@"Folding Bottom Hover"] ],
|
||
};
|
||
|
||
[gutterView reloadData:self];
|
||
}
|
||
|
||
- (IBAction)makeTextLarger:(id)sender { [self setFont:[NSFont fontWithName:[textView.font fontName] size:[textView.font pointSize] + 1]]; }
|
||
- (IBAction)makeTextSmaller:(id)sender { [self setFont:[NSFont fontWithName:[textView.font fontName] size:std::max<CGFloat>([textView.font pointSize] - 1, 5)]]; }
|
||
|
||
- (void)changeFont:(id)sender
|
||
{
|
||
if(NSFont* newFont = [sender convertFont:textView.font ?: [NSFont userFixedPitchFontOfSize:0]])
|
||
{
|
||
settings_t::set(kSettingsFontNameKey, to_s([newFont fontName]));
|
||
settings_t::set(kSettingsFontSizeKey, [newFont pointSize]);
|
||
[self setFont:newFont];
|
||
}
|
||
}
|
||
|
||
- (void)observeValueForKeyPath:(NSString*)aKeyPath ofObject:(id)observableController change:(NSDictionary*)changeDictionary context:(void*)userData
|
||
{
|
||
if(observableController != textView || ![self.observedKeys containsObject:aKeyPath])
|
||
return;
|
||
|
||
if([aKeyPath isEqualToString:@"selectionString"])
|
||
{
|
||
NSString* str = [textView valueForKey:@"selectionString"];
|
||
[gutterView setHighlightedRange:to_s(str ?: @"1")];
|
||
[statusBar setSelectionString:str];
|
||
_symbolChooser.selectionString = str;
|
||
|
||
ng::buffer_t const& buf = document->buffer();
|
||
text::selection_t sel(to_s(str));
|
||
size_t i = buf.convert(sel.last().max());
|
||
statusBar.symbolName = [NSString stringWithCxxString:buf.symbol_at(i)];
|
||
}
|
||
else if([aKeyPath isEqualToString:@"tabSize"])
|
||
{
|
||
statusBar.tabSize = textView.tabSize;
|
||
}
|
||
else if([aKeyPath isEqualToString:@"softTabs"])
|
||
{
|
||
statusBar.softTabs = textView.softTabs;
|
||
}
|
||
else
|
||
{
|
||
[statusBar setValue:[textView valueForKey:aKeyPath] forKey:aKeyPath];
|
||
}
|
||
}
|
||
|
||
- (void)dealloc
|
||
{
|
||
gutterView.partnerView = nil;
|
||
gutterView.delegate = nil;
|
||
statusBar.delegate = nil;
|
||
|
||
for(NSString* keyPath in self.observedKeys)
|
||
[textView removeObserver:self forKeyPath:keyPath];
|
||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||
|
||
[self setDocument:document::document_ptr()];
|
||
delete callback;
|
||
|
||
self.symbolChooser = nil;
|
||
}
|
||
|
||
- (document::document_ptr const&)document
|
||
{
|
||
return document;
|
||
}
|
||
|
||
- (void)setDocument:(document::document_ptr const&)aDocument
|
||
{
|
||
document::document_ptr oldDocument = document;
|
||
if(oldDocument)
|
||
oldDocument->remove_callback(callback);
|
||
|
||
if(aDocument)
|
||
aDocument->sync_open();
|
||
|
||
if(document = aDocument)
|
||
{
|
||
document->add_callback(callback);
|
||
document->show();
|
||
|
||
for(auto const& item : bundles::query(bundles::kFieldGrammarScope, document->file_type()))
|
||
statusBar.grammarName = [NSString stringWithCxxString:item->name()];
|
||
statusBar.tabSize = document->buffer().indent().tab_size();
|
||
statusBar.softTabs = document->buffer().indent().soft_tabs();
|
||
}
|
||
|
||
[textView setDocument:document];
|
||
[gutterView reloadData:self];
|
||
[self updateStyle];
|
||
|
||
if(_symbolChooser)
|
||
{
|
||
_symbolChooser.document = document;
|
||
_symbolChooser.selectionString = textView.selectionString;
|
||
}
|
||
|
||
if(oldDocument)
|
||
{
|
||
oldDocument->hide();
|
||
oldDocument->close();
|
||
}
|
||
}
|
||
|
||
- (void)updateStyle
|
||
{
|
||
if(document && [textView theme])
|
||
{
|
||
auto theme = [textView theme];
|
||
|
||
[[self window] setOpaque:!theme->is_transparent() && !theme->gutter_styles().is_transparent()];
|
||
[textScrollView setBackgroundColor:[NSColor tmColorWithCGColor:theme->background(document->file_type())]];
|
||
|
||
if(theme->is_dark())
|
||
{
|
||
NSImage* whiteIBeamImage = [NSImage imageNamed:@"IBeam white" inSameBundleAsClass:[self class]];
|
||
[whiteIBeamImage setSize:[[[NSCursor IBeamCursor] image] size]];
|
||
[textView setIbeamCursor:[[NSCursor alloc] initWithImage:whiteIBeamImage hotSpot:NSMakePoint(4, 9)]];
|
||
[textScrollView setScrollerKnobStyle:NSScrollerKnobStyleLight];
|
||
}
|
||
else
|
||
{
|
||
[textView setIbeamCursor:[NSCursor IBeamCursor]];
|
||
[textScrollView setScrollerKnobStyle:NSScrollerKnobStyleDark];
|
||
}
|
||
|
||
[self setFont:textView.font]; // trigger update of gutter view’s line number font
|
||
auto const& styles = theme->gutter_styles();
|
||
|
||
gutterView.foregroundColor = [NSColor tmColorWithCGColor:styles.foreground];
|
||
gutterView.backgroundColor = [NSColor tmColorWithCGColor:styles.background];
|
||
gutterView.iconColor = [NSColor tmColorWithCGColor:styles.icons];
|
||
gutterView.iconHoverColor = [NSColor tmColorWithCGColor:styles.iconsHover];
|
||
gutterView.iconPressedColor = [NSColor tmColorWithCGColor:styles.iconsPressed];
|
||
gutterView.selectionForegroundColor = [NSColor tmColorWithCGColor:styles.selectionForeground];
|
||
gutterView.selectionBackgroundColor = [NSColor tmColorWithCGColor:styles.selectionBackground];
|
||
gutterView.selectionIconColor = [NSColor tmColorWithCGColor:styles.selectionIcons];
|
||
gutterView.selectionIconHoverColor = [NSColor tmColorWithCGColor:styles.selectionIconsHover];
|
||
gutterView.selectionIconPressedColor = [NSColor tmColorWithCGColor:styles.selectionIconsPressed];
|
||
gutterView.selectionBorderColor = [NSColor tmColorWithCGColor:styles.selectionBorder];
|
||
gutterScrollView.backgroundColor = gutterView.backgroundColor;
|
||
gutterDividerView.activeBackgroundColor = [NSColor tmColorWithCGColor:styles.divider];
|
||
|
||
[gutterView setNeedsDisplay:YES];
|
||
}
|
||
}
|
||
|
||
- (IBAction)toggleLineNumbers:(id)sender
|
||
{
|
||
D(DBF_OakDocumentView, bug("show line numbers %s\n", BSTR([gutterView visibilityForColumnWithIdentifier:GVLineNumbersColumnIdentifier])););
|
||
BOOL isVisibleFlag = ![gutterView visibilityForColumnWithIdentifier:GVLineNumbersColumnIdentifier];
|
||
[gutterView setVisibility:isVisibleFlag forColumnWithIdentifier:GVLineNumbersColumnIdentifier];
|
||
if(isVisibleFlag)
|
||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"DocumentView Disable Line Numbers"];
|
||
else [[NSUserDefaults standardUserDefaults] setObject:@YES forKey:@"DocumentView Disable Line Numbers"];
|
||
}
|
||
|
||
- (BOOL)validateMenuItem:(NSMenuItem*)aMenuItem
|
||
{
|
||
if([aMenuItem action] == @selector(toggleLineNumbers:))
|
||
[aMenuItem setTitle:[gutterView visibilityForColumnWithIdentifier:GVLineNumbersColumnIdentifier] ? @"Hide Line Numbers" : @"Show Line Numbers"];
|
||
else if([aMenuItem action] == @selector(takeThemeUUIDFrom:))
|
||
[aMenuItem setState:[textView theme]->uuid() == [[aMenuItem representedObject] UTF8String] ? NSOnState : NSOffState];
|
||
else if([aMenuItem action] == @selector(takeTabSizeFrom:))
|
||
[aMenuItem setState:textView.tabSize == [aMenuItem tag] ? NSOnState : NSOffState];
|
||
else if([aMenuItem action] == @selector(showTabSizeSelectorPanel:))
|
||
{
|
||
static NSInteger const predefined[] = { 2, 3, 4, 8 };
|
||
if(oak::contains(std::begin(predefined), std::end(predefined), textView.tabSize))
|
||
{
|
||
[aMenuItem setTitle:@"Other…"];
|
||
[aMenuItem setState:NSOffState];
|
||
}
|
||
else
|
||
{
|
||
[aMenuItem setTitle:[NSString stringWithFormat:@"Other (%zd)…", textView.tabSize]];
|
||
[aMenuItem setState:NSOnState];
|
||
}
|
||
}
|
||
else if([aMenuItem action] == @selector(setIndentWithTabs:))
|
||
[aMenuItem setState:textView.softTabs ? NSOffState : NSOnState];
|
||
else if([aMenuItem action] == @selector(setIndentWithSpaces:))
|
||
[aMenuItem setState:textView.softTabs ? NSOnState : NSOffState];
|
||
else if([aMenuItem action] == @selector(takeGrammarUUIDFrom:))
|
||
{
|
||
NSString* uuidString = [aMenuItem representedObject];
|
||
if(bundles::item_ptr bundleItem = bundles::lookup(to_s(uuidString)))
|
||
{
|
||
bool selectedGrammar = document && document->file_type() == bundleItem->value_for_field(bundles::kFieldGrammarScope);
|
||
[aMenuItem setState:selectedGrammar ? NSOnState : NSOffState];
|
||
}
|
||
}
|
||
else if([aMenuItem action] == @selector(toggleCurrentBookmark:))
|
||
{
|
||
text::selection_t sel([textView.selectionString UTF8String]);
|
||
size_t lineNumber = sel.last().max().line;
|
||
|
||
ng::buffer_t const& buf = document->buffer();
|
||
[aMenuItem setTitle:buf.get_marks(buf.begin(lineNumber), buf.eol(lineNumber), kBookmarkType).empty() ? @"Set Bookmark" : @"Remove Bookmark"];
|
||
}
|
||
return YES;
|
||
}
|
||
|
||
// ===================
|
||
// = Auxiliary Views =
|
||
// ===================
|
||
|
||
- (void)addAuxiliaryView:(NSView*)aView atEdge:(NSRectEdge)anEdge
|
||
{
|
||
[aView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||
|
||
topAuxiliaryViews = topAuxiliaryViews ?: [NSMutableArray new];
|
||
bottomAuxiliaryViews = bottomAuxiliaryViews ?: [NSMutableArray new];
|
||
if(anEdge == NSMinYEdge)
|
||
[bottomAuxiliaryViews addObject:aView];
|
||
else [topAuxiliaryViews addObject:aView];
|
||
[self addSubview:aView];
|
||
[self setNeedsUpdateConstraints:YES];
|
||
}
|
||
|
||
- (void)removeAuxiliaryView:(NSView*)aView
|
||
{
|
||
if([topAuxiliaryViews containsObject:aView])
|
||
[topAuxiliaryViews removeObject:aView];
|
||
else if([bottomAuxiliaryViews containsObject:aView])
|
||
[bottomAuxiliaryViews removeObject:aView];
|
||
else
|
||
return;
|
||
[aView removeFromSuperview];
|
||
[self setNeedsUpdateConstraints:YES];
|
||
}
|
||
|
||
// ======================
|
||
// = Pasteboard History =
|
||
// ======================
|
||
|
||
- (void)showClipboardHistory:(id)sender
|
||
{
|
||
OakPasteboardChooser* chooser = [OakPasteboardChooser sharedChooserForName:NSGeneralPboard];
|
||
chooser.action = @selector(paste:);
|
||
[chooser showWindowRelativeToFrame:[self.window convertRectToScreen:[textView convertRect:[textView visibleRect] toView:nil]]];
|
||
}
|
||
|
||
- (void)showFindHistory:(id)sender
|
||
{
|
||
OakPasteboardChooser* chooser = [OakPasteboardChooser sharedChooserForName:NSFindPboard];
|
||
chooser.action = @selector(findNext:);
|
||
[chooser showWindowRelativeToFrame:[self.window convertRectToScreen:[textView convertRect:[textView visibleRect] toView:nil]]];
|
||
}
|
||
|
||
// ==================
|
||
// = Symbol Chooser =
|
||
// ==================
|
||
|
||
- (void)selectAndCenter:(NSString*)aSelectionString
|
||
{
|
||
textView.selectionString = aSelectionString;
|
||
[textView centerSelectionInVisibleArea:self];
|
||
}
|
||
|
||
- (void)setSymbolChooser:(SymbolChooser*)aSymbolChooser
|
||
{
|
||
if(_symbolChooser == aSymbolChooser)
|
||
return;
|
||
|
||
if(_symbolChooser)
|
||
{
|
||
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:_symbolChooser.window];
|
||
|
||
_symbolChooser.target = nil;
|
||
_symbolChooser.document = document::document_ptr();
|
||
}
|
||
|
||
if(_symbolChooser = aSymbolChooser)
|
||
{
|
||
_symbolChooser.target = self;
|
||
_symbolChooser.action = @selector(symbolChooserDidSelectItems:);
|
||
_symbolChooser.filterString = @"";
|
||
_symbolChooser.document = document;
|
||
_symbolChooser.selectionString = textView.selectionString;
|
||
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(symbolChooserWillClose:) name:NSWindowWillCloseNotification object:_symbolChooser.window];
|
||
}
|
||
}
|
||
|
||
- (void)symbolChooserWillClose:(NSNotification*)aNotification
|
||
{
|
||
self.symbolChooser = nil;
|
||
}
|
||
|
||
- (IBAction)showSymbolChooser:(id)sender
|
||
{
|
||
self.symbolChooser = [SymbolChooser sharedInstance];
|
||
[self.symbolChooser showWindowRelativeToFrame:[self.window convertRectToScreen:[textView convertRect:[textView visibleRect] toView:nil]]];
|
||
}
|
||
|
||
- (void)symbolChooserDidSelectItems:(id)sender
|
||
{
|
||
for(id item in [sender selectedItems])
|
||
[self selectAndCenter:[item selectionString]];
|
||
}
|
||
|
||
// =======================
|
||
// = Status bar delegate =
|
||
// =======================
|
||
|
||
- (void)takeGrammarUUIDFrom:(id)sender
|
||
{
|
||
if(bundles::item_ptr item = bundles::lookup(to_s((NSString*)[sender representedObject])))
|
||
[textView performBundleItem:item];
|
||
}
|
||
|
||
- (void)goToSymbol:(id)sender
|
||
{
|
||
[self selectAndCenter:[sender representedObject]];
|
||
}
|
||
|
||
- (void)showSymbolSelector:(NSPopUpButton*)symbolPopUp
|
||
{
|
||
NSMenu* symbolMenu = symbolPopUp.menu;
|
||
[symbolMenu removeAllItems];
|
||
|
||
ng::buffer_t const& buf = document->buffer();
|
||
text::selection_t sel([textView.selectionString UTF8String]);
|
||
size_t i = buf.convert(sel.last().max());
|
||
|
||
NSInteger index = 0;
|
||
for(auto pair : buf.symbols())
|
||
{
|
||
if(pair.second == "-")
|
||
{
|
||
[symbolMenu addItem:[NSMenuItem separatorItem]];
|
||
}
|
||
else
|
||
{
|
||
std::string const emSpace = " ";
|
||
|
||
std::string::size_type offset = 0;
|
||
while(pair.second.find(emSpace, offset) == offset)
|
||
offset += emSpace.size();
|
||
|
||
NSMenuItem* item = [symbolMenu addItemWithTitle:[NSString stringWithCxxString:pair.second.substr(offset)] action:@selector(goToSymbol:) keyEquivalent:@""];
|
||
[item setIndentationLevel:offset / emSpace.size()];
|
||
[item setTarget:self];
|
||
[item setRepresentedObject:[NSString stringWithCxxString:buf.convert(pair.first)]];
|
||
}
|
||
|
||
if(pair.first <= i)
|
||
++index;
|
||
}
|
||
|
||
if(symbolMenu.numberOfItems == 0)
|
||
[symbolMenu addItemWithTitle:@"No symbols to show for current document." action:@selector(nop:) keyEquivalent:@""];
|
||
|
||
[symbolPopUp selectItemAtIndex:(index ? index-1 : 0)];
|
||
}
|
||
|
||
- (void)showBundlesMenu:(id)sender
|
||
{
|
||
[NSApp sendAction:_cmd to:self.statusBar from:self];
|
||
}
|
||
|
||
- (void)showBundleItemSelector:(NSPopUpButton*)bundleItemsPopUp
|
||
{
|
||
NSMenu* bundleItemsMenu = bundleItemsPopUp.menu;
|
||
[bundleItemsMenu removeAllItems];
|
||
|
||
std::multimap<std::string, bundles::item_ptr, text::less_t> ordered;
|
||
for(auto item : bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle))
|
||
ordered.emplace(item->name(), item);
|
||
|
||
NSMenuItem* selectedItem = nil;
|
||
for(auto pair : ordered)
|
||
{
|
||
bool selectedGrammar = false;
|
||
for(auto item : bundles::query(bundles::kFieldGrammarScope, document->file_type(), scope::wildcard, bundles::kItemTypeGrammar, pair.second->uuid(), true, true))
|
||
selectedGrammar = true;
|
||
if(!selectedGrammar && pair.second->hidden_from_user() || pair.second->menu().empty())
|
||
continue;
|
||
|
||
NSMenuItem* menuItem = [bundleItemsMenu addItemWithTitle:[NSString stringWithCxxString:pair.first] action:NULL keyEquivalent:@""];
|
||
menuItem.submenu = [[NSMenu alloc] initWithTitle:[NSString stringWithCxxString:pair.second->uuid()]];
|
||
menuItem.submenu.delegate = [BundleMenuDelegate sharedInstance];
|
||
|
||
if(selectedGrammar)
|
||
{
|
||
[menuItem setState:NSOnState];
|
||
selectedItem = menuItem;
|
||
}
|
||
}
|
||
|
||
if(ordered.empty())
|
||
[bundleItemsMenu addItemWithTitle:@"No Bundles Loaded" action:@selector(nop:) keyEquivalent:@""];
|
||
|
||
if(selectedItem)
|
||
[bundleItemsPopUp selectItem:selectedItem];
|
||
}
|
||
|
||
- (IBAction)takeTabSizeFrom:(id)sender
|
||
{
|
||
D(DBF_OakDocumentView, bug("\n"););
|
||
ASSERT([sender respondsToSelector:@selector(tag)]);
|
||
if([sender tag] > 0)
|
||
{
|
||
textView.tabSize = [sender tag];
|
||
settings_t::set(kSettingsTabSizeKey, (size_t)[sender tag], document->file_type());
|
||
}
|
||
}
|
||
|
||
- (IBAction)setIndentWithSpaces:(id)sender
|
||
{
|
||
D(DBF_OakDocumentView, bug("\n"););
|
||
textView.softTabs = YES;
|
||
settings_t::set(kSettingsSoftTabsKey, true, document->file_type());
|
||
}
|
||
|
||
- (IBAction)setIndentWithTabs:(id)sender
|
||
{
|
||
D(DBF_OakDocumentView, bug("\n"););
|
||
textView.softTabs = NO;
|
||
settings_t::set(kSettingsSoftTabsKey, false, document->file_type());
|
||
}
|
||
|
||
- (IBAction)showTabSizeSelectorPanel:(id)sender
|
||
{
|
||
if(!tabSizeSelectorPanel)
|
||
[NSBundle loadNibNamed:@"TabSizeSetting" owner:self];
|
||
[tabSizeSelectorPanel makeKeyAndOrderFront:self];
|
||
}
|
||
|
||
- (void)toggleMacroRecording:(id)sender { [textView toggleMacroRecording:sender]; }
|
||
|
||
- (IBAction)takeThemeUUIDFrom:(id)sender
|
||
{
|
||
[self setThemeWithUUID:[sender representedObject]];
|
||
}
|
||
|
||
- (void)setThemeWithUUID:(NSString*)themeUUID
|
||
{
|
||
if(bundles::item_ptr const& themeItem = bundles::lookup(to_s(themeUUID)))
|
||
{
|
||
[textView setTheme:parse_theme(themeItem)];
|
||
settings_t::set(kSettingsThemeKey, to_s(themeUUID));
|
||
[self updateStyle];
|
||
}
|
||
}
|
||
|
||
// =============================
|
||
// = GutterView Delegate Proxy =
|
||
// =============================
|
||
|
||
- (GVLineRecord)lineRecordForPosition:(CGFloat)yPos { return [textView lineRecordForPosition:yPos]; }
|
||
- (GVLineRecord)lineFragmentForLine:(NSUInteger)aLine column:(NSUInteger)aColumn { return [textView lineFragmentForLine:aLine column:aColumn]; }
|
||
|
||
// =========================
|
||
// = GutterView DataSource =
|
||
// =========================
|
||
|
||
enum bookmark_state_t { kBookmarkNoMark, kBookmarkRegularMark, kBookmarkSearchMark };
|
||
|
||
static std::string const kBookmarkType = "bookmark";
|
||
static std::string const kSearchmarkType = "search";
|
||
|
||
- (NSUInteger)stateForColumnWithIdentifier:(id)columnIdentifier atLine:(NSUInteger)lineNumber
|
||
{
|
||
if([columnIdentifier isEqualToString:kBookmarksColumnIdentifier])
|
||
{
|
||
ng::buffer_t const& buf = document->buffer();
|
||
if(!buf.get_marks(buf.begin(lineNumber), buf.eol(lineNumber), kBookmarkType).empty())
|
||
return kBookmarkRegularMark;
|
||
if(!buf.get_marks(buf.begin(lineNumber), buf.eol(lineNumber), kSearchmarkType).empty())
|
||
return kBookmarkSearchMark;
|
||
return kBookmarkNoMark;
|
||
}
|
||
else if([columnIdentifier isEqualToString:kFoldingsColumnIdentifier])
|
||
{
|
||
return [textView foldingStateForLine:lineNumber];
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
- (NSImage*)imageForState:(NSUInteger)state forColumnWithIdentifier:(id)identifier
|
||
{
|
||
NSArray* array = _gutterImages[identifier];
|
||
return [array safeObjectAtIndex:state];
|
||
}
|
||
|
||
- (NSImage*)hoverImageForState:(NSUInteger)state forColumnWithIdentifier:(id)identifier
|
||
{
|
||
NSArray* array = _gutterHoverImages[identifier];
|
||
return [array safeObjectAtIndex:state];
|
||
}
|
||
|
||
- (NSImage*)pressedImageForState:(NSUInteger)state forColumnWithIdentifier:(id)identifier
|
||
{
|
||
NSArray* array = _gutterPressedImages[identifier];
|
||
return [array safeObjectAtIndex:state];
|
||
}
|
||
|
||
// =============================
|
||
// = Bookmark Submenu Delegate =
|
||
// =============================
|
||
|
||
- (void)takeBookmarkFrom:(id)sender
|
||
{
|
||
if([sender respondsToSelector:@selector(representedObject)])
|
||
[self selectAndCenter:[sender representedObject]];
|
||
}
|
||
|
||
- (void)updateBookmarksMenu:(NSMenu*)aMenu
|
||
{
|
||
ng::buffer_t& buf = document->buffer();
|
||
std::map<size_t, std::string> const& marks = buf.get_marks(0, buf.size(), kBookmarkType);
|
||
for(auto const& pair : marks)
|
||
{
|
||
size_t n = buf.convert(pair.first).line;
|
||
NSMenuItem* item = [aMenu addItemWithTitle:[NSString stringWithCxxString:text::pad(n+1, 4) + ": " + buf.substr(buf.begin(n), buf.eol(n))] action:@selector(takeBookmarkFrom:) keyEquivalent:@""];
|
||
[item setRepresentedObject:[NSString stringWithCxxString:buf.convert(pair.first)]];
|
||
}
|
||
|
||
if(!marks.empty())
|
||
[aMenu addItem:[NSMenuItem separatorItem]];
|
||
[aMenu addItemWithTitle:@"Clear Bookmarks" action:marks.empty() ? NULL : @selector(clearAllBookmarks:) keyEquivalent:@""];
|
||
}
|
||
|
||
// =======================
|
||
// = GutterView Delegate =
|
||
// =======================
|
||
|
||
- (void)userDidClickColumnWithIdentifier:(id)columnIdentifier atLine:(NSUInteger)lineNumber
|
||
{
|
||
if([columnIdentifier isEqualToString:kBookmarksColumnIdentifier])
|
||
{
|
||
ng::buffer_t& buf = document->buffer();
|
||
std::map<size_t, std::string> const& marks = buf.get_marks(buf.begin(lineNumber), buf.eol(lineNumber), kBookmarkType);
|
||
for(auto const& pair : marks)
|
||
{
|
||
if(pair.second == kBookmarkType)
|
||
return buf.remove_mark(buf.begin(lineNumber) + pair.first, pair.second);
|
||
}
|
||
buf.set_mark(buf.begin(lineNumber), kBookmarkType);
|
||
}
|
||
else if([columnIdentifier isEqualToString:kFoldingsColumnIdentifier])
|
||
{
|
||
[textView toggleFoldingAtLine:lineNumber recursive:OakIsAlternateKeyOrMouseEvent()];
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:GVColumnDataSourceDidChange object:self];
|
||
}
|
||
}
|
||
|
||
// ====================
|
||
// = Bookmark Actions =
|
||
// ====================
|
||
|
||
- (IBAction)toggleCurrentBookmark:(id)sender
|
||
{
|
||
ng::buffer_t& buf = document->buffer();
|
||
|
||
text::selection_t sel([textView.selectionString UTF8String]);
|
||
size_t lineNumber = sel.last().max().line;
|
||
|
||
std::vector<size_t> toRemove;
|
||
std::map<size_t, std::string> const& marks = buf.get_marks(buf.begin(lineNumber), buf.eol(lineNumber), kBookmarkType);
|
||
for(auto const& pair : marks)
|
||
{
|
||
if(pair.second == kBookmarkType)
|
||
toRemove.push_back(buf.begin(lineNumber) + pair.first);
|
||
}
|
||
|
||
if(toRemove.empty())
|
||
{
|
||
buf.set_mark(buf.convert(sel.last().max()), kBookmarkType);
|
||
}
|
||
else
|
||
{
|
||
for(auto const& index : toRemove)
|
||
buf.remove_mark(index, kBookmarkType);
|
||
}
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:GVColumnDataSourceDidChange object:self];
|
||
}
|
||
|
||
- (IBAction)goToNextBookmark:(id)sender
|
||
{
|
||
text::selection_t sel([textView.selectionString UTF8String]);
|
||
|
||
ng::buffer_t const& buf = document->buffer();
|
||
std::pair<size_t, std::string> const& pair = buf.next_mark(buf.convert(sel.last().max()), kBookmarkType);
|
||
if(pair.second != NULL_STR)
|
||
textView.selectionString = [NSString stringWithCxxString:buf.convert(pair.first)];
|
||
}
|
||
|
||
- (IBAction)goToPreviousBookmark:(id)sender
|
||
{
|
||
text::selection_t sel([textView.selectionString UTF8String]);
|
||
|
||
ng::buffer_t const& buf = document->buffer();
|
||
std::pair<size_t, std::string> const& pair = buf.prev_mark(buf.convert(sel.last().max()), kBookmarkType);
|
||
if(pair.second != NULL_STR)
|
||
textView.selectionString = [NSString stringWithCxxString:buf.convert(pair.first)];
|
||
}
|
||
|
||
- (void)clearAllBookmarks:(id)sender
|
||
{
|
||
document->buffer().remove_all_marks(kBookmarkType);
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:GVColumnDataSourceDidChange object:self];
|
||
}
|
||
|
||
// =================
|
||
// = Accessibility =
|
||
// =================
|
||
|
||
- (BOOL)accessibilityIsIgnored
|
||
{
|
||
return NO;
|
||
}
|
||
|
||
- (NSSet*)myAccessibilityAttributeNames
|
||
{
|
||
static NSSet* set = [NSSet setWithArray:@[
|
||
NSAccessibilityRoleAttribute,
|
||
NSAccessibilityDescriptionAttribute,
|
||
]];
|
||
return set;
|
||
}
|
||
|
||
- (NSArray*)accessibilityAttributeNames
|
||
{
|
||
static NSArray* attributes = [[[self myAccessibilityAttributeNames] setByAddingObjectsFromArray:[super accessibilityAttributeNames]] allObjects];
|
||
return attributes;
|
||
}
|
||
|
||
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute
|
||
{
|
||
if([[self myAccessibilityAttributeNames] containsObject:attribute])
|
||
return NO;
|
||
return [super accessibilityIsAttributeSettable:attribute];
|
||
}
|
||
|
||
- (id)accessibilityAttributeValue:(NSString*)attribute
|
||
{
|
||
if([attribute isEqualToString:NSAccessibilityRoleAttribute])
|
||
return NSAccessibilityGroupRole;
|
||
else if([attribute isEqualToString:NSAccessibilityDescriptionAttribute])
|
||
return @"Editor";
|
||
else
|
||
return [super accessibilityAttributeValue:attribute];
|
||
}
|
||
@end
|
||
|
||
// ============
|
||
// = Printing =
|
||
// ============
|
||
|
||
@interface OakPrintDocumentView : NSView
|
||
{
|
||
document::document_ptr document;
|
||
NSString* fontName;
|
||
CGFloat fontSize;
|
||
|
||
std::shared_ptr<ng::layout_t> layout;
|
||
std::vector<CGRect> pageRects;
|
||
}
|
||
@property (nonatomic) CGFloat pageWidth;
|
||
@property (nonatomic) CGFloat pageHeight;
|
||
@property (nonatomic) CGFloat fontScale;
|
||
@property (nonatomic) NSString* themeUUID;
|
||
|
||
@property (nonatomic) BOOL needsLayout;
|
||
@end
|
||
|
||
@implementation OakPrintDocumentView
|
||
- (id)initWithDocument:(document::document_ptr const&)aDocument fontName:(NSString*)aFontName fontSize:(CGFloat)aFontSize
|
||
{
|
||
if(self = [self initWithFrame:NSZeroRect])
|
||
{
|
||
document = aDocument;
|
||
fontName = aFontName;
|
||
fontSize = aFontSize;
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (BOOL)isFlipped
|
||
{
|
||
return YES;
|
||
}
|
||
|
||
- (NSString*)printJobTitle
|
||
{
|
||
return [NSString stringWithCxxString:document->display_name()];
|
||
}
|
||
|
||
- (BOOL)knowsPageRange:(NSRangePointer)range
|
||
{
|
||
NSPrintInfo* info = [[NSPrintOperation currentOperation] printInfo];
|
||
|
||
NSRect display = NSIntersectionRect(info.imageablePageBounds, (NSRect){ NSZeroPoint, info.paperSize });
|
||
info.leftMargin = NSMinX(display);
|
||
info.rightMargin = info.paperSize.width - NSMaxX(display);
|
||
info.topMargin = info.paperSize.height - NSMaxY(display);
|
||
info.bottomMargin = NSMinY(display);
|
||
|
||
self.pageWidth = floor(info.paperSize.width - info.leftMargin - info.rightMargin);
|
||
self.pageHeight = floor(info.paperSize.height - info.topMargin - info.bottomMargin);
|
||
self.fontScale = [[[info dictionary] objectForKey:NSPrintScalingFactor] floatValue];
|
||
self.themeUUID = [[info dictionary] objectForKey:@"OakPrintThemeUUID"];
|
||
|
||
[self layoutIfNeeded];
|
||
[self setFrame:NSMakeRect(0, 0, self.pageWidth, layout->height())];
|
||
|
||
range->location = 1;
|
||
range->length = pageRects.size();
|
||
|
||
return YES;
|
||
}
|
||
|
||
- (NSRect)rectForPage:(NSInteger)pageNumber
|
||
{
|
||
NSParameterAssert(0 < pageNumber && pageNumber <= pageRects.size());
|
||
return pageRects[pageNumber-1];
|
||
}
|
||
|
||
- (void)drawRect:(NSRect)aRect
|
||
{
|
||
NSEraseRect(aRect);
|
||
if(![NSGraphicsContext currentContextDrawingToScreen] && layout)
|
||
layout->draw((CGContextRef)[[NSGraphicsContext currentContext] graphicsPort], aRect, [self isFlipped], /* selection: */ ng::ranges_t(), /* highlight: */ ng::ranges_t(), /* draw background: */ false);
|
||
}
|
||
|
||
- (void)layoutIfNeeded
|
||
{
|
||
if(!self.needsLayout)
|
||
return;
|
||
|
||
pageRects.clear();
|
||
|
||
theme_ptr theme = parse_theme(bundles::lookup(to_s(self.themeUUID)));
|
||
theme = theme->copy_with_font_name_and_size(to_s(fontName), fontSize * self.fontScale);
|
||
layout = std::make_shared<ng::layout_t>(document->buffer(), theme, /* softWrap: */ true);
|
||
layout->set_viewport_size(CGSizeMake(self.pageWidth, self.pageHeight));
|
||
layout->update_metrics(CGRectMake(0, 0, CGFLOAT_MAX, CGFLOAT_MAX));
|
||
|
||
CGRect pageRect = CGRectMake(0, 0, self.pageWidth, self.pageHeight);
|
||
while(true)
|
||
{
|
||
CGRect lineRect = layout->rect_at_index(layout->index_at_point(CGPointMake(NSMinX(pageRect), NSMaxY(pageRect))).index);
|
||
if(NSMaxY(lineRect) <= NSMinY(pageRect))
|
||
break;
|
||
else if(CGRectContainsRect(pageRect, lineRect))
|
||
pageRect.size.height = NSMaxY(lineRect) - NSMinY(pageRect);
|
||
else
|
||
pageRect.size.height = NSMinY(lineRect) - NSMinY(pageRect);
|
||
pageRects.push_back(pageRect);
|
||
|
||
pageRect.origin.y = NSMaxY(pageRect);
|
||
pageRect.size.height = self.pageHeight;
|
||
}
|
||
|
||
self.needsLayout = NO;
|
||
}
|
||
|
||
- (void)setPageWidth:(CGFloat)newPageWidth { if(_pageWidth != newPageWidth) { _needsLayout = YES; _pageWidth = newPageWidth; } }
|
||
- (void)setPageHeight:(CGFloat)newPageHeight { if(_pageHeight != newPageHeight) { _needsLayout = YES; _pageHeight = newPageHeight; } }
|
||
- (void)setFontScale:(CGFloat)newFontScale { if(_fontScale != newFontScale) { _needsLayout = YES; _fontScale = newFontScale; } }
|
||
- (void)setThemeUUID:(NSString*)newThemeUUID { if(![_themeUUID isEqualToString:newThemeUUID]) { _needsLayout = YES; _themeUUID = newThemeUUID; } }
|
||
@end
|
||
|
||
@interface OakTextViewPrintOptionsViewController : NSViewController <NSPrintPanelAccessorizing>
|
||
{
|
||
std::vector<oak::uuid_t> themeUUIDs;
|
||
}
|
||
@end
|
||
|
||
#ifndef CONSTRAINT
|
||
#define CONSTRAINT(str, align) [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:str options:align metrics:nil views:views]]
|
||
#endif
|
||
|
||
@implementation OakTextViewPrintOptionsViewController
|
||
- (id)init
|
||
{
|
||
if((self = [super init]))
|
||
{
|
||
NSView* contentView = [[NSView alloc] initWithFrame:NSZeroRect];
|
||
[contentView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||
|
||
NSTextField* themesLabel = OakCreateLabel(@"Theme:");
|
||
NSPopUpButton* themes = OakCreatePopUpButton();
|
||
NSButton* printHeaders = OakCreateCheckBox(@"Print header and footer");
|
||
|
||
NSMenu* themesMenu = themes.menu;
|
||
[themesMenu removeAllItems];
|
||
|
||
std::multimap<std::string, bundles::item_ptr, text::less_t> ordered;
|
||
for(auto item : bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeTheme))
|
||
ordered.emplace(item->name(), item);
|
||
|
||
for(auto pair : ordered)
|
||
{
|
||
[themesMenu addItemWithTitle:[NSString stringWithCxxString:pair.first] action:NULL keyEquivalent:@""];
|
||
themeUUIDs.push_back(pair.second->uuid());
|
||
}
|
||
|
||
if(ordered.empty())
|
||
[themesMenu addItemWithTitle:@"No Themes Loaded" action:@selector(nop:) keyEquivalent:@""];
|
||
|
||
[themes bind:NSSelectedIndexBinding toObject:self withKeyPath:@"themeIndex" options:nil];
|
||
[printHeaders bind:NSValueBinding toObject:self withKeyPath:@"printHeaderAndFooter" options:nil];
|
||
|
||
NSDictionary* views = @{
|
||
@"themesLabel" : themesLabel,
|
||
@"themes" : themes,
|
||
@"printHeaders" : printHeaders
|
||
};
|
||
|
||
for(NSView* view in [views allValues])
|
||
{
|
||
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||
[contentView addSubview:view];
|
||
}
|
||
|
||
NSMutableArray* constraints = [NSMutableArray array];
|
||
CONSTRAINT(@"H:|-[themesLabel]-[themes]-|", NSLayoutFormatAlignAllBaseline);
|
||
CONSTRAINT(@"H:[printHeaders]-|", 0);
|
||
CONSTRAINT(@"V:|-[themes]-[printHeaders]-|", NSLayoutFormatAlignAllLeft);
|
||
[contentView addConstraints:constraints];
|
||
|
||
self.view = contentView;
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)setRepresentedObject:(NSPrintInfo*)printInfo
|
||
{
|
||
[super setRepresentedObject:printInfo];
|
||
[self setThemeIndex:[self themeIndex]];
|
||
[self setPrintHeaderAndFooter:[self printHeaderAndFooter]];
|
||
}
|
||
|
||
- (void)setThemeIndex:(NSInteger)anIndex
|
||
{
|
||
if(anIndex < themeUUIDs.size())
|
||
{
|
||
NSPrintInfo* info = [self representedObject];
|
||
[[info dictionary] setObject:[NSString stringWithCxxString:themeUUIDs[anIndex]] forKey:@"OakPrintThemeUUID"];
|
||
[[NSUserDefaults standardUserDefaults] setObject:[NSString stringWithCxxString:themeUUIDs[anIndex]] forKey:@"OakPrintThemeUUID"];
|
||
}
|
||
}
|
||
|
||
- (NSInteger)themeIndex
|
||
{
|
||
NSPrintInfo* info = [self representedObject];
|
||
if(NSString* themeUUID = [[info dictionary] objectForKey:@"OakPrintThemeUUID"])
|
||
{
|
||
for(size_t i = 0; i < themeUUIDs.size(); ++i)
|
||
{
|
||
if(themeUUIDs[i] == to_s(themeUUID))
|
||
return i;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
- (void)setPrintHeaderAndFooter:(BOOL)flag
|
||
{
|
||
NSPrintInfo* info = [self representedObject];
|
||
[[info dictionary] setObject:@(flag) forKey:NSPrintHeaderAndFooter];
|
||
[[NSUserDefaults standardUserDefaults] setObject:@(flag) forKey:@"OakPrintHeaderAndFooter"];
|
||
}
|
||
|
||
- (BOOL)printHeaderAndFooter
|
||
{
|
||
return [[[[self representedObject] dictionary] objectForKey:NSPrintHeaderAndFooter] boolValue];
|
||
}
|
||
|
||
- (NSSet*)keyPathsForValuesAffectingPreview
|
||
{
|
||
return [NSSet setWithObjects:@"themeIndex", @"printHeaderAndFooter", nil];
|
||
}
|
||
|
||
- (NSArray*)localizedSummaryItems
|
||
{
|
||
return @[ ]; // TODO
|
||
}
|
||
|
||
- (NSString*)title
|
||
{
|
||
return @"TextMate";
|
||
}
|
||
@end
|
||
|
||
@implementation OakDocumentView (Printing)
|
||
+ (void)initialize
|
||
{
|
||
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
|
||
@"OakPrintThemeUUID" : @"71D40D9D-AE48-11D9-920A-000D93589AF6",
|
||
@"OakPrintHeaderAndFooter" : @NO,
|
||
}];
|
||
}
|
||
|
||
- (void)printDocument:(id)sender
|
||
{
|
||
NSPrintOperation* printer = [NSPrintOperation printOperationWithView:[[OakPrintDocumentView alloc] initWithDocument:document fontName:textView.font.fontName fontSize:11]];
|
||
|
||
NSMutableDictionary* info = [[printer printInfo] dictionary];
|
||
info[@"OakPrintThemeUUID"] = [[NSUserDefaults standardUserDefaults] objectForKey:@"OakPrintThemeUUID"];
|
||
info[NSPrintHeaderAndFooter] = [[NSUserDefaults standardUserDefaults] objectForKey:@"OakPrintHeaderAndFooter"];
|
||
|
||
[[printer printInfo] setVerticallyCentered:NO];
|
||
[[printer printPanel] setOptions:[[printer printPanel] options] | NSPrintPanelShowsPaperSize | NSPrintPanelShowsOrientation | NSPrintPanelShowsScaling];
|
||
[[printer printPanel] addAccessoryController:[OakTextViewPrintOptionsViewController new]];
|
||
|
||
[printer runOperationModalForWindow:[self window] delegate:nil didRunSelector:NULL contextInfo:nil];
|
||
}
|
||
@end
|