#import "DocumentController.h" #import "ProjectLayoutView.h" #import "DocumentOpenHelper.h" #import "DocumentSaveHelper.h" #import "DocumentCommand.h" // show_command_error #import "OakRunCommandWindowController.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import namespace find_tags { enum { in_document = 1, in_selection, in_project, in_folder }; } // From AppController.h static NSString* const OakDocumentPboardType = @"OakDocumentPboardType"; // drag’n’drop of tabs static BOOL IsInShouldTerminateEventLoop = NO; @interface QuickLookNSURLWrapper : NSObject @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 DocumentController () @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) 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; + (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* AllControllers () { static NSMutableDictionary* res = [NSMutableDictionary new]; return res; } static NSArray* 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)]) { DocumentController* delegate = (DocumentController*)window.delegate; if(id controller = AllControllers()[delegate.identifier]) [res addObject:controller]; } } } return res; } // ====================== // = document_t helpers = // ====================== struct tracking_info_t : document::document_t::callback_t { tracking_info_t (DocumentController* self, document::document_ptr const& document) : _self(self), _document(document) { } ~tracking_info_t () { ASSERT_EQ(_open_count, 0); } void track () { if(++_open_count == 1) { _document->add_callback(this); // TODO Add kqueue watching of documents } if(!_did_open && _document->is_open()) { _document->open(); _did_open = true; } } bool untrack () { if(_open_count == 1) _document->remove_callback(this); if(--_open_count == 0 && _did_open) { _document->close(); _did_open = false; } return _open_count == 0; } void handle_document_event (document::document_ptr document, event_t event) { if(document && _self.selectedDocument && *document == *_self.selectedDocument) { switch(event) { case did_change_path: [_self setDocumentPath:[NSString stringWithCxxString:document->path()]]; [_self setDocumentDisplayName:[NSString stringWithCxxString:document->display_name()]]; break; case did_change_on_disk_status: [_self setDocumentIsOnDisk:document->is_on_disk()]; break; case did_change_modified_status: [_self setDocumentIsModified:document->is_modified()]; break; } } switch(event) { case did_change_modified_status: case did_change_path: [_self updateFileBrowserStatus:nil]; [_self.tabBarView reloadData]; [[_self class] scheduleSessionBackup:nil]; break; } } private: __weak DocumentController* _self; document::document_ptr _document; size_t _open_count = 0; bool _did_open = false; }; static bool is_disposable (document::document_ptr const& doc) { return doc && !doc->is_modified() && !doc->is_on_disk() && doc->path() == NULL_STR && doc->buffer().empty(); } static size_t merge_documents_splitting_at (std::vector const& oldDocuments, std::vector const& newDocuments, size_t splitAt, std::vector& out) { std::set uuids; std::transform(newDocuments.begin(), newDocuments.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); splitAt = std::min(splitAt, oldDocuments.size()); std::copy_if(oldDocuments.begin(), oldDocuments.begin() + splitAt, back_inserter(out), [&uuids](document::document_ptr const& doc){ return uuids.find(doc->identifier()) == uuids.end(); }); std::copy(newDocuments.begin(), newDocuments.end(), back_inserter(out)); size_t res = out.empty() ? 0 : out.size() - 1; std::copy_if(oldDocuments.begin() + splitAt, oldDocuments.end(), back_inserter(out), [&uuids](document::document_ptr const& doc){ return uuids.find(doc->identifier()) == uuids.end(); }); return res; } static std::vector make_vector (document::document_ptr const& document) { return std::vector(1, document); } static document::document_ptr create_untitled_document_in_folder (std::string const& suggestedFolder) { return document::from_content("", settings_for_path(NULL_STR, file::path_attributes(NULL_STR), suggestedFolder).get(kSettingsFileTypeKey, "text.plain")); } } @implementation DocumentController { OBJC_WATCH_LEAKS(DocumentController); std::vector _documents; std::map _trackedDocuments; document::document_ptr _selectedDocument; command::runner_ptr _runner; scm::info_ptr _projectSCMInfo; std::map _projectSCMVariables; std::vector _projectScopeAttributes; std::vector _externalScopeAttributes; scm::info_ptr _documentSCMInfo; std::map _documentSCMVariables; std::vector _documentScopeAttributes; } - (id)init { if((self = [super init])) { 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; self.window = [[NSWindow alloc] initWithContentRect:NSZeroRect styleMask:(NSTitledWindowMask|NSClosableWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask|NSTexturedBackgroundWindowMask) backing:NSBackingStoreBuffered defer:NO]; self.window.autorecalculatesKeyViewLoop = YES; 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]; [self.layoutView setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.window.contentView addSubview:self.layoutView]; [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 }]]; [OakWindowFrameHelper windowFrameHelperWithWindow:self.window]; [[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.tabBarView.dataSource = nil; self.tabBarView.delegate = nil; self.textView.delegate = nil; } - (void)windowWillClose:(NSNotification*)aNotification { self.documents = std::vector(); self.selectedDocument = document::document_ptr(); self.window.delegate = nil; 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(_documents.empty()) { document::document_ptr defaultDocument = create_untitled_document_in_folder(to_s(self.untitledSavePath)); self.documents = make_vector(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] objectForKey:kUserDefaultsHTMLOutputPlacementKey] isEqualToString:@"window"]; self.disableFileBrowserWindowResize = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableFileBrowserWindowResizeKey]; if(self.layoutView.fileBrowserOnRight != [[[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsFileBrowserPlacementKey] isEqualToString:@"right"]) { self.oldWindowFrame = self.newWindowFrame = NSZeroRect; self.layoutView.fileBrowserOnRight = !self.layoutView.fileBrowserOnRight; } } - (void)applicationDidBecomeActiveNotification:(NSNotification*)aNotification { if(!_documents.empty()) [self.textView performSelector:@selector(applicationDidBecomeActiveNotification:) withObject:aNotification]; } - (void)applicationDidResignActiveNotification:(NSNotification*)aNotification { if(!_documents.empty()) [self.textView performSelector:@selector(applicationDidResignActiveNotification:) withObject:aNotification]; } // ================= // = Close Methods = // ================= - (void)showCloseWarningUIForDocuments:(std::vector const&)someDocuments completionHandler:(void(^)(BOOL canClose))callback { if(someDocuments.empty()) return callback(YES); [[self.window attachedSheet] orderOut:self]; NSAlert* alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSWarningAlertStyle]; [alert addButtons:@"Save", @"Cancel", @"Don’t Save", nil]; if(someDocuments.size() == 1) { document::document_ptr document = someDocuments.front(); [alert setMessageText:[NSString stringWithCxxString:text::format("Do you want to save the changes you made in the document “%s”?", document->display_name().c_str())]]; [alert setInformativeText:@"Your changes will be lost if you don’t save them."]; } else { std::string body = ""; for(auto document : someDocuments) body += text::format("• “%s”\n", document->display_name().c_str()); [alert setMessageText:@"Do you want to save documents with changes?"]; [alert setInformativeText:[NSString stringWithCxxString:body]]; } bool windowModal = true; if(someDocuments.size() == 1) { NSUInteger index = 0; for(auto document : _documents) { if(*document == *someDocuments.front()) { self.selectedTabIndex = index; [self openAndSelectDocument:document]; break; } ++index; } } else { std::set uuids; std::transform(_documents.begin(), _documents.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); for(auto document : someDocuments) { if(uuids.find(document->identifier()) == uuids.end()) windowModal = false; } } std::vector documentsToSave(someDocuments); auto block = ^(NSInteger returnCode) { switch(returnCode) { case NSAlertFirstButtonReturn: /* "Save" */ { struct callback_t : document_save_callback_t { callback_t (void(^callback)(BOOL), size_t count) : _callback([callback copy]), _count(count) { } void did_save_document (document::document_ptr document, bool flag, std::string const& message, oak::uuid_t const& filter) { if(_callback && (_count == 1 || !flag)) _callback(flag); if(--_count == 0 || !flag) delete this; } private: void(^_callback)(BOOL); size_t _count; }; if(IsInShouldTerminateEventLoop) { IsInShouldTerminateEventLoop = NO; [NSApp replyToApplicationShouldTerminate:NO]; } [DocumentSaveHelper trySaveDocuments:documentsToSave forWindow:self.window defaultDirectory:self.untitledSavePath andCallback:new callback_t(callback, documentsToSave.size())]; } break; case NSAlertSecondButtonReturn: /* "Cancel" */ { callback(NO); } break; case NSAlertThirdButtonReturn: /* "Don't Save" */ { callback(YES); } break; } }; if(windowModal) OakShowAlertForWindow(alert, self.window, block); else block([alert runModal]); } - (void)closeTabsAtIndexes:(NSIndexSet*)anIndexSet askToSaveChanges:(BOOL)askToSaveFlag createDocumentIfEmpty:(BOOL)createIfEmptyFlag { std::vector documentsToClose; for(NSUInteger index = [anIndexSet firstIndex]; index != NSNotFound; index = [anIndexSet indexGreaterThanIndex:index]) documentsToClose.push_back(_documents[index]); if(askToSaveFlag) { std::vector documents; std::copy_if(documentsToClose.begin(), documentsToClose.end(), back_inserter(documents), [](document::document_ptr const& doc){ return doc->is_modified(); }); if(!documents.empty()) { [self showCloseWarningUIForDocuments:documents 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(_documents[index]->is_modified()) [newIndexes removeIndex:index]; } [self closeTabsAtIndexes:newIndexes askToSaveChanges:YES createDocumentIfEmpty:createIfEmptyFlag]; } }]; return; } } std::set uuids; std::transform(documentsToClose.begin(), documentsToClose.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); std::vector newDocuments; NSUInteger newSelectedTabIndex = _selectedTabIndex; oak::uuid_t const selectedUUID = _documents[_selectedTabIndex]->identifier(); for(auto document : _documents) { 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(create_untitled_document_in_folder(to_s(self.untitledSavePath))); self.selectedTabIndex = newSelectedTabIndex; self.documents = newDocuments; if(!newDocuments.empty() && newDocuments[newSelectedTabIndex]->identifier() != selectedUUID) [self openAndSelectDocument:newDocuments[newSelectedTabIndex]]; } - (IBAction)performCloseTab:(id)sender { if(_documents.empty() || _documents.size() == 1 && (is_disposable(_selectedDocument) || !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, _documents.size())]; for(size_t i = 0; i < _documents.size(); ++i) { if(_documents[i]->is_modified() && _documents[i]->path() == NULL_STR) [allTabs removeIndex:i]; } [self closeTabsAtIndexes:allTabs askToSaveChanges:YES createDocumentIfEmpty:YES]; } - (IBAction)performCloseOtherTabs:(id)sender { NSUInteger tabIndex = [sender isKindOfClass:[OakTabBarView class]] ? [sender tag] : _selectedTabIndex; NSMutableIndexSet* otherTabs = [NSMutableIndexSet indexSet]; for(size_t i = 0; i < _documents.size(); ++i) { if(i != tabIndex && (!_documents[i]->is_modified() || _documents[i]->path() != NULL_STR)) [otherTabs addIndex:i]; } [self closeTabsAtIndexes:otherTabs askToSaveChanges:YES createDocumentIfEmpty:YES]; } - (BOOL)windowShouldClose:(id)sender { [self.htmlOutputView stopLoading]; std::vector documents; std::copy_if(_documents.begin(), _documents.end(), back_inserter(documents), [](document::document_ptr const& doc){ return doc->is_modified(); }); if(documents.empty()) return YES; [self showCloseWarningUIForDocuments:documents completionHandler:^(BOOL canClose){ if(canClose) [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 < _documents.size(); ++i) { document::document_ptr doc = _documents[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]; } + (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender { BOOL restoresSession = ![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableSessionRestoreKey]; std::vector documents; for(DocumentController* delegate in SortedControllers()) std::copy_if(delegate.documents.begin(), delegate.documents.end(), back_inserter(documents), [&restoresSession](document::document_ptr const& doc){ return doc->is_modified() && (doc->path() != NULL_STR || !restoresSession); }); if(documents.empty()) { [DocumentController saveSessionIncludingUntitledDocuments:restoresSession]; if(restoresSession) { // Ensure we do not remove backup files, as they are used to restore untitled documents for(DocumentController* controller in [SortedControllers() reverseObjectEnumerator]) { for(auto document : controller.documents) document->detach_backup(); } } return NSTerminateNow; } IsInShouldTerminateEventLoop = YES; DocumentController* controller = [SortedControllers() firstObject]; [controller showCloseWarningUIForDocuments:documents completionHandler:^(BOOL canClose){ if(canClose) [DocumentController saveSessionIncludingUntitledDocuments:NO]; if(IsInShouldTerminateEventLoop) [NSApp replyToApplicationShouldTerminate:canClose]; else if(canClose) [NSApp terminate:self]; IsInShouldTerminateEventLoop = NO; }]; return NSTerminateLater; } // ===================== // = Document Tracking = // ===================== - (void)trackDocument:(document::document_ptr)aDocument { if(aDocument) { auto iter = _trackedDocuments.find(aDocument->identifier()); if(iter == _trackedDocuments.end()) iter = _trackedDocuments.insert(std::make_pair(aDocument->identifier(), tracking_info_t(self, aDocument))).first; iter->second.track(); } } - (void)untrackDocument:(document::document_ptr)aDocument { if(aDocument) { auto iter = _trackedDocuments.find(aDocument->identifier()); ASSERT(iter != _trackedDocuments.end()); if(iter->second.untrack()) _trackedDocuments.erase(iter); } } // ==================== // = 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"; for(auto item : bundles::query(bundles::kFieldGrammarScope, settings_for_path(NULL_STR, "attr.untitled", to_s(folder)).get(kSettingsFileTypeKey, "text.plain"))) { 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 window:self.window]) { document::document_ptr doc = document::create(to_s([url path])); doc->open(); [self setSelectedDocument:doc]; doc->close(); size_t selectedIndex = _selectedTabIndex; std::vector documents = _documents; if(is_disposable(documents[selectedIndex])) documents[selectedIndex] = doc; else documents.insert(documents.begin() + ++selectedIndex, doc); self.documents = documents; self.selectedTabIndex = selectedIndex; [self.fileBrowser editURL:url]; } } } - (IBAction)moveDocumentToNewWindow:(id)sender { if(_documents.size() > 1) [self takeTabsToTearOffFrom:[NSIndexSet indexSetWithIndex:_selectedTabIndex]]; } - (IBAction)mergeAllWindows:(id)sender { std::vector documents = _documents; for(DocumentController* delegate in SortedControllers()) { if(delegate != self && ![delegate.window isMiniaturized]) documents.insert(documents.end(), delegate.documents.begin(), delegate.documents.end()); } self.documents = documents; for(DocumentController* delegate in SortedControllers()) { if(delegate != self && ![delegate.window isMiniaturized]) [delegate.window close]; } } - (void)openItems:(NSArray*)items closingOtherTabs:(BOOL)closeOtherTabsFlag { std::vector documents; for(id item in items) { std::string const path = to_s((NSString*)[item objectForKey:@"path"]); std::string const uuid = to_s((NSString*)[item objectForKey:@"identifier"]); std::string const range = to_s((NSString*)[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::vector oldDocuments = _documents; NSUInteger split = _selectedTabIndex; std::set oldUUIDs, newUUIDs, actualNewUUIDs; std::transform(oldDocuments.begin(), oldDocuments.end(), inserter(oldUUIDs, oldUUIDs.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); std::transform(documents.begin(), documents.end(), inserter(newUUIDs, newUUIDs.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); std::set_difference(newUUIDs.begin(), newUUIDs.end(), oldUUIDs.begin(), oldUUIDs.end(), inserter(actualNewUUIDs, actualNewUUIDs.end())); if(!actualNewUUIDs.empty() && !oldDocuments.empty() && is_disposable(oldDocuments[split])) oldDocuments.erase(oldDocuments.begin() + split); else ++split; std::vector newDocuments; split = merge_documents_splitting_at(oldDocuments, documents, split, newDocuments); self.documents = newDocuments; self.selectedTabIndex = split; if(!newDocuments.empty()) [self openAndSelectDocument:newDocuments[split]]; if(closeOtherTabsFlag) { std::set uuids; std::transform(documents.begin(), documents.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet]; for(size_t i = 0; i < newDocuments.size(); ++i) { document::document_ptr doc = newDocuments[i]; if(!doc->is_modified() && uuids.find(doc->identifier()) == uuids.end()) [indexSet addIndex:i]; } [self closeTabsAtIndexes:indexSet askToSaveChanges:YES createDocumentIfEmpty:NO]; } else { NSInteger excessTabs = _documents.size() - self.tabBarView.countOfVisibleTabs; if(self.tabBarView && excessTabs > 0) { std::set uuids; std::transform(documents.begin(), documents.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); std::multimap ranked; for(size_t i = 0; i < newDocuments.size(); ++i) { document::document_ptr doc = newDocuments[i]; if(!doc->is_modified() && doc->is_on_disk() && uuids.find(doc->identifier()) == uuids.end()) ranked.insert(std::make_pair(doc->lru(), i)); } NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet]; iterate(pair, ranked) { [indexSet addIndex:pair->second]; if([indexSet count] == excessTabs) break; } [self closeTabsAtIndexes:indexSet askToSaveChanges:NO createDocumentIfEmpty:NO]; } } } // ================ // = Document I/O = // ================ - (void)openAndSelectDocument:(document::document_ptr const&)aDocument { document::document_ptr doc = aDocument; [[DocumentOpenHelper new] tryOpenDocument:doc forWindow:self.window completionHandler:^(std::string const& error, oak::uuid_t const& filterUUID){ if(error == NULL_STR) { [self makeTextViewFirstResponder:self]; [self setSelectedDocument:doc]; } else { if(filterUUID) show_command_error(error, filterUUID); [self openAndSelectDocument:document::from_content("TODO Reselect previously open document")]; } }]; } - (IBAction)saveDocument:(id)sender { if(!_selectedDocument) return; if(_selectedDocument->path() != NULL_STR) { [DocumentSaveHelper trySaveDocument:_selectedDocument forWindow:self.window defaultDirectory:nil andCallback:NULL]; } else { NSString* const suggestedFolder = self.untitledSavePath; NSString* const suggestedName = DefaultSaveNameForDocument(_selectedDocument); encoding::type suggestedEncoding = _selectedDocument->encoding_for_save_as_path(to_s([suggestedFolder stringByAppendingPathComponent:suggestedName])); [OakSavePanel showWithPath:suggestedName directory:suggestedFolder fowWindow:self.window encoding:suggestedEncoding completionHandler:^(NSString* path, encoding::type const& encoding){ if(!path) return; std::vector const& paths = path::expand_braces(to_s(path)); ASSERT_LT(0, paths.size()); _selectedDocument->set_path(paths[0]); _selectedDocument->set_disk_encoding(encoding); // if(_selectedDocument->identifier() == scratchDocument) // scratchDocument = oak::uuid_t(); if(paths.size() > 1) { // FIXME check if paths[0] already exists (overwrite) std::vector documents, newDocuments; 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; }); merge_documents_splitting_at(_documents, documents, _selectedTabIndex + 1, newDocuments); self.documents = newDocuments; } [DocumentSaveHelper trySaveDocument:_selectedDocument forWindow:self.window defaultDirectory:nil andCallback:NULL]; }]; } } - (IBAction)saveDocumentAs:(id)sender { if(!_selectedDocument) return; std::string const documentPath = _selectedDocument->path(); NSString* const suggestedFolder = [NSString stringWithCxxString:path::parent(documentPath)] ?: self.untitledSavePath; NSString* const suggestedName = [NSString stringWithCxxString:path::name(documentPath)] ?: DefaultSaveNameForDocument(_selectedDocument); encoding::type suggestedEncoding = _selectedDocument->encoding_for_save_as_path(to_s([suggestedFolder stringByAppendingPathComponent:suggestedName])); [OakSavePanel showWithPath:suggestedName directory:suggestedFolder fowWindow:self.window encoding:suggestedEncoding completionHandler:^(NSString* path, encoding::type const& encoding){ if(!path) return; _selectedDocument->set_path(to_s(path)); _selectedDocument->set_disk_encoding(encoding); [DocumentSaveHelper trySaveDocument:_selectedDocument forWindow:self.window defaultDirectory:nil andCallback:NULL]; }]; } - (IBAction)saveAllDocuments:(id)sender { std::vector documentsToSave; for(auto document : _documents) { if(document->is_modified()) documentsToSave.push_back(document); } [DocumentSaveHelper trySaveDocuments:documentsToSave forWindow:self.window defaultDirectory:self.untitledSavePath andCallback:NULL]; } // ================ // = 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)updateWindowTitle { if(_selectedDocument) { std::map map = _documentSCMVariables.empty() ? _projectSCMVariables : _documentSCMVariables; if(self.projectPath) map["projectDirectory"] = to_s(self.projectPath); std::string docDirectory = _selectedDocument->path() != NULL_STR ? path::parent(_selectedDocument->path()) : to_s(self.untitledSavePath); settings_t const settings = settings_for_path(_selectedDocument->virtual_path(), _selectedDocument->file_type() + " " + to_s(self.scopeAttributes), docDirectory, _selectedDocument->variables(map, false)); 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 { { "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", }, }; _externalScopeAttributes.clear(); if(!_documentPath && !_projectPath) return; std::string const projectDir = to_s(_projectPath ?: NSHomeDirectory()); std::string const documentPath = _documentPath ? to_s(_documentPath) : path::join(projectDir, "dummy"); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ std::vector res; std::set 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 = _documentPath ? to_s(_documentPath) : 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 DocumentController* weakSelf = self; _projectSCMInfo->add_callback(^(scm::info_t const& info){ weakSelf.projectSCMVariables = info.variables(); }); } else { self.projectSCMVariables = std::map(); } _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 updateWindowTitle]; } } - (void)setDocumentPath:(NSString*)newDocumentPath { if(_documentPath != newDocumentPath && !([_documentPath isEqualToString:newDocumentPath]) || _documentScopeAttributes.empty()) { _documentPath = newDocumentPath; std::string docDirectory = _documentPath ? path::parent(to_s(_documentPath)) : to_s(self.projectPath); _documentScopeAttributes = { text::format("attr.os-version.%zu.%zu.%zu", oak::os_major(), oak::os_minor(), oak::os_patch()) }; if(_documentPath) { std::string const path = to_s(_documentPath); std::vector revPath; citerate(token, text::tokenize(path.begin(), path.end(), '/')) { std::string tmp = *token; citerate(subtoken, text::tokenize(tmp.begin(), tmp.end(), '.')) { if((*subtoken).empty()) continue; revPath.push_back(*subtoken); std::replace(revPath.back().begin(), revPath.back().end(), ' ', '_'); } } revPath.push_back("rev-path"); revPath.push_back("attr"); std::reverse(revPath.begin(), revPath.end()); _documentScopeAttributes.push_back(text::join(revPath, ".")); } else { _documentScopeAttributes.push_back("attr.untitled"); } std::string const customAttributes = settings_for_path(to_s(_documentPath), text::join(_documentScopeAttributes, " "), docDirectory).get(kSettingsScopeAttributesKey, NULL_STR); if(customAttributes != NULL_STR) _documentScopeAttributes.push_back(customAttributes); if(_documentSCMInfo = scm::info(docDirectory)) { __weak DocumentController* weakSelf = self; _documentSCMInfo->add_callback(^(scm::info_t const& info){ weakSelf.documentSCMStatus = info.status(to_s(weakSelf.documentPath)); weakSelf.documentSCMVariables = info.variables(); }); } else { self.documentSCMStatus = scm::status::unknown; self.documentSCMVariables = std::map(); } [self updateExternalAttributes]; [self updateProxyIcon]; [self updateWindowTitle]; } } - (void)setDocumentDisplayName:(NSString*)newDisplayName { if(_documentDisplayName != newDisplayName && ![_documentDisplayName isEqualToString:newDisplayName]) { _documentDisplayName = newDisplayName; [self updateWindowTitle]; } } - (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 const&)newVariables { if(_projectSCMVariables != newVariables) { _projectSCMVariables = newVariables; [self updateWindowTitle]; } } - (void)setDocumentSCMVariables:(std::map const&)newVariables { if(_documentSCMVariables != newVariables) { _documentSCMVariables = newVariables; [self updateWindowTitle]; } } - (std::map const&)projectSCMVariables { return _projectSCMVariables; } - (std::map 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 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 = // ============== - (void)setDocuments:(std::vector const&)newDocuments { for(auto document : newDocuments) [self trackDocument:document]; for(auto document : _documents) [self untrackDocument:document]; _documents = newDocuments; if(_documents.size()) [self.tabBarView reloadData]; [self updateFileBrowserStatus:self]; [[self class] scheduleSessionBackup:self]; } - (void)setSelectedDocument:(document::document_ptr const&)newSelectedDocument { ASSERT(!newSelectedDocument || newSelectedDocument->is_open()); if(_selectedDocument == newSelectedDocument) { [self.documentView setDocument:_selectedDocument]; return; } [self trackDocument:newSelectedDocument]; [self untrackDocument:_selectedDocument]; if(_selectedDocument = newSelectedDocument) { NSString* projectPath = self.defaultProjectPath ?: self.fileBrowser.path ?: [NSString stringWithCxxString:path::parent(_selectedDocument->path())]; if(projectPath) { std::map const map = { { "projectDirectory", to_s(projectPath) } }; settings_t const settings = settings_for_path(NULL_STR, NULL_STR, to_s(projectPath), map); std::string const userProjectDirectory = settings.get(kSettingsProjectDirectoryKey, NULL_STR); if(path::is_absolute(userProjectDirectory)) projectPath = [NSString stringWithCxxString:path::normalize(userProjectDirectory)]; } self.projectPath = projectPath; self.documentPath = [NSString stringWithCxxString:_selectedDocument->path()]; self.documentDisplayName = [NSString stringWithCxxString:_selectedDocument->display_name()]; self.documentIsModified = _selectedDocument->is_modified(); self.documentIsOnDisk = _selectedDocument->is_on_disk(); [self.documentView setDocument:_selectedDocument]; [[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 _documents.size(); } - (NSString*)tabBarView:(OakTabBarView*)aTabBarView titleForIndex:(NSUInteger)anIndex { return [NSString stringWithCxxString:_documents[anIndex]->display_name()]; } - (NSString*)tabBarView:(OakTabBarView*)aTabBarView toolTipForIndex:(NSUInteger)anIndex { return [NSString stringWithCxxString:path::with_tilde(_documents[anIndex]->path())] ?: @""; } - (BOOL)tabBarView:(OakTabBarView*)aTabBarView isEditedAtIndex:(NSUInteger)anIndex { return _documents[anIndex]->is_modified(); } // ============================== // = OakTabBarView Context Menu = // ============================== - (NSIndexSet*)tryObtainIndexSetFrom:(id)sender { id res = sender; if([sender respondsToSelector:@selector(representedObject)]) res = [sender representedObject]; return [res isKindOfClass:[NSIndexSet class]] ? res : nil; } - (void)takeNewTabIndexFrom:(id)sender { if(NSIndexSet* indexSet = [self tryObtainIndexSetFrom:sender]) { document::document_ptr doc = create_untitled_document_in_folder(to_s(self.untitledSavePath)); doc->open(); [self setSelectedDocument:doc]; doc->close(); std::vector newDocuments; size_t pos = merge_documents_splitting_at(_documents, make_vector(doc), [indexSet firstIndex], newDocuments); self.documents = newDocuments; self.selectedTabIndex = pos; } } - (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 documents; for(NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index]) documents.push_back(_documents[index]); if(documents.size() == 1) { DocumentController* controller = [DocumentController new]; controller.documents = make_vector(documents[0]); [controller openAndSelectDocument:documents[0]]; [controller showWindow:self]; [self closeTabsAtIndexes:indexSet askToSaveChanges:NO createDocumentIfEmpty:YES]; } } } - (NSMenu*)menuForTabBarView:(OakTabBarView*)aTabBarView { NSInteger tabIndex = aTabBarView.tag; NSInteger total = _documents.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)]]; } SEL closeSingleTabSelector = tabIndex == _selectedTabIndex ? @selector(performCloseTab:) : @selector(takeTabsToCloseFrom:); NSMenu* menu = [NSMenu new]; [menu setAutoenablesItems:NO]; [menu addItemWithTitle:@"New Tab" action:@selector(takeNewTabIndexFrom:) 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:@"Move Tab to New Window" action:@selector(takeTabsToTearOffFrom:) keyEquivalent:@""]; NSIndexSet* indexSets[] = { newTabAtTab, nil, clickedTab, otherTabs, rightSideTabs, nil, total > 1 ? clickedTab : [NSIndexSet indexSet] }; for(size_t i = 0; i < sizeofA(indexSets); ++i) { if(NSIndexSet* indexSet = indexSets[i]) { if([indexSet count] == 0) [[menu itemAtIndex:i] setEnabled:NO]; else [[menu itemAtIndex:i] setRepresentedObject:indexSet]; } } return menu; } // ========================= // = OakTabBarViewDelegate = // ========================= - (BOOL)tabBarView:(OakTabBarView*)aTabBarView shouldSelectIndex:(NSUInteger)anIndex { [self openAndSelectDocument:_documents[anIndex]]; self.selectedTabIndex = anIndex; return YES; } - (void)tabBarView:(OakTabBarView*)aTabBarView didDoubleClickIndex:(NSUInteger)anIndex { if(_documents.size() > 1) [self takeTabsToTearOffFrom:[NSMutableIndexSet indexSetWithIndex:anIndex]]; } - (void)tabBarViewDidDoubleClick:(OakTabBarView*)aTabBarView { [self takeNewTabIndexFrom:[NSMutableIndexSet indexSetWithIndex:_documents.size()]]; } // ================ // = Tab Dragging = // ================ - (void)setupPasteboard:(NSPasteboard*)aPasteboard forTabAtIndex:(NSUInteger)draggedTabIndex { document::document_ptr document = _documents[draggedTabIndex]; if(document->path() != NULL_STR) { [aPasteboard addTypes:@[ NSFilenamesPboardType ] owner:nil]; [aPasteboard setPropertyList:@[ [NSString stringWithCxxString:document->path()] ] forType:NSFilenamesPboardType]; } [aPasteboard addTypes:@[ OakDocumentPboardType ] owner:nil]; [aPasteboard setPropertyList:@{ @"index" : @(draggedTabIndex), @"document" : [NSString stringWithCxxString:document->identifier()], @"collection" : self.identifier, } forType:OakDocumentPboardType]; } - (BOOL)performTabDropFromTabBar:(OakTabBarView*)aTabBar atIndex:(NSUInteger)droppedIndex fromPasteboard:(NSPasteboard*)aPasteboard operation:(NSDragOperation)operation { NSDictionary* plist = [aPasteboard propertyListForType:OakDocumentPboardType]; oak::uuid_t docId = to_s((NSString*)plist[@"document"]); std::vector newDocuments; merge_documents_splitting_at(_documents, make_vector(document::find(docId)), droppedIndex, newDocuments); self.documents = newDocuments; if(_selectedDocument) { oak::uuid_t selectedUUID = _selectedDocument->identifier(); auto iter = std::find_if(newDocuments.begin(), newDocuments.end(), [&selectedUUID](document::document_ptr const& doc){ return doc->identifier() == selectedUUID; }); if(iter != newDocuments.end()) self.selectedTabIndex = iter - newDocuments.begin(); } oak::uuid_t srcProjectId = to_s((NSString*)plist[@"collection"]); if(operation == NSDragOperationMove && srcProjectId != to_s(self.identifier)) { for(DocumentController* delegate in SortedControllers()) { if(srcProjectId == oak::uuid_t(to_s(delegate.identifier))) { if(delegate.fileBrowserVisible || [delegate documents].size() > 1) [delegate closeTabsAtIndexes:[NSIndexSet indexSetWithIndex:[plist[@"index"] unsignedIntValue]] askToSaveChanges:NO createDocumentIfEmpty:YES]; else [delegate close]; return YES; } } } return YES; } - (IBAction)selectNextTab:(id)sender { self.selectedTabIndex = (_selectedTabIndex + 1) % _documents.size(); [self openAndSelectDocument:_documents[_selectedTabIndex]]; } - (IBAction)selectPreviousTab:(id)sender { self.selectedTabIndex = (_selectedTabIndex + _documents.size() - 1) % _documents.size(); [self openAndSelectDocument:_documents[_selectedTabIndex]]; } - (IBAction)takeSelectedTabIndexFrom:(id)sender { self.selectedTabIndex = [sender tag]; [self openAndSelectDocument:_documents[_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 = _documents; 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(!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; if(makeVisibleFlag) { self.fileBrowser.nextResponder = self.fileBrowser.view.nextResponder; self.fileBrowser.view.nextResponder = self.fileBrowser; } 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 : _documents) { 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(_selectedDocument) { self.fileBrowserVisible = YES; [self.fileBrowser selectURL:[NSURL fileURLWithPath:[NSString stringWithCxxString:_selectedDocument->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.layoutView.htmlOutputView || [self.htmlOutputWindowController.window isVisible]; } - (void)setHtmlOutputVisible:(BOOL)makeVisibleFlag { if(self.htmlOutputVisible == makeVisibleFlag) return; if(makeVisibleFlag) { if(self.htmlOutputInWindow) { [self.htmlOutputWindowController.window makeKeyAndOrderFront:self]; } else { if(!self.htmlOutputView) 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]; [self.htmlOutputWindowController.window orderOut:self]; self.layoutView.htmlOutputView = nil; } } - (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 { self.htmlOutputVisible = !self.htmlOutputVisible; } - (BOOL)setCommandRunner:(command::runner_ptr const&)aRunner { if(self.htmlOutputInWindow) { _runner = aRunner; if(!self.htmlOutputWindowController || [self.htmlOutputWindowController running]) self.htmlOutputWindowController = [HTMLOutputWindowController HTMLOutputWindowWithRunner:_runner]; else [self.htmlOutputWindowController setCommandRunner:_runner]; } else { if(_runner && _runner->running()) { NSInteger choice = [[NSAlert alertWithMessageText:@"Stop current task first?" defaultButton:@"Stop Task" alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@"There already is a task running. If you stop this then the task it is performing will not be completed."] runModal]; if(choice != NSAlertDefaultReturn) /* "Stop" */ return NO; } _runner = aRunner; self.htmlOutputVisible = YES; [self.window makeFirstResponder:self.htmlOutputView.webView]; [self.htmlOutputView setEnvironment:_runner->environment()]; [self.htmlOutputView loadRequest:URLRequestForCommandRunner(_runner) autoScrolls:_runner->auto_scroll_output()]; } return YES; } // ============================= // = Opening Auxiliary Windows = // ============================= - (void)positionWindow:(NSWindow*)aWindow { if(![aWindow isVisible]) { [aWindow layoutIfNeeded]; NSRect frame = [aWindow frame]; NSRect parent = [_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]; } } - (NSString*)selectedDocumentUUID { return _selectedDocument ? [NSString stringWithCxxString:_selectedDocument->identifier()] : nil; } - (IBAction)orderFrontFindPanel:(id)sender { Find* find = [Find sharedInstance]; find.documentIdentifier = self.selectedDocumentUUID; find.projectFolder = self.projectPath ?: self.untitledSavePath ?: NSHomeDirectory(); find.projectIdentifier = self.identifier; NSInteger mode = [sender respondsToSelector:@selector(tag)] ? [sender tag] : find_tags::in_document; if(mode == find_tags::in_document && [self.window isKeyWindow] && self.textView.hasMultiLineSelection) mode = find_tags::in_selection; switch(mode) { case find_tags::in_document: return [find showFindWindowFor:FFSearchInDocument]; case find_tags::in_selection: return [find showFindWindowFor:FFSearchInSelection]; case find_tags::in_folder: return [find showFolderSelectionPanel:self]; case find_tags::in_project: { BOOL fileBrowserHasFocus = [self.window.firstResponder respondsToSelector:@selector(isDescendantOf:)] && [(NSView*)self.window.firstResponder isDescendantOf:self.fileBrowser.view]; NSString* searchFolder = fileBrowserHasFocus ? self.untitledSavePath : find.projectFolder; if(find.isVisible && find.searchFolder) searchFolder = find.searchFolder; [find showFindWindowFor:searchFolder]; } break; } } - (IBAction)orderFrontFindPanelForFileBrowser:(id)sender { Find* find = [Find sharedInstance]; find.documentIdentifier = self.selectedDocumentUUID; find.projectFolder = self.projectPath ?: self.untitledSavePath ?: NSHomeDirectory(); find.projectIdentifier = self.identifier; [find showFindWindowFor:self.untitledSavePath]; } - (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.filterString = @""; fc.openDocuments = _documents; fc.currentDocument = _selectedDocument ? _selectedDocument->identifier() : oak::uuid_t(); 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)) fc.filterString = entry.string; } [self positionWindow:fc.window]; [fc showWindow:nil]; } - (void)fileChooserDidSelectItems:(FileChooser*)sender { ASSERT([sender respondsToSelector:@selector(selectedItems)]); [self openItems:[sender selectedItems] closingOtherTabs:OakIsAlternateKeyOrMouseEvent()]; } // =========== // = Methods = // =========== - (NSString*)untitledSavePath { NSString* res = self.projectPath; 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; } - (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)goToFileCounterpart:(id)sender { if(!_selectedDocument) return; std::string const documentPath = _selectedDocument->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 candidates(&documentName, &documentName + 1); for(auto document : _documents) { if(documentDir == path::parent(document->path()) && documentBase == path::strip_extensions(path::name(document->path()))) candidates.insert(path::name(document->path())); } citerate(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); } } settings_t const settings = _selectedDocument->settings(); path::glob_t const excludeGlob(settings.get(kSettingsExcludeKey, "")); path::glob_t const binaryGlob(settings.get(kSettingsBinaryKey, "")); std::vector v; iterate(path, candidates) { if(*path == documentPath || !binaryGlob.does_match(*path) && !excludeGlob.does_match(*path)) v.push_back(*path); } if(v.size() == 1) return (void)NSBeep(); std::vector::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]; } // =========================== // = Go to Tab Menu Delegate = // =========================== - (void)updateGoToMenu:(NSMenu*)aMenu { if(![self.window isKeyWindow]) { [aMenu addItemWithTitle:@"No Tabs" action:@selector(nop:) keyEquivalent:@""]; return; } int i = 0; for(auto document : _documents) { NSMenuItem* item = [aMenu addItemWithTitle:[NSString stringWithCxxString:document->display_name()] action:@selector(takeSelectedTabIndexFrom:) keyEquivalent:i <= 10 ? [NSString stringWithFormat:@"%c", '0' + (i+1) % 10] : @""]; item.tag = i; item.toolTip = [[NSString stringWithCxxString:document->path()] stringByAbbreviatingWithTildeInPath]; item.image = [OakFileIconImage fileIconImageWithPath:[NSString stringWithCxxString:document->path()] isModified:document->is_modified()]; 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:@""]; } // ==================== // = NSMenuValidation = // ==================== - (BOOL)validateMenuItem:(NSMenuItem*)menuItem { 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 ? @"Hide HTML Output" : @"Show HTML Output"]; active = !self.htmlOutputInWindow || self.htmlOutputWindowController; } else if([menuItem action] == @selector(newDocumentInDirectory:)) active = self.fileBrowserVisible && [self.fileBrowser directoryForNewItems] != nil; else if([menuItem action] == @selector(newFolder:) || [menuItem action] == @selector(goBack:) || [menuItem action] == @selector(goForward:)) active = self.fileBrowserVisible && [self.fileBrowser validateMenuItem:menuItem]; else if([menuItem action] == @selector(moveDocumentToNewWindow:)) active = _documents.size() > 1; else if([menuItem action] == @selector(selectNextTab:) || [menuItem action] == @selector(selectPreviousTab:)) active = _documents.size() > 1; else if([menuItem action] == @selector(revealFileInProject:) || [menuItem action] == @selector(revealFileInProjectByExpandingAncestors:)) active = _selectedDocument && _selectedDocument->path() != NULL_STR; 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(reload:) || [menuItem action] == @selector(deselectAll:)) active = self.fileBrowserVisible; 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]; 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 )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 )previewPanel:(QLPreviewPanel*)panel previewItemAtIndex:(NSInteger)index { return [[QuickLookNSURLWrapper alloc] initWithURL:self.urlArrayForQuickLook[index]]; } // ====================== // = Session Management = // ====================== + (void)initialize { static NSString* const WindowNotifications[] = { NSWindowDidBecomeKeyNotification, NSWindowDidDeminiaturizeNotification, NSWindowDidExposeNotification, NSWindowDidMiniaturizeNotification, NSWindowDidMoveNotification, NSWindowDidResizeNotification, NSWindowWillCloseNotification }; iterate(notification, WindowNotifications) [[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; + (BOOL)restoreSession { BOOL res = NO; ++DisableSessionSavingCount; NSDictionary* session = [NSDictionary dictionaryWithContentsOfFile:[self sessionPath]]; for(NSDictionary* project in session[@"projects"]) { DocumentController* controller = [DocumentController new]; if(NSString* fileBrowserWidth = project[@"fileBrowserWidth"]) controller.fileBrowserWidth = [fileBrowserWidth floatValue]; if(NSString* htmlOutputSize = project[@"htmlOutputSize"]) controller.htmlOutputSize = NSSizeFromString(htmlOutputSize); controller.defaultProjectPath = project[@"projectPath"]; controller.fileBrowserHistory = project[@"fileBrowserState"]; controller.fileBrowserVisible = [project[@"fileBrowserVisible"] boolValue]; if(NSString* windowFrame = project[@"windowFrame"]) [controller.window setFrame:NSRectFromString(windowFrame) display:NO]; std::vector 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), true))) { NSString* path = info[@"path"]; doc = path ? document::create(to_s(path)) : create_untitled_document_in_folder(to_s(controller.untitledSavePath)); if(NSString* displayName = info[@"displayName"]) doc->set_custom_name(to_s(displayName)); } doc->set_recent_tracking(false); documents.push_back(doc); if([info[@"selected"] boolValue]) selectedTabIndex = documents.size() - 1; } if(documents.empty()) documents.push_back(create_untitled_document_in_folder(to_s(controller.untitledSavePath))); controller.documents = documents; controller.selectedTabIndex = selectedTabIndex; [controller openAndSelectDocument:documents[selectedTabIndex]]; [controller showWindow:nil]; if([project[@"miniaturized"] boolValue]) [controller.window miniaturize:nil]; else if([project[@"fullScreen"] boolValue]) [controller.window toggleFullScreen:self]; res = YES; } --DisableSessionSavingCount; return res; } + (BOOL)saveSessionIncludingUntitledDocuments:(BOOL)includeUntitled { if(DisableSessionSavingCount) return NO; NSMutableArray* projects = [NSMutableArray array]; for(DocumentController* controller in [SortedControllers() reverseObjectEnumerator]) { NSMutableDictionary* res = [NSMutableDictionary dictionary]; if(NSString* projectPath = controller.defaultProjectPath) res[@"projectPath"] = projectPath; if(NSDictionary* history = controller.fileBrowserHistory) res[@"fileBrowserState"] = history; if(([controller.window styleMask] & NSFullScreenWindowMask) == NSFullScreenWindowMask) res[@"fullScreen"] = @YES; else res[@"windowFrame"] = NSStringFromRect([controller.window frame]); res[@"miniaturized"] = @([controller.window isMiniaturized]); res[@"htmlOutputSize"] = NSStringFromSize(controller.htmlOutputSize); res[@"fileBrowserVisible"] = @(controller.fileBrowserVisible); res[@"fileBrowserWidth"] = @(controller.fileBrowserWidth); NSMutableArray* docs = [NSMutableArray array]; for(auto document : controller.documents) { 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_open()) document->backup(); } if(document->path() != NULL_STR) doc[@"path"] = [NSString stringWithCxxString:document->path()]; if(document->display_name() != NULL_STR) doc[@"displayName"] = [NSString stringWithCxxString:document->display_name()]; if(document == controller.selectedDocument) doc[@"selected"] = @YES; [docs addObject:doc]; } res[@"documents"] = docs; [projects addObject:res]; } NSDictionary* session = @{ @"projects" : projects }; return [session writeToFile:[self sessionPath] atomically:YES]; } // ================================ // = OakWindowFrameHelperDelegate = // ================================ - (NSRect)savableWindowFrame { NSRect res = [self.window frame]; if(self.fileBrowserVisible) res.size.width -= self.fileBrowserWidth; return res; } // ========== // = Legacy = // ========== - (void)updateVariables:(std::map&)env { [self.fileBrowser updateVariables:env]; if(NSString* projectDir = self.projectPath) { env["TM_PROJECT_DIRECTORY"] = [projectDir fileSystemRepresentation]; env["TM_PROJECT_UUID"] = to_s(self.identifier); } if(auto theme = self.textView.theme) { if(auto themeItem = bundles::lookup(theme->uuid())) { if(!themeItem->paths().empty()) env["TM_CURRENT_THEME_PATH"] = themeItem->paths().back(); } } } + (instancetype)controllerForDocument:(document::document_ptr const&)aDocument { if(!aDocument) return nil; for(DocumentController* delegate in SortedControllers()) { if(delegate.fileBrowserVisible && aDocument->path() != NULL_STR && aDocument->path().find(to_s(delegate.projectPath)) == 0) return delegate; for(auto document : delegate.documents) { if(*document == *aDocument) return delegate; } } return nil; } + (void)load { static struct proxy_t : document::ui_proxy_t { private: static void bring_to_front (DocumentController* aController) { if([NSApp isHidden]) { __block id observerId = [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationDidUnhideNotification object:NSApp queue:nil usingBlock:^(NSNotification*){ [aController showWindow:nil]; SetFrontProcessWithOptions(&(ProcessSerialNumber){ 0, kCurrentProcess }, kSetFrontProcessFrontWindowOnly); [[NSNotificationCenter defaultCenter] removeObserver:observerId]; }]; [NSApp unhideWithoutActivation]; } else { [aController showWindow:nil]; SetFrontProcessWithOptions(&(ProcessSerialNumber){ 0, kCurrentProcess }, kSetFrontProcessFrontWindowOnly); } } static DocumentController* find_or_create_controller (std::vector const& documents, oak::uuid_t const& projectUUID) { ASSERT(!documents.empty()); // ========================================= // = Return requested window, if it exists = // ========================================= if(projectUUID != document::kCollectionAny) { if(DocumentController* res = AllControllers()[[NSString stringWithCxxString:projectUUID]]) return res; } // ========================================= // = Find window with one of our documents = // ========================================= std::set uuids; std::transform(documents.begin(), documents.end(), inserter(uuids, uuids.end()), [](document::document_ptr const& doc){ return doc->identifier(); }); for(DocumentController* candidate in SortedControllers()) { for(auto document : candidate.documents) { if(uuids.find(document->identifier()) != uuids.end()) return candidate; } } // ================================================================ // = Find window with project folder closest to document’s parent = // ================================================================ std::vector documentsWithPath; std::copy_if(documents.begin(), documents.end(), back_inserter(documentsWithPath), [](document::document_ptr const& doc){ return doc->path() != NULL_STR; }); std::set parents; std::transform(documentsWithPath.begin(), documentsWithPath.end(), inserter(parents, parents.end()), [](document::document_ptr const& doc){ return path::parent(doc->path()); }); std::map candidates; for(DocumentController* candidate in SortedControllers()) { if(candidate.projectPath) { std::string const projectPath = to_s(candidate.projectPath); iterate(parent, parents) { if(path::is_child(*parent, projectPath)) candidates.insert(std::make_pair(parent->size() - projectPath.size(), candidate)); } } } if(!candidates.empty()) return candidates.begin()->second; // ============================================== // = Use frontmost window if a “scratch” window = // ============================================== if(DocumentController* candidate = [SortedControllers() firstObject]) { if(!candidate.fileBrowserVisible && candidate.documents.size() == 1 && is_disposable(candidate.selectedDocument)) return candidate; } // =================================== // = Give up and create a new window = // =================================== DocumentController* res = [DocumentController new]; if(!parents.empty()) // setup project folder for new window { std::vector 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; } static DocumentController* controller_with_documents (std::vector const& documents, oak::uuid_t const& projectUUID = document::kCollectionAny) { DocumentController* controller = find_or_create_controller(documents, projectUUID); if(controller.documents.empty()) { controller.documents = documents; } else { std::vector oldDocuments = controller.documents; NSUInteger split = controller.selectedTabIndex; if(is_disposable(oldDocuments[split])) oldDocuments.erase(oldDocuments.begin() + split); else ++split; std::vector newDocuments; split = merge_documents_splitting_at(oldDocuments, documents, split, newDocuments); controller.documents = newDocuments; controller.selectedTabIndex = split; } return controller; } public: void show_browser (std::string const& path) const { std::string const folder = path::resolve(path); [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:[NSString stringWithCxxString:folder]]]; for(DocumentController* candidate in SortedControllers()) { if(folder == to_s(candidate.projectPath)) return bring_to_front(candidate); } DocumentController* controller = nil; for(DocumentController* candidate in SortedControllers()) { if(!candidate.fileBrowserVisible && candidate.documents.size() == 1 && is_disposable(candidate.selectedDocument)) { controller = candidate; break; } } if(!controller) controller = [DocumentController new]; else if(controller.selectedDocument) [controller selectedDocument]->set_custom_name("not untitled"); // release potential untitled token used controller.defaultProjectPath = [NSString stringWithCxxString:folder]; controller.fileBrowserVisible = YES; controller.documents = make_vector(create_untitled_document_in_folder(folder)); controller.fileBrowser.url = [NSURL fileURLWithPath:[NSString stringWithCxxString:folder]]; [controller openAndSelectDocument:[controller documents][controller.selectedTabIndex]]; bring_to_front(controller); } void show_documents (std::vector const& documents) const { DocumentController* controller = controller_with_documents(documents); bring_to_front(controller); [controller openAndSelectDocument:[controller documents][controller.selectedTabIndex]]; } void show_document (oak::uuid_t const& collection, document::document_ptr document, text::range_t const& range, bool bringToFront) const { if(range != text::range_t::undefined) document->set_selection(range); DocumentController* controller = controller_with_documents(make_vector(document), collection); if(bringToFront) bring_to_front(controller); else if(![controller.window isVisible]) [controller.window orderWindow:NSWindowBelow relativeTo:[([NSApp keyWindow] ?: [NSApp mainWindow]) windowNumber]]; [controller openAndSelectDocument:document]; } void run (bundle_command_t const& command, ng::buffer_t const& buffer, ng::ranges_t const& selection, document::document_ptr document, std::map const& env, document::run_callback_ptr callback) { ::run(command, buffer, selection, document, env, callback); } } proxy; document::set_ui_proxy(&proxy); } @end