mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
This fixes problems with symbolic links, for example file browser may show contents of /private/tmp when we ask to select /tmp/foo.txt. Comparing file path URLs will fail even though /tmp is a symbolic link for /private/tmp. Note that this fix only works for when the current document is already part of the items in the file browser, as we still store pending URLs to select as file path URLs.
2303 lines
77 KiB
Plaintext
2303 lines
77 KiB
Plaintext
#import "FileBrowserViewController.h"
|
||
#import "FileBrowserView.h"
|
||
#import "FileBrowserOutlineView.h"
|
||
#import "FileBrowserNotifications.h"
|
||
#import "FileItem.h"
|
||
#import "FileItemTableCellView.h"
|
||
#import "SCMManager.h"
|
||
#import "FSEventsManager.h"
|
||
#import "OFB/OFBHeaderView.h"
|
||
#import "OFB/OFBActionsView.h"
|
||
#import "OFB/OFBFinderTagsChooser.h"
|
||
#import <MenuBuilder/MenuBuilder.h>
|
||
#import <OakAppKit/NSMenuItem Additions.h>
|
||
#import <OakAppKit/OakAppKit.h>
|
||
#import <OakAppKit/OakOpenWithMenu.h>
|
||
#import <OakAppKit/OakFinderTag.h>
|
||
#import <OakAppKit/OakZoomingIcon.h>
|
||
#import <OakFoundation/OakFoundation.h>
|
||
#import <OakCommand/OakCommand.h>
|
||
#import <Preferences/Keys.h>
|
||
#import <bundles/bundles.h>
|
||
#import <ns/ns.h>
|
||
#import <regexp/glob.h>
|
||
#import <settings/settings.h>
|
||
#import <text/ctype.h>
|
||
#import <text/utf8.h>
|
||
|
||
static bool is_binary (std::string const& path)
|
||
{
|
||
if(path == NULL_STR)
|
||
return false;
|
||
|
||
settings_t const& settings = settings_for_path(path);
|
||
if(settings.has(kSettingsBinaryKey))
|
||
return path::glob_t(settings.get(kSettingsBinaryKey, "")).does_match(path);
|
||
|
||
return false;
|
||
}
|
||
|
||
static NSMutableIndexSet* MutableLongestCommonSubsequence (NSArray* lhs, NSArray* rhs)
|
||
{
|
||
NSInteger width = lhs.count + 1;
|
||
NSInteger height = rhs.count + 1;
|
||
|
||
NSInteger* matrix = new NSInteger[width * height];
|
||
|
||
for(NSInteger i = lhs.count; i >= 0; --i)
|
||
{
|
||
for(NSInteger j = rhs.count; j >= 0; --j)
|
||
{
|
||
if(i == lhs.count || j == rhs.count)
|
||
matrix[width*i + j] = 0;
|
||
else if([lhs[i] isEqual:rhs[j]])
|
||
matrix[width*i + j] = matrix[width*(i+1) + j+1] + 1;
|
||
else
|
||
matrix[width*i + j] = MAX(matrix[width*(i+1) + j], matrix[width*i + j+1]);
|
||
}
|
||
}
|
||
|
||
NSMutableIndexSet* res = [NSMutableIndexSet indexSet];
|
||
for(NSInteger i = 0, j = 0; i < lhs.count && j < rhs.count; )
|
||
{
|
||
if([lhs[i] isEqual:rhs[j]])
|
||
{
|
||
[res addIndex:i];
|
||
i++;
|
||
j++;
|
||
}
|
||
else if(matrix[width*i + j+1] < matrix[width*(i+1) + j])
|
||
{
|
||
i++;
|
||
}
|
||
else
|
||
{
|
||
j++;
|
||
}
|
||
}
|
||
|
||
delete [] matrix;
|
||
|
||
return res;
|
||
}
|
||
|
||
@interface FileBrowserViewController () <NSMenuDelegate, NSOutlineViewDataSource, NSOutlineViewDelegate, NSTextFieldDelegate, QLPreviewPanelDataSource>
|
||
{
|
||
NSUndoManager* _fileBrowserUndoManager;
|
||
NSArray<FileItem*>* _previewItems;
|
||
|
||
NSMutableDictionary<NSURL*, id>* _fileItemObservers;
|
||
|
||
NSMutableSet<NSURL*>* _loadingURLs;
|
||
NSArray<void(^)()>* _loadingURLsCompletionHandlers;
|
||
|
||
NSInteger _expandingChildrenCounter;
|
||
NSInteger _collapsingChildrenCounter;
|
||
NSInteger _nestedCollapsingChildrenCounter;
|
||
}
|
||
@property (nonatomic) BOOL canExpandSymbolicLinks;
|
||
@property (nonatomic) BOOL canExpandPackages;
|
||
@property (nonatomic) BOOL sortDirectoriesBeforeFiles;
|
||
|
||
@property (nonatomic) BOOL showExcludedItems;
|
||
|
||
@property (nonatomic, readonly) NSArray<FileItem*>* selectedItems;
|
||
@property (nonatomic, readonly) NSArray<FileItem*>* previewableItems;
|
||
|
||
@property (nonatomic) NSMutableSet<NSURL*>* expandedURLs;
|
||
@property (nonatomic) NSMutableSet<NSURL*>* selectedURLs;
|
||
|
||
- (void)expandURLs:(NSArray<NSURL*>*)expandURLs selectURLs:(NSArray<NSURL*>*)selectURLs;
|
||
- (NSRect)imageRectOfItem:(FileItem*)item;
|
||
|
||
- (void)updateDisambiguationSuffixInParent:(FileItem*)item;
|
||
|
||
// =============================
|
||
// = FileBrowserViewController =
|
||
// =============================
|
||
|
||
@property (nonatomic) NSMenuItem* currentLocationMenuItem;
|
||
@property (nonatomic) NSMutableArray<NSDictionary*>* history;
|
||
@property (nonatomic) NSInteger historyIndex;
|
||
@property (nonatomic) FileBrowserView* fileBrowserView;
|
||
@end
|
||
|
||
@implementation FileBrowserViewController
|
||
+ (NSSet*)keyPathsForValuesAffectingCanGoBack { return [NSSet setWithObjects:@"historyIndex", nil]; }
|
||
+ (NSSet*)keyPathsForValuesAffectingCanGoForward { return [NSSet setWithObjects:@"historyIndex", nil]; }
|
||
|
||
- (instancetype)init
|
||
{
|
||
if(self = [super init])
|
||
{
|
||
_fileItemObservers = [NSMutableDictionary dictionary];
|
||
_loadingURLs = [NSMutableSet set];
|
||
|
||
_canExpandSymbolicLinks = [NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsAllowExpandingLinksKey];
|
||
_canExpandPackages = [NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsAllowExpandingPackagesKey];
|
||
_sortDirectoriesBeforeFiles = [NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsFoldersOnTopKey];
|
||
|
||
_expandedURLs = [NSMutableSet set];
|
||
_selectedURLs = [NSMutableSet set];
|
||
|
||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:NSUserDefaults.standardUserDefaults];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)dealloc
|
||
{
|
||
[NSNotificationCenter.defaultCenter removeObserver:self name:NSUserDefaultsDidChangeNotification object:NSUserDefaults.standardUserDefaults];
|
||
|
||
for(id observer in _fileItemObservers.allValues)
|
||
[FileItem removeObserver:observer];
|
||
_fileItemObservers = nil;
|
||
|
||
if(_currentLocationMenuItem)
|
||
{
|
||
[_currentLocationMenuItem unbind:NSTitleBinding];
|
||
[_currentLocationMenuItem unbind:NSImageBinding];
|
||
}
|
||
|
||
if(OFBHeaderView* headerView = _fileBrowserView.headerView)
|
||
{
|
||
[headerView.goBackButton unbind:NSEnabledBinding];
|
||
[headerView.goForwardButton unbind:NSEnabledBinding];
|
||
|
||
[NSNotificationCenter.defaultCenter removeObserver:self name:NSPopUpButtonWillPopUpNotification object:headerView.folderPopUpButton];
|
||
}
|
||
}
|
||
|
||
- (void)userDefaultsDidChange:(id)sender
|
||
{
|
||
self.canExpandSymbolicLinks = [NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsAllowExpandingLinksKey];
|
||
self.canExpandPackages = [NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsAllowExpandingPackagesKey];
|
||
self.sortDirectoriesBeforeFiles = [NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsFoldersOnTopKey];
|
||
}
|
||
|
||
- (void)loadView
|
||
{
|
||
self.view = self.fileBrowserView;
|
||
}
|
||
|
||
- (FileBrowserView*)fileBrowserView
|
||
{
|
||
if(!_fileBrowserView)
|
||
{
|
||
_fileBrowserView = [[FileBrowserView alloc] initWithFrame:NSZeroRect];
|
||
|
||
_currentLocationMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(takeURLFrom:) keyEquivalent:@""];
|
||
_currentLocationMenuItem.target = self;
|
||
[_currentLocationMenuItem bind:NSTitleBinding toObject:self withKeyPath:@"fileItem.displayName" options:nil];
|
||
[_currentLocationMenuItem bind:NSImageBinding toObject:self withKeyPath:@"fileItem.image" options:nil];
|
||
|
||
NSOutlineView* outlineView = _fileBrowserView.outlineView;
|
||
outlineView.dataSource = self;
|
||
outlineView.delegate = self;
|
||
outlineView.target = self;
|
||
outlineView.action = @selector(didSingleClickOutlineView:);
|
||
outlineView.doubleAction = @selector(didDoubleClickOutlineView:);
|
||
|
||
outlineView.menu = [[NSMenu alloc] init];
|
||
outlineView.menu.delegate = self;
|
||
|
||
OFBHeaderView* headerView = _fileBrowserView.headerView;
|
||
headerView.goBackButton.target = self;
|
||
headerView.goBackButton.action = @selector(goBack:);
|
||
headerView.goBackButton.enabled = NO;
|
||
|
||
headerView.goForwardButton.target = self;
|
||
headerView.goForwardButton.action = @selector(goForward:);
|
||
headerView.goForwardButton.enabled = NO;
|
||
|
||
[headerView.goBackButton bind:NSEnabledBinding toObject:self withKeyPath:@"canGoBack" options:nil];
|
||
[headerView.goForwardButton bind:NSEnabledBinding toObject:self withKeyPath:@"canGoForward" options:nil];
|
||
|
||
NSMenu* folderPopUpMenu = headerView.folderPopUpButton.menu;
|
||
[folderPopUpMenu removeAllItems];
|
||
[folderPopUpMenu addItem:_currentLocationMenuItem];
|
||
[headerView.folderPopUpButton selectItem:_currentLocationMenuItem];
|
||
|
||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(folderPopUpButtonWillPopUp:) name:NSPopUpButtonWillPopUpNotification object:headerView.folderPopUpButton];
|
||
|
||
OFBActionsView* actionsView = _fileBrowserView.actionsView;
|
||
|
||
actionsView.createButton.action = @selector(newDocumentInDirectory:);
|
||
actionsView.reloadButton.target = self;
|
||
actionsView.reloadButton.action = @selector(reload:);
|
||
actionsView.searchButton.action = @selector(orderFrontFindPanelForFileBrowser:);
|
||
actionsView.favoritesButton.target = self;
|
||
actionsView.favoritesButton.action = @selector(goToFavorites:);
|
||
actionsView.scmButton.target = self;
|
||
actionsView.scmButton.action = @selector(goToSCMDataSource:);
|
||
|
||
actionsView.actionsPopUpButton.menu.delegate = self;
|
||
}
|
||
return _fileBrowserView;
|
||
}
|
||
|
||
- (void)toggleShowInvisibles:(id)sender
|
||
{
|
||
self.showExcludedItems = !self.showExcludedItems;
|
||
}
|
||
|
||
- (NSView*)headerView { return self.fileBrowserView.headerView; }
|
||
- (NSOutlineView*)outlineView { return self.fileBrowserView.outlineView; }
|
||
- (NSString*)path { return self.URL.filePathURL.path; }
|
||
- (NSArray<NSURL*>*)selectedFileURLs { return [[self.selectedItems filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"URL.isFileURL == YES"]] valueForKeyPath:@"URL"]; }
|
||
|
||
- (BOOL)canGoBack { return _historyIndex > 0; }
|
||
- (BOOL)canGoForward { return _historyIndex+1 < self.history.count; }
|
||
|
||
- (void)goBack:(id)sender { self.historyIndex = _historyIndex - 1; }
|
||
- (void)goForward:(id)sender { self.historyIndex = _historyIndex + 1; }
|
||
|
||
- (NSMutableArray<NSDictionary*>*)history
|
||
{
|
||
if(!_history)
|
||
_history = [NSMutableArray array];
|
||
return _history;
|
||
}
|
||
|
||
- (void)setHistoryIndex:(NSInteger)index
|
||
{
|
||
_historyIndex = index;
|
||
self.URL = self.history[index][@"url"];
|
||
}
|
||
|
||
- (void)addHistoryURL:(NSURL*)url
|
||
{
|
||
if(_historyIndex + 1 < self.history.count)
|
||
[self.history removeObjectsInRange:NSMakeRange(_historyIndex + 1, self.history.count - (_historyIndex + 1))];
|
||
|
||
if(NSDictionary* dict = _history.lastObject)
|
||
{
|
||
_history[_history.count-1] = @{
|
||
@"url": dict[@"url"],
|
||
@"scrollOffset": @(NSMinY(self.outlineView.visibleRect)),
|
||
};
|
||
}
|
||
|
||
[self.history addObject:@{ @"url": url }];
|
||
self.historyIndex = self.history.count-1;
|
||
}
|
||
|
||
- (void)goToURL:(NSURL*)url
|
||
{
|
||
if(url && ![self.URL isEqual:url])
|
||
[self addHistoryURL:url];
|
||
}
|
||
|
||
- (void)goToComputer:(id)sender { [self goToURL:kURLLocationComputer]; }
|
||
- (void)goToHome:(id)sender { [self goToURL:[NSURL fileURLWithPath:NSHomeDirectory()]]; }
|
||
- (void)goToDesktop:(id)sender { [self goToURL:[NSFileManager.defaultManager URLForDirectory:NSDesktopDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil]]; }
|
||
|
||
- (void)goToFavorites:(id)sender
|
||
{
|
||
if(![self.URL isEqual:kURLLocationFavorites])
|
||
[self goToURL:kURLLocationFavorites];
|
||
else if(self.canGoBack)
|
||
[self goBack:sender];
|
||
}
|
||
|
||
- (void)goToSCMDataSource:(id)sender
|
||
{
|
||
NSURL* url = self.URL;
|
||
if([url.scheme isEqualToString:@"file"])
|
||
{
|
||
SCMRepository* repository = [SCMManager.sharedInstance repositoryAtURL:url];
|
||
if(repository && repository.enabled)
|
||
{
|
||
[self goToURL:[NSURL URLWithString:[NSString stringWithFormat:@"scm://localhost%@/", [url.path stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet]]]];
|
||
}
|
||
else
|
||
{
|
||
NSAlert* alert = [[NSAlert alloc] init];
|
||
|
||
if(repository)
|
||
{
|
||
alert.messageText = [NSString stringWithFormat:@"Version control is disabled for “%@”.", self.fileItem.localizedName];
|
||
alert.informativeText = @"For performance reasons TextMate will not monitor version control information for this folder.";
|
||
}
|
||
else
|
||
{
|
||
alert.messageText = [NSString stringWithFormat:@"Version control is not available for “%@”.", self.fileItem.localizedName];
|
||
alert.informativeText = @"You need to initialize the folder using your favorite version control system before TextMate can show you status.";
|
||
}
|
||
|
||
[alert addButtonWithTitle:@"OK"];
|
||
[alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response){ }];
|
||
}
|
||
}
|
||
else if([url.scheme isEqualToString:@"scm"])
|
||
{
|
||
if(self.canGoBack)
|
||
[self goBack:self];
|
||
else if(NSURL* parentURL = self.fileItem.parentURL)
|
||
[self goToURL:parentURL];
|
||
}
|
||
else
|
||
{
|
||
NSBeep();
|
||
}
|
||
}
|
||
|
||
- (void)goToParentFolder:(id)sender
|
||
{
|
||
if(NSURL* url = self.fileItem.parentURL)
|
||
{
|
||
NSURL* cameFromURL = self.URL;
|
||
[self goToURL:url];
|
||
[self expandURLs:nil selectURLs:@[ cameFromURL ]];
|
||
}
|
||
}
|
||
|
||
- (void)takeURLFrom:(id)sender
|
||
{
|
||
[self goToURL:[sender representedObject]];
|
||
}
|
||
|
||
- (void)folderPopUpButtonWillPopUp:(NSNotification*)aNotification
|
||
{
|
||
NSMenu* menu = self.fileBrowserView.headerView.folderPopUpButton.menu;
|
||
while(menu.numberOfItems > 1)
|
||
[menu removeItemAtIndex:menu.numberOfItems-1];
|
||
|
||
FileItem* fileItem = self.fileItem;
|
||
while(fileItem = [FileItem fileItemWithURL:fileItem.parentURL])
|
||
{
|
||
NSMenuItem* menuItem = [menu addItemWithTitle:fileItem.localizedName action:@selector(takeURLFrom:) keyEquivalent:@""];
|
||
menuItem.representedObject = fileItem.resolvedURL;
|
||
menuItem.image = fileItem.image;
|
||
menuItem.target = self;
|
||
}
|
||
|
||
[menu addItem:[NSMenuItem separatorItem]];
|
||
[[menu addItemWithTitle:@"Other…" action:@selector(orderFrontGoToFolder:) keyEquivalent:@""] setTarget:self];
|
||
|
||
FileItem* item = self.fileItem;
|
||
if(NSURL* url = item.URL.filePathURL)
|
||
{
|
||
[menu addItem:[NSMenuItem separatorItem]];
|
||
[[menu addItemWithTitle:[NSString stringWithFormat:@"Use “%@” as Project Folder", item.localizedName] action:@selector(takeProjectPathFrom:) keyEquivalent:@""] setRepresentedObject:url.path];
|
||
}
|
||
}
|
||
|
||
- (void)openItems:(NSArray<FileItem*>*)items animate:(BOOL)animateFlag
|
||
{
|
||
NSMutableArray<FileItem*>* itemsToOpen = [NSMutableArray array];
|
||
NSMutableArray<FileItem*>* itemsToOpenInTextMate = [NSMutableArray array];
|
||
NSMutableArray<FileItem*>* itemsToShowInFinder = [NSMutableArray array];
|
||
NSMutableArray<FileItem*>* itemsToShowInFileBrowser = [NSMutableArray array];
|
||
|
||
NSEventType eventType = NSApp.currentEvent.type;
|
||
NSEventModifierFlags eventFlags = NSApp.currentEvent.modifierFlags & (NSEventModifierFlagControl|NSEventModifierFlagOption|NSEventModifierFlagShift|NSEventModifierFlagCommand);
|
||
BOOL isMouseEvent = eventType == NSEventTypeLeftMouseUp || eventType == NSEventTypeOtherMouseUp || eventType == NSEventTypeOtherMouseUp;
|
||
BOOL commandKeyDown = isMouseEvent && eventFlags == NSEventModifierFlagCommand;
|
||
BOOL optionKeyDown = isMouseEvent && eventFlags == NSEventModifierFlagOption;
|
||
BOOL treatPackageAsDirectory = [NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsAllowExpandingPackagesKey];
|
||
|
||
for(FileItem* item in items)
|
||
{
|
||
if(commandKeyDown)
|
||
[itemsToShowInFinder addObject:item];
|
||
else if(item.isDirectory && (treatPackageAsDirectory || !item.isPackage) || item.isLinkToDirectory && (treatPackageAsDirectory || !item.isLinkToPackage) || optionKeyDown && (item.isPackage || item.isLinkToDirectory))
|
||
[itemsToShowInFileBrowser addObject:item];
|
||
else if(item.isPackage || item.isLinkToPackage || item.URL.isFileURL && is_binary(item.URL.fileSystemRepresentation))
|
||
[itemsToOpen addObject:item];
|
||
else
|
||
[itemsToOpenInTextMate addObject:item];
|
||
}
|
||
|
||
if(itemsToShowInFileBrowser.count > 0)
|
||
return [self goToURL:itemsToShowInFileBrowser.firstObject.resolvedURL];
|
||
|
||
if(animateFlag && ![NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsFileBrowserOpenAnimationDisabled])
|
||
{
|
||
for(NSArray* items in @[ itemsToOpen, itemsToOpenInTextMate ])
|
||
{
|
||
for(FileItem* item in items)
|
||
[OakZoomingIcon zoomIcon:item.image fromRect:[self imageRectOfItem:item]];
|
||
}
|
||
}
|
||
|
||
if(itemsToShowInFinder.count > 0)
|
||
[NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:[itemsToShowInFinder valueForKeyPath:@"URL"]];
|
||
|
||
for(FileItem* item in itemsToOpen)
|
||
[NSWorkspace.sharedWorkspace openFile:item.resolvedURL.path];
|
||
|
||
if(itemsToOpenInTextMate.count > 0)
|
||
[self.delegate fileBrowser:self openURLs:[itemsToOpenInTextMate valueForKeyPath:@"URL"]];
|
||
}
|
||
|
||
- (void)didSingleClickOutlineView:(id)sender
|
||
{
|
||
if(NSEvent.modifierFlags & (NSEventModifierFlagControl|NSEventModifierFlagShift|NSEventModifierFlagCommand))
|
||
return;
|
||
|
||
if([NSUserDefaults.standardUserDefaults boolForKey:kUserDefaultsFileBrowserSingleClickToOpenKey])
|
||
{
|
||
FileItem* item = [self.outlineView itemAtRow:self.outlineView.clickedRow];
|
||
if(item && !item.isDirectory && !item.isLinkToDirectory && !item.isPackage && !item.isLinkToPackage && !item.isApplication)
|
||
[self openItems:@[ item ] animate:NO];
|
||
}
|
||
}
|
||
|
||
- (void)didDoubleClickOutlineView:(id)sender
|
||
{
|
||
[self openItems:self.selectedItems animate:YES];
|
||
}
|
||
|
||
// ===============
|
||
// = Action Menu =
|
||
// ===============
|
||
|
||
- (void)updateMenu:(NSMenu*)menu
|
||
{
|
||
NSInteger kRequiresSelectionTag = 1;
|
||
|
||
NSMenuItem* openWithMenuItem;
|
||
NSMenuItem* insertBundleItemsMenuItem;
|
||
NSMenuItem* finderTagsMenuItem;
|
||
|
||
MBMenu const items = {
|
||
{ @"Open", @selector(openSelectedItems:) },
|
||
{ @"Open With", @selector(openWithMenuAction:), .ref = &openWithMenuItem },
|
||
{ /* -------- */ },
|
||
{ @"Show Original", @selector(showOriginal:) },
|
||
{ @"Show Enclosing Folder", @selector(showEnclosingFolder:) },
|
||
{ @"Show Package Contents", @selector(showPackageContents:) },
|
||
{ @"Show in Finder", @selector(showSelectedEntriesInFinder:) },
|
||
{ /* -------- */ },
|
||
{ @"New File", @selector(newDocumentInDirectory:), @"n", NSEventModifierFlagCommand|NSEventModifierFlagControl },
|
||
{ @"New Folder", @selector(newFolder:), @"n", NSEventModifierFlagCommand|NSEventModifierFlagShift },
|
||
{ /* -------- */ },
|
||
{ @"Rename", @selector(editSelectedEntries:) },
|
||
{ @"Duplicate", @selector(duplicateSelectedEntries:) },
|
||
{ @"Quick Look", @selector(toggleQuickLookPreview:), .target = self },
|
||
{ @"Add to Favorites", @selector(addSelectedEntriesToFavorites:) },
|
||
{ @"Remove From Favorites", @selector(removeSelectedEntriesFromFavorites:) },
|
||
{ /* -------- */ },
|
||
{ @"Move to Trash", @selector(deleteURLs:) },
|
||
{ /* -------- */ .ref = &insertBundleItemsMenuItem },
|
||
{ /* -------- */ },
|
||
{ @"Copy", @selector(copyURLs:) },
|
||
{ @"Copy as Pathname", @selector(copyAsPathname:), @"", NSEventModifierFlagOption, .alternate = YES, .tag = kRequiresSelectionTag },
|
||
{ @"Paste", @selector(pasteURLs:) },
|
||
{ @"Move Items Here", @selector(moveURLs:), @"v", NSEventModifierFlagCommand|NSEventModifierFlagOption },
|
||
{ /* -------- */ },
|
||
{ @"Finder Tag", .ref = &finderTagsMenuItem, .tag = kRequiresSelectionTag },
|
||
{ /* -------- */ },
|
||
{ @"Undo", @selector(undo:), .target = self },
|
||
{ @"Redo", @selector(redo:), .target = self },
|
||
{ /* -------- */ },
|
||
};
|
||
|
||
MBCreateMenu(items, menu);
|
||
|
||
if(self.previewableItems.count == 0)
|
||
{
|
||
NSInteger i = [menu indexOfItemWithTag:kRequiresSelectionTag];
|
||
while(i != -1)
|
||
{
|
||
[menu removeItemAtIndex:i];
|
||
i = [menu indexOfItemWithTag:kRequiresSelectionTag];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
NSArray<OakFinderTag*>* allTags = [self.selectedItems valueForKeyPath:@"@unionOfArrays.finderTags"];
|
||
NSCountedSet* finderTagsCountedSet = [[NSCountedSet alloc] initWithArray:allTags];
|
||
|
||
NSMutableArray<OakFinderTag*>* removeFinderTags = [NSMutableArray array];
|
||
for(OakFinderTag* tag in finderTagsCountedSet)
|
||
{
|
||
if([finderTagsCountedSet countForObject:tag] == self.selectedItems.count)
|
||
[removeFinderTags addObject:tag];
|
||
}
|
||
|
||
OFBFinderTagsChooser* chooser = [OFBFinderTagsChooser finderTagsChooserWithSelectedTags:finderTagsCountedSet.objectEnumerator.allObjects andSelectedTagsToRemove:[removeFinderTags copy] forMenu:menu];
|
||
chooser.action = @selector(didChangeFinderTag:);
|
||
chooser.target = self;
|
||
|
||
finderTagsMenuItem.view = chooser;
|
||
|
||
// ================
|
||
// = Bundle Items =
|
||
// ================
|
||
|
||
std::multimap<std::string, bundles::item_ptr, text::less_t> sorted;
|
||
for(auto const& item : bundles::query(bundles::kFieldSemanticClass, "callback.file-browser.action-menu"))
|
||
sorted.emplace(item->name(), item);
|
||
|
||
NSInteger i = [menu indexOfItem:insertBundleItemsMenuItem];
|
||
for(auto pair : sorted)
|
||
{
|
||
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:to_ns(pair.first) action:@selector(executeBundleCommand:) keyEquivalent:@""];
|
||
item.representedObject = to_ns(pair.second->uuid());
|
||
[menu insertItem:item atIndex:++i];
|
||
}
|
||
}
|
||
|
||
for(NSMenuItem* menuItem in menu.itemArray)
|
||
{
|
||
if(!menuItem.target && menuItem.action && [self respondsToSelector:menuItem.action])
|
||
menuItem.target = self;
|
||
}
|
||
|
||
[OakOpenWithMenu addOpenWithMenuForPaths:[NSSet setWithArray:[self.previewableItems valueForKeyPath:@"resolvedURL.path"]] toMenuItem:openWithMenuItem];
|
||
}
|
||
|
||
- (void)menuNeedsUpdate:(NSMenu*)menu
|
||
{
|
||
[menu removeAllItems];
|
||
[self updateMenu:menu];
|
||
}
|
||
|
||
// ==================
|
||
// = Action Methods =
|
||
// ==================
|
||
|
||
- (void)openSelectedItems:(id)sender
|
||
{
|
||
[self openItems:self.selectedItems animate:YES];
|
||
}
|
||
|
||
- (void)openWithMenuAction:(id)sender
|
||
{
|
||
// This action only exist to have validateMenuItem: called for the Open With menu
|
||
}
|
||
|
||
- (void)showOriginal:(id)sender
|
||
{
|
||
NSURL* resolvedURL = self.selectedItems.firstObject.resolvedURL;
|
||
NSURL* parentURL;
|
||
if([resolvedURL getResourceValue:&parentURL forKey:NSURLParentDirectoryURLKey error:nil])
|
||
{
|
||
[self goToURL:parentURL];
|
||
[self expandURLs:nil selectURLs:@[ resolvedURL ]];
|
||
}
|
||
}
|
||
|
||
- (void)showEnclosingFolder:(id)sender
|
||
{
|
||
NSURL* url = self.selectedItems.firstObject.URL;
|
||
if(NSURL* enclosingFolder = url.URLByDeletingLastPathComponent)
|
||
{
|
||
[self goToURL:enclosingFolder];
|
||
[self expandURLs:nil selectURLs:@[ url ]];
|
||
}
|
||
}
|
||
|
||
- (void)showPackageContents:(id)sender
|
||
{
|
||
[self goToURL:self.previewableItems.firstObject.resolvedURL];
|
||
}
|
||
|
||
- (void)showSelectedEntriesInFinder:(id)sender
|
||
{
|
||
[NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:[self.previewableItems valueForKeyPath:@"resolvedURL"]];
|
||
}
|
||
|
||
- (NSURL*)newFile:(id)sender
|
||
{
|
||
NSURL* directoryURL = self.directoryURLForNewItems;
|
||
if(!directoryURL)
|
||
return nil;
|
||
|
||
NSString* pathExtension = @"txt";
|
||
|
||
std::string fileType = settings_for_path(NULL_STR, "attr.untitled", directoryURL.fileSystemRepresentation).get(kSettingsFileTypeKey, "text.plain");
|
||
for(auto item : bundles::query(bundles::kFieldGrammarScope, fileType))
|
||
{
|
||
if(NSString* ext = to_ns(item->value_for_field(bundles::kFieldGrammarExtension)))
|
||
pathExtension = ext;
|
||
}
|
||
|
||
NSURL* newFileURL = [[directoryURL URLByAppendingPathComponent:@"untitled" isDirectory:NO] URLByAppendingPathExtension:pathExtension];
|
||
NSArray<NSURL*>* urls = [self performOperation:FBOperationNewFile sourceURLs:nil destinationURLs:@[ newFileURL ] unique:YES select:YES];
|
||
if(urls.count == 1 && self.outlineView.numberOfSelectedRows == 1)
|
||
{
|
||
FileItem* newItem = [self.outlineView itemAtRow:self.outlineView.selectedRow];
|
||
if([newItem.URL isEqual:urls.firstObject])
|
||
{
|
||
[self.outlineView scrollRowToVisible:self.outlineView.selectedRow];
|
||
[self.outlineView editColumn:0 row:self.outlineView.selectedRow withEvent:nil select:YES];
|
||
}
|
||
}
|
||
return urls.firstObject;
|
||
}
|
||
|
||
- (NSURL*)newFolder:(id)sender
|
||
{
|
||
NSURL* directoryURL = self.directoryURLForNewItems;
|
||
if(!directoryURL)
|
||
return nil;
|
||
|
||
NSURL* newFolderURL = [directoryURL URLByAppendingPathComponent:@"untitled folder" isDirectory:YES];
|
||
NSArray<NSURL*>* urls = [self performOperation:FBOperationNewFolder sourceURLs:nil destinationURLs:@[ newFolderURL ] unique:YES select:YES];
|
||
if(urls.count == 1 && self.outlineView.numberOfSelectedRows == 1)
|
||
{
|
||
FileItem* newItem = [self.outlineView itemAtRow:self.outlineView.selectedRow];
|
||
if([newItem.URL isEqual:urls.firstObject])
|
||
{
|
||
[self.outlineView scrollRowToVisible:self.outlineView.selectedRow];
|
||
[self.outlineView editColumn:0 row:self.outlineView.selectedRow withEvent:nil select:YES];
|
||
}
|
||
}
|
||
return urls.firstObject;
|
||
}
|
||
|
||
- (void)editSelectedEntries:(id)sender
|
||
{
|
||
NSArray<FileItem*>* items = self.previewableItems;
|
||
if(items.count == 1 && items.firstObject.canRename)
|
||
{
|
||
NSInteger row = [self.outlineView rowForItem:items.firstObject];
|
||
if(row != -1)
|
||
{
|
||
[NSApp activateIgnoringOtherApps:YES];
|
||
[self.outlineView.window makeKeyWindow];
|
||
[self.outlineView editColumn:0 row:row withEvent:nil select:YES];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)addSelectedEntriesToFavorites:(id)sender
|
||
{
|
||
NSURL* url = kURLLocationFavorites;
|
||
NSError* error;
|
||
if([NSFileManager.defaultManager createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:&error])
|
||
{
|
||
for(FileItem* item in self.previewableItems)
|
||
{
|
||
NSURL* linkURL = [url URLByAppendingPathComponent:item.localizedName];
|
||
if(![NSFileManager.defaultManager createSymbolicLinkAtURL:linkURL withDestinationURL:item.resolvedURL error:&error])
|
||
[self.view.window presentError:error];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
[self.view.window presentError:error];
|
||
}
|
||
}
|
||
|
||
- (void)removeSelectedEntriesFromFavorites:(id)sender
|
||
{
|
||
for(FileItem* item in self.previewableItems)
|
||
{
|
||
NSError* error;
|
||
if(![NSFileManager.defaultManager trashItemAtURL:item.URL resultingItemURL:nil error:&error])
|
||
[self.view.window presentError:error];
|
||
}
|
||
}
|
||
|
||
- (void)executeBundleCommand:(id)sender
|
||
{
|
||
if(bundles::item_ptr item = bundles::lookup(to_s([sender representedObject])))
|
||
{
|
||
// TODO For commands that have ‘input = document’ we should provide the document
|
||
OakCommand* command = [[OakCommand alloc] initWithBundleCommand:parse_command(item)];
|
||
command.firstResponder = self;
|
||
[command executeWithInput:nil variables:item->bundle_variables() outputHandler:nil];
|
||
}
|
||
}
|
||
|
||
- (BOOL)writeItems:(NSArray<FileItem*>*)items toPasteboard:(NSPasteboard*)pboard
|
||
{
|
||
if(!items.count)
|
||
return NO;
|
||
|
||
[pboard clearContents];
|
||
[pboard writeObjects:[items valueForKeyPath:@"URL"]];
|
||
|
||
// If we use writeObjects: then Terminal.app will paste both URLs and their fallback strings
|
||
if(![pboard availableTypeFromArray:@[ NSPasteboardTypeString ]])
|
||
[pboard setString:[[items valueForKeyPath:@"localizedName"] componentsJoinedByString:@"\n"] forType:NSPasteboardTypeString];
|
||
|
||
return YES;
|
||
}
|
||
|
||
- (void)cutURLs:(id)sender
|
||
{
|
||
NSPasteboard* pboard = NSPasteboard.generalPasteboard;
|
||
if([self writeItems:self.previewableItems toPasteboard:pboard])
|
||
[pboard setString:@"cut" forType:@"OakFileBrowserOperation"];
|
||
}
|
||
|
||
- (void)copyURLs:(id)sender
|
||
{
|
||
[self writeItems:self.previewableItems toPasteboard:NSPasteboard.generalPasteboard];
|
||
}
|
||
|
||
- (void)copyAsPathname:(id)sender
|
||
{
|
||
NSMutableArray* pathnames = [NSMutableArray array];
|
||
for(FileItem* item in self.previewableItems)
|
||
{
|
||
if(NSString* path = item.URL.path)
|
||
[pathnames addObject:path];
|
||
}
|
||
|
||
[NSPasteboard.generalPasteboard clearContents];
|
||
[NSPasteboard.generalPasteboard writeObjects:pathnames];
|
||
}
|
||
|
||
- (void)pasteURLs:(id)sender
|
||
{
|
||
BOOL hasOperation = [[NSPasteboard.generalPasteboard availableTypeFromArray:@[ @"OakFileBrowserOperation" ]] isEqualToString:@"OakFileBrowserOperation"];
|
||
BOOL cut = hasOperation && [[NSPasteboard.generalPasteboard stringForType:@"OakFileBrowserOperation"] isEqualToString:@"cut"];
|
||
[self insertItemsAndRemoveOriginal:cut];
|
||
}
|
||
|
||
- (void)moveURLs:(id)sender
|
||
{
|
||
[self insertItemsAndRemoveOriginal:YES];
|
||
}
|
||
|
||
- (void)duplicateSelectedEntries:(id)sender
|
||
{
|
||
NSArray<FileItem*>* items = self.previewableItems;
|
||
|
||
NSMutableDictionary<NSURL*, NSURL*>* urls = [NSMutableDictionary dictionary];
|
||
if(items.count == 1)
|
||
{
|
||
if(NSURL* url = items.firstObject.URL)
|
||
{
|
||
NSString* base = url.lastPathComponent;
|
||
NSString* newBase;
|
||
|
||
NSRegularExpression* dateRegex = [NSRegularExpression regularExpressionWithPattern:@"(\\b|_)[1-2][0-9]{3}(-|_|)(?!00|1[3-9])[0-1][0-9]\\2(?!00|3[2-9])[0-3][0-9](\\b|_)" options:0 error:nil];
|
||
NSRegularExpression* numberRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\d{2,}" options:0 error:nil];
|
||
|
||
if(NSTextCheckingResult* match = [dateRegex firstMatchInString:base options:0 range:NSMakeRange(0, base.length)])
|
||
{
|
||
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
|
||
dateFormatter.dateFormat = [NSString stringWithFormat:@"yyyy%1$@MM%1$@dd", [base substringWithRange:[match rangeAtIndex:2]]];
|
||
newBase = [base stringByReplacingCharactersInRange:match.range withString:[dateRegex replacementStringForResult:match inString:base offset:0 template:[NSString stringWithFormat:@"$1%@$3", [dateFormatter stringFromDate:NSDate.date]]]];
|
||
}
|
||
else if(NSTextCheckingResult* match = [numberRegex firstMatchInString:base options:0 range:NSMakeRange(0, base.length)])
|
||
{
|
||
std::set<NSInteger> set;
|
||
for(NSURL* otherURL in [NSFileManager.defaultManager contentsOfDirectoryAtURL:url.URLByDeletingLastPathComponent includingPropertiesForKeys:nil options:0 error:nil])
|
||
{
|
||
NSString* otherBase = otherURL.lastPathComponent;
|
||
if(NSTextCheckingResult* tmp = [numberRegex firstMatchInString:otherBase options:0 range:NSMakeRange(0, otherBase.length)])
|
||
set.insert([otherBase substringWithRange:tmp.range].integerValue);
|
||
}
|
||
|
||
NSInteger i = [base substringWithRange:match.range].integerValue + 1;
|
||
while(set.find(i) != set.end())
|
||
++i;
|
||
|
||
NSString* number = [NSString stringWithFormat:@"%0*ld", (int)match.range.length, i];
|
||
newBase = [base stringByReplacingCharactersInRange:match.range withString:number];
|
||
}
|
||
|
||
if(newBase && ![newBase isEqualToString:base])
|
||
urls[url] = [url.URLByDeletingLastPathComponent URLByAppendingPathComponent:newBase isDirectory:url.hasDirectoryPath];
|
||
}
|
||
}
|
||
|
||
if(urls.count == 0)
|
||
{
|
||
NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:@"^(.*?)(?: copy(?: \\d+)?)?(\\.\\w+)?$" options:0 error:nil];
|
||
for(FileItem* item in items)
|
||
{
|
||
NSString* base = item.URL.lastPathComponent;
|
||
NSString* name = [regex stringByReplacingMatchesInString:base options:0 range:NSMakeRange(0, base.length) withTemplate:@"$1 copy$2"];
|
||
urls[item.URL] = [item.URL.URLByDeletingLastPathComponent URLByAppendingPathComponent:name isDirectory:item.isDirectory];
|
||
}
|
||
}
|
||
|
||
[self performOperation:FBOperationDuplicate withURLs:urls unique:YES select:YES];
|
||
if(urls.count == 1 && self.outlineView.numberOfSelectedRows == 1)
|
||
[self.outlineView editColumn:0 row:self.outlineView.selectedRow withEvent:nil select:YES];
|
||
}
|
||
|
||
- (void)deleteURLs:(id)sender
|
||
{
|
||
NSOutlineView* outlineView = self.outlineView;
|
||
|
||
NSIndexSet* selectedRowIndexes = outlineView.selectedRowIndexes;
|
||
NSInteger clickedRow = outlineView.clickedRow;
|
||
|
||
// User right-clicked a single item that is not part of the selection, only delete that item
|
||
if(clickedRow != -1 && ![selectedRowIndexes containsIndex:clickedRow])
|
||
{
|
||
FileItem* item = [outlineView itemAtRow:clickedRow];
|
||
if(NSURL* url = item.URL.filePathURL)
|
||
[self performOperation:FBOperationTrash sourceURLs:@[ url ] destinationURLs:nil unique:NO select:NO];
|
||
}
|
||
else
|
||
{
|
||
NSMutableArray<NSURL*>* urlsToTrash = [NSMutableArray array];
|
||
FileItem* selectItem;
|
||
FileItem* previousItem;
|
||
|
||
NSMutableArray<FileItem*>* stack = [self.fileItem.arrangedChildren mutableCopy];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
|
||
NSURL* url = item.URL.filePathURL;
|
||
if(url && [selectedRowIndexes containsIndex:[outlineView rowForItem:item]])
|
||
{
|
||
selectItem = previousItem;
|
||
[urlsToTrash addObject:url];
|
||
}
|
||
else
|
||
{
|
||
previousItem = item;
|
||
if([outlineView isItemExpanded:item])
|
||
stack = [[item.arrangedChildren arrayByAddingObjectsFromArray:stack] mutableCopy];
|
||
}
|
||
}
|
||
|
||
[self performOperation:FBOperationTrash sourceURLs:urlsToTrash destinationURLs:nil unique:NO select:NO];
|
||
|
||
NSInteger selectRow = [outlineView rowForItem:selectItem ?: self.fileItem.arrangedChildren.firstObject];
|
||
if(selectRow != -1)
|
||
{
|
||
[outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectRow] byExtendingSelection:NO];
|
||
[outlineView scrollRowToVisible:selectRow];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)cut:(id)sender { [self cutURLs:sender]; }
|
||
- (void)copy:(id)sender { [self copyURLs:sender]; }
|
||
- (void)paste:(id)sender { [self pasteURLs:sender]; }
|
||
- (void)pasteNext:(id)sender { [self moveURLs:sender]; }
|
||
- (void)delete:(id)sender { [self deleteURLs:sender]; }
|
||
|
||
- (void)insertItemsAndRemoveOriginal:(BOOL)removeOriginal
|
||
{
|
||
if(NSURL* directoryURL = self.directoryURLForNewItems)
|
||
{
|
||
NSMutableDictionary* urls = [NSMutableDictionary dictionary];
|
||
for(NSURL* srcURL in [self URLsFromPasteboard:NSPasteboard.generalPasteboard])
|
||
{
|
||
BOOL srcIsDirectory;
|
||
if([NSFileManager.defaultManager fileExistsAtPath:srcURL.path isDirectory:&srcIsDirectory])
|
||
{
|
||
NSURL* destURL = [directoryURL URLByAppendingPathComponent:srcURL.lastPathComponent isDirectory:srcIsDirectory];
|
||
if(![srcURL isEqual:destURL] || !removeOriginal)
|
||
urls[srcURL] = destURL;
|
||
}
|
||
}
|
||
|
||
if(removeOriginal)
|
||
[self performOperation:FBOperationMove withURLs:urls unique:YES select:YES];
|
||
else [self performOperation:FBOperationCopy withURLs:urls unique:YES select:YES];
|
||
}
|
||
}
|
||
|
||
- (NSArray<NSURL*>*)URLsFromPasteboard:(NSPasteboard*)pboard
|
||
{
|
||
return [pboard readObjectsForClasses:@[ [NSURL class] ] options:nil];
|
||
}
|
||
|
||
- (void)didChangeFinderTag:(OFBFinderTagsChooser*)finderTagsChooser
|
||
{
|
||
OakFinderTag* chosenTag = finderTagsChooser.chosenTag;
|
||
for(FileItem* item in self.previewableItems)
|
||
{
|
||
NSMutableArray<OakFinderTag*>* tags = [item.finderTags mutableCopy];
|
||
if(finderTagsChooser.removeChosenTag)
|
||
[tags removeObject:chosenTag];
|
||
else if(![tags containsObject:chosenTag])
|
||
[tags addObject:chosenTag];
|
||
|
||
[item.URL setResourceValue:[tags valueForKeyPath:@"displayName"] forKey:NSURLTagNamesKey error:nil];
|
||
item.finderTags = [OakFinderTagManager finderTagsForURL:item.URL];
|
||
}
|
||
}
|
||
|
||
- (BOOL)favoritesDirectoryContainsItems:(NSArray<FileItem*>*)items
|
||
{
|
||
for(FileItem* item in items)
|
||
{
|
||
if([kURLLocationFavorites isEqual:item.parentURL])
|
||
return YES;
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
- (BOOL)canPaste
|
||
{
|
||
return self.directoryURLForNewItems && [self URLsFromPasteboard:NSPasteboard.generalPasteboard].count;
|
||
}
|
||
|
||
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
|
||
{
|
||
NSArray<FileItem*>* selectedItems = self.selectedItems;
|
||
NSArray<FileItem*>* previewableItems = self.previewableItems;
|
||
|
||
BOOL res = YES;
|
||
|
||
if(menuItem.action == @selector(undo:))
|
||
{
|
||
menuItem.title = self.activeUndoManager.undoMenuItemTitle;
|
||
res = self.activeUndoManager.canUndo;
|
||
}
|
||
else if(menuItem.action == @selector(redo:))
|
||
{
|
||
menuItem.title = self.activeUndoManager.redoMenuItemTitle;
|
||
res = self.activeUndoManager.canRedo;
|
||
}
|
||
else if(menuItem.action == @selector(toggleQuickLookPreview:))
|
||
{
|
||
if([QLPreviewPanel sharedPreviewPanelExists] && [QLPreviewPanel sharedPreviewPanel].isVisible)
|
||
menuItem.title = @"Close Quick Look";
|
||
else if(self.previewableItems.count == 0)
|
||
menuItem.hidden = YES;
|
||
else if(self.previewableItems.count == 1)
|
||
menuItem.title = [NSString stringWithFormat:@"Quick Look “%@”", self.previewableItems.firstObject.localizedName];
|
||
else
|
||
menuItem.title = [NSString stringWithFormat:@"Quick Look %ld Items", self.previewableItems.count];
|
||
}
|
||
else if(menuItem.action == @selector(toggleShowInvisibles:))
|
||
menuItem.dynamicTitle = self.showExcludedItems ? @"Hide Invisible Files" : @"Show Invisible Files";
|
||
else if(menuItem.action == @selector(goBack:))
|
||
res = self.canGoBack;
|
||
else if(menuItem.action == @selector(goForward:))
|
||
res = self.canGoForward;
|
||
else if(menuItem.action == @selector(newFolder:))
|
||
res = self.directoryURLForNewItems ? YES : NO;
|
||
else if(menuItem.action == @selector(openSelectedItems:))
|
||
menuItem.hidden = previewableItems.count == 0;
|
||
else if(menuItem.action == @selector(openWithMenuAction:))
|
||
menuItem.hidden = previewableItems.count == 0 || selectedItems.count == 1 && selectedItems.firstObject.isApplication;
|
||
else if(menuItem.action == @selector(showSelectedEntriesInFinder:))
|
||
menuItem.hidden = previewableItems.count == 0;
|
||
else if(menuItem.action == @selector(showOriginal:))
|
||
menuItem.hidden = selectedItems.count != 1 || [selectedItems.firstObject.URL isEqual:selectedItems.firstObject.resolvedURL];
|
||
else if(menuItem.action == @selector(showEnclosingFolder:))
|
||
menuItem.hidden = selectedItems.count != 1 || [selectedItems.firstObject.parentURL isEqual:((FileItem*)[self.outlineView parentForItem:selectedItems.firstObject]).URL ?: selectedItems.firstObject.parentURL];
|
||
else if(menuItem.action == @selector(showPackageContents:))
|
||
menuItem.hidden = previewableItems.count != 1 || previewableItems.firstObject.isPackage == NO;
|
||
else if(menuItem.action == @selector(editSelectedEntries:))
|
||
menuItem.hidden = previewableItems.count != 1 || previewableItems.firstObject.canRename == NO;
|
||
else if(menuItem.action == @selector(addSelectedEntriesToFavorites:))
|
||
menuItem.hidden = previewableItems.count == 0 || [self favoritesDirectoryContainsItems:previewableItems];
|
||
else if(menuItem.action == @selector(removeSelectedEntriesFromFavorites:))
|
||
menuItem.hidden = previewableItems.count == 0 || ![self favoritesDirectoryContainsItems:previewableItems];
|
||
else if(menuItem.action == @selector(cutURLs:))
|
||
menuItem.hidden = previewableItems.count == 0;
|
||
else if(menuItem.action == @selector(copyURLs:))
|
||
menuItem.hidden = previewableItems.count == 0;
|
||
else if(menuItem.action == @selector(copyAsPathname:))
|
||
menuItem.hidden = previewableItems.count == 0;
|
||
else if(menuItem.action == @selector(pasteURLs:))
|
||
menuItem.hidden = self.canPaste == NO;
|
||
else if(menuItem.action == @selector(moveURLs:))
|
||
menuItem.hidden = self.canPaste == NO;
|
||
else if(menuItem.action == @selector(duplicateSelectedEntries:))
|
||
menuItem.hidden = previewableItems.count == 0;
|
||
else if(menuItem.action == @selector(deleteURLs:))
|
||
menuItem.hidden = previewableItems.count == 0;
|
||
else if(menuItem.action == @selector(cut:))
|
||
res = previewableItems.count != 0;
|
||
else if(menuItem.action == @selector(copy:))
|
||
res = previewableItems.count != 0;
|
||
else if(menuItem.action == @selector(paste:))
|
||
res = self.canPaste;
|
||
else if(menuItem.action == @selector(pasteNext:))
|
||
res = self.canPaste;
|
||
else if(menuItem.action == @selector(delete:))
|
||
res = previewableItems.count != 0;
|
||
|
||
if(res && !menuItem.hidden)
|
||
{
|
||
NSString* copyAsPathnameTitle = previewableItems.count > 1 ? @"Copy%@ as Pathnames" : @"Copy%@ as Pathname";
|
||
|
||
struct { NSString* format; SEL action; } const menuTitles[] =
|
||
{
|
||
{ @"Cut%@", @selector(cut:) },
|
||
{ @"Cut%@", @selector(cutURLs:) },
|
||
{ @"Copy%@", @selector(copy:) },
|
||
{ @"Copy%@", @selector(copyURLs:) },
|
||
{ copyAsPathnameTitle, @selector(copyAsPathname:) },
|
||
{ @"Show%@ in Finder", @selector(showSelectedEntriesInFinder:) },
|
||
{ @"Add%@ to Favorites", @selector(addSelectedEntriesToFavorites:) },
|
||
{ @"Remove%@ From Favorites", @selector(removeSelectedEntriesFromFavorites:) },
|
||
};
|
||
|
||
for(auto const& info : menuTitles)
|
||
{
|
||
if(info.action == menuItem.action)
|
||
{
|
||
NSString* items;
|
||
switch(previewableItems.count)
|
||
{
|
||
case 0: items = [NSString stringWithFormat:@" “%@”", self.fileItem.localizedName]; break;
|
||
case 1: items = [NSString stringWithFormat:@" “%@”", previewableItems.firstObject.localizedName]; break;
|
||
default: items = [NSString stringWithFormat:@" %ld Items", previewableItems.count]; break;
|
||
}
|
||
menuItem.dynamicTitle = [NSString stringWithFormat:info.format, items];
|
||
}
|
||
}
|
||
|
||
NSString* folderNameForNewItems;
|
||
if([self.directoryURLForNewItems getResourceValue:&folderNameForNewItems forKey:NSURLLocalizedNameKey error:nil])
|
||
{
|
||
if(menuItem.action == @selector(newFolder:))
|
||
menuItem.dynamicTitle = [NSString stringWithFormat:@"New Folder in “%@”", folderNameForNewItems];
|
||
|
||
if(menuItem.action == @selector(paste:) || menuItem.action == @selector(pasteURLs:))
|
||
{
|
||
NSInteger count = [self URLsFromPasteboard:NSPasteboard.generalPasteboard].count;
|
||
if(count == 1)
|
||
menuItem.dynamicTitle = [NSString stringWithFormat:@"Paste Item in “%@”", folderNameForNewItems];
|
||
else menuItem.dynamicTitle = [NSString stringWithFormat:@"Paste %ld Items in “%@”", count, folderNameForNewItems];
|
||
}
|
||
else if(menuItem.action == @selector(pasteNext:) || menuItem.action == @selector(moveURLs:))
|
||
{
|
||
NSInteger count = [self URLsFromPasteboard:NSPasteboard.generalPasteboard].count;
|
||
if(count == 1)
|
||
menuItem.dynamicTitle = [NSString stringWithFormat:@"Move Item to “%@”", folderNameForNewItems];
|
||
else menuItem.dynamicTitle = [NSString stringWithFormat:@"Move %ld Items to “%@”", count, folderNameForNewItems];
|
||
}
|
||
}
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
// =====================
|
||
// = NSRestorableState =
|
||
// =====================
|
||
|
||
+ (NSArray<NSString*>*)restorableStateKeyPaths
|
||
{
|
||
return @[ @"showExcludedItems" ];
|
||
}
|
||
|
||
- (void)restoreStateWithCoder:(NSCoder*)state
|
||
{
|
||
[super restoreStateWithCoder:state];
|
||
|
||
NSArray* newHistory = [state decodeObjectForKey:@"history"];
|
||
if(newHistory.count)
|
||
{
|
||
self.history = [newHistory mutableCopy];
|
||
self.historyIndex = std::clamp<NSInteger>([state decodeIntegerForKey:@"historyIndex"], 0, newHistory.count);
|
||
|
||
NSArray<NSURL*>* expandedURLs = [state decodeObjectForKey:@"expandedURLs"];
|
||
NSArray<NSURL*>* selectedURLs = [state decodeObjectForKey:@"selectedURLs"];
|
||
[self expandURLs:expandedURLs selectURLs:selectedURLs];
|
||
}
|
||
}
|
||
|
||
- (void)encodeRestorableStateWithCoder:(NSCoder*)state
|
||
{
|
||
[super encodeRestorableStateWithCoder:state];
|
||
|
||
NSMutableArray* history = [NSMutableArray array];
|
||
NSUInteger from = _history.count > 5 ? _history.count - 5 : 0;
|
||
for(NSUInteger i = from; i < _history.count; ++i)
|
||
{
|
||
NSDictionary* record = _history[i];
|
||
NSNumber* scrollOffset = i == _historyIndex ? @(NSMinY(self.outlineView.visibleRect)) : record[@"scrollOffset"];
|
||
if(scrollOffset && scrollOffset.doubleValue > 0)
|
||
{
|
||
[history addObject:@{
|
||
@"url": record[@"url"],
|
||
@"scrollOffset": scrollOffset,
|
||
}];
|
||
}
|
||
else
|
||
{
|
||
[history addObject:@{ @"url": record[@"url"], }];
|
||
}
|
||
}
|
||
|
||
[state encodeObject:history forKey:@"history"];
|
||
[state encodeInteger:_historyIndex - from forKey:@"historyIndex"];
|
||
[state encodeObject:self.selectedURLs.allObjects forKey:@"selectedURLs"];
|
||
[state encodeObject:self.expandedURLs.allObjects forKey:@"expandedURLs"];
|
||
}
|
||
|
||
// ==============
|
||
// = Public API =
|
||
// ==============
|
||
|
||
- (id)sessionState
|
||
{
|
||
if(NSKeyedArchiver* coder = [[NSKeyedArchiver alloc] init])
|
||
{
|
||
[self encodeRestorableStateWithCoder:coder];
|
||
[coder finishEncoding];
|
||
return coder.encodedData;
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
- (void)setupViewWithState:(id)state
|
||
{
|
||
if([state isKindOfClass:[NSData class]])
|
||
{
|
||
if(NSCoder* coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:state])
|
||
[self restoreStateWithCoder:coder];
|
||
}
|
||
else if([state isKindOfClass:[NSDictionary class]])
|
||
{
|
||
NSDictionary* fileBrowserState = state;
|
||
|
||
self.showExcludedItems = [fileBrowserState[@"showHidden"] boolValue];
|
||
|
||
NSMutableArray* newHistory = [NSMutableArray array];
|
||
for(NSDictionary* entry in fileBrowserState[@"history"])
|
||
{
|
||
if(NSString* urlString = entry[@"url"])
|
||
{
|
||
[newHistory addObject:@{
|
||
@"url": [NSURL URLWithString:urlString],
|
||
}];
|
||
}
|
||
}
|
||
|
||
if(newHistory.count)
|
||
{
|
||
self.history = newHistory;
|
||
self.historyIndex = std::clamp([fileBrowserState[@"historyIndex"] unsignedIntegerValue], (NSUInteger)0, newHistory.count);
|
||
|
||
NSMutableArray<NSURL*>* expandedURLs = [NSMutableArray array];
|
||
for(NSString* urlString in fileBrowserState[@"expanded"])
|
||
[expandedURLs addObject:[NSURL URLWithString:urlString]];
|
||
|
||
NSMutableArray<NSURL*>* selectedURLs = [NSMutableArray array];
|
||
for(NSString* urlString in fileBrowserState[@"selection"])
|
||
[selectedURLs addObject:[NSURL URLWithString:urlString]];
|
||
|
||
[self expandURLs:expandedURLs selectURLs:selectedURLs];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (std::map<std::string, std::string>)variables
|
||
{
|
||
std::map<std::string, std::string> env;
|
||
|
||
if(self.selectedFileURLs.count)
|
||
{
|
||
std::vector<std::string> paths;
|
||
for(NSURL* url in self.selectedFileURLs)
|
||
paths.emplace_back(path::escape(url.fileSystemRepresentation));
|
||
|
||
env["TM_SELECTED_FILE"] = self.selectedFileURLs.lastObject.fileSystemRepresentation;
|
||
env["TM_SELECTED_FILES"] = text::join(paths, " ");
|
||
}
|
||
|
||
return env;
|
||
}
|
||
|
||
- (void)selectURL:(NSURL*)url withParentURL:(NSURL*)parentURL
|
||
{
|
||
url = url.fileReferenceURL;
|
||
|
||
for(NSInteger i = 0; i < self.outlineView.numberOfRows; ++i)
|
||
{
|
||
FileItem* item = [self.outlineView itemAtRow:i];
|
||
if([url isEqual:item.fileReferenceURL])
|
||
{
|
||
[self.outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:i] byExtendingSelection:NO];
|
||
[self centerSelectionInVisibleArea:self];
|
||
return;
|
||
}
|
||
}
|
||
|
||
NSURL* currentParent = self.URL;
|
||
NSMutableSet<NSURL*>* expandURLs = [self.expandedURLs mutableCopy];
|
||
|
||
NSURL* childURL = url;
|
||
while(true)
|
||
{
|
||
NSNumber* flag;
|
||
if([childURL getResourceValue:&flag forKey:NSURLIsVolumeKey error:nil] && flag.boolValue)
|
||
break;
|
||
|
||
NSURL* potentialParentURL;
|
||
if(![childURL getResourceValue:&potentialParentURL forKey:NSURLParentDirectoryURLKey error:nil] || [childURL isEqual:potentialParentURL])
|
||
break;
|
||
|
||
childURL = potentialParentURL;
|
||
if([childURL isEqual:currentParent])
|
||
{
|
||
parentURL = currentParent;
|
||
break;
|
||
}
|
||
|
||
if([childURL isEqual:parentURL])
|
||
break;
|
||
|
||
[expandURLs addObject:childURL];
|
||
}
|
||
|
||
if([childURL isEqual:parentURL])
|
||
{
|
||
[self goToURL:parentURL];
|
||
[self expandURLs:expandURLs.allObjects selectURLs:@[ url ]];
|
||
}
|
||
else
|
||
{
|
||
[self goToURL:url.URLByDeletingLastPathComponent];
|
||
[self expandURLs:nil selectURLs:@[ url ]];
|
||
}
|
||
}
|
||
|
||
- (void)deselectAll:(id)sender
|
||
{
|
||
[self.outlineView deselectAll:sender];
|
||
}
|
||
|
||
- (void)orderFrontGoToFolder:(id)sender
|
||
{
|
||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||
|
||
panel.canChooseFiles = NO;
|
||
panel.canChooseDirectories = YES;
|
||
panel.allowsMultipleSelection = NO;
|
||
panel.directoryURL = self.URL.filePathURL;
|
||
|
||
[panel beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse result) {
|
||
if(result == NSModalResponseOK)
|
||
[self goToURL:panel.URLs.lastObject];
|
||
}];
|
||
}
|
||
|
||
// =========
|
||
// = Swipe =
|
||
// =========
|
||
|
||
- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(NSEventGestureAxis)axis
|
||
{
|
||
return axis == NSEventGestureAxisHorizontal;
|
||
}
|
||
|
||
- (void)scrollWheel:(NSEvent*)anEvent
|
||
{
|
||
if(!NSEvent.isSwipeTrackingFromScrollEventsEnabled || anEvent.phase == NSEventPhaseNone || fabs(anEvent.scrollingDeltaX) <= fabs(anEvent.scrollingDeltaY))
|
||
return;
|
||
|
||
[anEvent trackSwipeEventWithOptions:0 dampenAmountThresholdMin:(self.canGoForward ? -1 : 0) max:(self.canGoBack ? +1 : 0) usingHandler:^(CGFloat gestureAmount, NSEventPhase phase, BOOL isComplete, BOOL* stop) {
|
||
if(phase == NSEventPhaseBegan)
|
||
{
|
||
// Setup animation overlay layers
|
||
}
|
||
|
||
// Update animation overlay to match gestureAmount
|
||
|
||
if(phase == NSEventPhaseEnded)
|
||
{
|
||
if(gestureAmount > 0 && self.canGoBack)
|
||
[self goBack:self];
|
||
else if(gestureAmount < 0 && self.canGoForward)
|
||
[self goForward:self];
|
||
}
|
||
|
||
if(isComplete)
|
||
{
|
||
// Tear down animation overlay here
|
||
}
|
||
}];
|
||
}
|
||
|
||
// ========================
|
||
// = From FileBrowserView =
|
||
// ========================
|
||
|
||
- (void)setCanExpandSymbolicLinks:(BOOL)flag
|
||
{
|
||
if(_canExpandSymbolicLinks == flag)
|
||
return;
|
||
_canExpandSymbolicLinks = flag;
|
||
|
||
if(!self.fileItem)
|
||
return;
|
||
|
||
NSMutableArray<FileItem*>* stack = [self.fileItem.arrangedChildren mutableCopy];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
if(item.isLinkToDirectory && (_canExpandPackages || !item.isLinkToPackage))
|
||
[self.outlineView reloadItem:item reloadChildren:YES];
|
||
if([self.outlineView isExpandable:item] && item.arrangedChildren)
|
||
[stack addObjectsFromArray:item.arrangedChildren];
|
||
}
|
||
}
|
||
|
||
- (void)setCanExpandPackages:(BOOL)flag
|
||
{
|
||
if(_canExpandPackages == flag)
|
||
return;
|
||
_canExpandPackages = flag;
|
||
|
||
if(!self.fileItem)
|
||
return;
|
||
|
||
NSMutableArray<FileItem*>* stack = [self.fileItem.arrangedChildren mutableCopy];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
if(item.isDirectory && item.isPackage)
|
||
[self.outlineView reloadItem:item reloadChildren:YES];
|
||
if([self.outlineView isExpandable:item] && item.arrangedChildren)
|
||
[stack addObjectsFromArray:item.arrangedChildren];
|
||
}
|
||
}
|
||
|
||
- (void)setSortDirectoriesBeforeFiles:(BOOL)flag
|
||
{
|
||
if(_sortDirectoriesBeforeFiles == flag)
|
||
return;
|
||
_sortDirectoriesBeforeFiles = flag;
|
||
|
||
if(!self.fileItem)
|
||
return;
|
||
|
||
NSMutableArray<FileItem*>* stack = [NSMutableArray arrayWithObject:self.fileItem];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
[self rearrangeChildrenInParent:item];
|
||
if(item == self.fileItem || [self.outlineView isItemExpanded:item])
|
||
[stack addObjectsFromArray:item.arrangedChildren];
|
||
}
|
||
}
|
||
|
||
- (void)setShowExcludedItems:(BOOL)flag
|
||
{
|
||
if(_showExcludedItems == flag)
|
||
return;
|
||
_showExcludedItems = flag;
|
||
|
||
if(!self.fileItem)
|
||
return;
|
||
|
||
NSMutableArray<FileItem*>* stack = [NSMutableArray arrayWithObject:self.fileItem];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
[self rearrangeChildrenInParent:item];
|
||
if(item == self.fileItem || [self.outlineView isItemExpanded:item])
|
||
[stack addObjectsFromArray:item.arrangedChildren];
|
||
}
|
||
}
|
||
|
||
- (NSComparator)itemComparator
|
||
{
|
||
NSArray<NSSortDescriptor*>* sortDescriptors = @[
|
||
[NSSortDescriptor sortDescriptorWithKey:@"localizedName" ascending:YES selector:@selector(localizedCompare:)],
|
||
[NSSortDescriptor sortDescriptorWithKey:@"URL.URLByDeletingLastPathComponent.lastPathComponent" ascending:YES selector:@selector(localizedCompare:)],
|
||
];
|
||
|
||
return ^NSComparisonResult(FileItem* lhs, FileItem* rhs){
|
||
if(_sortDirectoriesBeforeFiles)
|
||
{
|
||
if((lhs.isDirectory || lhs.isLinkToDirectory) && !(rhs.isDirectory || rhs.isLinkToDirectory))
|
||
return NSOrderedAscending;
|
||
else if((rhs.isDirectory || rhs.isLinkToDirectory) && !(lhs.isDirectory || lhs.isLinkToDirectory))
|
||
return NSOrderedDescending;
|
||
}
|
||
|
||
for(NSSortDescriptor* sortDescriptor in sortDescriptors)
|
||
{
|
||
NSComparisonResult order = [sortDescriptor compareObject:lhs toObject:rhs];
|
||
if(order != NSOrderedSame)
|
||
return order;
|
||
}
|
||
|
||
return NSOrderedSame;
|
||
};
|
||
}
|
||
|
||
- (NSPredicate*)itemPredicateForChildrenInParent:(FileItem*)parentOrNil
|
||
{
|
||
NSPredicate* predicate = [NSPredicate predicateWithValue:YES];
|
||
if(!_showExcludedItems)
|
||
{
|
||
NSURL* directoryURL = (parentOrNil ?: self.fileItem).URL;
|
||
settings_t const settings = settings_for_path(NULL_STR, "", directoryURL.fileSystemRepresentation);
|
||
bool excludeMissingFiles = [directoryURL.scheme isEqual:@"scm"] ? false : settings.get(kSettingsExcludeSCMDeletedKey, false);
|
||
|
||
path::glob_list_t globs;
|
||
globs.add_exclude_glob(settings.get(kSettingsExcludeDirectoriesInBrowserKey), path::kPathItemDirectory);
|
||
globs.add_exclude_glob(settings.get(kSettingsExcludeDirectoriesKey), path::kPathItemDirectory);
|
||
globs.add_exclude_glob(settings.get(kSettingsExcludeFilesInBrowserKey), path::kPathItemFile);
|
||
globs.add_exclude_glob(settings.get(kSettingsExcludeFilesKey), path::kPathItemFile);
|
||
globs.add_exclude_glob(settings.get(kSettingsExcludeInBrowserKey), path::kPathItemAny);
|
||
globs.add_exclude_glob(settings.get(kSettingsExcludeKey), path::kPathItemAny);
|
||
|
||
globs.add_include_glob(settings.get(kSettingsIncludeDirectoriesInBrowserKey), path::kPathItemDirectory);
|
||
globs.add_include_glob(settings.get(kSettingsIncludeDirectoriesKey), path::kPathItemDirectory);
|
||
globs.add_include_glob(settings.get(kSettingsIncludeFilesInBrowserKey), path::kPathItemFile);
|
||
globs.add_include_glob(settings.get(kSettingsIncludeFilesKey), path::kPathItemFile);
|
||
globs.add_include_glob(settings.get(kSettingsIncludeInBrowserKey), path::kPathItemAny);
|
||
globs.add_include_glob(settings.get(kSettingsIncludeKey, "*"), path::kPathItemAny);
|
||
|
||
predicate = [NSPredicate predicateWithBlock:^BOOL(FileItem* item, NSDictionary* bindings){
|
||
if(item.hidden && ![item.URL.lastPathComponent hasPrefix:@"."])
|
||
return NO;
|
||
|
||
if(excludeMissingFiles && item.isMissing)
|
||
return NO;
|
||
|
||
char const* path = item.URL.fileSystemRepresentation;
|
||
size_t itemType = item.isDirectory ? path::kPathItemDirectory : path::kPathItemFile;
|
||
return item.hidden ? globs.include(path, itemType) : !globs.exclude(path, itemType);
|
||
}];
|
||
}
|
||
return predicate;
|
||
}
|
||
|
||
- (NSArray<FileItem*>*)arrangeChildren:(NSArray<FileItem*>*)children inParent:(FileItem*)parentOrNil
|
||
{
|
||
return [[children filteredArrayUsingPredicate:[self itemPredicateForChildrenInParent:parentOrNil]] sortedArrayUsingComparator:self.itemComparator];
|
||
}
|
||
|
||
- (void)rearrangeChildrenInParent:(FileItem*)item
|
||
{
|
||
NSMutableArray<FileItem*>* existingChildren = item.arrangedChildren;
|
||
if(existingChildren && existingChildren.count * item.children.count < 250000)
|
||
{
|
||
NSArray* newArrangedChildren = [self arrangeChildren:item.children inParent:item];
|
||
|
||
// ================
|
||
// = Remove Items =
|
||
// ================
|
||
|
||
NSMutableIndexSet* indexesToRemove = [NSMutableIndexSet indexSet];
|
||
for(NSUInteger i = 0; i < existingChildren.count; ++i)
|
||
{
|
||
if(![newArrangedChildren containsObject:existingChildren[i]])
|
||
[indexesToRemove addIndex:i];
|
||
}
|
||
|
||
if(indexesToRemove.count)
|
||
{
|
||
BOOL wasFirstResponderInOutlineView = [self.outlineView.window.firstResponder isKindOfClass:[NSView class]] && [(NSView*)self.outlineView.window.firstResponder isDescendantOf:self.outlineView];
|
||
|
||
[existingChildren removeObjectsAtIndexes:indexesToRemove];
|
||
[self.outlineView removeItemsAtIndexes:indexesToRemove inParent:(item != self.fileItem ? item : nil) withAnimation:NSTableViewAnimationEffectFade|NSTableViewAnimationSlideUp];
|
||
|
||
if(wasFirstResponderInOutlineView && !([self.outlineView.window.firstResponder isKindOfClass:[NSView class]] && [(NSView*)self.outlineView.window.firstResponder isDescendantOf:self.outlineView]))
|
||
[self.outlineView.window makeFirstResponder:self.outlineView];
|
||
}
|
||
|
||
// =======================
|
||
// = Move Items (rename) =
|
||
// =======================
|
||
|
||
NSComparator compare = self.itemComparator;
|
||
|
||
BOOL alreadySorted = YES;
|
||
for(NSUInteger i = 1; alreadySorted && i < existingChildren.count; ++i)
|
||
alreadySorted = compare(existingChildren[i-1], existingChildren[i]) != NSOrderedDescending;
|
||
|
||
if(!alreadySorted)
|
||
{
|
||
NSMutableIndexSet* lcs = MutableLongestCommonSubsequence(existingChildren, newArrangedChildren);
|
||
|
||
std::vector<std::pair<BOOL, FileItem*>> v;
|
||
for(NSUInteger i = 0; i < existingChildren.count; ++i)
|
||
v.emplace_back([lcs containsIndex:i], existingChildren[i]);
|
||
|
||
for(NSUInteger i = 0; i < v.size(); )
|
||
{
|
||
if(v[i].first == YES)
|
||
{
|
||
i++;
|
||
}
|
||
else
|
||
{
|
||
FileItem* child = existingChildren[i];
|
||
|
||
v.erase(v.begin() + i);
|
||
NSInteger newIndex = 0;
|
||
for(; newIndex < v.size(); ++newIndex)
|
||
{
|
||
if(v[newIndex].first && compare(child, v[newIndex].second) == NSOrderedAscending)
|
||
break;
|
||
}
|
||
v.emplace(v.begin() + newIndex, YES, child);
|
||
|
||
[existingChildren removeObjectAtIndex:i];
|
||
[existingChildren insertObject:child atIndex:newIndex];
|
||
[self.outlineView moveItemAtIndex:i inParent:(item != self.fileItem ? item : nil) toIndex:newIndex inParent:(item != self.fileItem ? item : nil)];
|
||
}
|
||
}
|
||
}
|
||
|
||
// ================
|
||
// = Insert Items =
|
||
// ================
|
||
|
||
NSMutableIndexSet* insertionIndexes = [NSMutableIndexSet indexSet];
|
||
for(NSUInteger i = 0; i < newArrangedChildren.count; ++i)
|
||
{
|
||
FileItem* child = newArrangedChildren[i];
|
||
if(![existingChildren containsObject:child])
|
||
[insertionIndexes addIndex:i];
|
||
}
|
||
|
||
if(insertionIndexes.count)
|
||
{
|
||
[existingChildren insertObjects:[newArrangedChildren objectsAtIndexes:insertionIndexes] atIndexes:insertionIndexes];
|
||
[self.outlineView insertItemsAtIndexes:insertionIndexes inParent:(item != self.fileItem ? item : nil) withAnimation:NSTableViewAnimationEffectFade|NSTableViewAnimationSlideUp];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
item.arrangedChildren = [[self arrangeChildren:item.children inParent:item] mutableCopy];
|
||
[self.outlineView reloadItem:(item != self.fileItem ? item : nil) reloadChildren:YES];
|
||
|
||
if(item == self.fileItem)
|
||
[self.outlineView setNeedsDisplay:YES];
|
||
}
|
||
|
||
[self updateDisambiguationSuffixInParent:item];
|
||
}
|
||
|
||
- (NSString*)disambiguationSuffixForURL:(NSURL*)url numberOfParents:(NSInteger)numberOfParents
|
||
{
|
||
NSMutableArray* parentNames = [NSMutableArray array];
|
||
for(NSUInteger i = 0; i < numberOfParents; ++i)
|
||
{
|
||
NSNumber* flag;
|
||
if([url getResourceValue:&flag forKey:NSURLIsVolumeKey error:nil] && flag.boolValue)
|
||
return nil;
|
||
|
||
NSURL* parentURL;
|
||
if(![url getResourceValue:&parentURL forKey:NSURLParentDirectoryURLKey error:nil] || [url isEqual:parentURL])
|
||
return nil;
|
||
|
||
NSString* parentName;
|
||
if(![parentURL getResourceValue:&parentName forKey:NSURLLocalizedNameKey error:nil])
|
||
return nil;
|
||
|
||
[parentNames addObject:parentName];
|
||
url = parentURL;
|
||
}
|
||
return [[parentNames.reverseObjectEnumerator allObjects] componentsJoinedByString:@"/"];
|
||
}
|
||
|
||
- (void)updateDisambiguationSuffixInParent:(FileItem*)item
|
||
{
|
||
NSArray* children = [item.arrangedChildren filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"URL.isFileURL == YES"]];
|
||
for(FileItem* child in children)
|
||
child.disambiguationSuffix = nil;
|
||
|
||
NSInteger showNumberOfParents = 1;
|
||
while(children.count)
|
||
{
|
||
NSCountedSet* countOfConflicts = [[NSCountedSet alloc] initWithArray:[children valueForKeyPath:@"displayName"]];
|
||
NSMutableArray* conflictedChildren = [NSMutableArray array];
|
||
for(FileItem* child in children)
|
||
{
|
||
if([countOfConflicts countForObject:child.displayName] == 1)
|
||
continue;
|
||
|
||
if(NSString* newSuffix = [self disambiguationSuffixForURL:child.URL numberOfParents:showNumberOfParents])
|
||
{
|
||
child.disambiguationSuffix = [@" — " stringByAppendingString:newSuffix];
|
||
[conflictedChildren addObject:child];
|
||
}
|
||
}
|
||
children = conflictedChildren;
|
||
++showNumberOfParents;
|
||
}
|
||
}
|
||
|
||
- (void)setModifiedURLs:(NSArray<NSURL*>*)newModifiedURLs
|
||
{
|
||
_modifiedURLs = newModifiedURLs;
|
||
|
||
if(!self.fileItem)
|
||
return;
|
||
|
||
NSMutableArray<FileItem*>* stack = [NSMutableArray arrayWithObject:self.fileItem];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
item.modified = [_modifiedURLs containsObject:item.URL];
|
||
if(item.children)
|
||
[stack addObjectsFromArray:item.children];
|
||
}
|
||
}
|
||
|
||
- (void)setOpenURLs:(NSArray<NSURL*>*)newOpenURLs
|
||
{
|
||
_openURLs = newOpenURLs;
|
||
|
||
if(!self.fileItem)
|
||
return;
|
||
|
||
NSMutableArray<FileItem*>* stack = [NSMutableArray arrayWithObject:self.fileItem];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
item.open = [_openURLs containsObject:item.URL];
|
||
if(item.children)
|
||
[stack addObjectsFromArray:item.children];
|
||
}
|
||
}
|
||
|
||
// ===========================
|
||
// = Loading/Expanding Items =
|
||
// ===========================
|
||
|
||
- (void)setFileItem:(FileItem*)item
|
||
{
|
||
if(_fileItem)
|
||
{
|
||
// Remove visible but non-selected/expanded items from pending selection/expansion
|
||
_expandedURLs = [self.expandedURLs mutableCopy];
|
||
_selectedURLs = [self.selectedURLs mutableCopy];
|
||
|
||
for(id observer in _fileItemObservers.allValues)
|
||
[FileItem removeObserver:observer];
|
||
_fileItemObservers = [NSMutableDictionary dictionary];
|
||
}
|
||
|
||
_fileItem = item;
|
||
|
||
[self.outlineView reloadItem:nil reloadChildren:YES];
|
||
[self.outlineView deselectAll:self];
|
||
[self.outlineView scrollRowToVisible:0];
|
||
|
||
[self loadChildrenForItem:item expandChildren:NO];
|
||
|
||
[self invalidateRestorableState];
|
||
}
|
||
|
||
- (void)outlineViewItemDidExpand:(NSNotification*)aNotification
|
||
{
|
||
FileItem* item = aNotification.userInfo[@"NSObject"];
|
||
[self loadChildrenForItem:item expandChildren:_expandingChildrenCounter > 0];
|
||
[self invalidateRestorableState];
|
||
}
|
||
|
||
- (void)loadChildrenForItem:(FileItem*)item expandChildren:(BOOL)flag
|
||
{
|
||
if(item.arrangedChildren || item.children)
|
||
return;
|
||
|
||
NSURL* url = item.URL;
|
||
|
||
if(_fileItemObservers[url])
|
||
{
|
||
// ================
|
||
// = Debug Output =
|
||
// ================
|
||
|
||
NSMutableArray<NSString*>* itemInfo = [NSMutableArray array];
|
||
|
||
NSMutableArray<FileItem*>* stack = [NSMutableArray arrayWithObject:self.fileItem];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
if(item.isDirectory)
|
||
{
|
||
NSMutableString* info = [item.URL.path mutableCopy];
|
||
if(item == self.fileItem || [self.outlineView isItemExpanded:item])
|
||
[info appendString:@" [expanded]"];
|
||
if([_fileItemObservers objectForKey:item.URL])
|
||
[info appendString:@" [observing]"];
|
||
if([_loadingURLs containsObject:item.URL])
|
||
[info appendString:@" [loading]"];
|
||
if(item.arrangedChildren || item.children)
|
||
[info appendFormat:@" [%lu / %lu children]", item.arrangedChildren.count, item.children.count];
|
||
[itemInfo addObject:info];
|
||
}
|
||
|
||
[stack removeObjectAtIndex:0];
|
||
if(NSArray<FileItem*>* children = item.arrangedChildren)
|
||
[stack addObjectsFromArray:children];
|
||
}
|
||
|
||
NSLog(@"%s *** Observer already exists for: %@\n%@", sel_getName(_cmd), url, [itemInfo componentsJoinedByString:@"\n"]);
|
||
|
||
// ===================================
|
||
// = Temporary (possible) workaround =
|
||
// ===================================
|
||
|
||
[FileItem removeObserver:_fileItemObservers[url]];
|
||
_fileItemObservers[url] = nil;
|
||
}
|
||
|
||
[_loadingURLs addObject:url];
|
||
|
||
__weak FileBrowserViewController* weakSelf = self;
|
||
_fileItemObservers[url] = [FileItem addObserverToDirectoryAtURL:item.resolvedURL usingBlock:^(NSArray<NSURL*>* urls){
|
||
[weakSelf didReceiveURLs:urls forItemWithURL:url expandChildren:flag];
|
||
}];
|
||
}
|
||
|
||
- (FileItem*)findItemForURL:(NSURL*)url
|
||
{
|
||
NSMutableArray<FileItem*>* stack = [NSMutableArray arrayWithObject:self.fileItem];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
if([item.URL isEqual:url])
|
||
return item;
|
||
if(NSArray<FileItem*>* children = item.arrangedChildren)
|
||
[stack addObjectsFromArray:children];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
- (void)didReceiveURLs:(NSArray<NSURL*>*)urls forItemWithURL:(NSURL*)url expandChildren:(BOOL)flag
|
||
{
|
||
FileItem* item = [self findItemForURL:url];
|
||
if(!item)
|
||
{
|
||
NSLog(@"%s *** unable to find item for %@", sel_getName(_cmd), url);
|
||
}
|
||
else if(item != self.fileItem && ![self.outlineView isItemExpanded:item])
|
||
{
|
||
NSLog(@"%s *** item no longer expanded: %@", sel_getName(_cmd), item);
|
||
|
||
item.children = nil;
|
||
item.arrangedChildren = nil;
|
||
[self.outlineView reloadItem:item reloadChildren:YES];
|
||
|
||
[FileItem removeObserver:_fileItemObservers[url]];
|
||
_fileItemObservers[url] = nil;
|
||
}
|
||
else
|
||
{
|
||
NSMutableArray* children = [NSMutableArray array];
|
||
|
||
if(item.children)
|
||
{
|
||
NSMutableSet<NSURL*>* newURLs = [NSMutableSet setWithArray:urls];
|
||
|
||
for(FileItem* child in item.children)
|
||
{
|
||
if(NSURL* url = child.fileReferenceURL.filePathURL)
|
||
child.URL = url;
|
||
|
||
if([newURLs containsObject:child.URL])
|
||
{
|
||
[newURLs removeObject:child.URL];
|
||
[child updateFileProperties];
|
||
[children addObject:child];
|
||
}
|
||
}
|
||
|
||
urls = newURLs.allObjects;
|
||
}
|
||
|
||
for(NSURL* url in urls)
|
||
{
|
||
FileItem* newItem = [FileItem fileItemWithURL:url];
|
||
newItem.open = [_openURLs containsObject:url];
|
||
newItem.modified = [_modifiedURLs containsObject:url];
|
||
[children addObject:newItem];
|
||
}
|
||
|
||
item.children = [children copy];
|
||
[self rearrangeChildrenInParent:item];
|
||
|
||
for(FileItem* child in item.arrangedChildren)
|
||
{
|
||
if((flag && !child.isSymbolicLink || [_expandedURLs containsObject:child.URL] || [child.URL.scheme isEqualToString:@"scm"]) && [self.outlineView isExpandable:child])
|
||
[self.outlineView expandItem:child expandChildren:flag && !child.isSymbolicLink];
|
||
|
||
if([_selectedURLs containsObject:child.URL])
|
||
{
|
||
[self.outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[self.outlineView rowForItem:child]] byExtendingSelection:YES];
|
||
[_selectedURLs removeObject:child.URL];
|
||
}
|
||
}
|
||
}
|
||
|
||
[_loadingURLs removeObject:url];
|
||
[self checkLoadCompletionHandlers];
|
||
}
|
||
|
||
- (void)checkLoadCompletionHandlers
|
||
{
|
||
if(_loadingURLs.count == 0)
|
||
{
|
||
NSArray<void(^)()>* completionHandlers = _loadingURLsCompletionHandlers;
|
||
_loadingURLsCompletionHandlers = nil;
|
||
for(void(^handler)() in completionHandlers)
|
||
handler();
|
||
}
|
||
}
|
||
|
||
- (void)expandURLs:(NSArray<NSURL*>*)expandURLs selectURLs:(NSArray<NSURL*>*)selectURLs
|
||
{
|
||
_loadingURLsCompletionHandlers = [(_loadingURLsCompletionHandlers ?: @[ ]) arrayByAddingObject:^{
|
||
[self performSelector:@selector(centerSelectionInVisibleArea:) withObject:self afterDelay:0];
|
||
}];
|
||
|
||
_expandedURLs = expandURLs ? [NSMutableSet setWithArray:expandURLs] : _expandedURLs;
|
||
_selectedURLs = selectURLs ? [NSMutableSet setWithArray:selectURLs] : _selectedURLs;
|
||
|
||
NSMutableArray<FileItem*>* stack = [self.fileItem.arrangedChildren mutableCopy];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
if([_expandedURLs containsObject:item.URL])
|
||
{
|
||
[self.outlineView expandItem:item];
|
||
if(NSArray<FileItem*>* arrangedChildren = item.arrangedChildren)
|
||
[stack addObjectsFromArray:arrangedChildren];
|
||
}
|
||
}
|
||
|
||
NSMutableIndexSet* indexesToSelect = [NSMutableIndexSet indexSet];
|
||
for(NSUInteger i = 0; i < self.outlineView.numberOfRows; ++i)
|
||
{
|
||
FileItem* item = [self.outlineView itemAtRow:i];
|
||
if([_selectedURLs containsObject:item.URL])
|
||
[indexesToSelect addIndex:i];
|
||
}
|
||
[self.outlineView selectRowIndexes:indexesToSelect byExtendingSelection:NO];
|
||
|
||
[self checkLoadCompletionHandlers];
|
||
}
|
||
|
||
- (void)outlineView:(NSOutlineView*)outlineView willExpandItem:(FileItem*)item expandChildren:(BOOL)flag
|
||
{
|
||
_expandingChildrenCounter += flag ? 1 : 0;
|
||
}
|
||
|
||
- (void)outlineView:(NSOutlineView*)outlineView didExpandItem:(FileItem*)item expandChildren:(BOOL)flag
|
||
{
|
||
_expandingChildrenCounter -= flag ? 1 : 0;
|
||
}
|
||
|
||
- (void)outlineView:(NSOutlineView*)outlineView willCollapseItem:(id)someItem collapseChildren:(BOOL)flag
|
||
{
|
||
_collapsingChildrenCounter += flag ? 1 : 0;
|
||
}
|
||
|
||
- (void)outlineView:(NSOutlineView*)outlineView didCollapseItem:(id)someItem collapseChildren:(BOOL)flag
|
||
{
|
||
_collapsingChildrenCounter -= flag ? 1 : 0;
|
||
}
|
||
|
||
- (void)outlineViewItemWillCollapse:(NSNotification*)aNotification
|
||
{
|
||
FileItem* item = aNotification.userInfo[@"NSObject"];
|
||
if(_nestedCollapsingChildrenCounter == 0 || _collapsingChildrenCounter > 0)
|
||
[_expandedURLs removeObject:item.URL];
|
||
|
||
_nestedCollapsingChildrenCounter += 1;
|
||
}
|
||
|
||
- (void)outlineViewItemDidCollapse:(NSNotification*)aNotification
|
||
{
|
||
_nestedCollapsingChildrenCounter -= 1;
|
||
|
||
if(_nestedCollapsingChildrenCounter == 0)
|
||
[self invalidateRestorableState];
|
||
}
|
||
|
||
- (void)outlineViewSelectionDidChange:(NSNotification*)aNotification
|
||
{
|
||
[self invalidateRestorableState];
|
||
}
|
||
|
||
// ================
|
||
// = Location URL =
|
||
// ================
|
||
|
||
- (NSURL*)URL
|
||
{
|
||
return _fileItem.URL;
|
||
}
|
||
|
||
- (void)setURL:(NSURL*)url
|
||
{
|
||
if(FileItem* item = [FileItem fileItemWithURL:url])
|
||
self.fileItem = item;
|
||
}
|
||
|
||
- (void)reload:(id)sender
|
||
{
|
||
NSMutableArray<FileItem*>* stack = [NSMutableArray arrayWithObject:self.fileItem];
|
||
while(FileItem* item = stack.firstObject)
|
||
{
|
||
[stack removeObjectAtIndex:0];
|
||
if(!item.arrangedChildren)
|
||
continue;
|
||
[FSEventsManager.sharedInstance reloadDirectoryAtURL:item.resolvedURL];
|
||
[stack addObjectsFromArray:item.arrangedChildren];
|
||
}
|
||
}
|
||
|
||
- (NSArray<FileItem*>*)selectedItems
|
||
{
|
||
NSIndexSet* indexSet;
|
||
|
||
NSInteger clickedRow = self.outlineView.clickedRow;
|
||
if(0 <= clickedRow && clickedRow < self.outlineView.numberOfRows && ![self.outlineView.selectedRowIndexes containsIndex:clickedRow])
|
||
indexSet = [NSIndexSet indexSetWithIndex:clickedRow];
|
||
else indexSet = self.outlineView.selectedRowIndexes;
|
||
|
||
NSMutableArray* res = [NSMutableArray array];
|
||
for(NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
|
||
[res addObject:[self.outlineView itemAtRow:index]];
|
||
return res;
|
||
}
|
||
|
||
- (NSURL*)directoryURLForNewItems
|
||
{
|
||
NSMutableArray<NSURL*>* candidates = [NSMutableArray array];
|
||
for(FileItem* item in self.selectedItems)
|
||
{
|
||
if(item.resolvedURL.isFileURL && [self.outlineView isItemExpanded:item])
|
||
{
|
||
[candidates addObject:item.resolvedURL];
|
||
}
|
||
else if(FileItem* parentItem = [self.outlineView parentForItem:item])
|
||
{
|
||
if(parentItem.resolvedURL.isFileURL)
|
||
[candidates addObject:parentItem.resolvedURL];
|
||
}
|
||
}
|
||
return candidates.lastObject ?: self.fileItem.URL.filePathURL;
|
||
}
|
||
|
||
- (void)centerSelectionInVisibleArea:(id)sender
|
||
{
|
||
if(self.outlineView.numberOfSelectedRows == 0)
|
||
return;
|
||
|
||
NSInteger row = self.outlineView.selectedRowIndexes.firstIndex;
|
||
|
||
NSRect rowRect = [self.outlineView rectOfRow:row];
|
||
NSRect visibleRect = self.outlineView.visibleRect;
|
||
if(NSMinY(rowRect) < NSMinY(visibleRect) || NSMaxY(rowRect) > NSMaxY(visibleRect))
|
||
[self.outlineView scrollPoint:NSMakePoint(NSMinX(rowRect), round(NSMidY(rowRect) - NSHeight(visibleRect)/2))];
|
||
}
|
||
|
||
- (NSSet<NSURL*>*)selectedURLs
|
||
{
|
||
NSMutableSet<NSURL*>* res = [_selectedURLs mutableCopy];
|
||
NSIndexSet* selectedIndexes = self.outlineView.selectedRowIndexes;
|
||
for(NSUInteger i = 0; i < self.outlineView.numberOfRows; ++i)
|
||
{
|
||
FileItem* item = [self.outlineView itemAtRow:i];
|
||
if([selectedIndexes containsIndex:i])
|
||
[res addObject:item.URL];
|
||
else [res removeObject:item.URL];
|
||
}
|
||
return [res copy];
|
||
}
|
||
|
||
- (NSSet<NSURL*>*)expandedURLs
|
||
{
|
||
NSMutableSet<NSURL*>* res = [_expandedURLs mutableCopy];
|
||
for(NSUInteger i = 0; i < self.outlineView.numberOfRows; ++i)
|
||
{
|
||
FileItem* item = [self.outlineView itemAtRow:i];
|
||
if([self.outlineView isItemExpanded:item] && ![item.URL.scheme isEqualToString:@"scm"])
|
||
[res addObject:item.URL];
|
||
else [res removeObject:item.URL];
|
||
}
|
||
return [res copy];
|
||
}
|
||
|
||
// ===========================
|
||
// = NSOutlineViewDataSource =
|
||
// ===========================
|
||
|
||
- (NSInteger)outlineView:(NSOutlineView*)outlineView numberOfChildrenOfItem:(FileItem*)item
|
||
{
|
||
return (item ?: _fileItem).arrangedChildren.count;
|
||
}
|
||
|
||
- (id)outlineView:(NSOutlineView*)outlineView child:(NSInteger)childIndex ofItem:(FileItem*)item
|
||
{
|
||
return (item ?: _fileItem).arrangedChildren[childIndex];
|
||
}
|
||
|
||
- (BOOL)outlineView:(NSOutlineView*)outlineView isItemExpandable:(FileItem*)item
|
||
{
|
||
return item.isDirectory && (_canExpandPackages || !item.isPackage) || (_canExpandSymbolicLinks && item.isLinkToDirectory && (_canExpandPackages || !item.isLinkToPackage));
|
||
}
|
||
|
||
- (BOOL)outlineView:(NSOutlineView*)outlineView isGroupItem:(FileItem*)item
|
||
{
|
||
return [item.URL.scheme isEqualToString:@"scm"];
|
||
}
|
||
|
||
- (BOOL)outlineView:(NSOutlineView*)outlineView shouldSelectItem:(FileItem*)item
|
||
{
|
||
return item.URL.isFileURL;
|
||
}
|
||
|
||
- (id)outlineView:(NSOutlineView*)outlineView objectValueForTableColumn:(NSTableColumn*)tableColumn byItem:(FileItem*)item
|
||
{
|
||
return item;
|
||
}
|
||
|
||
- (id <NSPasteboardWriting>)outlineView:(NSOutlineView*)outlineView pasteboardWriterForItem:(FileItem*)item
|
||
{
|
||
return item.URL.filePathURL;
|
||
}
|
||
|
||
// ===============================
|
||
// = Table cell view constructor =
|
||
// ===============================
|
||
|
||
- (NSView*)outlineView:(NSOutlineView*)outlineView viewForTableColumn:(NSTableColumn*)tableColumn item:(FileItem*)item
|
||
{
|
||
FileItemTableCellView* res = [outlineView makeViewWithIdentifier:tableColumn.identifier owner:self];
|
||
if(!res)
|
||
{
|
||
res = [[FileItemTableCellView alloc] init];
|
||
res.identifier = tableColumn.identifier;
|
||
res.target = self;
|
||
res.closeAction = @selector(takeItemToCloseFrom:);
|
||
res.openButton.target = self;
|
||
res.openButton.action = @selector(takeItemToOpenFrom:);
|
||
res.textField.delegate = self;
|
||
}
|
||
return res;
|
||
}
|
||
|
||
- (void)takeItemToOpenFrom:(id)sender
|
||
{
|
||
NSInteger row = [self.outlineView rowForView:sender];
|
||
if(row != -1)
|
||
{
|
||
FileItem* item = [self.outlineView itemAtRow:row];
|
||
[self openItems:@[ item ] animate:YES];
|
||
}
|
||
}
|
||
|
||
- (void)takeItemToCloseFrom:(id)sender
|
||
{
|
||
NSInteger row = [self.outlineView rowForView:sender];
|
||
if(row != -1)
|
||
{
|
||
FileItem* item = [self.outlineView itemAtRow:row];
|
||
[self.delegate fileBrowser:self closeURL:item.URL];
|
||
}
|
||
}
|
||
|
||
- (BOOL)control:(NSTextField*)textField textShouldEndEditing:(NSText*)fieldEditor
|
||
{
|
||
NSInteger row = [self.outlineView rowForView:textField];
|
||
if(row == -1)
|
||
return NO;
|
||
|
||
FileItem* item = [self.outlineView itemAtRow:row];
|
||
NSURL* newURL = [[item.URL URLByDeletingLastPathComponent] URLByAppendingPathComponent:fieldEditor.string isDirectory:item.isDirectory];
|
||
if(![item.URL isEqual:newURL])
|
||
{
|
||
// Because of the animation we need to run this after field editor has been removed
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
[self performOperation:FBOperationRename withURLs:@{ item.URL: newURL } unique:NO select:YES];
|
||
});
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
// =============
|
||
// = QuickLook =
|
||
// =============
|
||
|
||
- (NSArray<FileItem*>*)previewableItems
|
||
{
|
||
return [self.selectedItems filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"previewItemURL != nil"]];
|
||
}
|
||
|
||
- (void)toggleQuickLookPreview:(id)sender
|
||
{
|
||
if([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible])
|
||
[[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
|
||
else [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
|
||
}
|
||
|
||
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel*)previewPanel
|
||
{
|
||
return YES;
|
||
}
|
||
|
||
- (void)beginPreviewPanelControl:(QLPreviewPanel*)previewPanel
|
||
{
|
||
_previewItems = self.previewableItems;
|
||
previewPanel.delegate = self;
|
||
previewPanel.dataSource = self;
|
||
}
|
||
|
||
- (void)endPreviewPanelControl:(QLPreviewPanel*)previewPanel
|
||
{
|
||
_previewItems = nil;
|
||
}
|
||
|
||
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel*)previewPanel
|
||
{
|
||
return _previewItems.count;
|
||
}
|
||
|
||
- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel*)panel previewItemAtIndex:(NSInteger)index
|
||
{
|
||
return _previewItems[index];
|
||
}
|
||
|
||
- (NSRect)previewPanel:(QLPreviewPanel*)previewPanel sourceFrameOnScreenForPreviewItem:(id <QLPreviewItem>)item
|
||
{
|
||
return [self imageRectOfItem:item];
|
||
}
|
||
|
||
- (NSRect)imageRectOfItem:(FileItem*)item
|
||
{
|
||
NSInteger row = [self.outlineView rowForItem:item];
|
||
if(row != -1)
|
||
{
|
||
FileItemTableCellView* view = [self.outlineView viewAtColumn:0 row:row makeIfNecessary:YES];
|
||
if([view isKindOfClass:[FileItemTableCellView class]])
|
||
{
|
||
NSButton* imageButton = view.openButton;
|
||
NSRect imageRect = NSIntersectionRect([imageButton convertRect:imageButton.bounds toView:nil], [self.outlineView convertRect:self.outlineView.visibleRect toView:nil]);
|
||
return NSIsEmptyRect(imageRect) ? NSZeroRect : [view.window convertRectToScreen:imageRect];
|
||
}
|
||
}
|
||
return NSZeroRect;
|
||
}
|
||
|
||
- (BOOL)previewPanel:(QLPreviewPanel*)previewPanel handleEvent:(NSEvent*)event
|
||
{
|
||
std::string const eventString = to_s(event);
|
||
if((event.type == NSEventTypeKeyUp || event.type == NSEventTypeKeyDown) && (eventString == utf8::to_s(NSUpArrowFunctionKey) || eventString == utf8::to_s(NSDownArrowFunctionKey)))
|
||
{
|
||
[self.view.window sendEvent:event];
|
||
_previewItems = self.previewableItems;
|
||
[previewPanel reloadData];
|
||
return YES;
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
// ============
|
||
// = Services =
|
||
// ============
|
||
|
||
+ (void)initialize
|
||
{
|
||
[NSApplication.sharedApplication registerServicesMenuSendTypes:@[ NSFilenamesPboardType, NSURLPboardType ] returnTypes:@[ ]];
|
||
}
|
||
|
||
- (id)validRequestorForSendType:(NSString*)sendType returnType:(NSString*)returnType
|
||
{
|
||
return returnType == nil && sendType != nil && [@[ NSFilenamesPboardType, NSURLPboardType ] containsObject:sendType] ? self : nil;
|
||
}
|
||
|
||
- (BOOL)writeSelectionToPasteboard:(NSPasteboard*)pboard types:(NSArray*)types
|
||
{
|
||
NSArray<NSURL*>* urls = [self.previewableItems valueForKeyPath:@"URL"];
|
||
if(urls.count == 0)
|
||
return NO;
|
||
|
||
[pboard clearContents];
|
||
return [pboard writeObjects:urls];
|
||
}
|
||
|
||
// ===================
|
||
// = Accepting Drops =
|
||
// ===================
|
||
|
||
- (NSDragOperation)outlineView:(NSOutlineView*)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(FileItem*)item proposedChildIndex:(NSInteger)childIndex
|
||
{
|
||
NSURL* dropURL = (item ?: self.fileItem).resolvedURL.filePathURL;
|
||
if(![self.outlineView isExpandable:item] || !dropURL || ![NSFileManager.defaultManager fileExistsAtPath:dropURL.path])
|
||
return NSDragOperationNone;
|
||
|
||
NSPasteboard* pboard = info.draggingPasteboard;
|
||
NSArray* draggedPaths = [pboard propertyListForType:NSFilenamesPboardType];
|
||
|
||
dev_t targetDevice = path::device(dropURL.fileSystemRepresentation);
|
||
BOOL linkOperation = (NSApp.currentEvent.modifierFlags & NSEventModifierFlagControl) == NSEventModifierFlagControl;
|
||
BOOL toggleOperation = (NSApp.currentEvent.modifierFlags & NSEventModifierFlagOption) == NSEventModifierFlagOption;
|
||
|
||
// We accept the drop as long as it is valid for at least one of the items
|
||
for(NSString* draggedPath in draggedPaths)
|
||
{
|
||
BOOL sameSource = (path::device(draggedPath.fileSystemRepresentation) == targetDevice);
|
||
NSDragOperation operation = linkOperation ? NSDragOperationLink : ((sameSource != toggleOperation) ? NSDragOperationMove : NSDragOperationCopy);
|
||
|
||
// Can’t move into same location
|
||
NSString* parentPath = draggedPath.stringByDeletingLastPathComponent;
|
||
if(operation == NSDragOperationMove && [parentPath isEqualToString:dropURL.path])
|
||
continue;
|
||
|
||
[outlineView setDropItem:item dropChildIndex:NSOutlineViewDropOnItemIndex];
|
||
return operation;
|
||
}
|
||
return NSDragOperationNone;
|
||
}
|
||
|
||
static NSDragOperation filter (NSDragOperation mask)
|
||
{
|
||
return (mask & NSDragOperationMove) ? NSDragOperationMove : ((mask & NSDragOperationCopy) ? NSDragOperationCopy : ((mask & NSDragOperationLink) ? NSDragOperationLink : 0));
|
||
}
|
||
|
||
- (BOOL)outlineView:(NSOutlineView*)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(FileItem*)item childIndex:(NSInteger)childIndex
|
||
{
|
||
FileItem* newParent = item ?: self.fileItem;
|
||
|
||
NSDragOperation op = filter(info.draggingSourceOperationMask);
|
||
if(op == 0 || ![self.outlineView isExpandable:newParent] || !newParent.resolvedURL.isFileURL)
|
||
return NO;
|
||
|
||
NSMutableDictionary<NSURL*, NSURL*>* urls = [NSMutableDictionary dictionary];
|
||
for(NSURL* url in [self URLsFromPasteboard:info.draggingPasteboard])
|
||
urls[url] = [newParent.resolvedURL URLByAppendingPathComponent:url.lastPathComponent isDirectory:op != NSDragOperationLink && url.hasDirectoryPath];
|
||
|
||
switch(op)
|
||
{
|
||
case NSDragOperationLink: [self performOperation:FBOperationLink withURLs:urls unique:NO select:NO]; break;
|
||
case NSDragOperationCopy: [self performOperation:FBOperationCopy withURLs:urls unique:NO select:NO]; break;
|
||
case NSDragOperationMove: [self performOperation:FBOperationMove withURLs:urls unique:NO select:NO]; break;
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
- (void)outlineView:(NSOutlineView*)outlineView didTrashURLs:(NSArray<NSURL*>*)someURLs
|
||
{
|
||
[self performOperation:FBOperationTrash sourceURLs:someURLs destinationURLs:nil unique:NO select:NO];
|
||
}
|
||
|
||
// =============
|
||
// = Undo/Redo =
|
||
// =============
|
||
|
||
- (NSUndoManager*)undoManager
|
||
{
|
||
if(!_fileBrowserUndoManager)
|
||
_fileBrowserUndoManager = [[NSUndoManager alloc] init];
|
||
return _fileBrowserUndoManager;
|
||
}
|
||
|
||
- (NSUndoManager*)activeUndoManager
|
||
{
|
||
NSResponder* firstResponder = self.view.window.firstResponder;
|
||
if([firstResponder isKindOfClass:[NSView class]] && [(NSView*)firstResponder isDescendantOf:self.view])
|
||
return firstResponder.undoManager;
|
||
else return self.undoManager;
|
||
}
|
||
|
||
- (void)undo:(id)sender
|
||
{
|
||
[self.activeUndoManager undo];
|
||
}
|
||
|
||
- (void)redo:(id)sender
|
||
{
|
||
[self.activeUndoManager redo];
|
||
}
|
||
@end
|