Files
textmate/Frameworks/DocumentWindow/src/DocumentWindowController.mm
2016-09-24 09:37:30 +02:00

2974 lines
104 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "DocumentWindowController.h"
#import "ProjectLayoutView.h"
#import "SelectGrammarViewController.h"
#import "OakRunCommandWindowController.h"
#import <document/document.h>
#import <document/OakDocument.h>
#import <document/OakDocumentController.h>
#import <OakAppKit/NSAlert Additions.h>
#import <OakAppKit/NSMenuItem Additions.h>
#import <OakAppKit/OakAppKit.h>
#import <OakAppKit/OakFileIconImage.h>
#import <OakAppKit/OakFileManager.h>
#import <OakAppKit/OakPasteboard.h>
#import <OakAppKit/OakSavePanel.h>
#import <OakAppKit/OakTabBarView.h>
#import <OakFoundation/NSString Additions.h>
#import <Preferences/Keys.h>
#import <OakTextView/OakDocumentView.h>
#import <OakFileBrowser/OakFileBrowser.h>
#import <OakCommand/OakCommand.h>
#import <HTMLOutputWindow/HTMLOutputWindow.h>
#import <OakFilterList/FileChooser.h>
#import <OakSystem/application.h>
#import <Find/Find.h>
#import <BundlesManager/BundlesManager.h>
#import <BundleEditor/BundleEditor.h>
#import <network/network.h>
#import <file/path_info.h>
#import <io/entries.h>
#import <scm/scm.h>
#import <text/parse.h>
#import <text/tokenize.h>
#import <text/utf8.h>
#import <regexp/glob.h>
#import <settings/settings.h>
#import <ns/ns.h>
#import <kvdb/kvdb.h>
static NSString* const kUserDefaultsAlwaysFindInDocument = @"alwaysFindInDocument";
static NSString* const kUserDefaultsDisableFolderStateRestore = @"disableFolderStateRestore";
static NSString* const kUserDefaultsHideStatusBarKey = @"hideStatusBar";
static NSString* const kUserDefaultsDisableBundleSuggestionsKey = @"disableBundleSuggestions";
static NSString* const kUserDefaultsGrammarsToNeverSuggestKey = @"grammarsToNeverSuggest";
static void show_command_error (std::string const& message, oak::uuid_t const& uuid, NSWindow* window = nil, std::string commandName = NULL_STR)
{
bundles::item_ptr bundleItem = bundles::lookup(uuid);
if(commandName == NULL_STR)
commandName = bundleItem ? bundleItem->name() : "(unknown)";
NSAlert* alert = [[NSAlert alloc] init];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert setMessageText:[NSString stringWithCxxString:text::format("Failure running “%.*s”.", (int)commandName.size(), commandName.data())]];
[alert setInformativeText:[NSString stringWithCxxString:message] ?: @"No output"];
[alert addButtonWithTitle:@"OK"];
if(bundleItem)
[alert addButtonWithTitle:@"Edit Command"];
OakShowAlertForWindow(alert, window, ^(NSInteger button){
if(button == NSAlertSecondButtonReturn)
[[BundleEditor sharedInstance] revealBundleItem:bundleItem];
});
}
@interface QuickLookNSURLWrapper : NSObject <QLPreviewItem>
@property (nonatomic) NSURL* url;
@end
@implementation QuickLookNSURLWrapper
- (id)initWithURL:(NSURL*)aURL
{
if((self = [super init]))
self.url = aURL;
return self;
}
- (NSURL*)previewItemURL
{
return self.url;
}
@end
@interface DocumentWindowController () <NSWindowDelegate, OakTabBarViewDelegate, OakTabBarViewDataSource, OakTextViewDelegate, OakFileBrowserDelegate, QLPreviewPanelDelegate, QLPreviewPanelDataSource>
{
OBJC_WATCH_LEAKS(DocumentWindowController);
NSMutableDictionary<NSUUID*, NSNumber*>* _trackedDocuments;
NSMutableSet<NSUUID*>* _stickyDocumentIdentifiers;
NSArray<OakDocument*>* _documents;
scm::info_ptr _projectSCMInfo;
std::map<std::string, std::string> _projectSCMVariables;
std::vector<std::string> _projectScopeAttributes; // kSettingsScopeAttributesKey
std::vector<std::string> _externalScopeAttributes; // attr.scm.git, attr.project.ninja
scm::info_ptr _documentSCMInfo;
std::map<std::string, std::string> _documentSCMVariables;
std::vector<std::string> _documentScopeAttributes; // attr.os-version, attr.untitled / attr.rev-path + kSettingsScopeAttributesKey
}
@property (nonatomic) ProjectLayoutView* layoutView;
@property (nonatomic) OakTabBarView* tabBarView;
@property (nonatomic) OakDocumentView* documentView;
@property (nonatomic) OakTextView* textView;
@property (nonatomic) OakFileBrowser* fileBrowser;
@property (nonatomic) BOOL disableFileBrowserWindowResize;
@property (nonatomic) BOOL autoRevealFile;
@property (nonatomic) NSRect oldWindowFrame;
@property (nonatomic) NSRect newWindowFrame;
@property (nonatomic) HTMLOutputWindowController* htmlOutputWindowController;
@property (nonatomic) OakHTMLOutputView* htmlOutputView;
@property (nonatomic) BOOL htmlOutputInWindow;
@property (nonatomic) NSString* projectPath;
@property (nonatomic) NSString* documentPath;
@property (nonatomic) NSString* documentDisplayName;
@property (nonatomic) BOOL documentIsModified;
@property (nonatomic) BOOL documentIsOnDisk;
@property (nonatomic) scm::status::type documentSCMStatus;
@property (nonatomic) NSArray* urlArrayForQuickLook;
@property (nonatomic) NSArray<Bundle*>* bundlesAlreadySuggested;
@property (nonatomic) std::vector<document::document_ptr> cppDocuments;
@property (nonatomic) document::document_ptr selectedCppDocument;
+ (void)scheduleSessionBackup:(id)sender;
- (void)makeTextViewFirstResponder:(id)sender;
- (void)updateFileBrowserStatus:(id)sender;
- (void)fileBrowser:(OakFileBrowser*)aFileBrowser openURLs:(NSArray*)someURLs;
- (void)fileBrowser:(OakFileBrowser*)aFileBrowser closeURL:(NSURL*)anURL;
- (void)takeNewTabIndexFrom:(id)sender; // used by newDocumentInTab:
- (void)takeTabsToTearOffFrom:(id)sender; // used by moveDocumentToNewWindow:
@end
namespace
{
// ==========================================
// = tracking document controller instances =
// ==========================================
static NSMutableDictionary<NSString*, DocumentWindowController*>* AllControllers ()
{
static NSMutableDictionary* res = [NSMutableDictionary new];
return res;
}
static NSArray<DocumentWindowController*>* SortedControllers ()
{
NSMutableArray* res = [NSMutableArray array];
for(NSNumber* flag in @[ @NO, @YES ])
{
for(NSWindow* window in [NSApp orderedWindows])
{
if([window isMiniaturized] == [flag boolValue] && [window.delegate respondsToSelector:@selector(identifier)])
{
DocumentWindowController* delegate = (DocumentWindowController*)window.delegate;
if(id controller = AllControllers()[delegate.identifier])
[res addObject:controller];
}
}
}
return res;
}
// ======================
// = document_t helpers =
// ======================
static bool is_disposable (document::document_ptr const& doc)
{
return doc && !doc->is_modified() && !doc->is_on_disk() && doc->path() == NULL_STR && doc->is_loaded() && doc->buffer().empty();
}
}
@implementation DocumentWindowController
+ (KVDB*)sharedProjectStateDB
{
NSString* appSupport = [[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"TextMate"];
return [KVDB sharedDBUsingFile:@"RecentProjects.db" inDirectory:appSupport];
}
- (id)init
{
if((self = [super init]))
{
_trackedDocuments = [NSMutableDictionary dictionary];
self.identifier = [NSString stringWithCxxString:oak::uuid_t().generate()];
self.tabBarView = [[OakTabBarView alloc] initWithFrame:NSZeroRect];
self.tabBarView.dataSource = self;
self.tabBarView.delegate = self;
self.documentView = [[OakDocumentView alloc] init];
self.textView = self.documentView.textView;
self.textView.delegate = self;
self.layoutView = [[ProjectLayoutView alloc] initWithFrame:NSZeroRect];
self.layoutView.tabBarView = self.tabBarView;
self.layoutView.documentView = self.documentView;
NSUInteger windowStyle = (NSTitledWindowMask|NSClosableWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask|NSTexturedBackgroundWindowMask);
self.window = [[NSWindow alloc] initWithContentRect:[NSWindow contentRectForFrameRect:[self frameRectForNewWindow] styleMask:windowStyle] styleMask:windowStyle backing:NSBackingStoreBuffered defer:NO];
self.window.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary;
self.window.delegate = self;
self.window.releasedWhenClosed = NO;
[self.window setContentBorderThickness:0 forEdge:NSMaxYEdge]; // top border
[self.window setContentBorderThickness:0 forEdge:NSMinYEdge]; // bottom border
[self.window setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
[self.window setAutorecalculatesContentBorderThickness:NO forEdge:NSMinYEdge];
OakAddAutoLayoutViewsToSuperview(@[ self.layoutView ], self.window.contentView);
OakSetupKeyViewLoop(@[ self.layoutView ], NO);
self.window.initialFirstResponder = self.textView;
[self.window.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{ @"view" : self.layoutView }]];
[self.window.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:@{ @"view" : self.layoutView }]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActiveNotification:) name:NSApplicationDidBecomeActiveNotification object:NSApp];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidResignActiveNotification:) name:NSApplicationDidResignActiveNotification object:NSApp];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileManagerWillDeleteItemAtPath:) name:OakFileManagerWillDeleteItemAtPath object:nil];
[self userDefaultsDidChange:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.window.delegate = nil;
self.tabBarView.dataSource = nil;
self.tabBarView.delegate = nil;
self.textView.delegate = nil;
// When option-clicking to close all windows then
// messages are sent to our window after windowWillClose:
__autoreleasing __attribute__ ((unused)) NSWindow* delayRelease = self.window;
}
// ======================================
// = Find suitable frame for new window =
// ======================================
- (NSRect)windowFrame
{
NSRect res = [self.window frame];
if(self.fileBrowserVisible && !self.disableFileBrowserWindowResize)
res.size.width -= self.fileBrowserWidth;
return res;
}
- (NSRect)cascadedWindowFrame
{
NSRect r = [self windowFrame];
return { { NSMinX(r) + 21, NSMinY(r) - 23 }, r.size };
}
- (NSRect)frameRectForNewWindow
{
std::map<CGFloat, NSWindow*> ourWindows;
for(NSWindow* win in [NSApp windows])
{
if([win isVisible] && [win isOnActiveSpace] && ![win isZoomed] && (([win styleMask] & NSFullScreenWindowMask)) != NSFullScreenWindowMask && [[win delegate] isKindOfClass:[self class]])
ourWindows.emplace(NSMaxY([win frame]), win);
}
if(!ourWindows.empty())
{
NSRect r = [(DocumentWindowController*)ourWindows.begin()->second.delegate cascadedWindowFrame];
NSRect scrRect = [[NSScreen mainScreen] visibleFrame];
if(NSContainsRect(scrRect, r))
return r;
r.origin.x = 61;
r.origin.y = NSMaxY(scrRect) - NSHeight(r);
BOOL alreadyHasWrappedWindow = NO;
for(auto pair : ourWindows)
{
if(NSEqualPoints([pair.second frame].origin, r.origin))
alreadyHasWrappedWindow = YES;
}
if(alreadyHasWrappedWindow)
{
NSWindow* mainWindow = [NSApp mainWindow];
if([[mainWindow delegate] isKindOfClass:[self class]])
r = [(DocumentWindowController*)mainWindow.delegate cascadedWindowFrame];
}
return r;
}
if(NSString* rectStr = [[NSUserDefaults standardUserDefaults] stringForKey:@"DocumentControllerWindowFrame"])
return NSRectFromString(rectStr);
NSRect r = [[NSScreen mainScreen] visibleFrame];
return r = NSIntegralRect(NSInsetRect(r, NSWidth(r) / 3, NSHeight(r) / 5));
}
// =========================
- (void)windowWillClose:(NSNotification*)aNotification
{
if((([self.window styleMask] & NSFullScreenWindowMask) != NSFullScreenWindowMask) && !self.window.isZoomed)
[[NSUserDefaults standardUserDefaults] setObject:NSStringFromRect([self windowFrame]) forKey:@"DocumentControllerWindowFrame"];
self.cppDocuments = { };
self.selectedCppDocument = document::document_ptr();
self.fileBrowserVisible = NO; // Make window frame small as we no longer respond to savableWindowFrame
self.identifier = nil; // This removes us from AllControllers and causes a release
}
- (void)showWindow:(id)sender
{
if(_cppDocuments.empty())
{
document::document_ptr defaultDocument = document::create();
self.cppDocuments = { defaultDocument };
[self openAndSelectDocument:defaultDocument];
}
[self.window makeKeyAndOrderFront:sender];
}
- (void)makeTextViewFirstResponder:(id)sender { [self.window makeFirstResponder:self.textView]; }
- (void)close { [self.window close]; }
- (IBAction)moveFocus:(id)sender
{
if([self.window firstResponder] == self.textView)
{
self.fileBrowserVisible = YES;
NSOutlineView* outlineView = self.fileBrowser.outlineView;
[self.window makeFirstResponder:outlineView];
if([outlineView numberOfSelectedRows] == 0)
{
for(NSUInteger row = 0; row < [outlineView numberOfRows]; ++row)
{
if([[outlineView delegate] respondsToSelector:@selector(outlineView:isGroupItem:)] && [[outlineView delegate] outlineView:outlineView isGroupItem:[outlineView itemAtRow:row]])
continue;
[outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
break;
}
}
}
else
{
[self makeTextViewFirstResponder:sender];
}
}
// ==========================
// = Notification Callbacks =
// ==========================
- (void)userDefaultsDidChange:(NSNotification*)aNotification
{
self.htmlOutputInWindow = [[[NSUserDefaults standardUserDefaults] stringForKey:kUserDefaultsHTMLOutputPlacementKey] isEqualToString:@"window"];
self.disableFileBrowserWindowResize = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableFileBrowserWindowResizeKey];
self.autoRevealFile = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsAutoRevealFileKey];
self.documentView.hideStatusBar = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsHideStatusBarKey];
if(self.layoutView.fileBrowserOnRight != [[[NSUserDefaults standardUserDefaults] stringForKey:kUserDefaultsFileBrowserPlacementKey] isEqualToString:@"right"])
{
self.oldWindowFrame = self.newWindowFrame = NSZeroRect;
self.layoutView.fileBrowserOnRight = !self.layoutView.fileBrowserOnRight;
}
}
- (void)applicationDidBecomeActiveNotification:(NSNotification*)aNotification
{
if(!_cppDocuments.empty())
[self.textView performSelector:@selector(applicationDidBecomeActiveNotification:) withObject:aNotification];
}
- (void)applicationDidResignActiveNotification:(NSNotification*)aNotification
{
static BOOL IsSaving = NO;
if(std::exchange(IsSaving, YES))
return;
NSMutableArray* documentsToSave = [NSMutableArray array];
for(auto doc : _cppDocuments)
{
if(doc->is_modified() && doc->path() != NULL_STR)
{
settings_t const settings = settings_for_path(doc->logical_path(), doc->file_type(), path::parent(doc->path()));
if(settings.get(kSettingsSaveOnBlurKey, false))
{
if(doc == _selectedCppDocument)
[_textView updateDocumentMetadata];
[documentsToSave addObject:doc->document()];
}
}
}
[self saveDocumentsUsingEnumerator:[documentsToSave objectEnumerator] completionHandler:^(OakDocumentIOResult result){
if(!_cppDocuments.empty())
[self.textView performSelector:@selector(applicationDidResignActiveNotification:) withObject:aNotification];
IsSaving = NO;
}];
}
// =================
// = Close Methods =
// =================
+ (NSAlert*)saveAlertForDocuments:(NSArray<OakDocument*>*)someDocuments
{
NSAlert* alert = [[NSAlert alloc] init];
[alert setAlertStyle:NSWarningAlertStyle];
if(someDocuments.count == 1)
{
OakDocument* document = someDocuments.firstObject;
[alert setMessageText:[NSString stringWithFormat:@"Do you want to save the changes you made in the document “%@”?", document.displayName]];
[alert setInformativeText:@"Your changes will be lost if you dont save them."];
[alert addButtons:@"Save", @"Cancel", @"Dont Save", nil];
}
else
{
NSString* body = @"";
for(OakDocument* document in someDocuments)
body = [body stringByAppendingFormat:@"• “%@”\n", document.displayName];
[alert setMessageText:@"Do you want to save documents with changes?"];
[alert setInformativeText:body];
[alert addButtons:@"Save All", @"Cancel", @"Dont Save", nil];
}
return alert;
}
- (void)showCloseWarningUIForDocuments:(NSArray<OakDocument*>*)someDocuments completionHandler:(void(^)(BOOL canClose))callback
{
if(!someDocuments.count)
return callback(YES);
if(someDocuments.count == 1)
{
for(size_t i = 0; i < _cppDocuments.size(); ++i)
{
if(someDocuments.firstObject == _cppDocuments[i]->document())
{
if(_selectedCppDocument != _cppDocuments[i])
{
self.selectedTabIndex = i;
[self openAndSelectDocument:_cppDocuments[i]];
}
break;
}
}
}
NSAlert* alert = [DocumentWindowController saveAlertForDocuments:someDocuments];
OakShowAlertForWindow(alert, self.window, ^(NSInteger returnCode){
switch(returnCode)
{
case NSAlertFirstButtonReturn: /* "Save" */
{
[self saveDocumentsUsingEnumerator:[someDocuments objectEnumerator] completionHandler:^(OakDocumentIOResult result){
callback(result == OakDocumentIOResultSuccess);
}];
}
break;
case NSAlertSecondButtonReturn: /* "Cancel" */
{
callback(NO);
}
break;
case NSAlertThirdButtonReturn: /* "Don't Save" */
{
callback(YES);
}
break;
}
});
}
- (void)closeTabsAtIndexes:(NSIndexSet*)anIndexSet askToSaveChanges:(BOOL)askToSaveFlag createDocumentIfEmpty:(BOOL)createIfEmptyFlag
{
if([anIndexSet count] == 0 || _cppDocuments.empty())
return;
std::vector<document::document_ptr> documentsToClose;
for(NSUInteger index = [anIndexSet firstIndex]; index != NSNotFound; index = [anIndexSet indexGreaterThanIndex:index])
documentsToClose.push_back(_cppDocuments[index]);
if(askToSaveFlag)
{
NSMutableArray<OakDocument*>* documentsToSave = [NSMutableArray array];
for(auto doc : documentsToClose)
{
if(doc->is_modified())
[documentsToSave addObject:doc->document()];
}
if(documentsToSave.count)
{
[self showCloseWarningUIForDocuments:documentsToSave completionHandler:^(BOOL canClose){
if(canClose)
{
[self closeTabsAtIndexes:anIndexSet askToSaveChanges:NO createDocumentIfEmpty:createIfEmptyFlag];
}
else
{
NSMutableIndexSet* newIndexes = [anIndexSet mutableCopy];
for(NSUInteger index = [anIndexSet firstIndex]; index != NSNotFound; index = [anIndexSet indexGreaterThanIndex:index])
{
if(_cppDocuments[index]->is_modified())
[newIndexes removeIndex:index];
}
[self closeTabsAtIndexes:newIndexes askToSaveChanges:YES createDocumentIfEmpty:createIfEmptyFlag];
}
}];
return;
}
}
std::set<oak::uuid_t> uuids;
std::transform(documentsToClose.begin(), documentsToClose.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); });
std::vector<document::document_ptr> newDocuments;
NSUInteger newSelectedTabIndex = _selectedTabIndex;
oak::uuid_t const selectedUUID = _cppDocuments[_selectedTabIndex]->identifier();
for(auto document : _cppDocuments)
{
oak::uuid_t const& uuid = document->identifier();
if(uuids.find(uuid) == uuids.end())
newDocuments.push_back(document);
if(selectedUUID == uuid)
newSelectedTabIndex = newDocuments.empty() ? 0 : newDocuments.size() - 1;
}
if(createIfEmptyFlag && newDocuments.empty())
newDocuments.push_back(document::create());
self.cppDocuments = newDocuments;
self.selectedTabIndex = newSelectedTabIndex;
if(!newDocuments.empty() && newDocuments[newSelectedTabIndex]->identifier() != selectedUUID)
[self openAndSelectDocument:newDocuments[newSelectedTabIndex]];
}
- (IBAction)performCloseTab:(id)sender
{
if(_cppDocuments.empty() || _cppDocuments.size() == 1 && (is_disposable(_selectedCppDocument) || !self.fileBrowserVisible))
return [self performCloseWindow:sender];
NSUInteger index = [sender isKindOfClass:[OakTabBarView class]] ? [sender tag] : _selectedTabIndex;
[self closeTabsAtIndexes:[NSIndexSet indexSetWithIndex:index] askToSaveChanges:YES createDocumentIfEmpty:YES];
}
- (IBAction)performCloseSplit:(id)sender
{
ASSERT(sender == self.layoutView.htmlOutputView);
self.htmlOutputVisible = NO;
}
- (IBAction)performCloseWindow:(id)sender
{
[self.window performClose:self];
}
- (IBAction)performCloseAllTabs:(id)sender
{
NSMutableIndexSet* allTabs = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, _cppDocuments.size())];
for(size_t i = 0; i < _cppDocuments.size(); ++i)
{
if(_cppDocuments[i]->is_modified() && _cppDocuments[i]->path() == NULL_STR || [self isDocumentSticky:_cppDocuments[i]])
[allTabs removeIndex:i];
}
[self closeTabsAtIndexes:allTabs askToSaveChanges:YES createDocumentIfEmpty:YES];
}
- (IBAction)performCloseOtherTabsXYZ:(id)sender
{
NSUInteger tabIndex = [sender isKindOfClass:[OakTabBarView class]] ? [sender tag] : _selectedTabIndex;
NSMutableIndexSet* otherTabs = [NSMutableIndexSet indexSet];
for(size_t i = 0; i < _cppDocuments.size(); ++i)
{
if(i != tabIndex && (!_cppDocuments[i]->is_modified() || _cppDocuments[i]->path() != NULL_STR) && ![self isDocumentSticky:_cppDocuments[i]])
[otherTabs addIndex:i];
}
[self closeTabsAtIndexes:otherTabs askToSaveChanges:YES createDocumentIfEmpty:YES];
}
- (IBAction)performCloseTabsToTheRight:(id)sender
{
NSUInteger from = _selectedTabIndex + 1, to = _cppDocuments.size();
if(from < to)
[self closeTabsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(from, to - from)] askToSaveChanges:YES createDocumentIfEmpty:YES];
}
- (void)saveProjectState
{
if(self.treatAsProjectWindow)
[[DocumentWindowController sharedProjectStateDB] setValue:[self sessionInfoIncludingUntitledDocuments:NO] forKey:self.projectPath];
}
- (BOOL)windowShouldClose:(id)sender
{
if(!self.htmlOutputInWindow && _htmlOutputView.isRunningCommand)
{
[_htmlOutputView stopLoadingWithUserInteraction:YES completionHandler:^(BOOL didStop){
if(didStop)
[sender performSelector:@selector(performClose:) withObject:self afterDelay:0];
}];
return NO;
}
NSMutableArray<OakDocument*>* documentsToSave = [NSMutableArray array];
for(auto doc : _cppDocuments)
{
if(doc->is_modified())
[documentsToSave addObject:doc->document()];
}
if(!documentsToSave.count)
{
[self saveProjectState];
return YES;
}
[self showCloseWarningUIForDocuments:documentsToSave completionHandler:^(BOOL canClose){
if(canClose)
{
[self saveProjectState];
[self.window close];
}
}];
return NO;
}
- (void)fileManagerWillDeleteItemAtPath:(NSNotification*)aNotification
{
NSDictionary* userInfo = [aNotification userInfo];
NSString* path = userInfo[OakFileManagerPathKey];
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
for(size_t i = 0; i < _cppDocuments.size(); ++i)
{
document::document_ptr doc = _cppDocuments[i];
if(!doc->is_modified() && path::is_child(doc->path(), to_s(path)))
[indexSet addIndex:i];
}
id oldFirstResponder = self.window.firstResponder;
[self closeTabsAtIndexes:indexSet askToSaveChanges:NO createDocumentIfEmpty:YES];
if(oldFirstResponder && oldFirstResponder != self.window.firstResponder)
[self.window makeFirstResponder:oldFirstResponder];
}
- (void)fileBrowserDidDuplicateAtURLs:(NSNotification*)aNotification
{
NSDictionary* userInfo = [aNotification userInfo];
NSDictionary* urls = userInfo[OakFileBrowserURLMapKey];
for(NSURL* url in urls)
{
if([url.path isEqualToString:self.documentPath])
[self openItems:@[ @{ @"path" : [urls[url] path] } ] closingOtherTabs:NO];
}
}
+ (void)saveSessionAndDetachBackups
{
BOOL restoresSession = ![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableSessionRestoreKey];
[DocumentWindowController saveSessionIncludingUntitledDocuments:restoresSession];
for(DocumentWindowController* controller in [SortedControllers() reverseObjectEnumerator])
{
[controller saveProjectState];
// Ensure we do not remove backup files, as they are used to restore untitled documents
if(restoresSession)
{
for(auto document : controller.cppDocuments)
{
std::string const backupPath = document->backup_path();
document->detach_backup();
if(backupPath != NULL_STR && document->path() != NULL_STR)
unlink(backupPath.c_str());
}
}
}
}
- (NSArray<OakDocument*>*)documentsNeedingSaving
{
BOOL restoresSession = ![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableSessionRestoreKey];
NSMutableArray<OakDocument*>* res = [NSMutableArray array];
for(auto doc : _cppDocuments)
{
if(doc->is_modified() && (doc->path() != NULL_STR || !restoresSession))
[res addObject:doc->document()];
}
return res.count ? res : nil;
}
+ (void)saveControllersUsingEnumerator:(NSEnumerator*)anEnumerator completionHandler:(void(^)(OakDocumentIOResult result))callback
{
if(DocumentWindowController* controller = [anEnumerator nextObject])
{
[controller saveDocumentsUsingEnumerator:[controller.documentsNeedingSaving objectEnumerator] completionHandler:^(OakDocumentIOResult result){
if(result == OakDocumentIOResultSuccess)
[self saveControllersUsingEnumerator:anEnumerator completionHandler:callback];
else if(callback)
callback(result);
}];
}
else if(callback)
{
callback(OakDocumentIOResultSuccess);
}
}
+ (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender
{
NSMutableArray<DocumentWindowController*>* controllers = [NSMutableArray array];
NSMutableArray<OakDocument*>* documents = [NSMutableArray array];
for(DocumentWindowController* controller in SortedControllers())
{
if(NSArray* newDocs = controller.documentsNeedingSaving)
{
[controllers addObject:controller];
[documents addObjectsFromArray:newDocs];
}
}
if(controllers.count == 0)
{
[self saveSessionAndDetachBackups];
return NSTerminateNow;
}
else if(controllers.count == 1)
{
DocumentWindowController* controller = controllers.firstObject;
[controller showCloseWarningUIForDocuments:controller.documentsNeedingSaving completionHandler:^(BOOL canClose){
if(canClose)
[self saveSessionAndDetachBackups];
[NSApp replyToApplicationShouldTerminate:canClose];
}];
}
else
{
switch([[DocumentWindowController saveAlertForDocuments:documents] runModal])
{
case NSAlertFirstButtonReturn: /* "Save" */
{
[self saveControllersUsingEnumerator:[controllers objectEnumerator] completionHandler:^(OakDocumentIOResult result){
if(result == OakDocumentIOResultSuccess)
[self saveSessionAndDetachBackups];
[NSApp replyToApplicationShouldTerminate:result == OakDocumentIOResultSuccess];
}];
}
break;
case NSAlertSecondButtonReturn: /* "Cancel" */
return NSTerminateCancel;
case NSAlertThirdButtonReturn: /* "Don't Save" */
return NSTerminateNow;
}
}
return NSTerminateLater;
}
// =====================
// = Document Tracking =
// =====================
- (void)trackDocument:(OakDocument*)document
{
if(!document)
return;
NSUInteger trackCount = [_trackedDocuments[document.identifier] intValue];
_trackedDocuments[document.identifier] = @(trackCount+1);
if(trackCount == 0)
{
for(NSString* keyPath in @[ @"path", @"onDisk", @"documentEdited" ])
[document addObserver:self forKeyPath:keyPath options:0 context:nullptr];
}
document.keepBackupFile = YES;
[document open];
}
- (void)untrackDocument:(OakDocument*)document
{
if(!document)
return;
NSUInteger trackCount = [_trackedDocuments[document.identifier] intValue];
_trackedDocuments[document.identifier] = @(trackCount-1);
if(trackCount == 1)
{
for(NSString* keyPath in @[ @"path", @"onDisk", @"documentEdited" ])
[document removeObserver:self forKeyPath:keyPath];
}
[document close];
}
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)anObject change:(NSDictionary*)change context:(void*)context
{
if(self.selectedDocument && anObject && [self.selectedDocument isEqual:anObject])
{
OakDocument* document = anObject;
if([keyPath isEqualToString:@"path"])
{
self.documentPath = document.virtualPath ?: document.path;
self.documentDisplayName = document.displayName;
if(!self.projectPath)
self.projectPath = [document.path stringByDeletingLastPathComponent];
}
else if([keyPath isEqualToString:@"onDisk"])
{
self.documentIsOnDisk = document.isOnDisk;
}
else if([keyPath isEqualToString:@"documentEdited"])
{
self.documentIsModified = document.isDocumentEdited;
}
}
if([keyPath isEqualToString:@"path"] || [keyPath isEqualToString:@"documentEdited"])
{
[self updateFileBrowserStatus:self];
[self.tabBarView reloadData];
[[self class] scheduleSessionBackup:self];
}
}
- (BOOL)isDocumentSticky:(document::document_ptr)aDocument
{
return [_stickyDocumentIdentifiers containsObject:aDocument->document().identifier];
}
- (void)setDocument:(document::document_ptr)aDocument sticky:(BOOL)stickyFlag
{
if(stickyFlag)
_stickyDocumentIdentifiers = _stickyDocumentIdentifiers ?: [NSMutableSet set];
if(stickyFlag)
[_stickyDocumentIdentifiers addObject:aDocument->document().identifier];
else [_stickyDocumentIdentifiers removeObject:aDocument->document().identifier];
}
// ====================
// = Create Documents =
// ====================
- (IBAction)newDocumentInTab:(id)sender
{
[self takeNewTabIndexFrom:[NSIndexSet indexSetWithIndex:_selectedTabIndex + 1]];
}
- (IBAction)newDocumentInDirectory:(id)sender
{
if(!self.fileBrowserVisible)
return;
if(NSString* folder = [self.fileBrowser directoryForNewItems])
{
std::string path = "untitled";
std::string fileType = settings_for_path(NULL_STR, "attr.untitled", to_s(folder)).get(kSettingsFileTypeKey, "text.plain");
for(auto item : bundles::query(bundles::kFieldGrammarScope, fileType))
{
std::string const& ext = item->value_for_field(bundles::kFieldGrammarExtension);
if(ext != NULL_STR)
path = "untitled." + ext;
}
NSURL* url = [NSURL fileURLWithPath:[NSString stringWithCxxString:path::unique(path::join([folder fileSystemRepresentation], path))]];
if([[OakFileManager sharedInstance] createFileAtURL:url view:self.fileBrowser.view])
{
document::document_ptr doc = document::create(to_s([url path]));
doc->set_file_type(fileType);
[self insertDocuments:{ doc } atIndex:_selectedTabIndex + 1 selecting:doc andClosing:[self disposableDocument]];
// Using openAndSelectDocument: will move focus to OakTextView
doc->sync_load();
self.selectedCppDocument = doc;
doc->close();
[self.fileBrowser editURL:url];
}
}
}
- (IBAction)moveDocumentToNewWindow:(id)sender
{
if(_cppDocuments.size() > 1)
[self takeTabsToTearOffFrom:[NSIndexSet indexSetWithIndex:_selectedTabIndex]];
}
- (IBAction)mergeAllWindows:(id)sender
{
std::vector<document::document_ptr> documents = _cppDocuments;
for(DocumentWindowController* delegate in SortedControllers())
{
if(delegate != self && ![delegate.window isMiniaturized])
{
auto delegateDocuments = delegate.cppDocuments; // Returns by-value so each result is unique
documents.insert(documents.end(), delegateDocuments.begin(), delegateDocuments.end());
}
}
self.cppDocuments = documents;
for(DocumentWindowController* delegate in SortedControllers())
{
if(delegate != self && ![delegate.window isMiniaturized])
[delegate.window close];
}
}
- (std::set<oak::uuid_t>)disposableDocument
{
if(_selectedTabIndex < _cppDocuments.size() && is_disposable(_cppDocuments[_selectedTabIndex]))
return { _cppDocuments[_selectedTabIndex]->identifier() };
return { };
}
- (void)insertDocuments:(std::vector<document::document_ptr> const&)documents atIndex:(NSInteger)index selecting:(document::document_ptr const&)selectDocument andClosing:(std::set<oak::uuid_t> const&)closeDocuments
{
std::set<oak::uuid_t> oldUUIDs, newUUIDs;
std::transform(_cppDocuments.begin(), _cppDocuments.end(), inserter(oldUUIDs, oldUUIDs.end()), [](auto const& doc){ return doc->identifier(); });
std::transform(documents.begin(), documents.end(), inserter(newUUIDs, newUUIDs.end()), [](auto const& doc){ return doc->identifier(); });
std::for_each(closeDocuments.begin(), closeDocuments.end(), [&oldUUIDs](auto const& uuid){ oldUUIDs.erase(uuid); });
BOOL shouldReorder = ![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableTabReorderingKey];
std::vector<document::document_ptr> newDocuments;
for(NSUInteger i = 0; i <= _cppDocuments.size(); ++i)
{
if(i == MIN(index, _cppDocuments.size()))
{
std::set<oak::uuid_t> didInsert;
for(NSUInteger j = 0; j < documents.size(); ++j)
{
if(didInsert.find(documents[j]->identifier()) == didInsert.end() && (shouldReorder || oldUUIDs.find(documents[j]->identifier()) == oldUUIDs.end()))
{
newDocuments.push_back(documents[j]);
didInsert.insert(documents[j]->identifier());
}
}
}
if(i == _cppDocuments.size())
break;
else if(shouldReorder && newUUIDs.find(_cppDocuments[i]->identifier()) != newUUIDs.end())
continue;
else if(closeDocuments.find(_cppDocuments[i]->identifier()) != closeDocuments.end())
continue;
newDocuments.push_back(_cppDocuments[i]);
}
self.cppDocuments = newDocuments;
self.selectedTabIndex = std::find_if(newDocuments.begin(), newDocuments.end(), [&selectDocument](auto const& doc){ return *doc == *selectDocument; }) - newDocuments.begin();
}
- (void)openItems:(NSArray*)items closingOtherTabs:(BOOL)closeOtherTabsFlag
{
std::vector<document::document_ptr> documents;
for(id item in items)
{
std::string const path = to_s([item objectForKey:@"path"]);
std::string const uuid = to_s([item objectForKey:@"identifier"]);
std::string const range = to_s([item objectForKey:@"selectionString"]);
document::document_ptr doc;
if(path == NULL_STR && oak::uuid_t::is_valid(uuid))
doc = document::find(uuid);
if(!doc && path != NULL_STR)
doc = document::create(path);
if(doc)
{
doc->set_recent_tracking(false);
if(range != NULL_STR)
doc->set_selection(range);
documents.push_back(doc);
}
}
if(documents.empty())
return;
std::set<oak::uuid_t> tabsToClose;
if(closeOtherTabsFlag)
{
for(auto const& doc : _cppDocuments)
{
if(!doc->is_modified() && ![self isDocumentSticky:doc])
tabsToClose.insert(doc->identifier());
}
}
else
{
tabsToClose = [self disposableDocument];
}
[self insertDocuments:documents atIndex:_selectedTabIndex + 1 selecting:documents.back() andClosing:tabsToClose];
[self openAndSelectDocument:documents.back()];
if(self.tabBarView && ![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableTabAutoCloseKey])
{
NSInteger excessTabs = _cppDocuments.size() - std::max<NSUInteger>(self.tabBarView.countOfVisibleTabs, 8);
if(excessTabs > 0)
{
std::multimap<NSInteger, size_t> ranked;
for(size_t i = 0; i < _cppDocuments.size(); ++i)
ranked.emplace(_cppDocuments[i]->lru(), i);
std::set<oak::uuid_t> newUUIDs;
std::transform(documents.begin(), documents.end(), inserter(newUUIDs, newUUIDs.end()), [](auto const& doc){ return doc->identifier(); });
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
for(auto const& pair : ranked)
{
document::document_ptr doc = _cppDocuments[pair.second];
if(!doc->is_modified() && ![self isDocumentSticky:doc] && doc->is_on_disk() && newUUIDs.find(doc->identifier()) == newUUIDs.end())
[indexSet addIndex:pair.second];
if([indexSet count] == excessTabs)
break;
}
[self closeTabsAtIndexes:indexSet askToSaveChanges:NO createDocumentIfEmpty:NO];
}
}
}
// ================
// = Document I/O =
// ================
- (void)didOpenDocuemntInTextView:(OakTextView*)textView
{
for(auto const& item : bundles::query(bundles::kFieldSemanticClass, "callback.document.did-open", [textView scopeContext], bundles::kItemTypeMost, oak::uuid_t(), false))
[textView performBundleItem:item];
}
- (void)openAndSelectDocument:(document::document_ptr const&)aDocument
{
document::document_ptr doc = aDocument;
[doc->document() loadModalForWindow:self.window completionHandler:^(OakDocumentIOResult result, NSString* errorMessage, oak::uuid_t const& filterUUID){
if(result == OakDocumentIOResultSuccess)
{
OakDocument* document = doc->document();
BOOL showBundleSuggestions = ![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableBundleSuggestionsKey];
if(!document.fileType && showBundleSuggestions)
{
NSArray<BundleGrammar*>* grammars = document.proposedGrammars;
if(NSArray* excludedGrammars = [[NSUserDefaults standardUserDefaults] stringArrayForKey:kUserDefaultsGrammarsToNeverSuggestKey])
grammars = [grammars filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"!(identifier.UUIDString IN %@)", excludedGrammars]];
if(_bundlesAlreadySuggested)
grammars = [grammars filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"!(bundle IN %@)", _bundlesAlreadySuggested]];
if([grammars count] && network::can_reach_host([[[NSURL URLWithString:@(REST_API)] host] UTF8String]))
{
self.bundlesAlreadySuggested = [(_bundlesAlreadySuggested ?: @[ ]) arrayByAddingObject:[grammars firstObject].bundle];
SelectGrammarViewController* installer = [[SelectGrammarViewController alloc] init];
installer.documentDisplayName = document.path || document.customName ? document.displayName : nil;
__weak __block id documentCloseObserver = [[NSNotificationCenter defaultCenter] addObserverForName:OakDocumentWillCloseNotification object:document queue:nil usingBlock:^(NSNotification*){
[installer dismiss];
[[NSNotificationCenter defaultCenter] removeObserver:documentCloseObserver];
}];
[installer showGrammars:grammars forView:_documentView completionHandler:^(SelectGrammarResponse response, BundleGrammar* grammar){
if(response == SelectGrammarResponseInstall && grammar.bundle.isInstalled)
{
for(document::document_ptr cppDoc : _cppDocuments)
{
OakDocument* doc = cppDoc->document();
if([doc isEqual:document] || [[doc proposedGrammars] containsObject:grammar])
doc.fileType = grammar.fileType;
}
}
else if(response == SelectGrammarResponseNever)
{
NSArray* excludedGrammars = [[NSUserDefaults standardUserDefaults] stringArrayForKey:kUserDefaultsGrammarsToNeverSuggestKey] ?: @[ ];
[[NSUserDefaults standardUserDefaults] setObject:[excludedGrammars arrayByAddingObject:grammar.identifier.UUIDString] forKey:kUserDefaultsGrammarsToNeverSuggestKey];
}
if(id observer = documentCloseObserver)
[[NSNotificationCenter defaultCenter] removeObserver:observer];
}];
}
std::string const docAttributes = document.path ? "attr.file.unknown-type" : "attr.untitled";
document.fileType = to_ns(settings_for_path(to_s(document.virtualPath ?: document.path), docAttributes, to_s(self.projectPath)).get(kSettingsFileTypeKey, "text.plain"));
}
[self makeTextViewFirstResponder:self];
self.selectedCppDocument = doc;
[self performSelector:@selector(didOpenDocuemntInTextView:) withObject:self.documentView.textView afterDelay:0];
[doc->document() close];
}
else
{
if(filterUUID)
show_command_error(to_s(errorMessage), filterUUID);
// Close the tab that failed to open
for(size_t i = 0; i < _cppDocuments.size(); ++i)
{
if(_cppDocuments[i]->identifier() == doc->identifier())
{
[self closeTabsAtIndexes:[NSIndexSet indexSetWithIndex:i] askToSaveChanges:NO createDocumentIfEmpty:self.fileBrowserVisible];
break;
}
}
if(_cppDocuments.empty())
[self close];
}
}];
}
- (IBAction)saveDocument:(id)sender
{
if(!_selectedCppDocument)
return;
if(_selectedCppDocument->path() != NULL_STR)
{
[self saveDocumentsUsingEnumerator:@[ _selectedCppDocument->document() ].objectEnumerator completionHandler:nil];
}
else
{
NSString* const suggestedFolder = self.untitledSavePath;
NSString* const suggestedName = [_selectedCppDocument->document() displayNameWithExtension:YES];
[OakSavePanel showWithPath:suggestedName directory:suggestedFolder fowWindow:self.window encoding:_selectedCppDocument->disk_encoding() completionHandler:^(NSString* path, encoding::type const& encoding){
if(!path)
return;
std::vector<std::string> const& paths = path::expand_braces(to_s(path));
ASSERT_LT(0, paths.size());
_selectedCppDocument->set_path(paths[0]);
_selectedCppDocument->set_disk_encoding(encoding);
// if(_selectedCppDocument->identifier() == scratchDocument)
// scratchDocument = oak::uuid_t();
if(paths.size() > 1)
{
// FIXME check if paths[0] already exists (overwrite)
std::vector<document::document_ptr> documents = { _selectedCppDocument };
std::transform(paths.begin() + 1, paths.end(), back_inserter(documents), [&encoding](std::string const& path) -> document::document_ptr {
document::document_ptr doc = document::create(path);
doc->set_disk_encoding(encoding);
return doc;
});
[self insertDocuments:documents atIndex:_selectedTabIndex selecting:documents.front() andClosing:{ }];
}
[self saveDocumentsUsingEnumerator:@[ _selectedCppDocument->document() ].objectEnumerator completionHandler:nil];
}];
}
}
- (IBAction)saveDocumentAs:(id)sender
{
if(!_selectedCppDocument)
return;
std::string const documentPath = _selectedCppDocument->path();
NSString* const suggestedFolder = [NSString stringWithCxxString:path::parent(documentPath)] ?: self.untitledSavePath;
NSString* const suggestedName = [NSString stringWithCxxString:path::name(documentPath)] ?: [_selectedCppDocument->document() displayNameWithExtension:YES];
[OakSavePanel showWithPath:suggestedName directory:suggestedFolder fowWindow:self.window encoding:_selectedCppDocument->disk_encoding() completionHandler:^(NSString* path, encoding::type const& encoding){
if(!path)
return;
_selectedCppDocument->set_path(to_s(path));
_selectedCppDocument->set_disk_encoding(encoding);
[self saveDocumentsUsingEnumerator:@[ _selectedCppDocument->document() ].objectEnumerator completionHandler:nil];
}];
}
- (void)saveDocumentsUsingEnumerator:(NSEnumerator*)anEnumerator completionHandler:(void(^)(OakDocumentIOResult result))callback
{
if(OakDocument* document = [anEnumerator nextObject])
{
id observerId = [[NSNotificationCenter defaultCenter] addObserverForName:OakDocumentWillShowAlertNotification object:document queue:nil usingBlock:^(NSNotification*){
for(size_t i = 0; i < _cppDocuments.size(); ++i)
{
if(document.isLoaded && _cppDocuments[i]->document() == document)
{
if(_selectedCppDocument != _cppDocuments[i])
{
self.selectedTabIndex = i;
self.selectedCppDocument = _cppDocuments[i];
}
if(NSApp.isActive && (self.window.isMiniaturized || !self.window.isKeyWindow))
[self.window makeKeyAndOrderFront:self];
break;
}
}
}];
[document saveModalForWindow:self.window completionHandler:^(OakDocumentIOResult result, NSString* errorMessage, oak::uuid_t const& filterUUID){
[[NSNotificationCenter defaultCenter] removeObserver:observerId];
if(result == OakDocumentIOResultSuccess)
{
[self saveDocumentsUsingEnumerator:anEnumerator completionHandler:callback];
}
else
{
if(result == OakDocumentIOResultFailure)
{
[self.window.attachedSheet orderOut:self];
if(filterUUID)
show_command_error(to_s(errorMessage), filterUUID, self.window);
else [[NSAlert tmAlertWithMessageText:[NSString stringWithFormat:@"The document “%@” could not be saved.", document.displayName] informativeText:(errorMessage ?: @"Please check Console output for reason.") buttons:@"OK", nil] beginSheetModalForWindow:self.window modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
}
if(callback)
callback(result);
}
}];
}
else
{
if(callback)
callback(OakDocumentIOResultSuccess);
}
}
- (IBAction)saveAllDocuments:(id)sender
{
NSMutableArray* documentsToSave = [NSMutableArray array];
for(auto document : _cppDocuments)
{
if(document->is_modified())
[documentsToSave addObject:document->document()];
}
[self saveDocumentsUsingEnumerator:[documentsToSave objectEnumerator] completionHandler:nil];
}
- (void)saveAllEditedDocuments:(BOOL)includeAllFlag completionHandler:(void(^)(BOOL didSave))callback
{
NSMutableArray* documentsToSave = [NSMutableArray array];
if(includeAllFlag)
{
for(auto document : _cppDocuments)
{
if(document->is_modified() && document->path() != NULL_STR)
[documentsToSave addObject:document->document()];
}
}
else
{
if(_selectedCppDocument && (_selectedCppDocument->is_modified() || !_selectedCppDocument->is_on_disk()))
[documentsToSave addObject:_selectedCppDocument->document()];
}
[self saveDocumentsUsingEnumerator:[documentsToSave objectEnumerator] completionHandler:^(OakDocumentIOResult result){
callback(result == OakDocumentIOResultSuccess);
}];
}
- (OakHTMLOutputView*)htmlOutputView:(BOOL)createFlag forIdentifier:(NSUUID*)identifier
{
// if createFlag == YES then return (potential new) OakHTMLOutputView where isRunningCommand == NO.
// If createFlag == NO and there is non-busy OakHTMLOutputView with commandIdentifier == identifier then return it
// otherwise return busy OakHTMLOutputView with commandIdentifier == identifier or nil.
if(!self.htmlOutputInWindow)
{
BOOL nonExistingOrNonBusy = !self.htmlOutputView || !self.htmlOutputView.isRunningCommand;
BOOL existsForOurIdentifier = self.htmlOutputView && [self.htmlOutputView.commandIdentifier isEqual:identifier];
if(createFlag ? nonExistingOrNonBusy : existsForOurIdentifier)
{
self.htmlOutputVisible = YES;
return self.htmlOutputView;
}
}
NSMutableArray <OakHTMLOutputView*>* htmlOutputViews = [NSMutableArray array];
if(self.htmlOutputWindowController)
[htmlOutputViews addObject:self.htmlOutputWindowController.htmlOutputView];
for(NSWindow* window in [NSApp orderedWindows])
{
if([window isVisible] && ![window isMiniaturized] && [window.delegate isKindOfClass:[HTMLOutputWindowController class]])
[htmlOutputViews addObject:[(HTMLOutputWindowController*)window.delegate htmlOutputView]];
}
NSArray* allHTMLViews = [htmlOutputViews filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"needsNewWebView == NO AND commandIdentifier == %@", identifier]];
NSArray* nonBusyViews = [allHTMLViews filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isRunningCommand == NO"]];
if(OakHTMLOutputView* view = [nonBusyViews firstObject])
{
return view;
}
else if(createFlag)
{
self.htmlOutputWindowController = [[HTMLOutputWindowController alloc] initWithIdentifier:identifier];
return self.htmlOutputWindowController.htmlOutputView;
}
return [allHTMLViews firstObject];
}
- (void)updateEnvironment:(std::map<std::string, std::string>&)res forCommand:(OakCommand*)aCommand
{
for(auto const& pair : [self variables])
res[pair.first] = pair.second;
if(aCommand.firstResponder == _fileBrowser)
{
NSURL* fileURL = [[_fileBrowser.selectedURLs filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isFileURL == YES"]] firstObject];
res = bundles::scope_variables(res);
res = variables_for_path(res, to_s(fileURL.path));
}
else if(aCommand.firstResponder != _textView)
{
if([aCommand.firstResponder respondsToSelector:@selector(updateEnvironment:)])
[(id)aCommand.firstResponder updateEnvironment:res];
}
else // OakTextView
{
[_textView updateEnvironment:res];
}
}
- (void)showDocument:(OakDocument*)aDocument
{
[OakDocumentController.sharedInstance showDocument:aDocument inProject:[[NSUUID alloc] initWithUUIDString:self.identifier] bringToFront:YES];
}
// ================
// = Window Title =
// ================
- (void)updateProxyIcon
{
if(self.documentPath && self.documentIsOnDisk)
{
OakFileIconImage* icon = [[OakFileIconImage alloc] initWithSize:NSMakeSize(16, 16)];
icon.path = self.documentPath;
icon.scmStatus = self.documentSCMStatus;
self.window.representedFilename = icon.path;
[self.window standardWindowButton:NSWindowDocumentIconButton].image = icon;
}
else
{
self.window.representedFilename = @"";
[self.window standardWindowButton:NSWindowDocumentIconButton].image = nil;
}
}
- (void)updateWindowTitleAndRevealFile
{
[self updateWindowTitle];
if(self.autoRevealFile && self.fileBrowserVisible)
{
if(_selectedCppDocument && _selectedCppDocument->path() != NULL_STR)
[self revealFileInProject:self];
}
}
- (void)updateWindowTitle
{
if(_selectedCppDocument && _documentDisplayName)
{
auto map = _selectedCppDocument->document_variables();
auto const& scm = _documentSCMVariables.empty() ? _projectSCMVariables : _documentSCMVariables;
map.insert(scm.begin(), scm.end());
if(self.projectPath)
map["projectDirectory"] = to_s(self.projectPath);
std::string docDirectory = _selectedCppDocument->path() != NULL_STR ? path::parent(_selectedCppDocument->path()) : to_s(self.untitledSavePath);
settings_t const settings = settings_for_path(_selectedCppDocument->logical_path(), _selectedCppDocument->file_type() + " " + to_s(self.scopeAttributes), docDirectory, map);
self.window.title = [NSString stringWithCxxString:settings.get(kSettingsWindowTitleKey, to_s(self.documentDisplayName))];
}
else
{
self.window.title = @"«no documents»";
}
}
- (void)updateExternalAttributes
{
struct attribute_rule_t { std::string attribute; path::glob_t glob; std::string group; };
static auto const rules = new std::vector<attribute_rule_t>
{
{ "attr.scm.svn", ".svn", "scm" },
{ "attr.scm.hg", ".hg", "scm" },
{ "attr.scm.git", ".git", "scm" },
{ "attr.scm.p4", ".p4config", "scm" },
{ "attr.project.ninja", "build.ninja", "build" },
{ "attr.project.make", "Makefile", "build" },
{ "attr.project.xcode", "*.xcodeproj", "build" },
{ "attr.project.rake", "Rakefile", "build" },
{ "attr.project.ant", "build.xml", "build" },
{ "attr.project.cmake", "CMakeLists.txt", "build" },
{ "attr.project.maven", "pom.xml", "build" },
{ "attr.project.scons", "SConstruct", "build" },
{ "attr.project.lein", "project.clj", "build" },
{ "attr.project.cargo", "Cargo.toml", "build" },
{ "attr.project.swift", "Package.swift", "build" },
{ "attr.project.vagrant", "Vagrantfile", "vagrant" },
{ "attr.project.jekyll", "_config.yml", "jekyll" },
{ "attr.test.rspec", ".rspec", "test" },
};
_externalScopeAttributes.clear();
if(!_selectedCppDocument && !_projectPath)
return;
std::string const projectDir = to_s(_projectPath ?: NSHomeDirectory());
std::string const documentPath = _selectedCppDocument && _selectedCppDocument->path() != NULL_STR ? _selectedCppDocument->path() : path::join(projectDir, "dummy");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
std::vector<std::string> res;
std::set<std::string> groups;
std::string dir = documentPath;
do {
dir = path::parent(dir);
auto entries = path::entries(dir);
for(auto rule : *rules)
{
if(groups.find(rule.group) != groups.end())
continue;
for(auto entry : entries)
{
if(rule.glob.does_match(entry->d_name))
{
res.push_back(rule.attribute);
if(rule.group != NULL_STR)
{
groups.insert(rule.group);
break;
}
}
}
}
} while(path::is_child(dir, projectDir) && dir != projectDir);
dispatch_async(dispatch_get_main_queue(), ^{
std::string const currentProjectDir = to_s(_projectPath ?: NSHomeDirectory());
std::string const currentDocumentPath = _selectedCppDocument ? _selectedCppDocument->path() : path::join(projectDir, "dummy");
if(projectDir == currentProjectDir && currentDocumentPath == currentDocumentPath)
_externalScopeAttributes = res;
});
});
}
- (void)setProjectPath:(NSString*)newProjectPath
{
if(_projectPath != newProjectPath && ![_projectPath isEqualToString:newProjectPath])
{
_projectPath = newProjectPath;
if(_projectSCMInfo = scm::info(to_s(_projectPath)))
{
__weak DocumentWindowController* weakSelf = self;
_projectSCMInfo->add_callback(^(scm::info_t const& info){
weakSelf.projectSCMVariables = info.scm_variables();
});
}
else
{
self.projectSCMVariables = std::map<std::string, std::string>();
}
_projectScopeAttributes.clear();
std::string const customAttributes = settings_for_path(NULL_STR, text::join(_projectScopeAttributes, " "), to_s(_projectPath)).get(kSettingsScopeAttributesKey, NULL_STR);
if(customAttributes != NULL_STR)
_projectScopeAttributes.push_back(customAttributes);
[self updateExternalAttributes];
[self updateWindowTitleAndRevealFile];
}
}
- (void)setDocumentPath:(NSString*)newDocumentPath
{
if(_documentPath != newDocumentPath && !([_documentPath isEqualToString:newDocumentPath]) || _documentScopeAttributes.empty())
{
_documentPath = newDocumentPath;
_documentScopeAttributes = text::split(file::path_attributes(to_s(_documentPath)), " ");
std::string docDirectory = _documentPath ? path::parent(to_s(_documentPath)) : to_s(self.projectPath);
if(_selectedCppDocument)
{
std::string const customAttributes = settings_for_path(to_s(_documentPath), _selectedCppDocument->file_type() + " " + text::join(_documentScopeAttributes, " "), docDirectory).get(kSettingsScopeAttributesKey, NULL_STR);
if(customAttributes != NULL_STR)
_documentScopeAttributes.push_back(customAttributes);
}
self.documentSCMStatus = scm::status::unknown;
self.documentSCMVariables = std::map<std::string, std::string>();
if(_documentSCMInfo = scm::info(docDirectory))
{
__weak DocumentWindowController* weakSelf = self;
_documentSCMInfo->add_callback(^(scm::info_t const& info){
weakSelf.documentSCMStatus = info.status(to_s(weakSelf.documentPath));
weakSelf.documentSCMVariables = info.scm_variables();
});
}
[self updateExternalAttributes];
[self updateProxyIcon];
[self updateWindowTitleAndRevealFile];
}
}
- (void)setDocumentDisplayName:(NSString*)newDisplayName
{
if(_documentDisplayName != newDisplayName && ![_documentDisplayName isEqualToString:newDisplayName])
{
_documentDisplayName = newDisplayName;
[self updateWindowTitleAndRevealFile];
}
}
- (void)setDocumentIsModified:(BOOL)newDocumentIsModified
{
if(_documentIsModified != newDocumentIsModified)
{
_documentIsModified = newDocumentIsModified;
self.window.documentEdited = _documentIsModified;
}
}
- (void)setDocumentIsOnDisk:(BOOL)newDocumentIsOnDisk
{
if(_documentIsOnDisk != newDocumentIsOnDisk)
{
_documentIsOnDisk = newDocumentIsOnDisk;
[self updateProxyIcon];
}
}
- (void)setDocumentSCMStatus:(scm::status::type)newDocumentSCMStatus
{
if(_documentSCMStatus != newDocumentSCMStatus)
{
_documentSCMStatus = newDocumentSCMStatus;
[self updateProxyIcon];
}
}
- (void)setProjectSCMVariables:(std::map<std::string, std::string> const&)newVariables
{
if(_projectSCMVariables != newVariables)
{
_projectSCMVariables = newVariables;
[self updateWindowTitleAndRevealFile];
}
}
- (void)setDocumentSCMVariables:(std::map<std::string, std::string> const&)newVariables
{
if(_documentSCMVariables != newVariables)
{
_documentSCMVariables = newVariables;
[self updateWindowTitleAndRevealFile];
}
}
- (std::map<std::string, std::string> const&)projectSCMVariables
{
return _projectSCMVariables;
}
- (std::map<std::string, std::string> const&)documentSCMVariables
{
return _documentSCMVariables;
}
- (void)takeProjectPathFrom:(NSMenuItem*)aMenuItem
{
if(NSString* path = [aMenuItem respondsToSelector:@selector(representedObject)] ? [aMenuItem representedObject] : nil)
self.projectPath = self.defaultProjectPath = path;
}
// ========================
// = OakTextView Delegate =
// ========================
- (NSString*)scopeAttributes
{
std::set<std::string> attributes;
auto const& vars = _documentSCMVariables.empty() ? _projectSCMVariables : _documentSCMVariables;
auto scmName = vars.find("TM_SCM_NAME");
if(scmName != vars.end())
attributes.insert("attr.scm." + scmName->second);
auto branch = vars.find("TM_SCM_BRANCH");
if(branch != vars.end())
attributes.insert("attr.scm.branch." + branch->second);
if(self.documentSCMStatus != scm::status::unknown)
attributes.insert("attr.scm.status." + to_s(self.documentSCMStatus));
attributes.insert(_documentScopeAttributes.begin(), _documentScopeAttributes.end());
attributes.insert(_projectScopeAttributes.begin(), _projectScopeAttributes.end());
attributes.insert(_externalScopeAttributes.begin(), _externalScopeAttributes.end());
return [NSString stringWithCxxString:text::join(attributes, " ")];
}
// ==============
// = Properties =
// ==============
- (OakDocument*)selectedDocument
{
return _selectedCppDocument ? _selectedCppDocument->document() : nil;
}
- (NSArray<OakDocument*>*)documents
{
return _documents;
}
- (void)setCppDocuments:(std::vector<document::document_ptr>)newDocuments
{
for(auto document : newDocuments)
{
[self trackDocument:document->document()];
// Avoid resetting directory when tearing off a tab (unless moved to new project)
if(!document->document().path && (self.projectPath || !document->document().directory))
document->document().directory = self.projectPath ?: self.defaultProjectPath;
}
for(OakDocument* document in self.documents)
[self untrackDocument:document];
_cppDocuments = newDocuments;
NSMutableArray* docs = [NSMutableArray array];
for(auto doc : _cppDocuments)
[docs addObject:doc->document()];
_documents = docs;
if(_cppDocuments.size())
[self.tabBarView reloadData];
[self updateFileBrowserStatus:self];
[[self class] scheduleSessionBackup:self];
}
- (void)setSelectedCppDocument:(document::document_ptr)newSelectedDocument
{
ASSERT(!newSelectedDocument || newSelectedDocument->is_loaded());
if(_selectedCppDocument == newSelectedDocument)
{
self.documentView.document = _selectedCppDocument ? _selectedCppDocument->document() : nil;
return;
}
if(_selectedCppDocument)
_selectedCppDocument->hide();
if(newSelectedDocument)
newSelectedDocument->show();
[self trackDocument:newSelectedDocument ? newSelectedDocument->document() : nil];
[self untrackDocument:self.selectedDocument];
if(_selectedCppDocument = newSelectedDocument)
{
NSString* projectPath = self.defaultProjectPath ?: self.fileBrowser.path ?: [NSString stringWithCxxString:path::parent(_selectedCppDocument->path())];
if(projectPath)
{
std::map<std::string, std::string> const map = { { "projectDirectory", to_s(projectPath) } };
settings_t const settings = settings_for_path(NULL_STR, scope::scope_t(), to_s(projectPath), map);
std::string const userProjectDirectory = settings.get(kSettingsProjectDirectoryKey, NULL_STR);
if(path::is_absolute(userProjectDirectory))
projectPath = [NSString stringWithCxxString:path::normalize(userProjectDirectory)];
}
else if(NSString* urlString = [[NSUserDefaults standardUserDefaults] stringForKey:kUserDefaultsInitialFileBrowserURLKey])
{
if(NSURL* url = [NSURL URLWithString:urlString])
projectPath = [[url filePathURL] path];
}
self.projectPath = projectPath;
self.documentPath = [NSString stringWithCxxString:_selectedCppDocument->logical_path()];
self.documentDisplayName = [NSString stringWithCxxString:_selectedCppDocument->display_name()];
self.documentIsModified = _selectedCppDocument->is_modified();
self.documentIsOnDisk = _selectedCppDocument->is_on_disk();
self.documentView.document = _selectedCppDocument->document();
[[self class] scheduleSessionBackup:self];
}
else
{
self.projectPath = nil;
self.documentPath = nil;
self.documentDisplayName = nil;
self.documentIsModified = NO;
self.documentIsOnDisk = NO;
}
}
- (void)setSelectedTabIndex:(NSUInteger)newSelectedTabIndex
{
_selectedTabIndex = newSelectedTabIndex;
[self.tabBarView setSelectedTab:newSelectedTabIndex];
}
- (void)setIdentifier:(NSString*)newIdentifier
{
if(_identifier == newIdentifier || [_identifier isEqualToString:newIdentifier])
return;
NSString* oldIdentifier = _identifier;
if(_identifier = newIdentifier)
[AllControllers() setObject:self forKey:newIdentifier];
if(oldIdentifier)
[AllControllers() removeObjectForKey:oldIdentifier]; // This may release our object
}
// ===========================
// = OakTabBarViewDataSource =
// ===========================
- (NSUInteger)numberOfRowsInTabBarView:(OakTabBarView*)aTabBarView { return _cppDocuments.size(); }
- (NSString*)tabBarView:(OakTabBarView*)aTabBarView titleForIndex:(NSUInteger)anIndex { return [NSString stringWithCxxString:_cppDocuments[anIndex]->display_name()]; }
- (NSString*)tabBarView:(OakTabBarView*)aTabBarView pathForIndex:(NSUInteger)anIndex { return [NSString stringWithCxxString:_cppDocuments[anIndex]->path()] ?: @""; }
- (NSString*)tabBarView:(OakTabBarView*)aTabBarView identifierForIndex:(NSUInteger)anIndex { return [NSString stringWithCxxString:_cppDocuments[anIndex]->identifier()]; }
- (BOOL)tabBarView:(OakTabBarView*)aTabBarView isEditedAtIndex:(NSUInteger)anIndex { return _cppDocuments[anIndex]->is_modified(); }
// ==============================
// = OakTabBarView Context Menu =
// ==============================
- (NSIndexSet*)tryObtainIndexSetFrom:(id)sender
{
id res = [sender respondsToSelector:@selector(representedObject)] ? [sender representedObject] : sender;
if([res isKindOfClass:[NSIndexSet class]])
return res;
else if(!_cppDocuments.empty())
return [NSIndexSet indexSetWithIndex:self.selectedTabIndex];
return nil;
}
- (void)takeNewTabIndexFrom:(id)sender
{
if(NSIndexSet* indexSet = [self tryObtainIndexSetFrom:sender])
{
document::document_ptr doc = document::create();
[self insertDocuments:{ doc } atIndex:[indexSet firstIndex] selecting:doc andClosing:{ }];
[self openAndSelectDocument:doc];
}
}
- (void)takeTabsToCloseFrom:(id)sender
{
if(NSIndexSet* indexSet = [self tryObtainIndexSetFrom:sender])
[self closeTabsAtIndexes:indexSet askToSaveChanges:YES createDocumentIfEmpty:YES];
}
- (void)takeTabsToTearOffFrom:(id)sender
{
if(NSIndexSet* indexSet = [self tryObtainIndexSetFrom:sender])
{
std::vector<document::document_ptr> documents;
for(NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
documents.push_back(_cppDocuments[index]);
if(documents.size() == 1)
{
DocumentWindowController* controller = [DocumentWindowController new];
controller.cppDocuments = { documents[0] };
if(path::is_child(documents[0]->path(), to_s(self.projectPath)))
controller.defaultProjectPath = self.projectPath;
[controller openAndSelectDocument:documents[0]];
[controller showWindow:self];
[self closeTabsAtIndexes:indexSet askToSaveChanges:NO createDocumentIfEmpty:YES];
}
}
}
- (IBAction)toggleSticky:(id)sender
{
if(NSIndexSet* indexSet = [self tryObtainIndexSetFrom:sender])
{
std::vector<document::document_ptr> documents;
for(NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
[self setDocument:_cppDocuments[index] sticky:![self isDocumentSticky:_cppDocuments[index]]];
}
}
- (NSMenu*)menuForTabBarView:(OakTabBarView*)aTabBarView
{
NSInteger tabIndex = aTabBarView.tag;
NSInteger total = _cppDocuments.size();
NSMutableIndexSet* newTabAtTab = tabIndex == -1 ? [NSMutableIndexSet indexSetWithIndex:total] : [NSMutableIndexSet indexSetWithIndex:tabIndex + 1];
NSMutableIndexSet* clickedTab = tabIndex == -1 ? [NSMutableIndexSet indexSet] : [NSMutableIndexSet indexSetWithIndex:tabIndex];
NSMutableIndexSet* otherTabs = tabIndex == -1 ? [NSMutableIndexSet indexSet] : [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, total)];
NSMutableIndexSet* rightSideTabs = tabIndex == -1 ? [NSMutableIndexSet indexSet] : [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, total)];
if(tabIndex != -1)
{
[otherTabs removeIndex:tabIndex];
[rightSideTabs removeIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, tabIndex + 1)]];
}
for(size_t i = 0; i < _cppDocuments.size(); ++i)
{
if([self isDocumentSticky:_cppDocuments[i]])
{
[otherTabs removeIndex:i];
[rightSideTabs removeIndex:i];
}
}
SEL closeSingleTabSelector = tabIndex == _selectedTabIndex ? @selector(performCloseTab:) : @selector(takeTabsToCloseFrom:);
NSMenu* menu = [NSMenu new];
[menu addItemWithTitle:@"New Tab" action:@selector(takeNewTabIndexFrom:) keyEquivalent:@""];
[menu addItemWithTitle:@"Move Tab to New Window" action:@selector(takeTabsToTearOffFrom:) keyEquivalent:@""];
[menu addItem:[NSMenuItem separatorItem]];
[menu addItemWithTitle:@"Close Tab" action:closeSingleTabSelector keyEquivalent:@""];
[menu addItemWithTitle:@"Close Other Tabs" action:@selector(takeTabsToCloseFrom:) keyEquivalent:@""];
[menu addItemWithTitle:@"Close Tabs to the Right" action:@selector(takeTabsToCloseFrom:) keyEquivalent:@""];
[menu addItem:[NSMenuItem separatorItem]];
[menu addItemWithTitle:@"Sticky" action:@selector(toggleSticky:) keyEquivalent:@""];
NSIndexSet* indexSets[] = { newTabAtTab, total > 1 ? clickedTab : [NSIndexSet indexSet], nil, clickedTab, otherTabs, rightSideTabs, nil, clickedTab };
for(size_t i = 0; i < sizeofA(indexSets); ++i)
{
if(NSIndexSet* indexSet = indexSets[i])
[[menu itemAtIndex:i] setRepresentedObject:indexSet];
}
return menu;
}
// =========================
// = OakTabBarViewDelegate =
// =========================
- (BOOL)tabBarView:(OakTabBarView*)aTabBarView shouldSelectIndex:(NSUInteger)anIndex
{
[self openAndSelectDocument:_cppDocuments[anIndex]];
self.selectedTabIndex = anIndex;
return YES;
}
- (void)tabBarView:(OakTabBarView*)aTabBarView didDoubleClickIndex:(NSUInteger)anIndex
{
if(_cppDocuments.size() > 1)
[self takeTabsToTearOffFrom:[NSMutableIndexSet indexSetWithIndex:anIndex]];
}
- (void)tabBarViewDidDoubleClick:(OakTabBarView*)aTabBarView
{
[self takeNewTabIndexFrom:[NSMutableIndexSet indexSetWithIndex:_cppDocuments.size()]];
}
// ================
// = Tab Dragging =
// ================
- (BOOL)performDropOfTabItem:(OakTabItem*)tabItem fromTabBar:(OakTabBarView*)sourceTabBar index:(NSUInteger)dragIndex toTabBar:(OakTabBarView*)destTabBar index:(NSUInteger)droppedIndex operation:(NSDragOperation)operation
{
document::document_ptr srcDocument = document::find(to_s(tabItem.identifier));
if(!srcDocument)
return NO;
[self insertDocuments:{ srcDocument } atIndex:droppedIndex selecting:_selectedCppDocument andClosing:{ srcDocument->identifier() }];
if(operation == NSDragOperationMove && sourceTabBar != destTabBar)
{
for(DocumentWindowController* delegate in SortedControllers())
{
if(delegate == sourceTabBar.delegate)
{
if(delegate.fileBrowserVisible || delegate.cppDocuments.size() > 1)
[delegate closeTabsAtIndexes:[NSIndexSet indexSetWithIndex:dragIndex] askToSaveChanges:NO createDocumentIfEmpty:YES];
else [delegate close];
return YES;
}
}
}
return YES;
}
- (IBAction)selectNextTab:(id)sender { self.selectedTabIndex = (_selectedTabIndex + 1) % _cppDocuments.size(); [self openAndSelectDocument:_cppDocuments[_selectedTabIndex]]; }
- (IBAction)selectPreviousTab:(id)sender { self.selectedTabIndex = (_selectedTabIndex + _cppDocuments.size() - 1) % _cppDocuments.size(); [self openAndSelectDocument:_cppDocuments[_selectedTabIndex]]; }
- (IBAction)takeSelectedTabIndexFrom:(id)sender { self.selectedTabIndex = [sender tag]; [self openAndSelectDocument:_cppDocuments[_selectedTabIndex]]; }
// ==================
// = OakFileBrowser =
// ==================
- (void)fileBrowser:(OakFileBrowser*)aFileBrowser openURLs:(NSArray*)someURLs
{
NSMutableArray* items = [NSMutableArray array];
for(NSURL* url in someURLs)
{
if([url isFileURL])
[items addObject:@{ @"path" : [url path] }];
}
[self openItems:items closingOtherTabs:OakIsAlternateKeyOrMouseEvent()];
}
- (void)fileBrowser:(OakFileBrowser*)aFileBrowser closeURL:(NSURL*)anURL
{
if(![anURL isFileURL])
return;
std::string const path = to_s([anURL path]);
auto documents = _cppDocuments;
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
for(size_t i = 0; i < documents.size(); ++i)
{
if(path == documents[i]->path())
[indexSet addIndex:i];
}
[self closeTabsAtIndexes:indexSet askToSaveChanges:YES createDocumentIfEmpty:YES];
}
- (void)setFileBrowserVisible:(BOOL)makeVisibleFlag
{
if(_fileBrowserVisible != makeVisibleFlag)
{
_fileBrowserVisible = makeVisibleFlag;
if(!self.fileBrowser && makeVisibleFlag)
{
self.fileBrowser = [OakFileBrowser new];
self.fileBrowser.delegate = self;
[self.fileBrowser setupViewWithState:_fileBrowserHistory];
if(self.projectPath && !_fileBrowserHistory)
self.fileBrowser.url = [NSURL fileURLWithPath:self.projectPath];
[self updateFileBrowserStatus:self];
if(self.layoutView.tabsAboveDocument)
[self.tabBarView expand];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileBrowserDidDuplicateAtURLs:) name:OakFileBrowserDidDuplicateURLs object:_fileBrowser];
}
if(!makeVisibleFlag && [[self.window firstResponder] isKindOfClass:[NSView class]] && [(NSView*)[self.window firstResponder] isDescendantOf:self.layoutView.fileBrowserView])
[self makeTextViewFirstResponder:self];
self.layoutView.fileBrowserView = makeVisibleFlag ? self.fileBrowser.view : nil;
self.layoutView.fileBrowserHeaderView = makeVisibleFlag ? self.fileBrowser.headerView : nil;
if(makeVisibleFlag)
{
self.fileBrowser.nextResponder = self.fileBrowser.view.nextResponder;
self.fileBrowser.view.nextResponder = self.fileBrowser;
if(self.autoRevealFile && _selectedCppDocument && _selectedCppDocument->path() != NULL_STR)
[self revealFileInProject:self];
}
if(!self.disableFileBrowserWindowResize && ([self.window styleMask] & NSFullScreenWindowMask) != NSFullScreenWindowMask)
{
NSRect windowFrame = self.window.frame;
if(NSEqualRects(windowFrame, self.newWindowFrame))
{
windowFrame = self.oldWindowFrame;
}
else if(makeVisibleFlag)
{
NSRect screenFrame = [[self.window screen] visibleFrame];
CGFloat minX = NSMinX(windowFrame);
CGFloat maxX = NSMaxX(windowFrame);
if(self.layoutView.fileBrowserOnRight)
maxX += self.fileBrowserWidth + 1;
else minX -= self.fileBrowserWidth + 1;
if(minX < NSMinX(screenFrame))
maxX += NSMinX(screenFrame) - minX;
if(maxX > NSMaxX(screenFrame))
minX -= maxX - NSMaxX(screenFrame);
minX = MAX(minX, NSMinX(screenFrame));
maxX = MIN(maxX, NSMaxX(screenFrame));
windowFrame.origin.x = minX;
windowFrame.size.width = maxX - minX;
}
else
{
windowFrame.size.width -= self.fileBrowserWidth + 1;
if(!self.layoutView.fileBrowserOnRight)
windowFrame.origin.x += self.fileBrowserWidth + 1;
}
self.oldWindowFrame = self.window.frame;
[self.window setFrame:windowFrame display:YES];
self.newWindowFrame = self.window.frame;
}
}
[[self class] scheduleSessionBackup:self];
}
- (IBAction)toggleFileBrowser:(id)sender { self.fileBrowserVisible = !self.fileBrowserVisible; }
- (void)updateFileBrowserStatus:(id)sender
{
NSMutableArray* openURLs = [NSMutableArray array];
NSMutableArray* modifiedURLs = [NSMutableArray array];
for(auto document : _cppDocuments)
{
if(document->path() != NULL_STR)
[openURLs addObject:[NSURL fileURLWithPath:[NSString stringWithCxxString:document->path()]]];
if(document->path() != NULL_STR && document->is_modified())
[modifiedURLs addObject:[NSURL fileURLWithPath:[NSString stringWithCxxString:document->path()]]];
}
self.fileBrowser.openURLs = openURLs;
self.fileBrowser.modifiedURLs = modifiedURLs;
}
- (NSDictionary*)fileBrowserHistory { return self.fileBrowser.sessionState ?: _fileBrowserHistory; }
- (CGFloat)fileBrowserWidth { return self.layoutView.fileBrowserWidth; }
- (void)setFileBrowserWidth:(CGFloat)aWidth { self.layoutView.fileBrowserWidth = aWidth; }
- (IBAction)newFolder:(id)sender { if(self.fileBrowser) [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)reload:(id)sender { if(self.fileBrowser) [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)deselectAll:(id)sender { if(self.fileBrowser) [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)revealFileInProject:(id)sender { if(_selectedCppDocument) { self.fileBrowserVisible = YES; [self.fileBrowser selectURL:[NSURL fileURLWithPath:[NSString stringWithCxxString:_selectedCppDocument->path()]] withParentURL:self.projectPath ? [NSURL fileURLWithPath:self.projectPath] : nil]; } }
- (IBAction)goToProjectFolder:(id)sender { self.fileBrowserVisible = YES; [self.fileBrowser goToURL:[NSURL fileURLWithPath:self.projectPath]]; }
- (IBAction)goBack:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)goForward:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)goToParentFolder:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)goToComputer:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)goToHome:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)goToDesktop:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)goToFavorites:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)goToSCMDataSource:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
- (IBAction)orderFrontGoToFolder:(id)sender { self.fileBrowserVisible = YES; [NSApp sendAction:_cmd to:self.fileBrowser from:sender]; }
// ===============
// = HTML Output =
// ===============
- (NSSize)htmlOutputSize { return self.layoutView.htmlOutputSize; }
- (void)setHtmlOutputSize:(NSSize)aSize { self.layoutView.htmlOutputSize = aSize; }
- (BOOL)htmlOutputVisible
{
return self.htmlOutputInWindow ? [self.htmlOutputWindowController.window isVisible] : self.layoutView.htmlOutputView != nil;
}
- (void)setHtmlOutputVisible:(BOOL)makeVisibleFlag
{
if(self.htmlOutputVisible == makeVisibleFlag)
return;
if(makeVisibleFlag)
{
if(self.htmlOutputInWindow)
{
[self.htmlOutputWindowController showWindow:self];
}
else
{
if(!self.htmlOutputView || self.htmlOutputView.needsNewWebView)
self.htmlOutputView = [[OakHTMLOutputView alloc] initWithFrame:NSZeroRect];
self.layoutView.htmlOutputView = self.htmlOutputView;
}
}
else
{
if(self.layoutView.htmlOutputView && [[self.window firstResponder] isKindOfClass:[NSView class]] && [(NSView*)[self.window firstResponder] isDescendantOf:self.layoutView.htmlOutputView])
[self makeTextViewFirstResponder:self];
if(self.layoutView.htmlOutputView)
self.layoutView.htmlOutputView = nil;
else [self.htmlOutputWindowController close];
}
}
- (void)setHtmlOutputInWindow:(BOOL)showInWindowFlag
{
if(_htmlOutputInWindow == showInWindowFlag)
return;
if(_htmlOutputInWindow = showInWindowFlag)
{
self.layoutView.htmlOutputView = nil;
self.htmlOutputView = nil;
}
else
{
self.htmlOutputWindowController = nil;
}
}
- (IBAction)toggleHTMLOutput:(id)sender
{
if(self.htmlOutputVisible && self.htmlOutputInWindow && ![self.htmlOutputWindowController.window isKeyWindow])
[self.htmlOutputWindowController showWindow:self];
else self.htmlOutputVisible = !self.htmlOutputVisible;
}
// =============================
// = Opening Auxiliary Windows =
// =============================
- (void)positionWindow:(NSWindow*)aWindow
{
if(![aWindow isVisible])
{
[aWindow layoutIfNeeded];
NSRect frame = [aWindow frame];
NSRect parent = [self.window convertRectToScreen:[_textView convertRect:[_textView visibleRect] toView:nil]];
frame.origin.x = NSMinX(parent) + round((NSWidth(parent) - NSWidth(frame)) * 1 / 4);
frame.origin.y = NSMinY(parent) + round((NSHeight(parent) - NSHeight(frame)) * 3 / 4);
[aWindow setFrame:frame display:NO];
}
}
- (NSUUID*)selectedDocumentUUID
{
return _selectedCppDocument ? _selectedCppDocument->document().identifier : nil;
}
- (Find*)prepareAndReturnFindPanel
{
Find* find = [Find sharedInstance];
find.documentIdentifier = self.selectedDocumentUUID;
find.projectFolder = self.projectPath ?: self.untitledSavePath ?: NSHomeDirectory();
find.projectIdentifier = self.identifier;
NSArray* items;
if(self.fileBrowserVisible)
{
items = [[self.fileBrowser.selectedURLs filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isFileURL == YES"]] valueForKey:@"path"];
if(items.count == 0)
items = @[ self.fileBrowser.path ];
}
find.fileBrowserItems = items.count ? items : nil;
return find;
}
- (IBAction)orderFrontFindPanel:(id)sender
{
Find* find = [Find sharedInstance];
BOOL didOwnDialog = [find.projectIdentifier isEqualToString:self.identifier];
[self prepareAndReturnFindPanel];
NSInteger mode = [sender respondsToSelector:@selector(tag)] ? [sender tag] : find_tags::in_document;
if(mode == find_tags::in_document && ![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsAlwaysFindInDocument] && [self.window isKeyWindow] && self.textView.hasMultiLineSelection)
mode = find_tags::in_selection;
switch(mode)
{
case find_tags::in_document: find.searchTarget = FFSearchTargetDocument; break;
case find_tags::in_selection: find.searchTarget = FFSearchTargetSelection; break;
case find_tags::in_folder: return [find showFolderSelectionPanel:self]; break;
case find_tags::in_project:
{
// Only reset search target if the dialog is not already showing potential search results from “Other…”
if(!find.isVisible || !didOwnDialog || find.searchTarget == FFSearchTargetDocument || find.searchTarget == FFSearchTargetSelection)
{
BOOL fileBrowserHasFocus = [self.window.firstResponder respondsToSelector:@selector(isDescendantOf:)] && [(NSView*)self.window.firstResponder isDescendantOf:self.fileBrowser.view];
find.searchTarget = fileBrowserHasFocus ? FFSearchTargetFileBrowserItems : FFSearchTargetProject;
}
}
break;
}
[find showWindow:self];
}
- (IBAction)orderFrontFindPanelForFileBrowser:(id)sender
{
Find* find = [self prepareAndReturnFindPanel];
find.searchTarget = FFSearchTargetFileBrowserItems;
[find showWindow:self];
}
- (IBAction)orderFrontRunCommandWindow:(id)sender
{
OakRunCommandWindowController* runCommand = [OakRunCommandWindowController sharedInstance];
[self positionWindow:runCommand.window];
[runCommand showWindow:nil];
}
// ==================
// = OakFileChooser =
// ==================
- (IBAction)goToFile:(id)sender
{
FileChooser* fc = [FileChooser sharedInstance];
fc.path = nil; // Disable potential work when updating filterString/currentDocument
fc.filterString = @"";
fc.currentDocument = self.selectedDocumentUUID;
fc.target = self;
fc.action = @selector(fileChooserDidSelectItems:);
fc.path = self.projectPath ?: self.untitledSavePath ?: NSHomeDirectory();
if(OakPasteboardEntry* entry = [[OakPasteboard pasteboardWithName:NSFindPboard] current])
{
std::string str = to_s(entry.string);
if(regexp::search("\\A.*?(\\.|/).*?:\\d+\\z", str))
{
if([entry.string hasPrefix:fc.path])
fc.filterString = [NSString stringWithCxxString:path::relative_to(str, to_s(fc.path))];
else fc.filterString = entry.string;
}
}
[fc showWindowRelativeToFrame:[self.window convertRectToScreen:[self.textView convertRect:[self.textView visibleRect] toView:nil]]];
}
- (void)fileChooserDidSelectItems:(FileChooser*)sender
{
ASSERT([sender respondsToSelector:@selector(selectedItems)]);
[self openItems:[sender selectedItems] closingOtherTabs:OakIsAlternateKeyOrMouseEvent()];
}
// ===========
// = Methods =
// ===========
- (NSString*)untitledSavePath
{
NSString* res = self.projectPath ?: (_selectedCppDocument ? to_ns(path::parent(_selectedCppDocument->path())) : nil);
if(self.fileBrowserVisible)
{
NSArray* selectedURLs = self.fileBrowser.selectedURLs;
if([selectedURLs count] == 1 && [[selectedURLs lastObject] isFileURL])
{
NSString* path = [[selectedURLs lastObject] path];
res = path::is_directory(to_s(path)) ? path : [path stringByDeletingLastPathComponent];
}
else if(NSString* folder = self.fileBrowser.path)
{
res = folder;
}
}
return res;
}
- (BOOL)treatAsProjectWindow
{
return self.projectPath && (self.fileBrowserVisible || _cppDocuments.size() > 1);
}
- (NSPoint)positionForWindowUnderCaret
{
return [self.textView positionForWindowUnderCaret];
}
- (void)performBundleItem:(bundles::item_ptr const&)anItem
{
if(anItem->kind() == bundles::kItemTypeTheme)
{
[self.documentView setThemeWithUUID:[NSString stringWithCxxString:anItem->uuid()]];
}
else
{
[self showWindow:self];
[self makeTextViewFirstResponder:self];
[self.textView performBundleItem:anItem];
}
}
- (IBAction)goToRelatedFile:(id)sender
{
if(!_selectedCppDocument)
return;
std::string const documentPath = _selectedCppDocument->path();
if(documentPath == NULL_STR)
return (void)NSBeep();
std::string const documentDir = path::parent(documentPath);
std::string const documentName = path::name(documentPath);
std::string const documentBase = path::strip_extensions(documentName);
std::set<std::string> candidates = { documentName };
for(auto document : _cppDocuments)
{
if(documentDir == path::parent(document->path()) && documentBase == path::strip_extensions(path::name(document->path())))
candidates.insert(path::name(document->path()));
}
auto map = _selectedCppDocument->document_variables();
auto const& scm = _documentSCMVariables.empty() ? _projectSCMVariables : _documentSCMVariables;
map.insert(scm.begin(), scm.end());
if(self.projectPath)
map["projectDirectory"] = to_s(self.projectPath);
settings_t const settings = settings_for_path(_selectedCppDocument->logical_path(), _selectedCppDocument->file_type() + " " + to_s(self.scopeAttributes), path::parent(documentPath), map);
std::string const customCandidate = settings.get(kSettingsRelatedFilePathKey, NULL_STR);
if(customCandidate != NULL_STR && customCandidate != documentPath && (std::find_if(_cppDocuments.begin(), _cppDocuments.end(), [&customCandidate](document::document_ptr const& doc){ return customCandidate == doc->path(); }) != _cppDocuments.end() || path::exists(customCandidate)))
return [self openItems:@[ @{ @"path" : [NSString stringWithCxxString:customCandidate] } ] closingOtherTabs:NO];
for(auto const& entry : path::entries(documentDir))
{
std::string const name = entry->d_name;
if(entry->d_type == DT_REG && documentBase == path::strip_extensions(name) && path::extensions(name) != "")
{
std::string const content = path::content(path::join(documentDir, name));
if(utf8::is_valid(content.data(), content.data() + content.size()))
candidates.insert(name);
}
}
path::glob_t const excludeGlob(settings.get(kSettingsExcludeKey, ""));
path::glob_t const binaryGlob(settings.get(kSettingsBinaryKey, ""));
std::vector<std::string> v;
for(auto const& name : candidates)
{
if(name == documentName || !binaryGlob.does_match(name) && !excludeGlob.does_match(name))
v.push_back(name);
}
if(v.size() == 1)
{
if(customCandidate == NULL_STR || customCandidate == documentPath)
return (void)NSBeep();
v.push_back(customCandidate);
}
std::vector<std::string>::const_iterator it = std::find(v.begin(), v.end(), documentName);
ASSERT(it != v.end());
NSString* path = [NSString stringWithCxxString:path::join(documentDir, v[((it - v.begin()) + 1) % v.size()])];
[self openItems:@[ @{ @"path" : path } ] closingOtherTabs:NO];
}
// ============================
// = Select Tab Menu Delegate =
// ============================
- (void)updateSelectTabMenu:(NSMenu*)aMenu
{
if(![self.window isKeyWindow])
{
[aMenu addItemWithTitle:@"No Tabs" action:@selector(nop:) keyEquivalent:@""];
return;
}
int i = 0;
for(auto document : _cppDocuments)
{
NSMenuItem* item = [aMenu addItemWithTitle:[NSString stringWithCxxString:document->display_name()] action:@selector(takeSelectedTabIndexFrom:) keyEquivalent:i < 8 ? [NSString stringWithFormat:@"%c", '1' + i] : @""];
item.tag = i;
item.toolTip = [[NSString stringWithCxxString:document->path()] stringByAbbreviatingWithTildeInPath];
if(aMenu.propertiesToUpdate & NSMenuPropertyItemImage)
item.image = document->document().icon;
if(i == _selectedTabIndex)
[item setState:NSOnState];
else if(document->is_modified())
[item setModifiedState:YES];
++i;
}
if(i == 0)
{
[aMenu addItemWithTitle:@"No Tabs Open" action:@selector(nop:) keyEquivalent:@""];
}
else
{
[aMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem* item = [aMenu addItemWithTitle:@"Last Tab" action:@selector(takeSelectedTabIndexFrom:) keyEquivalent:@"9"];
item.tag = _cppDocuments.size()-1;
item.toolTip = [NSString stringWithCxxString:_cppDocuments.back()->display_name()];
}
}
// ====================
// = NSMenuValidation =
// ====================
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
{
static std::set<SEL> const delegateToFileBrowser = {
@selector(newFolder:), @selector(goBack:), @selector(goForward:),
@selector(reload:), @selector(deselectAll:)
};
BOOL active = YES;
if([menuItem action] == @selector(toggleFileBrowser:))
[menuItem setTitle:self.fileBrowserVisible ? @"Hide File Browser" : @"Show File Browser"];
else if([menuItem action] == @selector(toggleHTMLOutput:))
{
[menuItem setTitle:(!self.htmlOutputVisible || self.htmlOutputInWindow) ? @"Show HTML Output" : @"Hide HTML Output"];
active = !self.htmlOutputInWindow || self.htmlOutputWindowController;
}
else if([menuItem action] == @selector(newDocumentInDirectory:))
active = self.fileBrowserVisible && [self.fileBrowser directoryForNewItems] != nil;
else if(delegateToFileBrowser.find([menuItem action]) != delegateToFileBrowser.end())
active = self.fileBrowserVisible && [self.fileBrowser validateMenuItem:menuItem];
else if([menuItem action] == @selector(moveDocumentToNewWindow:))
active = _cppDocuments.size() > 1;
else if([menuItem action] == @selector(selectNextTab:) || [menuItem action] == @selector(selectPreviousTab:))
active = _cppDocuments.size() > 1;
else if([menuItem action] == @selector(revealFileInProject:) || [menuItem action] == @selector(revealFileInProjectByExpandingAncestors:))
{
active = _selectedCppDocument && _selectedCppDocument->path() != NULL_STR;
[menuItem setDynamicTitle:active ? [NSString stringWithFormat:@"Select “%@”", [NSString stringWithCxxString:_selectedCppDocument->display_name()]] : @"Select Document"];
}
else if([menuItem action] == @selector(goToProjectFolder:))
active = self.projectPath != nil;
else if([menuItem action] == @selector(goToParentFolder:))
active = [self.window firstResponder] != self.textView;
else if([menuItem action] == @selector(moveFocus:))
[menuItem setTitle:self.window.firstResponder == self.textView ? @"Move Focus to File Browser" : @"Move Focus to Document"];
else if([menuItem action] == @selector(takeProjectPathFrom:))
[menuItem setState:[self.defaultProjectPath isEqualToString:[menuItem representedObject]] ? NSOnState : NSOffState];
else if([menuItem action] == @selector(performCloseOtherTabsXYZ:))
active = _cppDocuments.size() > 1;
else if([menuItem action] == @selector(performCloseTabsToTheRight:))
active = _selectedTabIndex + 1 < _cppDocuments.size();
SEL tabBarActions[] = { @selector(performCloseTab:), @selector(takeNewTabIndexFrom::), @selector(takeTabsToCloseFrom:), @selector(takeTabsToTearOffFrom:), @selector(toggleSticky:) };
if(oak::contains(std::begin(tabBarActions), std::end(tabBarActions), [menuItem action]))
{
if(NSIndexSet* indexSet = [self tryObtainIndexSetFrom:menuItem])
{
active = [indexSet count] != 0;
if(active && [menuItem action] == @selector(toggleSticky:))
[menuItem setState:[self isDocumentSticky:_cppDocuments[indexSet.firstIndex]] ? NSOnState : NSOffState];
}
}
return active;
}
// =============
// = QuickLook =
// =============
// QLPreviewPanelController
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel*)panel
{
return self.fileBrowserVisible && [self.fileBrowser.selectedURLs count];
}
- (void)beginPreviewPanelControl:(QLPreviewPanel*)panel
{
[QLPreviewPanel sharedPreviewPanel].delegate = self;
[QLPreviewPanel sharedPreviewPanel].dataSource = self;
self.urlArrayForQuickLook = self.fileBrowser.selectedURLs;
}
- (void)endPreviewPanelControl:(QLPreviewPanel*)panel
{
self.urlArrayForQuickLook = nil;
}
// QLPreviewPanelDelegate
- (NSRect)previewPanel:(QLPreviewPanel*)panel sourceFrameOnScreenForPreviewItem:(id <QLPreviewItem>)item
{
return [_fileBrowser iconFrameForURL:item.previewItemURL];
}
- (BOOL)previewPanel:(QLPreviewPanel*)panel handleEvent:(NSEvent*)event
{
if([event type] == NSKeyDown)
{
[self.fileBrowser.outlineView keyDown:event];
NSArray* newSelection = self.fileBrowser.selectedURLs;
if(![newSelection isEqualToArray:self.urlArrayForQuickLook])
{
self.urlArrayForQuickLook = newSelection;
[panel reloadData];
}
return YES;
}
return NO;
}
// QLPreviewPanelDataSource
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel*)panel
{
return self.urlArrayForQuickLook.count;
}
- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel*)panel previewItemAtIndex:(NSInteger)index
{
return [[QuickLookNSURLWrapper alloc] initWithURL:self.urlArrayForQuickLook[index]];
}
// ======================
// = Session Management =
// ======================
+ (void)initialize
{
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
for(NSString* notification in @[ NSWindowDidBecomeKeyNotification, NSWindowDidDeminiaturizeNotification, NSWindowDidExposeNotification, NSWindowDidMiniaturizeNotification, NSWindowDidMoveNotification, NSWindowDidResizeNotification, NSWindowWillCloseNotification ])
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scheduleSessionBackup:) name:notification object:nil];
});
}
+ (void)backupSessionFiredTimer:(NSTimer*)aTimer
{
[self saveSessionIncludingUntitledDocuments:YES];
}
+ (void)scheduleSessionBackup:(id)sender
{
static NSTimer* saveTimer;
[saveTimer invalidate];
saveTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(backupSessionFiredTimer:) userInfo:nil repeats:NO];
}
+ (NSString*)sessionPath
{
static NSString* const res = [NSString stringWithCxxString:path::join(oak::application_t::support("Session"), "Info.plist")];
return res;
}
static NSUInteger DisableSessionSavingCount = 0;
+ (void)disableSessionSave { ++DisableSessionSavingCount; }
+ (void)enableSessionSave { --DisableSessionSavingCount; }
+ (BOOL)restoreSession
{
BOOL res = NO;
++DisableSessionSavingCount;
NSWindow* keyWindow;
NSDictionary* session = [NSDictionary dictionaryWithContentsOfFile:[self sessionPath]];
for(NSDictionary* project in session[@"projects"])
{
DocumentWindowController* controller = [DocumentWindowController new];
[controller setupControllerForProject:project skipMissingFiles:NO];
if(controller.cppDocuments.empty())
continue;
if(NSString* windowFrame = project[@"windowFrame"])
{
if([windowFrame hasPrefix:@"{"]) // Legacy NSRect
[controller.window setFrame:NSRectFromString(windowFrame) display:NO];
else [controller.window setFrameFromString:windowFrame];
}
if([project[@"miniaturized"] boolValue])
{
[controller.window miniaturize:nil];
}
else
{
if([project[@"fullScreen"] boolValue])
[controller.window toggleFullScreen:self];
else if([project[@"zoomed"] boolValue])
[controller.window zoom:self];
[controller.window orderFront:self];
keyWindow = controller.window;
}
res = YES;
}
[keyWindow makeKeyWindow];
--DisableSessionSavingCount;
return res;
}
- (void)setupControllerForProject:(NSDictionary*)project skipMissingFiles:(BOOL)skipMissing
{
if(NSString* fileBrowserWidth = project[@"fileBrowserWidth"])
self.fileBrowserWidth = [fileBrowserWidth floatValue];
if(NSString* htmlOutputSize = project[@"htmlOutputSize"])
self.htmlOutputSize = NSSizeFromString(htmlOutputSize);
self.defaultProjectPath = project[@"projectPath"];
self.projectPath = project[@"projectPath"];
self.fileBrowserHistory = project[@"fileBrowserState"];
self.fileBrowserVisible = [project[@"fileBrowserVisible"] boolValue];
std::vector<document::document_ptr> documents;
NSInteger selectedTabIndex = 0;
for(NSDictionary* info in project[@"documents"])
{
document::document_ptr doc;
NSString* identifier = info[@"identifier"];
if(!identifier || !(doc = document::find(to_s(identifier))))
{
NSString* path = info[@"path"];
if(path && skipMissing && access([path fileSystemRepresentation], F_OK) != 0)
continue;
doc = document::create(to_s(path));
if(NSString* fileType = info[@"fileType"])
doc->set_file_type(to_s(fileType));
if(NSString* displayName = info[@"displayName"])
doc->set_custom_name(to_s(displayName));
if([info[@"sticky"] boolValue])
[self setDocument:doc sticky:YES];
}
if(doc->path() == NULL_STR)
doc->show(); // Add to LRU-list
doc->set_recent_tracking(false);
documents.push_back(doc);
if([info[@"selected"] boolValue])
selectedTabIndex = documents.size() - 1;
}
if(documents.empty())
documents.push_back(document::create());
self.cppDocuments = documents;
self.selectedTabIndex = selectedTabIndex;
[self openAndSelectDocument:documents[selectedTabIndex]];
}
- (NSDictionary*)sessionInfoIncludingUntitledDocuments:(BOOL)includeUntitled
{
NSMutableDictionary* res = [NSMutableDictionary dictionary];
if(NSString* projectPath = self.defaultProjectPath)
res[@"projectPath"] = projectPath;
if(NSDictionary* history = self.fileBrowserHistory)
res[@"fileBrowserState"] = history;
if(([self.window styleMask] & NSFullScreenWindowMask) == NSFullScreenWindowMask)
res[@"fullScreen"] = @YES;
else if(self.window.isZoomed)
res[@"zoomed"] = @YES;
else
res[@"windowFrame"] = [self.window stringWithSavedFrame];
res[@"miniaturized"] = @([self.window isMiniaturized]);
res[@"htmlOutputSize"] = NSStringFromSize(self.htmlOutputSize);
res[@"fileBrowserVisible"] = @(self.fileBrowserVisible);
res[@"fileBrowserWidth"] = @(self.fileBrowserWidth);
NSMutableArray* docs = [NSMutableArray array];
for(auto document : self.cppDocuments)
{
if(!includeUntitled && (document->path() == NULL_STR || !path::exists(document->path())))
continue;
NSMutableDictionary* doc = [NSMutableDictionary dictionary];
if(document->is_modified() || document->path() == NULL_STR)
{
doc[@"identifier"] = [NSString stringWithCxxString:document->identifier()];
if(document->is_loaded())
document->backup();
}
if(document->path() != NULL_STR)
doc[@"path"] = [NSString stringWithCxxString:document->path()];
if(document->file_type() != NULL_STR) // TODO Only necessary when document.isBufferEmpty
doc[@"fileType"] = [NSString stringWithCxxString:document->file_type()];
if(document->display_name() != NULL_STR)
doc[@"displayName"] = [NSString stringWithCxxString:document->display_name()];
if(document == self.selectedCppDocument)
doc[@"selected"] = @YES;
if([self isDocumentSticky:document])
doc[@"sticky"] = @YES;
[docs addObject:doc];
}
res[@"documents"] = docs;
res[@"lastRecentlyUsed"] = [NSDate date];
return res;
}
+ (BOOL)saveSessionIncludingUntitledDocuments:(BOOL)includeUntitled
{
if(DisableSessionSavingCount)
return NO;
NSArray* controllers = SortedControllers();
if(controllers.count == 1)
{
DocumentWindowController* controller = controllers.firstObject;
if(!controller.projectPath && !controller.fileBrowserVisible && controller.cppDocuments.size() == 1 && is_disposable(controller.selectedCppDocument))
controllers = nil;
}
NSMutableArray* projects = [NSMutableArray array];
for(DocumentWindowController* controller in [controllers reverseObjectEnumerator])
[projects addObject:[controller sessionInfoIncludingUntitledDocuments:includeUntitled]];
NSDictionary* session = @{ @"projects" : projects };
return [session writeToFile:[self sessionPath] atomically:YES];
}
// ==========
// = Legacy =
// ==========
- (std::map<std::string, std::string>)variables
{
std::map<std::string, std::string> res;
if(self.fileBrowser)
res = [self.fileBrowser variables];
auto const& scmVars = _documentSCMVariables.empty() ? _projectSCMVariables : _documentSCMVariables;
if(!scmVars.empty())
{
auto scmName = scmVars.find("TM_SCM_NAME");
if(scmName != scmVars.end())
res["TM_SCM_NAME"] = scmName->second;
}
else
{
for(auto const& attr : _externalScopeAttributes)
{
if(regexp::match_t const& m = regexp::search("^attr.scm.(?'TM_SCM_NAME'\\w+)$", attr))
{
res.insert(m.captures().begin(), m.captures().end());
break;
}
}
}
if(NSString* projectDir = self.projectPath)
{
res["TM_PROJECT_DIRECTORY"] = [projectDir fileSystemRepresentation];
res["TM_PROJECT_UUID"] = to_s(self.identifier);
}
return res;
}
+ (instancetype)controllerForDocument:(OakDocument*)aDocument
{
if(!aDocument)
return nil;
for(DocumentWindowController* delegate in SortedControllers())
{
if(delegate.fileBrowserVisible && aDocument.path && [aDocument.path hasPrefix:delegate.projectPath])
return delegate;
for(auto document : delegate.cppDocuments)
{
if([aDocument isEqual:document->document()])
return delegate;
}
}
return nil;
}
- (void)bringToFront
{
[self showWindow:nil];
if(![NSApp isActive])
{
__weak __block id observerId = [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationDidBecomeActiveNotification object:NSApp queue:nil usingBlock:^(NSNotification*){
// If our window is not on the active desktop but another one is, the system gives focus to the wrong window.
[self showWindow:nil];
[[NSNotificationCenter defaultCenter] removeObserver:observerId];
}];
[NSApp activateIgnoringOtherApps:YES];
}
}
@end
@implementation OakDocumentController (OakDocumentWindowControllerCategory)
- (DocumentWindowController*)findOrCreateController:(std::vector<document::document_ptr> const&)documents project:(NSUUID*)projectUUID
{
ASSERT(!documents.empty());
// =========================================
// = Return requested window, if it exists =
// =========================================
if(projectUUID)
{
if(DocumentWindowController* res = AllControllers()[projectUUID.UUIDString])
return res;
if([projectUUID.UUIDString isEqual:@"00000000-0000-0000-0000-000000000000"])
return [DocumentWindowController new];
}
// =========================================
// = Find window with one of our documents =
// =========================================
std::set<oak::uuid_t> uuids;
std::transform(documents.begin(), documents.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); });
for(DocumentWindowController* candidate in SortedControllers())
{
for(auto document : candidate.cppDocuments)
{
if(uuids.find(document->identifier()) != uuids.end())
return candidate;
}
}
// ================================================================
// = Find window with project folder closest to documents parent =
// ================================================================
std::vector<document::document_ptr> documentsWithPath;
std::copy_if(documents.begin(), documents.end(), back_inserter(documentsWithPath), [](document::document_ptr const& doc){ return doc->path() != NULL_STR; });
std::set<std::string> parents;
std::transform(documentsWithPath.begin(), documentsWithPath.end(), inserter(parents, parents.end()), [](document::document_ptr const& doc){ return path::parent(doc->path()); });
std::map<size_t, DocumentWindowController*> candidates;
for(DocumentWindowController* candidate in SortedControllers())
{
if(candidate.projectPath)
{
std::string const projectPath = to_s(candidate.projectPath);
for(auto const& parent : parents)
{
if(path::is_child(parent, projectPath))
candidates.emplace(parent.size() - projectPath.size(), candidate);
}
}
}
if(!candidates.empty())
return candidates.begin()->second;
// ==============================================
// = Use frontmost window if a “scratch” window =
// ==============================================
if(DocumentWindowController* candidate = [SortedControllers() firstObject])
{
if(!candidate.fileBrowserVisible && candidate.cppDocuments.size() == 1 && is_disposable(candidate.selectedCppDocument))
return candidate;
}
// ===================================
// = Give up and create a new window =
// ===================================
DocumentWindowController* res = [DocumentWindowController new];
if(!parents.empty()) // setup project folder for new window
{
std::vector<std::string> rankedParents(parents.begin(), parents.end());
std::sort(rankedParents.begin(), rankedParents.end(), [](std::string const& lhs, std::string const& rhs){ return lhs.size() < rhs.size(); });
res.defaultProjectPath = [NSString stringWithCxxString:rankedParents.front()];
}
return res;
}
- (DocumentWindowController*)controllerWithDocuments:(std::vector<document::document_ptr> const&)documents project:(NSUUID*)projectUUID
{
DocumentWindowController* controller = [self findOrCreateController:documents project:projectUUID];
auto documentToSelect = controller.cppDocuments.size() <= [controller disposableDocument].size() ? documents.front() : documents.back();
[controller insertDocuments:documents atIndex:controller.selectedTabIndex + 1 selecting:documentToSelect andClosing:[controller disposableDocument]];
return controller;
}
- (void)showDocument:(OakDocument*)aDocument andSelect:(text::range_t const&)range inProject:(NSUUID*)identifier bringToFront:(BOOL)bringToFront
{
auto document = std::make_shared<document::document_t>(aDocument);
if(range != text::range_t::undefined)
document->set_selection(range);
DocumentWindowController* controller = [self controllerWithDocuments:{ document } project:identifier];
if(bringToFront)
[controller bringToFront];
else if(![controller.window isVisible])
[controller.window orderWindow:NSWindowBelow relativeTo:[([NSApp keyWindow] ?: [NSApp mainWindow]) windowNumber]];
[controller openAndSelectDocument:document];
}
- (void)showDocuments:(NSArray<OakDocument*>*)someDocument
{
if(someDocument.count == 0)
return;
std::vector<document::document_ptr> documents;
for(OakDocument* document in someDocument)
documents.push_back(std::make_shared<document::document_t>(document));
DocumentWindowController* controller = [self controllerWithDocuments:documents project:nil];
[controller bringToFront];
[controller openAndSelectDocument:controller.cppDocuments[controller.selectedTabIndex]];
}
- (void)showFileBrowserAtPath:(NSString*)aPath
{
NSString* const folder = to_ns(path::resolve(to_s(aPath)));
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:folder]];
for(DocumentWindowController* candidate in SortedControllers())
{
if([folder isEqualToString:candidate.projectPath ?: candidate.defaultProjectPath])
return [candidate bringToFront];
}
DocumentWindowController* controller = nil;
for(DocumentWindowController* candidate in SortedControllers())
{
if(!candidate.fileBrowserVisible && candidate.cppDocuments.size() == 1 && is_disposable(candidate.selectedCppDocument))
{
controller = candidate;
break;
}
}
if(!controller)
controller = [DocumentWindowController new];
else if(controller.selectedCppDocument)
[controller selectedCppDocument]->set_custom_name("not untitled"); // release potential untitled token used
NSDictionary* project;
if(![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableFolderStateRestore])
project = [[DocumentWindowController sharedProjectStateDB] valueForKey:folder];
if(project && [project[@"documents"] count])
{
[controller setupControllerForProject:project skipMissingFiles:YES];
}
else
{
controller.defaultProjectPath = folder;
controller.fileBrowserVisible = YES;
controller.cppDocuments = { document::create() };
controller.fileBrowser.url = [NSURL fileURLWithPath:folder];
[controller openAndSelectDocument:controller.cppDocuments[controller.selectedTabIndex]];
}
[controller bringToFront];
}
@end