Index: chrome/browser/cocoa/bookmark_tree_controller_pasteboard.mm |
diff --git a/chrome/browser/cocoa/bookmark_tree_controller_pasteboard.mm b/chrome/browser/cocoa/bookmark_tree_controller_pasteboard.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4b94c5c2c368a9f16fa0e33e7786997ad0e688ef |
--- /dev/null |
+++ b/chrome/browser/cocoa/bookmark_tree_controller_pasteboard.mm |
@@ -0,0 +1,439 @@ |
+// Copyright (c) 2009 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "chrome/browser/cocoa/bookmark_tree_controller.h" |
+ |
+#include "base/sys_string_conversions.h" |
+#include "chrome/browser/bookmarks/bookmark_model.h" |
+#import "chrome/browser/cocoa/bookmark_manager_controller.h" |
+#include "googleurl/src/gurl.h" |
+ |
+ |
+// Safari uses this type, though it's not declared in any header. |
+static NSString* const BookmarkDictionaryListPboardType = |
+ @"BookmarkDictionaryListPboardType"; |
+ |
+// Mac WebKit uses this type, declared in WebKit/mac/History/WebURLsWithTitles.h |
+static NSString* const WebURLsWithTitlesPboardType = |
+ @"WebURLsWithTitlesPboardType"; |
+ |
+// Used internally to identify intra-outline drags. |
+static NSString* const kCustomPboardType = |
+ @"ChromeBookmarkTreeControllerPlaceholderType"; |
+ |
+ |
+@implementation BookmarkTreeController (Pasteboard) |
+ |
+ |
+// One-time dnd setup; called from -awakeFromNib. |
+- (void)registerDragTypes { |
+ [outline_ registerForDraggedTypes:[NSArray arrayWithObjects: |
+ BookmarkDictionaryListPboardType, |
+ WebURLsWithTitlesPboardType, |
+ NSURLPboardType, nil]]; |
+ [outline_ setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; |
+ [outline_ setDraggingSourceOperationMask:NSDragOperationEvery forLocal:NO]; |
+} |
+ |
+// Selects a range of items in a parent node. |
+- (void)selectNodesInFolder:(const BookmarkNode*)parent |
+ atIndexes:(NSRange)childRange { |
+ DCHECK(NSMaxRange(childRange) <= (NSUInteger)parent->GetChildCount()); |
+ id parentItem = [self itemFromNode:parent]; |
+ if (parentItem != nil) { |
+ // If parent is not the root, need to offset range by parent's index: |
+ int startRow = [outline_ rowForItem:parentItem]; |
+ if (startRow < 0) { |
+ return; |
+ } |
+ if ([outline_ isItemExpanded:parentItem]) { |
+ childRange.location += startRow + 1; |
+ } else { |
+ childRange.location = startRow; |
+ childRange.length = 1; |
+ } |
+ } |
+ NSIndexSet* indexes = [NSIndexSet indexSetWithIndexesInRange:childRange]; |
+ [outline_ selectRowIndexes:indexes byExtendingSelection:NO]; |
+} |
+ |
+ |
+#pragma mark - |
+#pragma mark DRAGGING OUT AND COPYING: |
+ |
+ |
+// Generates parallel arrays of URLs and titles for contents of a node. |
+static void flattenNode(const BookmarkNode* node, |
+ NSMutableArray* urlStrings, |
+ NSMutableArray* titles) { |
+ if (node->is_folder()) { |
+ for (int i = 0; i < node->GetChildCount(); i++) { |
+ flattenNode(node->GetChild(i), urlStrings, titles); |
+ } |
+ } else if (node->is_url()) { |
+ [urlStrings addObject:base::SysUTF8ToNSString( |
+ node->GetURL().possibly_invalid_spec())]; |
+ [titles addObject:base::SysWideToNSString(node->GetTitle())]; |
+ } |
+} |
+ |
+// Writes data to the pasteboard given a list of row items. |
+- (BOOL)writeItems:(NSArray*)items |
+ toPasteboard:(NSPasteboard*)pb |
+ includeCustom:(BOOL)includeCustom { |
+ if ([items count] == 0) { |
+ return NO; |
+ } |
+ |
+ [pb declareTypes:[NSMutableArray arrayWithObjects: |
+ WebURLsWithTitlesPboardType, |
+ NSStringPboardType, nil] |
+ owner:self]; |
+ |
+ // Add URLs and titles: |
+ NSMutableArray* urls = [NSMutableArray array]; |
+ NSMutableArray* titles = [NSMutableArray array]; |
+ for (id item in items) { |
+ flattenNode([self nodeFromItem:item], urls, titles); |
+ } |
+ [pb setPropertyList:[NSArray arrayWithObjects:urls, titles, nil] |
+ forType:WebURLsWithTitlesPboardType]; |
+ |
+ // Add plain text, as one URL per line: |
+ [pb setString:[urls componentsJoinedByString:@"\n"] |
+ forType:NSStringPboardType]; |
+ |
+ // Add custom type. The actual data doesn't matter since kCustomPboardType |
+ // drags aren't recognized by anyone but us. |
+ if (includeCustom) { |
+ draggedNodes_.clear(); |
+ for (id item in items) { |
+ draggedNodes_.push_back([self nodeFromItem:item]); |
+ } |
+ [pb addTypes:[NSArray arrayWithObject: kCustomPboardType] owner: self]; |
+ [pb setData:[NSData data] forType:kCustomPboardType]; |
+ } |
+ |
+ // Add single URL: |
+ if ([urls count] == 1) { |
+ [pb addTypes:[NSArray arrayWithObject: NSURLPboardType] owner: self]; |
+ NSString* firstURLStr = [urls objectAtIndex:0]; |
+ [pb setString:firstURLStr forType:NSURLPboardType]; |
+ } |
+ return YES; |
+} |
+ |
+// Invoked when dragging outline-view rows. |
+- (BOOL)outlineView:(NSOutlineView*)outlineView |
+ writeItems:(NSArray*)items |
+ toPasteboard:(NSPasteboard*)pb { |
+ [self writeItems:items toPasteboard:pb includeCustom:YES]; |
+ return YES; |
+} |
+ |
+ |
+// The Cut command. |
+- (IBAction)cut:(id)sender { |
+ if ([self writeItems:[self selectedItems] |
+ toPasteboard:[NSPasteboard generalPasteboard] |
+ includeCustom:NO]) { |
+ [self delete:self]; |
+ } else { |
+ NSBeep(); |
+ } |
+} |
+ |
+// The Copy command. |
+- (IBAction)copy:(id)sender { |
+ if (![self copyToPasteboard:[NSPasteboard generalPasteboard]]) |
+ NSBeep(); |
+} |
+ |
+// Copy to any pasteboard. |
+- (BOOL)copyToPasteboard:(NSPasteboard*)pb { |
+ return [self writeItems:[self selectedItems] |
viettrungluu
2010/01/04 21:19:01
Unindent 2 spaces.
|
+ toPasteboard:pb |
+ includeCustom:NO]; |
+} |
+ |
+ |
+#pragma mark - |
+#pragma mark INCOMING DRAGS AND PASTING: |
+ |
+ |
+// BookmarkDictionaryListPboardType represents bookmarks as dictionaries, |
+// which have the following keys. |
+// Strangely, folder nodes (whose WebBookmarkType is WebBookmarkTypeLeaf) have |
+// their title under 'Title', while leaf nodes have it in 'URIDictionary.title'. |
+static const NSString* kTitleKey = @"Title"; |
+static const NSString* kURIDictionaryKey = @"URIDictionary"; |
+static const NSString* kURIDictTitleKey = @"title"; |
+static const NSString* kURLStringKey = @"URLString"; |
+static const NSString* kTypeKey = @"WebBookmarkType"; |
+static const NSString* kLeafType = @"WebBookmarkTypeLeaf"; |
+//static const NSString* kListType = @"WebBookmarkTypeList"; // unused for now |
+static const NSString* kChildrenKey = @"Children"; |
+ |
+// Helper that creates a dictionary in BookmarkDictionaryListPboardType format. |
+// |name| may be nil, but |urlStr| is required. |
+static NSDictionary* makeBookmarkPlistEntry(NSString* name, NSString* urlStr) { |
+ if (!name) { |
+ name = urlStr; |
+ } |
+ NSDictionary* nameDict = [NSDictionary dictionaryWithObject:name |
+ forKey:kURIDictTitleKey]; |
+ return [NSDictionary dictionaryWithObjectsAndKeys: |
+ kLeafType, kTypeKey, |
+ nameDict, kURIDictionaryKey, |
+ urlStr, kURLStringKey, |
+ nil]; |
+} |
+ |
+// Reads URL(s) off the pasteboard and returns them in BookmarkDictionaryList- |
+// PboardType format, or nil on failure. |
+- (NSArray*)readPropertyListFromPasteboard:(NSPasteboard*)pb { |
+ NSString* type = [pb availableTypeFromArray: |
+ [outline_ registeredDraggedTypes]]; |
+ if ([type isEqualToString:BookmarkDictionaryListPboardType]) { |
+ // Safari's full bookmark plist type: |
+ return [pb propertyListForType:type]; |
+ |
+ } else if ([type isEqualToString:WebURLsWithTitlesPboardType]) { |
+ // Safari's parallel-URLs-and-titles type: |
+ NSArray* contents = [pb propertyListForType:type]; |
+ NSArray* urlStrings = [contents objectAtIndex:0]; |
+ NSArray* titles = [contents objectAtIndex:1]; |
+ NSUInteger n = [urlStrings count]; |
+ if (n == 0 || [titles count] != n) { |
+ return nil; |
+ } |
+ NSMutableArray* plist = [NSMutableArray array]; |
+ for (NSUInteger i = 0; i < n; i++) { |
+ [plist addObject:makeBookmarkPlistEntry([titles objectAtIndex:i], |
+ [urlStrings objectAtIndex:i])]; |
+ } |
+ return plist; |
+ |
+ } else if ([type isEqualToString:NSURLPboardType]) { |
+ // Standard URL type: |
+ NSString* urlStr = [[NSURL URLFromPasteboard:pb] absoluteString]; |
+ if (!urlStr) { |
+ return nil; |
+ } |
+ NSString* title = [pb stringForType:@"public.url-name"]; |
+ if (!title) |
+ title = [pb stringForType:NSStringPboardType]; |
+ return [NSArray arrayWithObject:makeBookmarkPlistEntry(title, urlStr)]; |
+ |
+ } else { |
+ return nil; |
+ } |
+} |
+ |
+ |
+// Moves BookmarkNodes into a parent folder, then selects them. |
+- (void)moveNodes:(std::vector<const BookmarkNode*>)nodes |
+ toFolder:(const BookmarkNode*)dstParent |
+ atIndex:(int)dstIndex { |
+ for (std::vector<const BookmarkNode*>::iterator it = nodes.begin(); |
+ it != nodes.end(); ++it) { |
+ // Use an autorelease pool to clean up after the various observers that |
+ // get called after each individual bookmark change. |
+ NSAutoreleasePool* pool = [NSAutoreleasePool new]; |
+ const BookmarkNode* srcNode = *it; |
+ const BookmarkNode* srcParent = srcNode->GetParent(); |
+ int srcIndex = srcParent->IndexOfChild(srcNode); |
+ [manager_ bookmarkModel]->Move(srcNode, dstParent, dstIndex); |
+ if (srcParent != dstParent || srcIndex >= dstIndex) { |
+ dstIndex++; |
+ } |
+ [pool drain]; |
+ } |
+ |
+ [self selectNodesInFolder:dstParent |
+ atIndexes:NSMakeRange(dstIndex - nodes.size(), nodes.size())]; |
+} |
+ |
+// Inserts bookmarks in BookmarkDictionaryListPboardType into a folder node. |
+- (void)insertPropertyList:(NSArray*)plist |
+ inFolder:(const BookmarkNode*)dstParent |
+ atIndex:(NSInteger)dstIndex { |
+ BookmarkModel* model = [manager_ bookmarkModel]; |
+ NSInteger i = 0; |
+ for (NSDictionary* plistItem in plist) { |
+ // Use an autorelease pool to clean up after the various observers that |
+ // get called after each individual bookmark change. |
+ NSAutoreleasePool* pool = [NSAutoreleasePool new]; |
+ if ([[plistItem objectForKey:kTypeKey] isEqual:kLeafType]) { |
+ NSString* title = [[plistItem objectForKey:kURIDictionaryKey] |
+ objectForKey:kURIDictTitleKey]; |
+ NSString* urlStr = [plistItem objectForKey:kURLStringKey]; |
+ if (title && urlStr) { |
+ model->AddURL(dstParent, |
+ dstIndex + i, |
+ base::SysNSStringToWide(title), |
+ GURL(base::SysNSStringToUTF8(urlStr))); |
+ ++i; |
+ } |
+ } else { |
+ NSString* title = [plistItem objectForKey:kTitleKey]; |
+ NSArray* children = [plistItem objectForKey:kChildrenKey]; |
+ if (title && children) { |
+ const BookmarkNode* newFolder; |
+ newFolder = model->AddGroup(dstParent, |
+ dstIndex + i, |
+ base::SysNSStringToWide(title)); |
+ ++i; |
+ [self insertPropertyList:children |
+ inFolder:newFolder |
+ atIndex:0]; |
+ } |
+ } |
+ [pool drain]; |
+ } |
+ [self selectNodesInFolder:dstParent |
+ atIndexes:NSMakeRange(dstIndex, [plist count])]; |
+} |
+ |
+ |
+// Validates whether or not the proposed drop is valid. |
+- (NSDragOperation)outlineView:(NSOutlineView*)outlineView |
+ validateDrop:(id <NSDraggingInfo>)info |
+ proposedItem:(id)item |
+ proposedChildIndex:(NSInteger)childIndex { |
+ NSPasteboard* pb = [info draggingPasteboard]; |
+ |
+ // Check to see what we are proposed to be dropping on |
+ const BookmarkNode*targetNode = [self nodeFromItem:item]; |
+ if (!targetNode->is_folder()) { |
+ // The target node is not a container, but a leaf. |
+ // Refuse the drop (we may get called again with a between) |
+ if (childIndex == NSOutlineViewDropOnItemIndex) { |
+ return NSDragOperationNone; |
+ } |
+ } |
+ |
+ // Dragging within the outline? |
+ if ([info draggingSource] == outlineView && |
+ [[pb types] containsObject:kCustomPboardType]) { |
+ // If we are allowing the drop, we see if we are dragging from ourselves |
+ // and dropping into a descendent, which wouldn't be allowed... |
+ // See if the appropriate drag information is available on the pasteboard. |
+ //TODO(snej): Re-implement this |
+ /* |
+ if (targetNode != group_ && |
+ [[[info draggingPasteboard] types] containsObject:kCustomPboardType]) { |
+ for (NSDictionary* draggedNode in draggedNodes_) { |
+ if ([self treeNode:targetNode isDescendantOfNode:draggedNode]) { |
+ // Yup, it is, refuse it. |
+ return NSDragOperationNone; |
+ break; |
+ } |
+ } |
+ */ |
+ return NSDragOperationMove; |
+ } |
+ |
+ // Drag from elsewhere is a copy. |
+ return NSDragOperationCopy; |
+} |
+ |
+// Actually handles the drop. |
+- (BOOL)outlineView:(NSOutlineView*)outlineView |
+ acceptDrop:(id <NSDraggingInfo>)info |
+ item:(id)item |
+ childIndex:(NSInteger)childIndex |
+{ |
+ NSPasteboard* pb = [info draggingPasteboard]; |
+ |
+ const BookmarkNode* targetNode = [self nodeFromItem:item]; |
+ |
+ // Determine the parent to insert into and the child index to insert at. |
+ if (!targetNode->is_folder()) { |
+ // If our target is a leaf, and we are dropping on it. |
+ if (childIndex == NSOutlineViewDropOnItemIndex) { |
+ return NO; |
+ } else { |
+ // We will be dropping on the item's parent at the target index |
+ // of this child, plus one. |
+ const BookmarkNode* oldTargetNode = targetNode; |
+ targetNode = targetNode->GetParent(); |
+ childIndex = targetNode->IndexOfChild(oldTargetNode) + 1; |
+ } |
+ } else { |
+ if (childIndex == NSOutlineViewDropOnItemIndex) { |
+ // Insert it at the end, if we were dropping on it |
+ childIndex = targetNode->GetChildCount(); |
+ } |
+ } |
+ |
+ if ([info draggingSource] == outlineView && |
+ [[pb types] containsObject:kCustomPboardType]) { |
+ // If the source was ourselves, move the selected nodes. |
+ [self moveNodes:draggedNodes_ |
+ toFolder:targetNode |
+ atIndex:childIndex]; |
+ } else { |
+ NSArray* plist = [self readPropertyListFromPasteboard:pb]; |
+ if (!plist) { |
+ return NO; |
+ } |
+ [self insertPropertyList:plist |
+ inFolder:targetNode |
+ atIndex:childIndex]; |
+ } |
+ return YES; |
+} |
+ |
+ |
+// The Paste command. |
+- (IBAction)paste:(id)sender { |
+ if (![self pasteFromPasteboard:[NSPasteboard generalPasteboard]]) |
+ NSBeep(); |
+} |
+ |
+- (BOOL)pasteFromPasteboard:(NSPasteboard*)pb { |
+ NSArray* plist = [self readPropertyListFromPasteboard: pb]; |
+ if (!plist) |
+ return NO; |
+ |
+ const BookmarkNode* targetNode; |
+ NSInteger childIndex; |
+ int selRow = [outline_ selectedRow]; |
+ if (selRow >= 0) { |
+ // Insert after selected row. |
+ const BookmarkNode* selNode = [self nodeFromItem: |
+ [outline_ itemAtRow:selRow]]; |
+ targetNode = selNode->GetParent(); |
+ childIndex = targetNode->IndexOfChild(selNode) + 1; |
+ } else { |
+ // ...or at very end if there's no selection: |
+ targetNode = [self nodeFromItem:group_]; |
+ childIndex = targetNode->GetChildCount(); |
+ } |
+ |
+ [self insertPropertyList:plist |
+ inFolder:targetNode |
+ atIndex:childIndex]; |
+ return YES; |
+} |
+ |
+ |
+// Selectively enables/disables menu commands. |
+- (BOOL)validateMenuItem:(NSMenuItem*)menuItem { |
+ SEL action = [menuItem action]; |
+ if (action == @selector(cut:) || action == @selector(copy:) || |
+ action == @selector(delete:)) { |
+ return [[outline_ selectedRowIndexes] count] > 0; |
+ } else if (action == @selector(paste:)) { |
+ return [[NSPasteboard generalPasteboard] |
+ availableTypeFromArray:[outline_ registeredDraggedTypes]] |
+ != nil; |
+ } else { |
+ return YES; |
+ } |
+} |
+ |
+ |
+@end |