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 |