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