mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
508 lines
15 KiB
Plaintext
508 lines
15 KiB
Plaintext
#import "DocumentTabs.h"
|
||
#import "DocumentSaveHelper.h"
|
||
#import "DocumentCommand.h"
|
||
#import <document/document.h>
|
||
#import <OakTextView/OakDocumentView.h>
|
||
#import <OakFoundation/NSString Additions.h>
|
||
#import <OakAppKit/NSAlert Additions.h>
|
||
#import <document/collection.h>
|
||
#import <document/session.h>
|
||
|
||
OAK_DEBUG_VAR(DocumentController_Tabs);
|
||
|
||
@interface DocumentController ()
|
||
@property (nonatomic, retain) NSString* windowTitle;
|
||
@property (nonatomic, retain) NSString* representedFile;
|
||
@property (nonatomic, assign) BOOL isDocumentEdited;
|
||
@end
|
||
|
||
namespace
|
||
{
|
||
struct callback_info_t
|
||
{
|
||
callback_info_t (std::vector<document::document_ptr> const& documents, close_warning_callback_t* callback) : documents(documents), callback(callback) { }
|
||
std::vector<document::document_ptr> documents;
|
||
close_warning_callback_t* callback;
|
||
};
|
||
}
|
||
|
||
@implementation DocumentController (Tabs)
|
||
- (NSInteger)selectedTabIndex
|
||
{
|
||
return selectedTabIndex;
|
||
}
|
||
|
||
- (void)setSelectedTabIndex:(NSInteger)newSelectedTabIndex
|
||
{
|
||
D(DBF_DocumentController_Tabs, bug("%d → %d, window visible: %s\n", (int)selectedTabIndex, (int)newSelectedTabIndex, BSTR([self.window isVisible])););
|
||
|
||
selectedTabIndex = newSelectedTabIndex;
|
||
[tabBarView setSelectedTab:selectedTabIndex];
|
||
document::schedule_session_backup();
|
||
|
||
// This is also set after open succeeds
|
||
settings_t const& settings = [self selectedDocument]->settings();
|
||
self.windowTitle = [NSString stringWithCxxString:settings.get("windowTitle", [self selectedDocument]->display_name())];
|
||
self.representedFile = [NSString stringWithCxxString:[self selectedDocument]->path()];
|
||
self.isDocumentEdited = [self selectedDocument]->is_modified();
|
||
|
||
if(windowHasLoaded)
|
||
[[DocumentOpenHelper new] tryOpenDocument:self.selectedDocument forWindow:self.window delegate:self];
|
||
}
|
||
|
||
- (document::document_ptr const&)selectedDocument
|
||
{
|
||
return *documentTabs[selectedTabIndex];
|
||
}
|
||
|
||
- (void)updateFileBrowserStatus:(id)sender
|
||
{
|
||
NSMutableArray* openURLs = [NSMutableArray array];
|
||
NSMutableArray* modifiedURLs = [NSMutableArray array];
|
||
iterate(tab, documentTabs)
|
||
{
|
||
document::document_ptr document = **tab;
|
||
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()]]];
|
||
}
|
||
fileBrowser.openURLs = openURLs;
|
||
fileBrowser.modifiedURLs = modifiedURLs;
|
||
}
|
||
|
||
- (void)documentDidChange:(document::document_ptr const&)aDocument
|
||
{
|
||
D(DBF_DocumentController_Tabs, bug("\n"););
|
||
if(*aDocument == *[self selectedDocument])
|
||
{
|
||
settings_t const& settings = [self selectedDocument]->settings();
|
||
self.windowTitle = [NSString stringWithCxxString:settings.get("windowTitle", [self selectedDocument]->display_name())];
|
||
self.representedFile = [NSString stringWithCxxString:[self selectedDocument]->path()];
|
||
self.isDocumentEdited = [self selectedDocument]->is_modified();
|
||
}
|
||
[tabBarView reloadData];
|
||
[self updateFileBrowserStatus:self];
|
||
document::schedule_session_backup();
|
||
}
|
||
|
||
- (void)addDocuments:(std::vector<document::document_ptr> const&)someDocuments atIndex:(size_t)insertAt andSelect:(NSInteger)selectionConstant closeOther:(BOOL)closeOtherFlag pruneTabBar:(BOOL)pruneTabBarFlag
|
||
{
|
||
D(DBF_DocumentController_Tabs, bug("%zu documents, close other %s, prune %s\n", someDocuments.size(), BSTR(closeOtherFlag), BSTR(pruneTabBarFlag)););
|
||
|
||
oak::uuid_t selectedDocumentIdentifier;
|
||
std::map<oak::uuid_t, document_tab_ptr> oldTabs;
|
||
iterate(tab, documentTabs)
|
||
{
|
||
document::document_ptr document = **tab;
|
||
oldTabs.insert(std::make_pair(document->identifier(), *tab));
|
||
if(selectedTabIndex == tab - documentTabs.begin())
|
||
selectedDocumentIdentifier = document->identifier();
|
||
}
|
||
|
||
std::set<oak::uuid_t> newDocs;
|
||
std::vector<document_tab_ptr> wrapped;
|
||
iterate(doc, someDocuments)
|
||
{
|
||
newDocs.insert((*doc)->identifier());
|
||
std::map<oak::uuid_t, document_tab_ptr>::const_iterator oldTab = oldTabs.find((*doc)->identifier());
|
||
if(oldTab != oldTabs.end())
|
||
{
|
||
wrapped.push_back(oldTab->second);
|
||
}
|
||
else
|
||
{
|
||
wrapped.push_back(document_tab_ptr(new document_tab_t(*doc)));
|
||
wrapped.back()->add_callback(self);
|
||
}
|
||
}
|
||
|
||
if(closeOtherFlag)
|
||
{
|
||
std::vector<document_tab_ptr> modifiedTabs;
|
||
iterate(tab, documentTabs)
|
||
{
|
||
document::document_ptr document = **tab;
|
||
if(document->is_modified())
|
||
modifiedTabs.push_back(*tab);
|
||
}
|
||
documentTabs.swap(modifiedTabs);
|
||
insertAt = 0;
|
||
}
|
||
else
|
||
{
|
||
if(scratchDocument)
|
||
{
|
||
iterate(tab, documentTabs)
|
||
{
|
||
document::document_ptr document = **tab;
|
||
if(document->identifier() == scratchDocument && !document->is_modified() && document->path() == NULL_STR)
|
||
newDocs.insert(scratchDocument); // causes scratch document to be closed
|
||
}
|
||
}
|
||
|
||
size_t i = documentTabs.size();
|
||
while(i-- != 0)
|
||
{
|
||
document::document_ptr doc = *documentTabs[i];
|
||
if(newDocs.find(doc->identifier()) != newDocs.end())
|
||
{
|
||
documentTabs.erase(documentTabs.begin() + i);
|
||
if(i < insertAt)
|
||
--insertAt;
|
||
}
|
||
}
|
||
}
|
||
|
||
documentTabs.insert(documentTabs.begin() + insertAt, wrapped.begin(), wrapped.end());
|
||
scratchDocument = oak::uuid_t();
|
||
[tabBarView reloadData];
|
||
[self updateFileBrowserStatus:self];
|
||
|
||
switch(selectionConstant)
|
||
{
|
||
case kSelectDocumentFirst: self.selectedTabIndex = insertAt; break;
|
||
case kSelectDocumentLast: self.selectedTabIndex = insertAt + wrapped.size() - 1; break;
|
||
case kSelectDocumentNone:
|
||
{
|
||
for(size_t i = 0; i < documentTabs.size(); ++i)
|
||
{
|
||
document::document_ptr doc = *documentTabs[i];
|
||
if(doc->identifier() == selectedDocumentIdentifier)
|
||
self.selectedTabIndex = i;
|
||
}
|
||
}
|
||
break;
|
||
default: self.selectedTabIndex = insertAt + selectionConstant; break;
|
||
}
|
||
|
||
if(pruneTabBarFlag)
|
||
{
|
||
NSInteger excessTabs = documentTabs.size() - tabBarView.countOfVisibleTabs;
|
||
if(tabBarView && excessTabs > 0)
|
||
{
|
||
std::multimap<oak::date_t, size_t> ranked;
|
||
for(size_t i = 0; i < documentTabs.size(); ++i)
|
||
{
|
||
document::document_ptr doc = *documentTabs[i];
|
||
if(!doc->is_modified() && doc->is_on_disk() && newDocs.find(doc->identifier()) == newDocs.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 quiet:NO];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)addDocuments:(std::vector<document::document_ptr> const&)someDocuments andSelect:(NSInteger)selectionConstant closeOther:(BOOL)closeOtherFlag pruneTabBar:(BOOL)pruneTabBarFlag
|
||
{
|
||
[self addDocuments:someDocuments atIndex:(documentTabs.empty() ? 0 : selectedTabIndex + 1) andSelect:selectionConstant closeOther:closeOtherFlag pruneTabBar:pruneTabBarFlag];
|
||
}
|
||
|
||
- (void)closeTabsAtIndexes:(NSIndexSet*)anIndexSet quiet:(BOOL)quietFlag
|
||
{
|
||
NSInteger newSelectedIndex = selectedTabIndex;
|
||
|
||
if(quietFlag == NO)
|
||
{
|
||
std::vector<document::document_ptr> documents;
|
||
NSUInteger i = documentTabs.size();
|
||
while((i = [anIndexSet indexLessThanIndex:i]) != NSNotFound)
|
||
{
|
||
document::document_ptr document = *documentTabs[i];
|
||
if(document->is_modified())
|
||
documents.push_back(document);
|
||
}
|
||
|
||
if(!documents.empty())
|
||
{
|
||
struct callback_t : close_warning_callback_t
|
||
{
|
||
callback_t (DocumentController* controller, NSIndexSet* indexSet)
|
||
{
|
||
_self = [controller retain];
|
||
_indexSet = [indexSet retain];
|
||
}
|
||
|
||
~callback_t ()
|
||
{
|
||
[_self release];
|
||
[_indexSet release];
|
||
}
|
||
|
||
void can_close_documents (bool flag)
|
||
{
|
||
if(flag)
|
||
[_self closeTabsAtIndexes:_indexSet quiet:YES];
|
||
delete this;
|
||
}
|
||
private:
|
||
DocumentController* _self;
|
||
NSIndexSet* _indexSet;
|
||
};
|
||
return (void)[self showCloseWarningUIForDocuments:documents andCallback:new callback_t(self, anIndexSet)];
|
||
}
|
||
}
|
||
|
||
NSUInteger i = documentTabs.size();
|
||
while((i = [anIndexSet indexLessThanIndex:i]) != NSNotFound)
|
||
{
|
||
if(i <= newSelectedIndex && newSelectedIndex)
|
||
--newSelectedIndex;
|
||
documentTabs.erase(documentTabs.begin() + i);
|
||
}
|
||
|
||
if(documentTabs.empty())
|
||
return [self close];
|
||
|
||
[tabBarView reloadData];
|
||
[self updateFileBrowserStatus:self];
|
||
self.selectedTabIndex = newSelectedIndex;
|
||
}
|
||
|
||
// ==============================
|
||
// = DocumentOpenHelperDelegate =
|
||
// ==============================
|
||
|
||
- (void)documentOpenHelper:(DocumentOpenHelper*)documentOpenHelper didOpenDocument:(document::document_ptr const&)aDocument
|
||
{
|
||
iterate(tab, documentTabs)
|
||
{
|
||
if(**tab == aDocument && !(*tab)->_did_open)
|
||
{
|
||
(*tab)->_document->open();
|
||
(*tab)->_did_open = true;
|
||
}
|
||
}
|
||
|
||
settings_t const& settings = aDocument->settings();
|
||
self.windowTitle = [NSString stringWithCxxString:settings.get("windowTitle", aDocument->display_name())];
|
||
self.representedFile = [NSString stringWithCxxString:aDocument->path()];
|
||
self.isDocumentEdited = aDocument->is_modified();
|
||
|
||
[documentView setDocument:aDocument];
|
||
[self makeTextViewFirstResponder:self];
|
||
|
||
[documentOpenHelper release];
|
||
}
|
||
|
||
- (void)documentOpenHelper:(DocumentOpenHelper*)documentOpenHelper failedToOpenDocument:(document::document_ptr const&)aDocument error:(std::string const&)aMessage usingFilter:(oak::uuid_t const&)filterUUID
|
||
{
|
||
if(filterUUID)
|
||
show_command_error(aMessage, filterUUID);
|
||
|
||
NSMutableIndexSet* set = [NSMutableIndexSet indexSet];
|
||
for(size_t i = 0; i < documentTabs.size(); ++i)
|
||
{
|
||
document::document_ptr document = *documentTabs[i];
|
||
if(*document == *aDocument)
|
||
[set addIndex:i];
|
||
}
|
||
[self closeTabsAtIndexes:set quiet:YES];
|
||
|
||
[documentOpenHelper release];
|
||
}
|
||
|
||
// =======================
|
||
// = Save/Close Warnings =
|
||
// =======================
|
||
|
||
- (void)closeWarningDidEnd:(NSAlert*)alert returnCode:(NSInteger)returnCode contextInfo:(callback_info_t*)info
|
||
{
|
||
switch(returnCode)
|
||
{
|
||
case NSAlertFirstButtonReturn: /* "Save" */
|
||
{
|
||
struct callback_t : document_save_callback_t
|
||
{
|
||
callback_t (close_warning_callback_t* callback, size_t count) : _callback(callback), _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->can_close_documents(flag);
|
||
|
||
if(--_count == 0 || !flag)
|
||
delete this;
|
||
}
|
||
|
||
private:
|
||
close_warning_callback_t* _callback;
|
||
size_t _count;
|
||
};
|
||
|
||
if(info->callback)
|
||
info->callback->will_save_documents();
|
||
|
||
[DocumentSaveHelper trySaveDocuments:info->documents forWindow:self.window defaultDirectory:self.untitledSavePath andCallback:new callback_t(info->callback, info->documents.size())];
|
||
}
|
||
break;
|
||
|
||
case NSAlertSecondButtonReturn: /* "Cancel" */
|
||
{
|
||
if(info->callback)
|
||
info->callback->can_close_documents(false);
|
||
}
|
||
break;
|
||
|
||
case NSAlertThirdButtonReturn: /* "Don't Save" */
|
||
{
|
||
if(info->callback)
|
||
info->callback->can_close_documents(true);
|
||
}
|
||
break;
|
||
}
|
||
delete info;
|
||
[alert release];
|
||
}
|
||
|
||
- (void)showCloseWarningUIForDocuments:(std::vector<document::document_ptr> const&)someDocuments andCallback:(close_warning_callback_t*)aCallback
|
||
{
|
||
D(DBF_DocumentController_Tabs, bug("%s — %zu documents\n", [self.window.title UTF8String], someDocuments.size()););
|
||
|
||
if(someDocuments.empty())
|
||
{
|
||
if(aCallback)
|
||
aCallback->can_close_documents(true);
|
||
return;
|
||
}
|
||
|
||
[[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 = "";
|
||
iterate(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)
|
||
{
|
||
D(DBF_DocumentController_Tabs, bug("select ‘%s’\n", someDocuments.back()->display_name().c_str()););
|
||
iterate(tab, documentTabs)
|
||
{
|
||
document::document_ptr document = **tab;
|
||
if(someDocuments.front()->identifier() == document->identifier())
|
||
self.selectedTabIndex = tab - documentTabs.begin();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
std::set<oak::uuid_t> docIdentifiers;
|
||
iterate(tab, documentTabs)
|
||
{
|
||
document::document_ptr document = **tab;
|
||
docIdentifiers.insert(document->identifier());
|
||
}
|
||
|
||
iterate(document, someDocuments)
|
||
{
|
||
if(docIdentifiers.find((*document)->identifier()) == docIdentifiers.end())
|
||
windowModal = false;
|
||
}
|
||
}
|
||
|
||
if(windowModal)
|
||
[alert beginSheetModalForWindow:self.window modalDelegate:self didEndSelector:@selector(closeWarningDidEnd:returnCode:contextInfo:) contextInfo:new callback_info_t(someDocuments, aCallback)];
|
||
else [self closeWarningDidEnd:alert returnCode:[alert runModal] contextInfo:new callback_info_t(someDocuments, aCallback)];
|
||
}
|
||
|
||
// ===========================
|
||
// = Application Termination =
|
||
// ===========================
|
||
|
||
- (void)shutdownCleanup
|
||
{
|
||
document::save_session(false);
|
||
for(NSWindow* window in [NSApp orderedWindows])
|
||
{
|
||
DocumentController* delegate = (DocumentController*)[window delegate];
|
||
if([delegate isKindOfClass:[DocumentController class]])
|
||
{
|
||
// it might be better to call [delegate close] but with auto release pools we can’t be 100% certain that this closes all documents (in this event loop cycle)
|
||
[delegate->documentView setDocument:document::create()];
|
||
delegate->documentTabs.clear();
|
||
}
|
||
}
|
||
}
|
||
|
||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender
|
||
{
|
||
D(DBF_DocumentController_Tabs, bug("%s\n", [self.window.title UTF8String]););
|
||
DocumentController* controller = nil;
|
||
|
||
std::set<oak::uuid_t> docIdentifiers;
|
||
std::vector<document::document_ptr> documents;
|
||
for(NSWindow* window in [NSApp orderedWindows])
|
||
{
|
||
DocumentController* delegate = (DocumentController*)[window delegate];
|
||
if([delegate isKindOfClass:[DocumentController class]])
|
||
{
|
||
iterate(tab, delegate->documentTabs)
|
||
{
|
||
document::document_ptr document = **tab;
|
||
if(document->is_modified() && docIdentifiers.find(document->identifier()) == docIdentifiers.end())
|
||
{
|
||
documents.push_back(document);
|
||
docIdentifiers.insert(document->identifier());
|
||
if(!controller)
|
||
controller = delegate;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if(!documents.empty())
|
||
{
|
||
struct callback_t : close_warning_callback_t
|
||
{
|
||
callback_t (DocumentController* self) : _self(self), _did_reply(false) { }
|
||
|
||
void will_save_documents ()
|
||
{
|
||
[NSApp replyToApplicationShouldTerminate:NO];
|
||
_did_reply = true;
|
||
}
|
||
|
||
void can_close_documents (bool flag)
|
||
{
|
||
if(!_did_reply && flag)
|
||
[_self shutdownCleanup];
|
||
|
||
if(!_did_reply)
|
||
[NSApp replyToApplicationShouldTerminate:flag];
|
||
else if(flag)
|
||
[NSApp terminate:nil];
|
||
delete this;
|
||
}
|
||
|
||
private:
|
||
DocumentController* _self;
|
||
bool _did_reply;
|
||
};
|
||
return [controller showCloseWarningUIForDocuments:documents andCallback:new callback_t(self)], NSTerminateLater;
|
||
}
|
||
|
||
[self shutdownCleanup];
|
||
return NSTerminateNow;
|
||
}
|
||
@end |