diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/cookies_window_controller.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/cookies_window_controller.mm | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/cookies_window_controller.mm b/chrome/browser/ui/cocoa/cookies_window_controller.mm new file mode 100644 index 0000000..bf1bf68 --- /dev/null +++ b/chrome/browser/ui/cocoa/cookies_window_controller.mm @@ -0,0 +1,448 @@ +// Copyright (c) 2010 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/ui/cocoa/cookies_window_controller.h" + +#include <queue> +#include <vector> + +#include "app/l10n_util_mac.h" +#include "app/resource_bundle.h" +#import "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/browsing_data_remover.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/cocoa/clear_browsing_data_controller.h" +#include "chrome/browser/ui/cocoa/cookie_details_view_controller.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "skia/ext/skia_utils_mac.h" +#include "third_party/apple/ImageAndTextCell.h" +#include "third_party/skia/include/core/SkBitmap.h" + +// Key path used for notifying KVO. +static NSString* const kCocoaTreeModel = @"cocoaTreeModel"; + +CookiesTreeModelObserverBridge::CookiesTreeModelObserverBridge( + CookiesWindowController* controller) + : window_controller_(controller), + batch_update_(false) { +} + +// Notification that nodes were added to the specified parent. +void CookiesTreeModelObserverBridge::TreeNodesAdded(TreeModel* model, + TreeModelNode* parent, + int start, + int count) { + // We're in for a major rebuild. Ignore this request. + if (batch_update_ || !HasCocoaModel()) + return; + + CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil); + NSMutableArray* cocoa_children = [cocoa_parent mutableChildren]; + + [window_controller_ willChangeValueForKey:kCocoaTreeModel]; + CookieTreeNode* cookie_parent = static_cast<CookieTreeNode*>(parent); + for (int i = 0; i < count; ++i) { + CookieTreeNode* cookie_child = cookie_parent->GetChild(start + i); + CocoaCookieTreeNode* new_child = CocoaNodeFromTreeNode(cookie_child); + [cocoa_children addObject:new_child]; + } + [window_controller_ didChangeValueForKey:kCocoaTreeModel]; +} + +// Notification that nodes were removed from the specified parent. +void CookiesTreeModelObserverBridge::TreeNodesRemoved(TreeModel* model, + TreeModelNode* parent, + int start, + int count) { + // We're in for a major rebuild. Ignore this request. + if (batch_update_ || !HasCocoaModel()) + return; + + CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil); + [window_controller_ willChangeValueForKey:kCocoaTreeModel]; + NSMutableArray* cocoa_children = [cocoa_parent mutableChildren]; + for (int i = start + count - 1; i >= start; --i) { + [cocoa_children removeObjectAtIndex:i]; + } + [window_controller_ didChangeValueForKey:kCocoaTreeModel]; +} + +// Notification that the contents of a node has changed. +void CookiesTreeModelObserverBridge::TreeNodeChanged(TreeModel* model, + TreeModelNode* node) { + // If we don't have a Cocoa model, only let the root node change. + if (batch_update_ || (!HasCocoaModel() && model->GetRoot() != node)) + return; + + if (HasCocoaModel()) { + // We still have a Cocoa model, so just rebuild the node. + [window_controller_ willChangeValueForKey:kCocoaTreeModel]; + CocoaCookieTreeNode* changed_node = FindCocoaNode(node, nil); + [changed_node rebuild]; + [window_controller_ didChangeValueForKey:kCocoaTreeModel]; + } else { + // Full rebuild. + [window_controller_ setCocoaTreeModel:CocoaNodeFromTreeNode(node)]; + } +} + +void CookiesTreeModelObserverBridge::TreeModelBeginBatch( + CookiesTreeModel* model) { + batch_update_ = true; +} + +void CookiesTreeModelObserverBridge::TreeModelEndBatch( + CookiesTreeModel* model) { + DCHECK(batch_update_); + CocoaCookieTreeNode* root = CocoaNodeFromTreeNode(model->GetRoot()); + [window_controller_ setCocoaTreeModel:root]; + batch_update_ = false; +} + +void CookiesTreeModelObserverBridge::InvalidateCocoaModel() { + [[[window_controller_ cocoaTreeModel] mutableChildren] removeAllObjects]; +} + +CocoaCookieTreeNode* CookiesTreeModelObserverBridge::CocoaNodeFromTreeNode( + TreeModelNode* node) { + CookieTreeNode* cookie_node = static_cast<CookieTreeNode*>(node); + return [[[CocoaCookieTreeNode alloc] initWithNode:cookie_node] autorelease]; +} + +// Does breadth-first search on the tree to find |node|. This method is most +// commonly used to find origin/folder nodes, which are at the first level off +// the root (hence breadth-first search). +CocoaCookieTreeNode* CookiesTreeModelObserverBridge::FindCocoaNode( + TreeModelNode* target, CocoaCookieTreeNode* start) { + if (!start) { + start = [window_controller_ cocoaTreeModel]; + } + if ([start treeNode] == target) { + return start; + } + + // Enqueue the root node of the search (sub-)tree. + std::queue<CocoaCookieTreeNode*> horizon; + horizon.push(start); + + // Loop until we've looked at every node or we found the target. + while (!horizon.empty()) { + // Dequeue the item at the front. + CocoaCookieTreeNode* node = horizon.front(); + horizon.pop(); + + // If this is the droid we're looking for, report it. + if ([node treeNode] == target) + return node; + + // "Move along, move along." by adding all child nodes to the queue. + if (![node isLeaf]) { + NSArray* children = [node children]; + for (CocoaCookieTreeNode* child in children) { + horizon.push(child); + } + } + } + + return nil; // We couldn't find the node. +} + +// Returns whether or not the Cocoa tree model is built. +bool CookiesTreeModelObserverBridge::HasCocoaModel() { + return ([[[window_controller_ cocoaTreeModel] children] count] > 0U); +} + +#pragma mark Window Controller + +@implementation CookiesWindowController + +@synthesize removeButtonEnabled = removeButtonEnabled_; +@synthesize treeController = treeController_; + +- (id)initWithProfile:(Profile*)profile + databaseHelper:(BrowsingDataDatabaseHelper*)databaseHelper + storageHelper:(BrowsingDataLocalStorageHelper*)storageHelper + appcacheHelper:(BrowsingDataAppCacheHelper*)appcacheHelper + indexedDBHelper:(BrowsingDataIndexedDBHelper*)indexedDBHelper { + DCHECK(profile); + NSString* nibpath = [mac_util::MainAppBundle() pathForResource:@"Cookies" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibpath owner:self])) { + profile_ = profile; + databaseHelper_ = databaseHelper; + storageHelper_ = storageHelper; + appcacheHelper_ = appcacheHelper; + indexedDBHelper_ = indexedDBHelper; + + [self loadTreeModelFromProfile]; + + // Register for Clear Browsing Data controller so we update appropriately. + ClearBrowsingDataController* clearingController = + [ClearBrowsingDataController controllerForProfile:profile_]; + if (clearingController) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(clearBrowsingDataNotification:) + name:kClearBrowsingDataControllerDidDelete + object:clearingController]; + } + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +- (void)awakeFromNib { + DCHECK([self window]); + DCHECK_EQ(self, [[self window] delegate]); + + detailsViewController_.reset([[CookieDetailsViewController alloc] init]); + + NSView* detailView = [detailsViewController_.get() view]; + NSRect viewFrameRect = [cookieDetailsViewPlaceholder_ frame]; + [[detailsViewController_.get() view] setFrame:viewFrameRect]; + [[cookieDetailsViewPlaceholder_ superview] + replaceSubview:cookieDetailsViewPlaceholder_ + with:detailView]; + + [detailsViewController_ configureBindingsForTreeController:treeController_]; +} + +- (void)windowWillClose:(NSNotification*)notif { + [searchField_ setTarget:nil]; + [outlineView_ setDelegate:nil]; + [self autorelease]; +} + +- (void)attachSheetTo:(NSWindow*)window { + [NSApp beginSheet:[self window] + modalForWindow:window + modalDelegate:self + didEndSelector:@selector(sheetEndSheet:returnCode:contextInfo:) + contextInfo:nil]; +} + +- (void)sheetEndSheet:(NSWindow*)sheet + returnCode:(NSInteger)returnCode + contextInfo:(void*)context { + [sheet close]; + [sheet orderOut:self]; +} + +- (IBAction)updateFilter:(id)sender { + DCHECK([sender isKindOfClass:[NSSearchField class]]); + NSString* string = [sender stringValue]; + // Invalidate the model here because all the nodes are going to be removed + // in UpdateSearchResults(). This could lead to there temporarily being + // invalid pointers in the Cocoa model. + modelObserver_->InvalidateCocoaModel(); + treeModel_->UpdateSearchResults(base::SysNSStringToWide(string)); +} + +- (IBAction)deleteCookie:(id)sender { + DCHECK_EQ(1U, [[treeController_ selectedObjects] count]); + [self deleteNodeAtIndexPath:[treeController_ selectionIndexPath]]; +} + +// This will delete the Cocoa model node as well as the backing model object at +// the specified index path in the Cocoa model. If the node that was deleted +// was the sole child of the parent node, this will be called recursively to +// delete empty parents. +- (void)deleteNodeAtIndexPath:(NSIndexPath*)path { + NSTreeNode* treeNode = + [[treeController_ arrangedObjects] descendantNodeAtIndexPath:path]; + if (!treeNode) + return; + + CocoaCookieTreeNode* node = [treeNode representedObject]; + CookieTreeNode* cookie = static_cast<CookieTreeNode*>([node treeNode]); + treeModel_->DeleteCookieNode(cookie); + // If there is a next cookie, this will select it because items will slide + // up. If there is no next cookie, this is a no-op. + [treeController_ setSelectionIndexPath:path]; + // If the above setting of the selection was in fact a no-op, find the next + // node to select. + if (![[treeController_ selectedObjects] count]) { + NSUInteger lastIndex = [path indexAtPosition:[path length] - 1]; + if (lastIndex != 0) { + // If there any nodes remaining, select the node that is in the list + // before this one. + path = [path indexPathByRemovingLastIndex]; + path = [path indexPathByAddingIndex:lastIndex - 1]; + [treeController_ setSelectionIndexPath:path]; + } + } +} + +- (IBAction)deleteAllCookies:(id)sender { + // Preemptively delete all cookies in the Cocoa model. + modelObserver_->InvalidateCocoaModel(); + treeModel_->DeleteAllStoredObjects(); +} + +- (IBAction)closeSheet:(id)sender { + [NSApp endSheet:[self window]]; +} + +- (void)clearBrowsingDataNotification:(NSNotification*)notif { + NSNumber* removeMask = + [[notif userInfo] objectForKey:kClearBrowsingDataControllerRemoveMask]; + if ([removeMask intValue] & BrowsingDataRemover::REMOVE_COOKIES) { + [self loadTreeModelFromProfile]; + } +} + +// Override keyDown on the controller (which is the first responder) to allow +// both backspace and delete to be captured by the Remove button. +- (void)keyDown:(NSEvent*)theEvent { + NSString* keys = [theEvent characters]; + if ([keys length]) { + unichar key = [keys characterAtIndex:0]; + // The button has a key equivalent of backspace, so examine this event for + // forward delete. + if ((key == NSDeleteCharacter || key == NSDeleteFunctionKey) && + [self removeButtonEnabled]) { + [removeButton_ performClick:self]; + return; + } + } + [super keyDown:theEvent]; +} + +#pragma mark Getters and Setters + +- (CocoaCookieTreeNode*)cocoaTreeModel { + return cocoaTreeModel_.get(); +} +- (void)setCocoaTreeModel:(CocoaCookieTreeNode*)model { + cocoaTreeModel_.reset([model retain]); +} + +- (CookiesTreeModel*)treeModel { + return treeModel_.get(); +} + +#pragma mark Outline View Delegate + +- (void)outlineView:(NSOutlineView*)outlineView + willDisplayCell:(id)cell + forTableColumn:(NSTableColumn*)tableColumn + item:(id)item { + CocoaCookieTreeNode* node = [item representedObject]; + int index = treeModel_->GetIconIndex([node treeNode]); + NSImage* icon = nil; + if (index >= 0) + icon = [icons_ objectAtIndex:index]; + else + icon = [icons_ lastObject]; + [(ImageAndTextCell*)cell setImage:icon]; +} + +- (void)outlineViewItemDidExpand:(NSNotification*)notif { + NSTreeNode* item = [[notif userInfo] objectForKey:@"NSObject"]; + CocoaCookieTreeNode* node = [item representedObject]; + NSArray* children = [node children]; + if ([children count] == 1U) { + // The node that will expand has one child. Do the user a favor and expand + // that node (saving her a click) if it is non-leaf. + CocoaCookieTreeNode* child = [children lastObject]; + if (![child isLeaf]) { + NSOutlineView* outlineView = [notif object]; + // Tell the OutlineView to expand the NSTreeNode, not the model object. + children = [item childNodes]; + DCHECK_EQ([children count], 1U); + [outlineView expandItem:[children lastObject]]; + // Select the first node in that child set. + NSTreeNode* folderChild = [children lastObject]; + if ([[folderChild childNodes] count] > 0) { + NSTreeNode* firstCookieChild = + [[folderChild childNodes] objectAtIndex:0]; + [treeController_ setSelectionIndexPath:[firstCookieChild indexPath]]; + } + } + } +} + +- (void)outlineViewSelectionDidChange:(NSNotification*)notif { + // Multi-selection should be disabled in the UI, but for sanity, double-check + // that they can't do it here. + NSArray* selectedObjects = [treeController_ selectedObjects]; + NSUInteger count = [selectedObjects count]; + if (count != 1U) { + DCHECK_LT(count, 1U) << "User was able to select more than 1 cookie node!"; + [self setRemoveButtonEnabled:NO]; + return; + } + + // Go through the selection's indexPath and make sure that the node that is + // being referenced actually exists in the Cocoa model. + NSIndexPath* selection = [treeController_ selectionIndexPath]; + NSUInteger length = [selection length]; + CocoaCookieTreeNode* node = [self cocoaTreeModel]; + for (NSUInteger i = 0; i < length; ++i) { + NSUInteger childIndex = [selection indexAtPosition:i]; + if (childIndex >= [[node children] count]) { + [self setRemoveButtonEnabled:NO]; + return; + } + node = [[node children] objectAtIndex:childIndex]; + } + + // If there is a valid selection, make sure that the remove + // button is enabled. + [self setRemoveButtonEnabled:YES]; +} + +#pragma mark Unit Testing + +- (CookiesTreeModelObserverBridge*)modelObserver { + return modelObserver_.get(); +} + +- (NSArray*)icons { + return icons_.get(); +} + +// Re-initializes the |treeModel_|, creates a new observer for it, and re- +// builds the |cocoaTreeModel_|. We use this to initialize the controller and +// to rebuild after the user clears browsing data. Because the models get +// clobbered, we rebuild the icon cache for safety (though they do not change). +- (void)loadTreeModelFromProfile { + treeModel_.reset(new CookiesTreeModel( + profile_->GetRequestContext()->GetCookieStore()->GetCookieMonster(), + databaseHelper_, + storageHelper_, + NULL, + appcacheHelper_, + indexedDBHelper_)); + modelObserver_.reset(new CookiesTreeModelObserverBridge(self)); + treeModel_->AddObserver(modelObserver_.get()); + + // Convert the model's icons from Skia to Cocoa. + std::vector<SkBitmap> skiaIcons; + treeModel_->GetIcons(&skiaIcons); + icons_.reset([[NSMutableArray alloc] init]); + for (std::vector<SkBitmap>::iterator it = skiaIcons.begin(); + it != skiaIcons.end(); ++it) { + [icons_ addObject:gfx::SkBitmapToNSImage(*it)]; + } + + // Default icon will be the last item in the array. + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + // TODO(rsesek): Rename this resource now that it's in multiple places. + [icons_ addObject:rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER)]; + + // Create the Cocoa model. + CookieTreeNode* root = static_cast<CookieTreeNode*>(treeModel_->GetRoot()); + scoped_nsobject<CocoaCookieTreeNode> model( + [[CocoaCookieTreeNode alloc] initWithNode:root]); + [self setCocoaTreeModel:model.get()]; // Takes ownership. +} + +@end |