| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #import "chrome/browser/ui/cocoa/options/cookies_window_controller.h" | |
| 6 | |
| 7 #include <queue> | |
| 8 #include <vector> | |
| 9 | |
| 10 #import "base/mac/mac_util.h" | |
| 11 #include "base/sys_string_conversions.h" | |
| 12 #include "chrome/browser/browsing_data_remover.h" | |
| 13 #include "chrome/browser/profiles/profile.h" | |
| 14 #include "chrome/browser/ui/cocoa/clear_browsing_data_controller.h" | |
| 15 #include "chrome/browser/ui/cocoa/content_settings/cookie_details_view_controlle
r.h" | |
| 16 #include "grit/generated_resources.h" | |
| 17 #include "grit/theme_resources.h" | |
| 18 #include "skia/ext/skia_utils_mac.h" | |
| 19 #include "third_party/apple/ImageAndTextCell.h" | |
| 20 #include "third_party/skia/include/core/SkBitmap.h" | |
| 21 #include "ui/base/l10n/l10n_util_mac.h" | |
| 22 #include "ui/base/resource/resource_bundle.h" | |
| 23 #include "ui/gfx/image.h" | |
| 24 | |
| 25 // Key path used for notifying KVO. | |
| 26 static NSString* const kCocoaTreeModel = @"cocoaTreeModel"; | |
| 27 | |
| 28 CookiesTreeModelObserverBridge::CookiesTreeModelObserverBridge( | |
| 29 CookiesWindowController* controller) | |
| 30 : window_controller_(controller), | |
| 31 batch_update_(false) { | |
| 32 } | |
| 33 | |
| 34 // Notification that nodes were added to the specified parent. | |
| 35 void CookiesTreeModelObserverBridge::TreeNodesAdded(ui::TreeModel* model, | |
| 36 ui::TreeModelNode* parent, | |
| 37 int start, | |
| 38 int count) { | |
| 39 // We're in for a major rebuild. Ignore this request. | |
| 40 if (batch_update_ || !HasCocoaModel()) | |
| 41 return; | |
| 42 | |
| 43 CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil); | |
| 44 NSMutableArray* cocoa_children = [cocoa_parent mutableChildren]; | |
| 45 | |
| 46 [window_controller_ willChangeValueForKey:kCocoaTreeModel]; | |
| 47 CookieTreeNode* cookie_parent = static_cast<CookieTreeNode*>(parent); | |
| 48 for (int i = 0; i < count; ++i) { | |
| 49 CookieTreeNode* cookie_child = cookie_parent->GetChild(start + i); | |
| 50 CocoaCookieTreeNode* new_child = CocoaNodeFromTreeNode(cookie_child); | |
| 51 [cocoa_children addObject:new_child]; | |
| 52 } | |
| 53 [window_controller_ didChangeValueForKey:kCocoaTreeModel]; | |
| 54 } | |
| 55 | |
| 56 // Notification that nodes were removed from the specified parent. | |
| 57 void CookiesTreeModelObserverBridge::TreeNodesRemoved(ui::TreeModel* model, | |
| 58 ui::TreeModelNode* parent, | |
| 59 int start, | |
| 60 int count) { | |
| 61 // We're in for a major rebuild. Ignore this request. | |
| 62 if (batch_update_ || !HasCocoaModel()) | |
| 63 return; | |
| 64 | |
| 65 CocoaCookieTreeNode* cocoa_parent = FindCocoaNode(parent, nil); | |
| 66 [window_controller_ willChangeValueForKey:kCocoaTreeModel]; | |
| 67 NSMutableArray* cocoa_children = [cocoa_parent mutableChildren]; | |
| 68 for (int i = start + count - 1; i >= start; --i) { | |
| 69 [cocoa_children removeObjectAtIndex:i]; | |
| 70 } | |
| 71 [window_controller_ didChangeValueForKey:kCocoaTreeModel]; | |
| 72 } | |
| 73 | |
| 74 // Notification that the contents of a node has changed. | |
| 75 void CookiesTreeModelObserverBridge::TreeNodeChanged(ui::TreeModel* model, | |
| 76 ui::TreeModelNode* node) { | |
| 77 // If we don't have a Cocoa model, only let the root node change. | |
| 78 if (batch_update_ || (!HasCocoaModel() && model->GetRoot() != node)) | |
| 79 return; | |
| 80 | |
| 81 if (HasCocoaModel()) { | |
| 82 // We still have a Cocoa model, so just rebuild the node. | |
| 83 [window_controller_ willChangeValueForKey:kCocoaTreeModel]; | |
| 84 CocoaCookieTreeNode* changed_node = FindCocoaNode(node, nil); | |
| 85 [changed_node rebuild]; | |
| 86 [window_controller_ didChangeValueForKey:kCocoaTreeModel]; | |
| 87 } else { | |
| 88 // Full rebuild. | |
| 89 [window_controller_ setCocoaTreeModel:CocoaNodeFromTreeNode(node)]; | |
| 90 } | |
| 91 } | |
| 92 | |
| 93 void CookiesTreeModelObserverBridge::TreeModelBeginBatch( | |
| 94 CookiesTreeModel* model) { | |
| 95 batch_update_ = true; | |
| 96 } | |
| 97 | |
| 98 void CookiesTreeModelObserverBridge::TreeModelEndBatch( | |
| 99 CookiesTreeModel* model) { | |
| 100 DCHECK(batch_update_); | |
| 101 CocoaCookieTreeNode* root = CocoaNodeFromTreeNode(model->GetRoot()); | |
| 102 [window_controller_ setCocoaTreeModel:root]; | |
| 103 batch_update_ = false; | |
| 104 } | |
| 105 | |
| 106 void CookiesTreeModelObserverBridge::InvalidateCocoaModel() { | |
| 107 [[[window_controller_ cocoaTreeModel] mutableChildren] removeAllObjects]; | |
| 108 } | |
| 109 | |
| 110 CocoaCookieTreeNode* CookiesTreeModelObserverBridge::CocoaNodeFromTreeNode( | |
| 111 ui::TreeModelNode* node) { | |
| 112 CookieTreeNode* cookie_node = static_cast<CookieTreeNode*>(node); | |
| 113 return [[[CocoaCookieTreeNode alloc] initWithNode:cookie_node] autorelease]; | |
| 114 } | |
| 115 | |
| 116 // Does breadth-first search on the tree to find |node|. This method is most | |
| 117 // commonly used to find origin/folder nodes, which are at the first level off | |
| 118 // the root (hence breadth-first search). | |
| 119 CocoaCookieTreeNode* CookiesTreeModelObserverBridge::FindCocoaNode( | |
| 120 ui::TreeModelNode* target, CocoaCookieTreeNode* start) { | |
| 121 if (!start) { | |
| 122 start = [window_controller_ cocoaTreeModel]; | |
| 123 } | |
| 124 if ([start treeNode] == target) { | |
| 125 return start; | |
| 126 } | |
| 127 | |
| 128 // Enqueue the root node of the search (sub-)tree. | |
| 129 std::queue<CocoaCookieTreeNode*> horizon; | |
| 130 horizon.push(start); | |
| 131 | |
| 132 // Loop until we've looked at every node or we found the target. | |
| 133 while (!horizon.empty()) { | |
| 134 // Dequeue the item at the front. | |
| 135 CocoaCookieTreeNode* node = horizon.front(); | |
| 136 horizon.pop(); | |
| 137 | |
| 138 // If this is the droid we're looking for, report it. | |
| 139 if ([node treeNode] == target) | |
| 140 return node; | |
| 141 | |
| 142 // "Move along, move along." by adding all child nodes to the queue. | |
| 143 if (![node isLeaf]) { | |
| 144 NSArray* children = [node children]; | |
| 145 for (CocoaCookieTreeNode* child in children) { | |
| 146 horizon.push(child); | |
| 147 } | |
| 148 } | |
| 149 } | |
| 150 | |
| 151 return nil; // We couldn't find the node. | |
| 152 } | |
| 153 | |
| 154 // Returns whether or not the Cocoa tree model is built. | |
| 155 bool CookiesTreeModelObserverBridge::HasCocoaModel() { | |
| 156 return ([[[window_controller_ cocoaTreeModel] children] count] > 0U); | |
| 157 } | |
| 158 | |
| 159 #pragma mark Window Controller | |
| 160 | |
| 161 @implementation CookiesWindowController | |
| 162 | |
| 163 @synthesize removeButtonEnabled = removeButtonEnabled_; | |
| 164 @synthesize treeController = treeController_; | |
| 165 | |
| 166 - (id)initWithProfile:(Profile*)profile | |
| 167 databaseHelper:(BrowsingDataDatabaseHelper*)databaseHelper | |
| 168 storageHelper:(BrowsingDataLocalStorageHelper*)storageHelper | |
| 169 appcacheHelper:(BrowsingDataAppCacheHelper*)appcacheHelper | |
| 170 indexedDBHelper:(BrowsingDataIndexedDBHelper*)indexedDBHelper { | |
| 171 DCHECK(profile); | |
| 172 NSString* nibpath = [base::mac::MainAppBundle() pathForResource:@"Cookies" | |
| 173 ofType:@"nib"]; | |
| 174 if ((self = [super initWithWindowNibPath:nibpath owner:self])) { | |
| 175 profile_ = profile; | |
| 176 databaseHelper_ = databaseHelper; | |
| 177 storageHelper_ = storageHelper; | |
| 178 appcacheHelper_ = appcacheHelper; | |
| 179 indexedDBHelper_ = indexedDBHelper; | |
| 180 | |
| 181 [self loadTreeModelFromProfile]; | |
| 182 | |
| 183 // Register for Clear Browsing Data controller so we update appropriately. | |
| 184 ClearBrowsingDataController* clearingController = | |
| 185 [ClearBrowsingDataController controllerForProfile:profile_]; | |
| 186 if (clearingController) { | |
| 187 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; | |
| 188 [center addObserver:self | |
| 189 selector:@selector(clearBrowsingDataNotification:) | |
| 190 name:kClearBrowsingDataControllerDidDelete | |
| 191 object:clearingController]; | |
| 192 } | |
| 193 } | |
| 194 return self; | |
| 195 } | |
| 196 | |
| 197 - (void)dealloc { | |
| 198 [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
| 199 [super dealloc]; | |
| 200 } | |
| 201 | |
| 202 - (void)awakeFromNib { | |
| 203 DCHECK([self window]); | |
| 204 DCHECK_EQ(self, [[self window] delegate]); | |
| 205 | |
| 206 detailsViewController_.reset([[CookieDetailsViewController alloc] init]); | |
| 207 | |
| 208 NSView* detailView = [detailsViewController_.get() view]; | |
| 209 NSRect viewFrameRect = [cookieDetailsViewPlaceholder_ frame]; | |
| 210 [[detailsViewController_.get() view] setFrame:viewFrameRect]; | |
| 211 [[cookieDetailsViewPlaceholder_ superview] | |
| 212 replaceSubview:cookieDetailsViewPlaceholder_ | |
| 213 with:detailView]; | |
| 214 | |
| 215 [detailsViewController_ configureBindingsForTreeController:treeController_]; | |
| 216 } | |
| 217 | |
| 218 - (void)windowWillClose:(NSNotification*)notif { | |
| 219 [searchField_ setTarget:nil]; | |
| 220 [outlineView_ setDelegate:nil]; | |
| 221 [self autorelease]; | |
| 222 } | |
| 223 | |
| 224 - (void)attachSheetTo:(NSWindow*)window { | |
| 225 [NSApp beginSheet:[self window] | |
| 226 modalForWindow:window | |
| 227 modalDelegate:self | |
| 228 didEndSelector:@selector(sheetEndSheet:returnCode:contextInfo:) | |
| 229 contextInfo:nil]; | |
| 230 } | |
| 231 | |
| 232 - (void)sheetEndSheet:(NSWindow*)sheet | |
| 233 returnCode:(NSInteger)returnCode | |
| 234 contextInfo:(void*)context { | |
| 235 [sheet close]; | |
| 236 [sheet orderOut:self]; | |
| 237 } | |
| 238 | |
| 239 - (IBAction)updateFilter:(id)sender { | |
| 240 DCHECK([sender isKindOfClass:[NSSearchField class]]); | |
| 241 NSString* string = [sender stringValue]; | |
| 242 // Invalidate the model here because all the nodes are going to be removed | |
| 243 // in UpdateSearchResults(). This could lead to there temporarily being | |
| 244 // invalid pointers in the Cocoa model. | |
| 245 modelObserver_->InvalidateCocoaModel(); | |
| 246 treeModel_->UpdateSearchResults(base::SysNSStringToWide(string)); | |
| 247 } | |
| 248 | |
| 249 - (IBAction)deleteCookie:(id)sender { | |
| 250 DCHECK_EQ(1U, [[treeController_ selectedObjects] count]); | |
| 251 [self deleteNodeAtIndexPath:[treeController_ selectionIndexPath]]; | |
| 252 } | |
| 253 | |
| 254 // This will delete the Cocoa model node as well as the backing model object at | |
| 255 // the specified index path in the Cocoa model. If the node that was deleted | |
| 256 // was the sole child of the parent node, this will be called recursively to | |
| 257 // delete empty parents. | |
| 258 - (void)deleteNodeAtIndexPath:(NSIndexPath*)path { | |
| 259 NSTreeNode* treeNode = | |
| 260 [[treeController_ arrangedObjects] descendantNodeAtIndexPath:path]; | |
| 261 if (!treeNode) | |
| 262 return; | |
| 263 | |
| 264 CocoaCookieTreeNode* node = [treeNode representedObject]; | |
| 265 CookieTreeNode* cookie = static_cast<CookieTreeNode*>([node treeNode]); | |
| 266 treeModel_->DeleteCookieNode(cookie); | |
| 267 // If there is a next cookie, this will select it because items will slide | |
| 268 // up. If there is no next cookie, this is a no-op. | |
| 269 [treeController_ setSelectionIndexPath:path]; | |
| 270 // If the above setting of the selection was in fact a no-op, find the next | |
| 271 // node to select. | |
| 272 if (![[treeController_ selectedObjects] count]) { | |
| 273 NSUInteger lastIndex = [path indexAtPosition:[path length] - 1]; | |
| 274 if (lastIndex != 0) { | |
| 275 // If there any nodes remaining, select the node that is in the list | |
| 276 // before this one. | |
| 277 path = [path indexPathByRemovingLastIndex]; | |
| 278 path = [path indexPathByAddingIndex:lastIndex - 1]; | |
| 279 [treeController_ setSelectionIndexPath:path]; | |
| 280 } | |
| 281 } | |
| 282 } | |
| 283 | |
| 284 - (IBAction)deleteAllCookies:(id)sender { | |
| 285 // Preemptively delete all cookies in the Cocoa model. | |
| 286 modelObserver_->InvalidateCocoaModel(); | |
| 287 treeModel_->DeleteAllStoredObjects(); | |
| 288 } | |
| 289 | |
| 290 - (IBAction)closeSheet:(id)sender { | |
| 291 [NSApp endSheet:[self window]]; | |
| 292 } | |
| 293 | |
| 294 - (void)clearBrowsingDataNotification:(NSNotification*)notif { | |
| 295 NSNumber* removeMask = | |
| 296 [[notif userInfo] objectForKey:kClearBrowsingDataControllerRemoveMask]; | |
| 297 if ([removeMask intValue] & BrowsingDataRemover::REMOVE_COOKIES) { | |
| 298 [self loadTreeModelFromProfile]; | |
| 299 } | |
| 300 } | |
| 301 | |
| 302 // Override keyDown on the controller (which is the first responder) to allow | |
| 303 // both backspace and delete to be captured by the Remove button. | |
| 304 - (void)keyDown:(NSEvent*)theEvent { | |
| 305 NSString* keys = [theEvent characters]; | |
| 306 if ([keys length]) { | |
| 307 unichar key = [keys characterAtIndex:0]; | |
| 308 // The button has a key equivalent of backspace, so examine this event for | |
| 309 // forward delete. | |
| 310 if ((key == NSDeleteCharacter || key == NSDeleteFunctionKey) && | |
| 311 [self removeButtonEnabled]) { | |
| 312 [removeButton_ performClick:self]; | |
| 313 return; | |
| 314 } | |
| 315 } | |
| 316 [super keyDown:theEvent]; | |
| 317 } | |
| 318 | |
| 319 #pragma mark Getters and Setters | |
| 320 | |
| 321 - (CocoaCookieTreeNode*)cocoaTreeModel { | |
| 322 return cocoaTreeModel_.get(); | |
| 323 } | |
| 324 - (void)setCocoaTreeModel:(CocoaCookieTreeNode*)model { | |
| 325 cocoaTreeModel_.reset([model retain]); | |
| 326 } | |
| 327 | |
| 328 - (CookiesTreeModel*)treeModel { | |
| 329 return treeModel_.get(); | |
| 330 } | |
| 331 | |
| 332 #pragma mark Outline View Delegate | |
| 333 | |
| 334 - (void)outlineView:(NSOutlineView*)outlineView | |
| 335 willDisplayCell:(id)cell | |
| 336 forTableColumn:(NSTableColumn*)tableColumn | |
| 337 item:(id)item { | |
| 338 CocoaCookieTreeNode* node = [item representedObject]; | |
| 339 int index = treeModel_->GetIconIndex([node treeNode]); | |
| 340 NSImage* icon = nil; | |
| 341 if (index >= 0) | |
| 342 icon = [icons_ objectAtIndex:index]; | |
| 343 else | |
| 344 icon = [icons_ lastObject]; | |
| 345 [(ImageAndTextCell*)cell setImage:icon]; | |
| 346 } | |
| 347 | |
| 348 - (void)outlineViewItemDidExpand:(NSNotification*)notif { | |
| 349 NSTreeNode* item = [[notif userInfo] objectForKey:@"NSObject"]; | |
| 350 CocoaCookieTreeNode* node = [item representedObject]; | |
| 351 NSArray* children = [node children]; | |
| 352 if ([children count] == 1U) { | |
| 353 // The node that will expand has one child. Do the user a favor and expand | |
| 354 // that node (saving her a click) if it is non-leaf. | |
| 355 CocoaCookieTreeNode* child = [children lastObject]; | |
| 356 if (![child isLeaf]) { | |
| 357 NSOutlineView* outlineView = [notif object]; | |
| 358 // Tell the OutlineView to expand the NSTreeNode, not the model object. | |
| 359 children = [item childNodes]; | |
| 360 DCHECK_EQ([children count], 1U); | |
| 361 [outlineView expandItem:[children lastObject]]; | |
| 362 // Select the first node in that child set. | |
| 363 NSTreeNode* folderChild = [children lastObject]; | |
| 364 if ([[folderChild childNodes] count] > 0) { | |
| 365 NSTreeNode* firstCookieChild = | |
| 366 [[folderChild childNodes] objectAtIndex:0]; | |
| 367 [treeController_ setSelectionIndexPath:[firstCookieChild indexPath]]; | |
| 368 } | |
| 369 } | |
| 370 } | |
| 371 } | |
| 372 | |
| 373 - (void)outlineViewSelectionDidChange:(NSNotification*)notif { | |
| 374 // Multi-selection should be disabled in the UI, but for sanity, double-check | |
| 375 // that they can't do it here. | |
| 376 NSArray* selectedObjects = [treeController_ selectedObjects]; | |
| 377 NSUInteger count = [selectedObjects count]; | |
| 378 if (count != 1U) { | |
| 379 DCHECK_LT(count, 1U) << "User was able to select more than 1 cookie node!"; | |
| 380 [self setRemoveButtonEnabled:NO]; | |
| 381 return; | |
| 382 } | |
| 383 | |
| 384 // Go through the selection's indexPath and make sure that the node that is | |
| 385 // being referenced actually exists in the Cocoa model. | |
| 386 NSIndexPath* selection = [treeController_ selectionIndexPath]; | |
| 387 NSUInteger length = [selection length]; | |
| 388 CocoaCookieTreeNode* node = [self cocoaTreeModel]; | |
| 389 for (NSUInteger i = 0; i < length; ++i) { | |
| 390 NSUInteger childIndex = [selection indexAtPosition:i]; | |
| 391 if (childIndex >= [[node children] count]) { | |
| 392 [self setRemoveButtonEnabled:NO]; | |
| 393 return; | |
| 394 } | |
| 395 node = [[node children] objectAtIndex:childIndex]; | |
| 396 } | |
| 397 | |
| 398 // If there is a valid selection, make sure that the remove | |
| 399 // button is enabled. | |
| 400 [self setRemoveButtonEnabled:YES]; | |
| 401 } | |
| 402 | |
| 403 #pragma mark Unit Testing | |
| 404 | |
| 405 - (CookiesTreeModelObserverBridge*)modelObserver { | |
| 406 return modelObserver_.get(); | |
| 407 } | |
| 408 | |
| 409 - (NSArray*)icons { | |
| 410 return icons_.get(); | |
| 411 } | |
| 412 | |
| 413 // Re-initializes the |treeModel_|, creates a new observer for it, and re- | |
| 414 // builds the |cocoaTreeModel_|. We use this to initialize the controller and | |
| 415 // to rebuild after the user clears browsing data. Because the models get | |
| 416 // clobbered, we rebuild the icon cache for safety (though they do not change). | |
| 417 - (void)loadTreeModelFromProfile { | |
| 418 treeModel_.reset(new CookiesTreeModel( | |
| 419 profile_->GetRequestContext()->GetCookieStore()->GetCookieMonster(), | |
| 420 databaseHelper_, | |
| 421 storageHelper_, | |
| 422 NULL, | |
| 423 appcacheHelper_, | |
| 424 indexedDBHelper_)); | |
| 425 modelObserver_.reset(new CookiesTreeModelObserverBridge(self)); | |
| 426 treeModel_->AddCookiesTreeObserver(modelObserver_.get()); | |
| 427 | |
| 428 // Convert the model's icons from Skia to Cocoa. | |
| 429 std::vector<SkBitmap> skiaIcons; | |
| 430 treeModel_->GetIcons(&skiaIcons); | |
| 431 icons_.reset([[NSMutableArray alloc] init]); | |
| 432 for (std::vector<SkBitmap>::iterator it = skiaIcons.begin(); | |
| 433 it != skiaIcons.end(); ++it) { | |
| 434 [icons_ addObject:gfx::SkBitmapToNSImage(*it)]; | |
| 435 } | |
| 436 | |
| 437 // Default icon will be the last item in the array. | |
| 438 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); | |
| 439 // TODO(rsesek): Rename this resource now that it's in multiple places. | |
| 440 [icons_ addObject:rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER)]; | |
| 441 | |
| 442 // Create the Cocoa model. | |
| 443 CookieTreeNode* root = static_cast<CookieTreeNode*>(treeModel_->GetRoot()); | |
| 444 scoped_nsobject<CocoaCookieTreeNode> model( | |
| 445 [[CocoaCookieTreeNode alloc] initWithNode:root]); | |
| 446 [self setCocoaTreeModel:model.get()]; // Takes ownership. | |
| 447 } | |
| 448 | |
| 449 @end | |
| OLD | NEW |