/* |
File: MyWindowController.m |
Abstract: Interface for MyWindowController class, the main controller class for this sample. |
|
Version: 1.5 |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
|
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
|
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import <WebKit/WebKit.h> |
|
#import "MyWindowController.h" |
|
#import "IconViewController.h" |
#import "FileViewController.h" |
#import "ChildEditController.h" |
#import "ChildNode.h" |
#import "ImageAndTextCell.h" |
#import "SeparatorCell.h" |
|
#define COLUMNID_NAME @"NameColumn" // the single column name in our outline view |
#define INITIAL_INFODICT @"Outline" // name of the dictionary file to populate our outline view |
|
#define ICONVIEW_NIB_NAME @"IconView" // nib name for the icon view |
#define FILEVIEW_NIB_NAME @"FileView" // nib name for the file view |
#define CHILDEDIT_NAME @"ChildEdit" // nib name for the child edit window controller |
|
#define UNTITLED_NAME @"Untitled" // default name for added folders and leafs |
|
#define HTTP_PREFIX @"http://" |
|
// default folder titles |
#define PLACES_NAME @"PLACES" |
#define BOOKMARKS_NAME @"BOOKMARKS" |
|
// keys in our disk-based dictionary representing our outline view's data |
#define KEY_NAME @"name" |
#define KEY_URL @"url" |
#define KEY_SEPARATOR @"separator" |
#define KEY_GROUP @"group" |
#define KEY_FOLDER @"folder" |
#define KEY_ENTRIES @"entries" |
|
#define kMinOutlineViewSplit 120.0f |
|
#define kIconImageSize 16.0 |
|
#define kNodesPBoardType @"myNodesPBoardType" // drag and drop pasteboard type |
|
#pragma mark - |
|
// ------------------------------------------------------------------------------- |
// TreeAdditionObj |
// |
// This object is used for passing data between the main and secondary thread |
// which populates the outline view. |
// ------------------------------------------------------------------------------- |
@interface TreeAdditionObj : NSObject |
{ |
NSIndexPath *__unsafe_unretained indexPath; |
NSString *__unsafe_unretained nodeURL; |
NSString *__unsafe_unretained nodeName; |
BOOL selectItsParent; |
} |
|
@property (unsafe_unretained, readonly) NSIndexPath *indexPath; |
@property (unsafe_unretained, readonly) NSString *nodeURL; |
@property (unsafe_unretained, readonly) NSString *nodeName; |
@property (readonly) BOOL selectItsParent; |
|
@end |
|
|
#pragma mark - |
|
@implementation TreeAdditionObj |
|
@synthesize indexPath, nodeURL, nodeName, selectItsParent; |
|
// ------------------------------------------------------------------------------- |
// initWithURL:url:name:select |
// ------------------------------------------------------------------------------- |
- (id)initWithURL:(NSString *)url withName:(NSString *)name selectItsParent:(BOOL)select |
{ |
self = [super init]; |
|
nodeName = name; |
nodeURL = url; |
selectItsParent = select; |
|
return self; |
} |
@end |
|
|
#pragma mark - |
|
@interface MyWindowController () |
{ |
IBOutlet NSOutlineView *myOutlineView; |
IBOutlet NSTreeController *treeController; |
IBOutlet NSView *placeHolderView; |
IBOutlet NSSplitView *splitView; |
IBOutlet WebView *webView; |
IBOutlet NSProgressIndicator *progIndicator; |
IBOutlet NSButton *addFolderButton; |
IBOutlet NSButton *removeButton; |
IBOutlet NSPopUpButton *actionButton; |
IBOutlet NSTextField *urlField; |
|
// cached images for generic folder and url document |
NSImage *folderImage; |
NSImage *urlImage; |
|
NSView *currentView; |
IconViewController *iconViewController; |
FileViewController *fileViewController; |
ChildEditController *childEditController; |
|
BOOL retargetWebView; |
|
SeparatorCell *separatorCell; // the cell used to draw a separator line in the outline view |
} |
|
@property (strong) NSArray *dragNodesArray; // used to keep track of dragged nodes |
@property (strong) NSMutableArray *contents; // used to keep track of dragged nodes |
|
@end |
|
|
#pragma mark - |
|
@implementation MyWindowController |
|
// ------------------------------------------------------------------------------- |
// initWithWindow:window |
// ------------------------------------------------------------------------------- |
- (id)initWithWindow:(NSWindow *)window |
{ |
self = [super initWithWindow:window]; |
if (self != nil) |
{ |
_contents = [[NSMutableArray alloc] init]; |
|
// cache the reused icon images |
folderImage = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGenericFolderIcon)]; |
[folderImage setSize:NSMakeSize(kIconImageSize, kIconImageSize)]; |
|
urlImage = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGenericURLIcon)]; |
[urlImage setSize:NSMakeSize(kIconImageSize, kIconImageSize)]; |
} |
|
return self; |
} |
|
// ------------------------------------------------------------------------------- |
// dealloc |
// ------------------------------------------------------------------------------- |
- (void)dealloc |
{ |
[[NSNotificationCenter defaultCenter] removeObserver:self name:kReceivedContentNotification object:nil]; |
} |
|
// ------------------------------------------------------------------------------- |
// awakeFromNib |
// ------------------------------------------------------------------------------- |
- (void)awakeFromNib |
{ |
// load the icon view controller for later use |
iconViewController = [[IconViewController alloc] initWithNibName:ICONVIEW_NIB_NAME bundle:nil]; |
|
// load the file view controller for later use |
fileViewController = [[FileViewController alloc] initWithNibName:FILEVIEW_NIB_NAME bundle:nil]; |
|
// load the child edit view controller for later use |
childEditController = [[ChildEditController alloc] initWithWindowNibName:CHILDEDIT_NAME]; |
|
[[self window] setAutorecalculatesContentBorderThickness:YES forEdge:NSMinYEdge]; |
[[self window] setContentBorderThickness:30 forEdge:NSMinYEdge]; |
|
// apply our custom ImageAndTextCell for rendering the first column's cells |
NSTableColumn *tableColumn = [myOutlineView tableColumnWithIdentifier:COLUMNID_NAME]; |
ImageAndTextCell *imageAndTextCell = [[ImageAndTextCell alloc] init]; |
[imageAndTextCell setEditable:YES]; |
[tableColumn setDataCell:imageAndTextCell]; |
|
separatorCell = [[SeparatorCell alloc] init]; |
[separatorCell setEditable:NO]; |
|
// add our content |
[self populateOutlineContents]; |
|
// add images to our add/remove buttons |
NSImage *addImage = [NSImage imageNamed:NSImageNameAddTemplate]; |
[addFolderButton setImage:addImage]; |
NSImage *removeImage = [NSImage imageNamed:NSImageNameRemoveTemplate]; |
[removeButton setImage:removeImage]; |
|
// insert an empty menu item at the beginning of the drown down button's menu and add its image |
NSImage *actionImage = [NSImage imageNamed:NSImageNameActionTemplate]; |
[actionImage setSize:NSMakeSize(10,10)]; |
|
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; |
[[actionButton menu] insertItem:menuItem atIndex:0]; |
[menuItem setImage:actionImage]; |
|
// truncate to the middle if the url is too long to fit |
[[urlField cell] setLineBreakMode:NSLineBreakByTruncatingMiddle]; |
|
// scroll to the top in case the outline contents is very long |
[[[myOutlineView enclosingScrollView] verticalScroller] setFloatValue:0.0]; |
[[[myOutlineView enclosingScrollView] contentView] scrollToPoint:NSMakePoint(0,0)]; |
|
// make our outline view appear with gradient selection, and behave like the Finder, iTunes, etc. |
[myOutlineView setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleSourceList]; |
|
// drag and drop support |
[myOutlineView registerForDraggedTypes:[NSArray arrayWithObjects: |
kNodesPBoardType, // our internal drag type |
NSURLPboardType, // single url from pasteboard |
NSFilenamesPboardType, // from Safari or Finder |
NSFilesPromisePboardType, // from Safari or Finder (multiple URLs) |
nil]]; |
|
[webView setUIDelegate:self]; // be the webView's delegate to capture NSResponder calls |
|
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(contentReceived:) |
name:kReceivedContentNotification object:nil]; |
} |
|
|
#pragma mark - Actions |
|
// ------------------------------------------------------------------------------- |
// selectParentFromSelection |
// |
// Take the currently selected node and select its parent. |
// ------------------------------------------------------------------------------- |
- (void)selectParentFromSelection |
{ |
if ([[treeController selectedNodes] count] > 0) |
{ |
NSTreeNode *firstSelectedNode = [[treeController selectedNodes] objectAtIndex:0]; |
NSTreeNode *parentNode = [firstSelectedNode parentNode]; |
if (parentNode) |
{ |
// select the parent |
NSIndexPath *parentIndex = [parentNode indexPath]; |
[treeController setSelectionIndexPath:parentIndex]; |
} |
else |
{ |
// no parent exists (we are at the top of tree), so make no selection in our outline |
NSArray *selectionIndexPaths = [treeController selectionIndexPaths]; |
[treeController removeSelectionIndexPaths:selectionIndexPaths]; |
} |
} |
} |
|
// ------------------------------------------------------------------------------- |
// performAddFolder:treeAddition |
// ------------------------------------------------------------------------------- |
- (void)performAddFolder:(TreeAdditionObj *)treeAddition |
{ |
// NSTreeController inserts objects using NSIndexPath, so we need to calculate this |
NSIndexPath *indexPath = nil; |
|
// if there is no selection, we will add a new group to the end of the contents array |
if ([[treeController selectedObjects] count] == 0) |
{ |
// there's no selection so add the folder to the top-level and at the end |
indexPath = [NSIndexPath indexPathWithIndex:self.contents.count]; |
} |
else |
{ |
// get the index of the currently selected node, then add the number its children to the path - |
// this will give us an index which will allow us to add a node to the end of the currently selected node's children array. |
// |
indexPath = [treeController selectionIndexPath]; |
if ([[[treeController selectedObjects] objectAtIndex:0] isLeaf]) |
{ |
// user is trying to add a folder on a selected child, |
// so deselect child and select its parent for addition |
[self selectParentFromSelection]; |
} |
else |
{ |
indexPath = [indexPath indexPathByAddingIndex:[[[[treeController selectedObjects] objectAtIndex:0] children] count]]; |
} |
} |
|
ChildNode *node = [[ChildNode alloc] init]; |
node.nodeTitle = [treeAddition nodeName]; |
|
// the user is adding a child node, tell the controller directly |
[treeController insertObject:node atArrangedObjectIndexPath:indexPath]; |
|
} |
|
// ------------------------------------------------------------------------------- |
// addFolder:folderName |
// ------------------------------------------------------------------------------- |
- (void)addFolder:(NSString *)folderName |
{ |
TreeAdditionObj *treeObjInfo = [[TreeAdditionObj alloc] initWithURL:nil withName:folderName selectItsParent:NO]; |
[self performAddFolder:treeObjInfo]; |
} |
|
// ------------------------------------------------------------------------------- |
// addFolderAction:sender: |
// ------------------------------------------------------------------------------- |
- (IBAction)addFolderAction:(id)sender |
{ |
[self addFolder:UNTITLED_NAME]; |
} |
|
// ------------------------------------------------------------------------------- |
// performAddChild:treeAddition |
// ------------------------------------------------------------------------------- |
- (void)performAddChild:(TreeAdditionObj *)treeAddition |
{ |
if ([[treeController selectedObjects] count] > 0) |
{ |
// we have a selection |
if ([[[treeController selectedObjects] objectAtIndex:0] isLeaf]) |
{ |
// trying to add a child to a selected leaf node, so select its parent for add |
[self selectParentFromSelection]; |
} |
} |
|
// find the selection to insert our node |
NSIndexPath *indexPath; |
if ([[treeController selectedObjects] count] > 0) |
{ |
// we have a selection, insert at the end of the selection |
indexPath = [treeController selectionIndexPath]; |
indexPath = [indexPath indexPathByAddingIndex:[[[[treeController selectedObjects] objectAtIndex:0] children] count]]; |
} |
else |
{ |
// no selection, just add the child to the end of the tree |
indexPath = [NSIndexPath indexPathWithIndex:self.contents.count]; |
} |
|
// create a leaf node |
ChildNode *node = [[ChildNode alloc] initLeaf]; |
node.urlString = [treeAddition nodeURL]; |
|
if ([treeAddition nodeURL]) |
{ |
if ([[treeAddition nodeURL] length] > 0) |
{ |
// the child to insert has a valid URL, use its display name as the node title |
if ([treeAddition nodeName]) |
node.nodeTitle = [treeAddition nodeName]; |
else |
node.nodeTitle = [[NSFileManager defaultManager] displayNameAtPath:[node urlString]]; |
} |
else |
{ |
// the child to insert will be an empty URL |
node.nodeTitle = UNTITLED_NAME; |
node.urlString = HTTP_PREFIX; |
} |
} |
|
// the user is adding a child node, tell the controller directly |
[treeController insertObject:node atArrangedObjectIndexPath:indexPath]; |
|
// adding a child automatically becomes selected by NSOutlineView, so keep its parent selected |
if ([treeAddition selectItsParent]) |
{ |
[self selectParentFromSelection]; |
} |
} |
|
// ------------------------------------------------------------------------------- |
// addChild:url:withName:selectParent |
// ------------------------------------------------------------------------------- |
- (void)addChild:(NSString *)url withName:(NSString *)nameStr selectParent:(BOOL)select |
{ |
TreeAdditionObj *treeObjInfo = [[TreeAdditionObj alloc] initWithURL:url |
withName:nameStr |
selectItsParent:select]; |
[self performAddChild:treeObjInfo]; |
} |
|
// ------------------------------------------------------------------------------- |
// addBookmarkAction:sender |
// ------------------------------------------------------------------------------- |
- (IBAction)addBookmarkAction:(id)sender |
{ |
// ask our edit sheet for information on the new child to be added |
NSDictionary *newValues = [childEditController edit:nil from:self]; |
if (![childEditController wasCancelled] && newValues) |
{ |
NSString *itemStr = [newValues objectForKey:@"name"]; |
[self addChild:[newValues objectForKey:@"url"] |
withName:([itemStr length] > 0) ? [newValues objectForKey:@"name"] : UNTITLED_NAME |
selectParent:NO]; // add empty untitled child |
} |
} |
|
// ------------------------------------------------------------------------------- |
// editChildAction:sender |
// ------------------------------------------------------------------------------- |
- (IBAction)editBookmarkAction:(id)sender |
{ |
NSIndexPath *indexPath = [treeController selectionIndexPath]; |
|
// get the selected item's name and url |
NSInteger selectedRow = [myOutlineView selectedRow]; |
BaseNode *node = [[myOutlineView itemAtRow:selectedRow] representedObject]; |
NSDictionary *editInfo = [NSDictionary dictionaryWithObjectsAndKeys: |
[node nodeTitle], @"name", |
[node urlString], @"url", |
nil]; |
|
// only open the edit alert sheet for URL leafs (not folders or file system objects) |
// |
if (([[node urlString] length] == 0) || (![[node urlString] hasPrefix:@"http://"])) |
{ |
// it's a folder or a file-system based object, just allow editing the cell title |
[myOutlineView editColumn:0 row:selectedRow withEvent:[NSApp currentEvent] select:YES]; |
} |
else |
{ |
// ask our sheet to edit these two values |
NSDictionary *newValues = [childEditController edit:editInfo from:self]; |
if (![childEditController wasCancelled] && newValues) |
{ |
// create a child node |
ChildNode *childNode = [[ChildNode alloc] initLeaf]; |
childNode.urlString = [newValues objectForKey:@"url"]; |
|
NSString *nodeStr = [newValues objectForKey:@"name"]; |
childNode.nodeTitle = ([nodeStr length] > 0) ? [newValues objectForKey:@"name"] : UNTITLED_NAME; |
// remove the current selection and replace it with the newly edited child |
[treeController remove:self]; |
[treeController insertObject:childNode atArrangedObjectIndexPath:indexPath]; |
} |
} |
} |
|
// ------------------------------------------------------------------------------- |
// addEntries:discloseParent: |
// ------------------------------------------------------------------------------- |
- (void)addEntries:(NSDictionary *)entries discloseParent:(BOOL)discloseParent |
{ |
for (id entry in entries) |
{ |
if ([entry isKindOfClass:[NSDictionary class]]) |
{ |
NSString *urlStr = [entry objectForKey:KEY_URL]; |
|
if ([entry objectForKey:KEY_SEPARATOR]) |
{ |
// its a separator mark, we treat is as a leaf |
[self addChild:nil withName:nil selectParent:YES]; |
} |
else if ([entry objectForKey:KEY_FOLDER]) |
{ |
// we treat file system folders as a leaf and show its contents in the NSCollectionView |
NSString *folderName = [entry objectForKey:KEY_FOLDER]; |
[self addChild:urlStr withName:folderName selectParent:YES]; |
} |
else if ([entry objectForKey:KEY_URL]) |
{ |
// its a leaf item with a URL |
NSString *nameStr = [entry objectForKey:KEY_NAME]; |
[self addChild:urlStr withName:nameStr selectParent:YES]; |
} |
else |
{ |
// it's a generic container |
NSString *folderName = [entry objectForKey:KEY_GROUP]; |
[self addFolder:folderName]; |
|
// add its children |
NSDictionary *newChildren = [entry objectForKey:KEY_ENTRIES]; |
[self addEntries:newChildren discloseParent:NO]; |
|
[self selectParentFromSelection]; |
} |
} |
} |
|
if (!discloseParent) |
{ |
// inserting children automatically expands its parent, we want to close it |
if ([[treeController selectedNodes] count] > 0) |
{ |
NSTreeNode *lastSelectedNode = [[treeController selectedNodes] objectAtIndex:0]; |
[myOutlineView collapseItem:lastSelectedNode]; |
} |
} |
} |
|
// ------------------------------------------------------------------------------- |
// populateOutline |
// |
// Populate the tree controller from disk-based dictionary (Outline.dict) |
// ------------------------------------------------------------------------------- |
- (void)populateOutline |
{ |
// add the "Bookmarks" section |
[self addFolder:BOOKMARKS_NAME]; |
|
NSDictionary *initData = [NSDictionary dictionaryWithContentsOfFile: |
[[NSBundle mainBundle] pathForResource:INITIAL_INFODICT ofType:@"dict"]]; |
NSDictionary *entries = [initData objectForKey:KEY_ENTRIES]; |
[self addEntries:entries discloseParent:YES]; |
|
[self selectParentFromSelection]; |
} |
|
// ------------------------------------------------------------------------------- |
// addPlacesSection |
// ------------------------------------------------------------------------------- |
- (void)addPlacesSection |
{ |
// add the "Places" section |
[self addFolder:PLACES_NAME]; |
|
// add its children |
[self addChild:NSHomeDirectory() withName:@"Home" selectParent:YES]; |
|
NSArray *appsDirectory = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSLocalDomainMask, YES); |
[self addChild:[appsDirectory objectAtIndex:0] withName:nil selectParent:YES]; |
|
[self selectParentFromSelection]; |
} |
|
// ------------------------------------------------------------------------------- |
// populateOutlineContents |
// ------------------------------------------------------------------------------- |
- (void)populateOutlineContents |
{ |
// hide the outline view - don't show it as we are building the content |
[myOutlineView setHidden:YES]; |
|
[self addPlacesSection]; // add the "Places" outline section |
[self populateOutline]; // add the "Bookmark" outline content |
|
// remove the current selection |
NSArray *selection = [treeController selectionIndexPaths]; |
[treeController removeSelectionIndexPaths:selection]; |
|
[myOutlineView setHidden:NO]; // we are done populating the outline view content, show it again |
} |
|
|
#pragma mark - WebView |
|
// ------------------------------------------------------------------------------- |
// webView:makeFirstResponder |
// |
// We want to keep the outline view in focus as the user clicks various URLs. |
// |
// So this workaround applies to an unwanted side affect to some web pages that might have |
// JavaScript code thatt focus their text fields as we target the web view with a particular URL. |
// |
// ------------------------------------------------------------------------------- |
- (void)webView:(WebView *)sender makeFirstResponder:(NSResponder *)responder |
{ |
if (retargetWebView) |
{ |
// we are targeting the webview ourselves as a result of the user clicking |
// a url in our outlineview: don't do anything, but reset our target check flag |
// |
retargetWebView = NO; |
} |
else |
{ |
// continue the responder chain |
[[self window] makeFirstResponder:sender]; |
} |
} |
|
|
#pragma mark - Menu management |
|
// ------------------------------------------------------------------------------- |
// validateMenuItem:item |
// ------------------------------------------------------------------------------- |
- (BOOL)validateMenuItem:(NSMenuItem *)item |
{ |
BOOL enabled = NO; |
|
// is it our "Edit..." menu item in our action button? |
if ([item action] == @selector(editBookmarkAction:)) |
{ |
if ([[treeController selectedNodes] count] > 0) |
{ |
// only allow for editing http url items or items with out a URL |
// (this avoids accidentally renaming real file system items) |
// |
NSTreeNode *firstSelectedNode = [[treeController selectedNodes] objectAtIndex:0]; |
BaseNode *node = [firstSelectedNode representedObject]; |
if (!node.urlString || [[node urlString] hasPrefix:HTTP_PREFIX]) |
enabled = YES; |
} |
} |
|
return enabled; |
} |
|
|
#pragma mark - Node checks |
|
// ------------------------------------------------------------------------------- |
// isSeparator:node |
// ------------------------------------------------------------------------------- |
- (BOOL)isSeparator:(BaseNode *)node |
{ |
return ([node nodeIcon] == nil && [[node nodeTitle] length] == 0); |
} |
|
// ------------------------------------------------------------------------------- |
// isSpecialGroup:groupNode |
// ------------------------------------------------------------------------------- |
- (BOOL)isSpecialGroup:(BaseNode *)groupNode |
{ |
return ([groupNode nodeIcon] == nil && |
([[groupNode nodeTitle] isEqualToString:BOOKMARKS_NAME] || [[groupNode nodeTitle] isEqualToString:PLACES_NAME])); |
} |
|
|
#pragma mark - Managing Views |
|
// ------------------------------------------------------------------------------- |
// contentReceived:notif |
// |
// Notification sent from IconViewController class, |
// indicating the file system content has been received |
// ------------------------------------------------------------------------------- |
- (void)contentReceived:(NSNotification *)notif |
{ |
[progIndicator setHidden:YES]; |
[progIndicator stopAnimation:self]; |
} |
|
// ------------------------------------------------------------------------------- |
// removeSubview |
// ------------------------------------------------------------------------------- |
- (void)removeSubview |
{ |
// empty selection |
NSArray *subViews = [placeHolderView subviews]; |
if ([subViews count] > 0) |
{ |
[[subViews objectAtIndex:0] removeFromSuperview]; |
} |
|
[placeHolderView displayIfNeeded]; // we want the removed views to disappear right away |
} |
|
// ------------------------------------------------------------------------------- |
// changeItemView |
// ------------------------------------------------------------------------------ |
- (void)changeItemView |
{ |
NSArray *selection = [treeController selectedNodes]; |
if ([selection count] > 0) |
{ |
BaseNode *node = [[selection objectAtIndex:0] representedObject]; |
NSString *urlStr = [node urlString]; |
if (urlStr) |
{ |
if ([urlStr hasPrefix:HTTP_PREFIX]) |
{ |
// 1) the url is a web-based url |
// |
if (currentView != webView) |
{ |
// change to web view |
[self removeSubview]; |
currentView = nil; |
[placeHolderView addSubview:webView]; |
currentView = webView; |
} |
|
// this will tell our WebUIDelegate not to retarget first responder since some web pages force |
// forus to their text fields - we want to keep our outline view in focus. |
retargetWebView = YES; |
|
[webView setMainFrameURL:urlStr]; // re-target to the new url |
} |
else |
{ |
// 2) the url is file-system based (folder or file) |
// |
if (currentView != [fileViewController view] || currentView != [iconViewController view]) |
{ |
NSURL *targetURL = [NSURL fileURLWithPath:urlStr]; |
|
NSURL *url = [NSURL fileURLWithPath:[node urlString]]; |
|
// detect if the url is a directory |
NSNumber *isDirectory = nil; |
|
[url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; |
if ([isDirectory boolValue]) |
{ |
// avoid a flicker effect by not removing the icon view if it is already embedded |
if (!(currentView == [iconViewController view])) |
{ |
// remove the old subview |
[self removeSubview]; |
currentView = nil; |
} |
|
// change to icon view to display folder contents |
[placeHolderView addSubview:[iconViewController view]]; |
currentView = [iconViewController view]; |
|
// its a directory - show its contents using NSCollectionView |
iconViewController.url = targetURL; |
|
// add a spinning progress gear in case populating the icon view takes too long |
[progIndicator setHidden:NO]; |
[progIndicator startAnimation:self]; |
|
// note: we will be notifed back to stop our progress indicator |
// as soon as iconViewController is done fetching its content. |
} |
else |
{ |
// 3) its a file, just show the item info |
// |
// remove the old subview |
[self removeSubview]; |
currentView = nil; |
|
// change to file view |
[placeHolderView addSubview:[fileViewController view]]; |
currentView = [fileViewController view]; |
|
// update the file's info |
fileViewController.url = targetURL; |
} |
} |
} |
|
NSRect newBounds; |
newBounds.origin.x = 0; |
newBounds.origin.y = 0; |
newBounds.size.width = [[currentView superview] frame].size.width; |
newBounds.size.height = [[currentView superview] frame].size.height; |
[currentView setFrame:[[currentView superview] frame]]; |
|
// make sure our added subview is placed and resizes correctly |
[currentView setFrameOrigin:NSMakePoint(0,0)]; |
[currentView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; |
} |
else |
{ |
// there's no url associated with this node |
// so a container was selected - no view to display |
[self removeSubview]; |
currentView = nil; |
} |
} |
} |
|
|
#pragma mark - NSOutlineViewDelegate |
|
// ------------------------------------------------------------------------------- |
// shouldSelectItem:item |
// ------------------------------------------------------------------------------- |
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item; |
{ |
// don't allow special group nodes (Devices and Places) to be selected |
BaseNode *node = [item representedObject]; |
return (![self isSpecialGroup:node] && ![self isSeparator:node]); |
} |
|
// ------------------------------------------------------------------------------- |
// dataCellForTableColumn:tableColumn:item |
// ------------------------------------------------------------------------------- |
- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item |
{ |
NSCell *returnCell = [tableColumn dataCell]; |
|
if ([[tableColumn identifier] isEqualToString:COLUMNID_NAME]) |
{ |
// we are being asked for the cell for the single and only column |
BaseNode *node = [item representedObject]; |
if ([self isSeparator:node]) |
returnCell = separatorCell; |
} |
|
return returnCell; |
} |
|
// ------------------------------------------------------------------------------- |
// textShouldEndEditing:fieldEditor |
// ------------------------------------------------------------------------------- |
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor |
{ |
if ([[fieldEditor string] length] == 0) |
{ |
// don't allow empty node names |
return NO; |
} |
else |
{ |
return YES; |
} |
} |
|
// ------------------------------------------------------------------------------- |
// shouldEditTableColumn:tableColumn:item |
// |
// Decide to allow the edit of the given outline view "item". |
// ------------------------------------------------------------------------------- |
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item |
{ |
BOOL result = YES; |
|
item = [item representedObject]; |
if ([self isSpecialGroup:item]) |
{ |
result = NO; // don't allow special group nodes to be renamed |
} |
else |
{ |
if ([[item urlString] isAbsolutePath]) |
result = NO; // don't allow file system objects to be renamed |
} |
|
return result; |
} |
|
// ------------------------------------------------------------------------------- |
// outlineView:willDisplayCell:forTableColumn:item |
// ------------------------------------------------------------------------------- |
- (void)outlineView:(NSOutlineView *)olv willDisplayCell:(NSCell*)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item |
{ |
if ([[tableColumn identifier] isEqualToString:COLUMNID_NAME]) |
{ |
// we are displaying the single and only column |
if ([cell isKindOfClass:[ImageAndTextCell class]]) |
{ |
item = [item representedObject]; |
if (item) |
{ |
if ([item isLeaf]) |
{ |
// does it have a URL string? |
NSString *urlStr = [item urlString]; |
if (urlStr) |
{ |
if ([item isLeaf]) |
{ |
NSImage *iconImage; |
if ([[item urlString] hasPrefix:HTTP_PREFIX]) |
iconImage = urlImage; |
else |
iconImage = [[NSWorkspace sharedWorkspace] iconForFile:urlStr]; |
[item setNodeIcon:iconImage]; |
} |
else |
{ |
NSImage* iconImage = [[NSWorkspace sharedWorkspace] iconForFile:urlStr]; |
[item setNodeIcon:iconImage]; |
} |
} |
else |
{ |
// it's a separator, don't bother with the icon |
} |
} |
else |
{ |
// check if it's a special folder (DEVICES or PLACES), we don't want it to have an icon |
if ([self isSpecialGroup:item]) |
{ |
[item setNodeIcon:nil]; |
} |
else |
{ |
// it's a folder, use the folderImage as its icon |
[item setNodeIcon:folderImage]; |
} |
} |
} |
|
// set the cell's image |
[[item nodeIcon] setSize:NSMakeSize(kIconImageSize, kIconImageSize)]; |
[(ImageAndTextCell*)cell setImage:[item nodeIcon]]; |
} |
} |
} |
|
// ------------------------------------------------------------------------------- |
// outlineViewSelectionDidChange:notification |
// ------------------------------------------------------------------------------- |
- (void)outlineViewSelectionDidChange:(NSNotification *)notification |
{ |
// ask the tree controller for the current selection |
NSArray *selection = [treeController selectedObjects]; |
if ([selection count] > 1) |
{ |
// multiple selection - clear the right side view |
[self removeSubview]; |
currentView = nil; |
} |
else |
{ |
if ([selection count] == 1) |
{ |
// single selection |
[self changeItemView]; |
} |
else |
{ |
// there is no current selection - no view to display |
[self removeSubview]; |
currentView = nil; |
} |
} |
} |
|
// ---------------------------------------------------------------------------------------- |
// outlineView:isGroupItem:item |
// ---------------------------------------------------------------------------------------- |
- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item |
{ |
return ([self isSpecialGroup:[item representedObject]] ? YES : NO); |
} |
|
|
#pragma mark - NSOutlineView drag and drop |
|
// ---------------------------------------------------------------------------------------- |
// draggingSourceOperationMaskForLocal <NSDraggingSource override> |
// ---------------------------------------------------------------------------------------- |
- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal |
{ |
return NSDragOperationMove; |
} |
|
// ---------------------------------------------------------------------------------------- |
// outlineView:writeItems:toPasteboard |
// ---------------------------------------------------------------------------------------- |
- (BOOL)outlineView:(NSOutlineView *)ov writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard |
{ |
[pboard declareTypes:[NSArray arrayWithObjects:kNodesPBoardType, nil] owner:self]; |
|
// keep track of this nodes for drag feedback in "validateDrop" |
self.dragNodesArray = items; |
|
return YES; |
} |
|
// ------------------------------------------------------------------------------- |
// outlineView:validateDrop:proposedItem:proposedChildrenIndex: |
// |
// This method is used by NSOutlineView to determine a valid drop target. |
// ------------------------------------------------------------------------------- |
- (NSDragOperation)outlineView:(NSOutlineView *)ov |
validateDrop:(id <NSDraggingInfo>)info |
proposedItem:(id)item |
proposedChildIndex:(NSInteger)index |
{ |
NSDragOperation result = NSDragOperationNone; |
|
if (!item) |
{ |
// no item to drop on |
result = NSDragOperationGeneric; |
} |
else |
{ |
if ([self isSpecialGroup:[item representedObject]]) |
{ |
// don't allow dragging into special grouped sections (i.e. Devices and Places) |
result = NSDragOperationNone; |
} |
else |
{ |
if (index == -1) |
{ |
// don't allow dropping on a child |
result = NSDragOperationNone; |
} |
else |
{ |
// drop location is a container |
result = NSDragOperationMove; |
} |
} |
} |
|
return result; |
} |
|
// ------------------------------------------------------------------------------- |
// handleWebURLDrops:pboard:withIndexPath: |
// |
// The user is dragging URLs from Safari. |
// ------------------------------------------------------------------------------- |
- (void)handleWebURLDrops:(NSPasteboard *)pboard withIndexPath:(NSIndexPath *)indexPath |
{ |
NSArray *pbArray = [pboard propertyListForType:@"WebURLsWithTitlesPboardType"]; |
NSArray *urlArray = [pbArray objectAtIndex:0]; |
NSArray *nameArray = [pbArray objectAtIndex:1]; |
|
NSInteger i; |
for (i = ([urlArray count] - 1); i >=0; i--) |
{ |
ChildNode *node = [[ChildNode alloc] init]; |
|
node.isLeaf = YES; |
|
node.nodeTitle = [nameArray objectAtIndex:i]; |
|
node.urlString = [urlArray objectAtIndex:i]; |
[treeController insertObject:node atArrangedObjectIndexPath:indexPath]; |
|
} |
} |
|
// ------------------------------------------------------------------------------- |
// handleInternalDrops:pboard:withIndexPath: |
// |
// The user is doing an intra-app drag within the outline view. |
// ------------------------------------------------------------------------------- |
- (void)handleInternalDrops:(NSPasteboard *)pboard withIndexPath:(NSIndexPath *)indexPath |
{ |
// user is doing an intra app drag within the outline view: |
// |
NSArray* newNodes = self.dragNodesArray; |
|
// move the items to their new place (we do this backwards, otherwise they will end up in reverse order) |
NSInteger idx; |
for (idx = ([newNodes count] - 1); idx >= 0; idx--) |
{ |
[treeController moveNode:[newNodes objectAtIndex:idx] toIndexPath:indexPath]; |
} |
|
// keep the moved nodes selected |
NSMutableArray *indexPathList = [NSMutableArray array]; |
for (NSUInteger i = 0; i < [newNodes count]; i++) |
{ |
[indexPathList addObject:[[newNodes objectAtIndex:i] indexPath]]; |
} |
[treeController setSelectionIndexPaths: indexPathList]; |
} |
|
// ------------------------------------------------------------------------------- |
// handleFileBasedDrops:pboard:withIndexPath: |
// |
// The user is dragging file-system based objects (probably from Finder) |
// ------------------------------------------------------------------------------- |
- (void)handleFileBasedDrops:(NSPasteboard *)pboard withIndexPath:(NSIndexPath *)indexPath |
{ |
NSArray *fileNames = [pboard propertyListForType:NSFilenamesPboardType]; |
if ([fileNames count] > 0) |
{ |
NSInteger i; |
NSInteger count = [fileNames count]; |
|
for (i = (count - 1); i >=0; i--) |
{ |
ChildNode *node = [[ChildNode alloc] init]; |
|
NSURL *url = [NSURL fileURLWithPath:[fileNames objectAtIndex:i]]; |
NSString *name = [[NSFileManager defaultManager] displayNameAtPath:[url path]]; |
node.isLeaf = YES; |
|
node.nodeTitle = name; |
node.urlString = [url path]; |
|
[treeController insertObject:node atArrangedObjectIndexPath:indexPath]; |
|
} |
} |
} |
|
// ------------------------------------------------------------------------------- |
// handleURLBasedDrops:pboard:withIndexPath: |
// |
// Handle dropping a raw URL. |
// ------------------------------------------------------------------------------- |
- (void)handleURLBasedDrops:(NSPasteboard *)pboard withIndexPath:(NSIndexPath *)indexPath |
{ |
NSURL *url = [NSURL URLFromPasteboard:pboard]; |
if (url) |
{ |
ChildNode *node = [[ChildNode alloc] init]; |
|
if ([url isFileURL]) |
{ |
// url is file-based, use it's display name |
NSString *name = [[NSFileManager defaultManager] displayNameAtPath:[url path]]; |
node.nodeTitle = name; |
node.urlString = [url path]; |
} |
else |
{ |
// url is non-file based (probably from Safari) |
// |
// the url might not end with a valid component name, use the best possible title from the URL |
if ([[[url path] pathComponents] count] == 1) |
{ |
if ([[url absoluteString] hasPrefix:HTTP_PREFIX]) |
{ |
// use the url portion without the prefix |
NSRange prefixRange = [[url absoluteString] rangeOfString:HTTP_PREFIX]; |
NSRange newRange = NSMakeRange(prefixRange.length, [[url absoluteString] length]- prefixRange.length - 1); |
node.nodeTitle = [[url absoluteString] substringWithRange:newRange]; |
} |
else |
{ |
// prefix unknown, just use the url as its title |
node.nodeTitle = [url absoluteString]; |
} |
} |
else |
{ |
// use the last portion of the URL as its title |
node.nodeTitle = [[url path] lastPathComponent]; |
} |
|
node.urlString = [url absoluteString]; |
} |
node.isLeaf = YES; |
|
[treeController insertObject:node atArrangedObjectIndexPath:indexPath]; |
|
} |
} |
|
// ------------------------------------------------------------------------------- |
// outlineView:acceptDrop:item:childIndex |
// |
// This method is called when the mouse is released over an outline view that previously decided to allow a drop |
// via the validateDrop method. The data source should incorporate the data from the dragging pasteboard at this time. |
// 'index' is the location to insert the data as a child of 'item', and are the values previously set in the validateDrop: method. |
// |
// ------------------------------------------------------------------------------- |
- (BOOL)outlineView:(NSOutlineView*)ov acceptDrop:(id <NSDraggingInfo>)info item:(id)targetItem childIndex:(NSInteger)index |
{ |
// note that "targetItem" is a NSTreeNode proxy |
// |
BOOL result = NO; |
|
// find the index path to insert our dropped object(s) |
NSIndexPath *indexPath; |
if (targetItem) |
{ |
// drop down inside the tree node: |
// feth the index path to insert our dropped node |
indexPath = [[targetItem indexPath] indexPathByAddingIndex:index]; |
} |
else |
{ |
// drop at the top root level |
if (index == -1) // drop area might be ambibuous (not at a particular location) |
indexPath = [NSIndexPath indexPathWithIndex:self.contents.count]; // drop at the end of the top level |
else |
indexPath = [NSIndexPath indexPathWithIndex:index]; // drop at a particular place at the top level |
} |
|
NSPasteboard *pboard = [info draggingPasteboard]; // get the pasteboard |
|
// check the dragging type - |
if ([pboard availableTypeFromArray:[NSArray arrayWithObject:kNodesPBoardType]]) |
{ |
// user is doing an intra-app drag within the outline view |
[self handleInternalDrops:pboard withIndexPath:indexPath]; |
result = YES; |
} |
else if ([pboard availableTypeFromArray:[NSArray arrayWithObject:@"WebURLsWithTitlesPboardType"]]) |
{ |
// the user is dragging URLs from Safari |
[self handleWebURLDrops:pboard withIndexPath:indexPath]; |
result = YES; |
} |
else if ([pboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]) |
{ |
// the user is dragging file-system based objects (probably from Finder) |
[self handleFileBasedDrops:pboard withIndexPath:indexPath]; |
result = YES; |
} |
else if ([pboard availableTypeFromArray:[NSArray arrayWithObject:NSURLPboardType]]) |
{ |
// handle dropping a raw URL |
[self handleURLBasedDrops:pboard withIndexPath:indexPath]; |
result = YES; |
} |
|
return result; |
} |
|
|
#pragma mark - NSSplitViewDelegate |
|
// ------------------------------------------------------------------------------- |
// splitView:constrainMinCoordinate: |
// |
// What you really have to do to set the minimum size of both subviews to kMinOutlineViewSplit points. |
// ------------------------------------------------------------------------------- |
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedCoordinate ofSubviewAt:(int)index |
{ |
return proposedCoordinate + kMinOutlineViewSplit; |
} |
|
// ------------------------------------------------------------------------------- |
// splitView:constrainMaxCoordinate: |
// ------------------------------------------------------------------------------- |
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedCoordinate ofSubviewAt:(int)index |
{ |
return proposedCoordinate - kMinOutlineViewSplit; |
} |
|
// ------------------------------------------------------------------------------- |
// splitView:resizeSubviewsWithOldSize: |
// |
// Keep the left split pane from resizing as the user moves the divider line. |
// ------------------------------------------------------------------------------- |
- (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize |
{ |
NSRect newFrame = [sender frame]; // get the new size of the whole splitView |
NSView *left = [[sender subviews] objectAtIndex:0]; |
NSRect leftFrame = [left frame]; |
NSView *right = [[sender subviews] objectAtIndex:1]; |
NSRect rightFrame = [right frame]; |
|
CGFloat dividerThickness = [sender dividerThickness]; |
|
leftFrame.size.height = newFrame.size.height; |
|
rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness; |
rightFrame.size.height = newFrame.size.height; |
rightFrame.origin.x = leftFrame.size.width + dividerThickness; |
|
[left setFrame:leftFrame]; |
[right setFrame:rightFrame]; |
} |
|
@end |