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 |