blob: c99753708abff7e633178f16785fea584953dc29 [file] [log] [blame]
// 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/cocoa/cookies_window_controller.h"
#include <vector>
#include "app/l10n_util_mac.h"
#include "app/resource_bundle.h"
#import "base/i18n/time_formatting.h"
#import "base/mac_util.h"
#include "base/sys_string_conversions.h"
#include "chrome/browser/browsing_data_remover.h"
#include "chrome/browser/cocoa/clear_browsing_data_controller.h"
#include "chrome/browser/profile.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) {
}
// 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 (!HasCocoaModel())
return;
CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil);
NSMutableArray* cocoa_children = [cocoa_parent children];
[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 (!HasCocoaModel())
return;
CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil);
[window_controller_ willChangeValueForKey:kCocoaTreeModel];
NSMutableArray* cocoa_children = [cocoa_parent children];
for (int i = start + count - 1; i >= start; --i) {
[cocoa_children removeObjectAtIndex:i];
}
[window_controller_ didChangeValueForKey:kCocoaTreeModel];
}
// Notification the children of |parent| have been reordered. Note, only
// the direct children of |parent| have been reordered, not descendants.
void CookiesTreeModelObserverBridge::TreeNodeChildrenReordered(TreeModel* model,
TreeModelNode* parent) {
// We're in for a major rebuild. Ignore this request.
if (!HasCocoaModel())
return;
CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil);
NSMutableArray* cocoa_children = [cocoa_parent children];
CookieTreeNode* cookie_parent = static_cast<CookieTreeNode*>(parent);
const int child_count = cookie_parent->GetChildCount();
[window_controller_ willChangeValueForKey:kCocoaTreeModel];
for (int i = 0; i < child_count; ++i) {
CookieTreeNode* swap_in = cookie_parent->GetChild(i);
for (int j = i; j < child_count; ++j) {
CocoaCookieTreeNode* child = [cocoa_children objectAtIndex:j];
TreeModelNode* swap_out = [child treeNode];
if (swap_in == swap_out) {
[cocoa_children exchangeObjectAtIndex:j withObjectAtIndex:i];
break;
}
}
}
[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 (!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::InvalidateCocoaModel() {
[[[window_controller_ cocoaTreeModel] children] removeAllObjects];
}
CocoaCookieTreeNode* CookiesTreeModelObserverBridge::CocoaNodeFromTreeNode(
TreeModelNode* node) {
CookieTreeNode* cookie_node = static_cast<CookieTreeNode*>(node);
return [[[CocoaCookieTreeNode alloc] initWithNode:cookie_node] autorelease];
}
// Does a pre-order traversal on the tree to find |node|.
CocoaCookieTreeNode* CookiesTreeModelObserverBridge::FindCocoaNode(
TreeModelNode* node, CocoaCookieTreeNode* start) {
if (!start) {
start = [window_controller_ cocoaTreeModel];
}
if ([start treeNode] == node) {
return start;
}
NSArray* children = [start children];
for (CocoaCookieTreeNode* child in children) {
if ([child treeNode] == node) {
return child;
}
// Search the children. Return the result if we find one.
CocoaCookieTreeNode* recurse = FindCocoaNode(node, child);
if (recurse)
return recurse;
}
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
storageHelper:(BrowsingDataLocalStorageHelper*)storageHelper {
DCHECK(profile);
NSString* nibpath = [mac_util::MainAppBundle() pathForResource:@"Cookies"
ofType:@"nib"];
if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
profile_ = profile;
storageHelper_ = storageHelper;
[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]);
}
- (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 {
NSIndexPath* selectionPath = [treeController_ selectionIndexPath];
// N.B.: I suspect that |-selectedObjects| does not retain/autorelease the
// return value, which may result in the pointer going to garbage before it
// even goes out of scope. Retaining it immediately will fix this.
NSArray* selection = [treeController_ selectedObjects];
if (selectionPath) {
DCHECK_EQ([selection count], 1U);
CocoaCookieTreeNode* node = [selection lastObject];
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:selectionPath];
}
}
- (IBAction)deleteAllCookies:(id)sender {
// Preemptively delete all cookies in the Cocoa model.
modelObserver_->InvalidateCocoaModel();
treeModel_->DeleteAllCookies();
}
- (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];
}
}
#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]];
}
}
}
- (void)outlineViewSelectionDidChange:(NSNotification*)notif {
// Multi-selection should be disabled in the UI, but for sanity, double-check
// that they can't do it here.
NSUInteger count = [[treeController_ 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];
}
[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_, storageHelper_));
modelObserver_.reset(new CookiesTreeModelObserverBridge(self));
treeModel_->SetObserver(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.GetNSImageNamed(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