OLD | NEW |
| (Empty) |
1 // Copyright (c) 2009 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 <Cocoa/Cocoa.h> | |
6 | |
7 #import "chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h" | |
8 | |
9 #import "base/mac/mac_util.h" | |
10 #include "base/lazy_instance.h" | |
11 #include "base/sys_string_conversions.h" | |
12 #include "chrome/browser/browser_process.h" | |
13 #include "chrome/browser/prefs/pref_service.h" | |
14 #include "chrome/browser/profiles/profile.h" | |
15 #include "chrome/browser/search_engines/template_url_model.h" | |
16 #include "chrome/browser/search_engines/template_url_table_model.h" | |
17 #import "chrome/browser/ui/cocoa/edit_search_engine_cocoa_controller.h" | |
18 #import "chrome/browser/ui/cocoa/window_size_autosaver.h" | |
19 #include "chrome/common/pref_names.h" | |
20 #include "grit/generated_resources.h" | |
21 #include "skia/ext/skia_utils_mac.h" | |
22 #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" | |
23 #include "third_party/skia/include/core/SkBitmap.h" | |
24 | |
25 namespace { | |
26 | |
27 const CGFloat kButtonBarHeight = 35.0; | |
28 | |
29 } // namespace | |
30 | |
31 @interface KeywordEditorCocoaController (Private) | |
32 - (void)adjustEditingButtons; | |
33 - (void)editKeyword:(id)sender; | |
34 - (int)indexInModelForRow:(NSUInteger)row; | |
35 @end | |
36 | |
37 // KeywordEditorModelObserver ------------------------------------------------- | |
38 | |
39 KeywordEditorModelObserver::KeywordEditorModelObserver( | |
40 KeywordEditorCocoaController* controller) | |
41 : controller_(controller), | |
42 icon_cache_(this) { | |
43 } | |
44 | |
45 KeywordEditorModelObserver::~KeywordEditorModelObserver() { | |
46 } | |
47 | |
48 // Notification that the template url model has changed in some way. | |
49 void KeywordEditorModelObserver::OnTemplateURLModelChanged() { | |
50 [controller_ modelChanged]; | |
51 } | |
52 | |
53 void KeywordEditorModelObserver::OnEditedKeyword( | |
54 const TemplateURL* template_url, | |
55 const string16& title, | |
56 const string16& keyword, | |
57 const std::string& url) { | |
58 KeywordEditorController* controller = [controller_ controller]; | |
59 if (template_url) { | |
60 controller->ModifyTemplateURL(template_url, title, keyword, url); | |
61 } else { | |
62 controller->AddTemplateURL(title, keyword, url); | |
63 } | |
64 } | |
65 | |
66 void KeywordEditorModelObserver::OnModelChanged() { | |
67 icon_cache_.OnModelChanged(); | |
68 [controller_ modelChanged]; | |
69 } | |
70 | |
71 void KeywordEditorModelObserver::OnItemsChanged(int start, int length) { | |
72 icon_cache_.OnItemsChanged(start, length); | |
73 [controller_ modelChanged]; | |
74 } | |
75 | |
76 void KeywordEditorModelObserver::OnItemsAdded(int start, int length) { | |
77 icon_cache_.OnItemsAdded(start, length); | |
78 [controller_ modelChanged]; | |
79 } | |
80 | |
81 void KeywordEditorModelObserver::OnItemsRemoved(int start, int length) { | |
82 icon_cache_.OnItemsRemoved(start, length); | |
83 [controller_ modelChanged]; | |
84 } | |
85 | |
86 int KeywordEditorModelObserver::RowCount() const { | |
87 return [controller_ controller]->table_model()->RowCount(); | |
88 } | |
89 | |
90 SkBitmap KeywordEditorModelObserver::GetIcon(int row) const { | |
91 return [controller_ controller]->table_model()->GetIcon(row); | |
92 } | |
93 | |
94 NSImage* KeywordEditorModelObserver::GetImageForRow(int row) { | |
95 return icon_cache_.GetImageForRow(row); | |
96 } | |
97 | |
98 // KeywordEditorCocoaController ----------------------------------------------- | |
99 | |
100 namespace { | |
101 | |
102 typedef std::map<Profile*,KeywordEditorCocoaController*> ProfileControllerMap; | |
103 | |
104 static base::LazyInstance<ProfileControllerMap> g_profile_controller_map( | |
105 base::LINKER_INITIALIZED); | |
106 | |
107 } // namespace | |
108 | |
109 @implementation KeywordEditorCocoaController | |
110 | |
111 + (KeywordEditorCocoaController*)sharedInstanceForProfile:(Profile*)profile { | |
112 ProfileControllerMap* map = g_profile_controller_map.Pointer(); | |
113 DCHECK(map != NULL); | |
114 ProfileControllerMap::iterator it = map->find(profile); | |
115 if (it != map->end()) { | |
116 return it->second; | |
117 } | |
118 return nil; | |
119 } | |
120 | |
121 // TODO(shess): The Windows code watches a single global window which | |
122 // is not distinguished by profile. This code could distinguish by | |
123 // profile by checking the controller's class and profile. | |
124 + (void)showKeywordEditor:(Profile*)profile { | |
125 // http://crbug.com/23359 describes a case where this panel is | |
126 // opened from an incognito window, which can leave the panel | |
127 // holding onto a stale profile. Since the same panel is used | |
128 // either way, arrange to use the original profile instead. | |
129 profile = profile->GetOriginalProfile(); | |
130 | |
131 ProfileControllerMap* map = g_profile_controller_map.Pointer(); | |
132 DCHECK(map != NULL); | |
133 ProfileControllerMap::iterator it = map->find(profile); | |
134 if (it == map->end()) { | |
135 // Since we don't currently support multiple profiles, this class | |
136 // has not been tested against them, so document that assumption. | |
137 DCHECK_EQ(map->size(), 0U); | |
138 | |
139 KeywordEditorCocoaController* controller = | |
140 [[self alloc] initWithProfile:profile]; | |
141 it = map->insert(std::make_pair(profile, controller)).first; | |
142 } | |
143 | |
144 [it->second showWindow:nil]; | |
145 } | |
146 | |
147 - (id)initWithProfile:(Profile*)profile { | |
148 DCHECK(profile); | |
149 NSString* nibpath = [base::mac::MainAppBundle() | |
150 pathForResource:@"KeywordEditor" | |
151 ofType:@"nib"]; | |
152 if ((self = [super initWithWindowNibPath:nibpath owner:self])) { | |
153 profile_ = profile; | |
154 controller_.reset(new KeywordEditorController(profile_)); | |
155 observer_.reset(new KeywordEditorModelObserver(self)); | |
156 controller_->table_model()->SetObserver(observer_.get()); | |
157 controller_->url_model()->AddObserver(observer_.get()); | |
158 groupCell_.reset([[NSTextFieldCell alloc] init]); | |
159 | |
160 if (g_browser_process && g_browser_process->local_state()) { | |
161 sizeSaver_.reset([[WindowSizeAutosaver alloc] | |
162 initWithWindow:[self window] | |
163 prefService:g_browser_process->local_state() | |
164 path:prefs::kKeywordEditorWindowPlacement]); | |
165 } | |
166 } | |
167 return self; | |
168 } | |
169 | |
170 - (void)dealloc { | |
171 controller_->table_model()->SetObserver(NULL); | |
172 controller_->url_model()->RemoveObserver(observer_.get()); | |
173 [tableView_ setDataSource:nil]; | |
174 observer_.reset(); | |
175 [super dealloc]; | |
176 } | |
177 | |
178 - (void)awakeFromNib { | |
179 // Make sure the button fits its label, but keep it the same height as the | |
180 // other two buttons. | |
181 [GTMUILocalizerAndLayoutTweaker sizeToFitView:makeDefaultButton_]; | |
182 NSSize size = [makeDefaultButton_ frame].size; | |
183 size.height = NSHeight([addButton_ frame]); | |
184 [makeDefaultButton_ setFrameSize:size]; | |
185 | |
186 [[self window] setAutorecalculatesContentBorderThickness:NO | |
187 forEdge:NSMinYEdge]; | |
188 [[self window] setContentBorderThickness:kButtonBarHeight | |
189 forEdge:NSMinYEdge]; | |
190 | |
191 [self adjustEditingButtons]; | |
192 [tableView_ setDoubleAction:@selector(editKeyword:)]; | |
193 [tableView_ setTarget:self]; | |
194 } | |
195 | |
196 // When the window closes, clean ourselves up. | |
197 - (void)windowWillClose:(NSNotification*)notif { | |
198 [self autorelease]; | |
199 | |
200 ProfileControllerMap* map = g_profile_controller_map.Pointer(); | |
201 ProfileControllerMap::iterator it = map->find(profile_); | |
202 // It should not be possible for this to be missing. | |
203 // TODO(shess): Except that the unit test reaches in directly. | |
204 // Consider circling around and refactoring that. | |
205 //DCHECK(it != map->end()); | |
206 if (it != map->end()) { | |
207 map->erase(it); | |
208 } | |
209 } | |
210 | |
211 - (void)modelChanged { | |
212 [tableView_ reloadData]; | |
213 [self adjustEditingButtons]; | |
214 } | |
215 | |
216 - (KeywordEditorController*)controller { | |
217 return controller_.get(); | |
218 } | |
219 | |
220 - (void)sheetDidEnd:(NSWindow*)sheet | |
221 returnCode:(NSInteger)code | |
222 context:(void*)context { | |
223 [sheet orderOut:self]; | |
224 } | |
225 | |
226 - (IBAction)addKeyword:(id)sender { | |
227 // The controller will release itself when the window closes. | |
228 EditSearchEngineCocoaController* editor = | |
229 [[EditSearchEngineCocoaController alloc] initWithProfile:profile_ | |
230 delegate:observer_.get() | |
231 templateURL:NULL]; | |
232 [NSApp beginSheet:[editor window] | |
233 modalForWindow:[self window] | |
234 modalDelegate:self | |
235 didEndSelector:@selector(sheetDidEnd:returnCode:context:) | |
236 contextInfo:NULL]; | |
237 } | |
238 | |
239 - (void)editKeyword:(id)sender { | |
240 const NSInteger clickedRow = [tableView_ clickedRow]; | |
241 if (clickedRow < 0 || [self tableView:tableView_ isGroupRow:clickedRow]) | |
242 return; | |
243 const TemplateURL* url = controller_->GetTemplateURL( | |
244 [self indexInModelForRow:clickedRow]); | |
245 // The controller will release itself when the window closes. | |
246 EditSearchEngineCocoaController* editor = | |
247 [[EditSearchEngineCocoaController alloc] initWithProfile:profile_ | |
248 delegate:observer_.get() | |
249 templateURL:url]; | |
250 [NSApp beginSheet:[editor window] | |
251 modalForWindow:[self window] | |
252 modalDelegate:self | |
253 didEndSelector:@selector(sheetDidEnd:returnCode:context:) | |
254 contextInfo:NULL]; | |
255 } | |
256 | |
257 - (IBAction)deleteKeyword:(id)sender { | |
258 NSIndexSet* selection = [tableView_ selectedRowIndexes]; | |
259 DCHECK_GT([selection count], 0U); | |
260 NSUInteger index = [selection lastIndex]; | |
261 while (index != NSNotFound) { | |
262 controller_->RemoveTemplateURL([self indexInModelForRow:index]); | |
263 index = [selection indexLessThanIndex:index]; | |
264 } | |
265 } | |
266 | |
267 - (IBAction)makeDefault:(id)sender { | |
268 NSIndexSet* selection = [tableView_ selectedRowIndexes]; | |
269 DCHECK_EQ([selection count], 1U); | |
270 int row = [self indexInModelForRow:[selection firstIndex]]; | |
271 controller_->MakeDefaultTemplateURL(row); | |
272 } | |
273 | |
274 // Called when the user hits the escape key. Closes the window. | |
275 - (void)cancel:(id)sender { | |
276 [[self window] performClose:self]; | |
277 } | |
278 | |
279 // Table View Data Source ----------------------------------------------------- | |
280 | |
281 - (NSInteger)numberOfRowsInTableView:(NSTableView*)table { | |
282 int rowCount = controller_->table_model()->RowCount(); | |
283 int numGroups = controller_->table_model()->GetGroups().size(); | |
284 if ([self tableView:table isGroupRow:rowCount + numGroups - 1]) { | |
285 // Don't show a group header with no rows underneath it. | |
286 --numGroups; | |
287 } | |
288 return rowCount + numGroups; | |
289 } | |
290 | |
291 - (id)tableView:(NSTableView*)tv | |
292 objectValueForTableColumn:(NSTableColumn*)tableColumn | |
293 row:(NSInteger)row { | |
294 if ([self tableView:tv isGroupRow:row]) { | |
295 DCHECK(!tableColumn); | |
296 ui::TableModel::Groups groups = controller_->table_model()->GetGroups(); | |
297 if (row == 0) { | |
298 return base::SysUTF16ToNSString(groups[0].title); | |
299 } else { | |
300 return base::SysUTF16ToNSString(groups[1].title); | |
301 } | |
302 } | |
303 | |
304 NSString* identifier = [tableColumn identifier]; | |
305 if ([identifier isEqualToString:@"name"]) { | |
306 // The name column is an NSButtonCell so we can have text and image in the | |
307 // same cell. As such, the "object value" for a button cell is either on | |
308 // or off, so we always return off so we don't act like a button. | |
309 return [NSNumber numberWithInt:NSOffState]; | |
310 } | |
311 if ([identifier isEqualToString:@"keyword"]) { | |
312 // The keyword object value is a normal string. | |
313 int index = [self indexInModelForRow:row]; | |
314 int columnID = IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN; | |
315 string16 text = controller_->table_model()->GetText(index, columnID); | |
316 return base::SysUTF16ToNSString(text); | |
317 } | |
318 | |
319 // And we shouldn't have any other columns... | |
320 NOTREACHED(); | |
321 return nil; | |
322 } | |
323 | |
324 // Table View Delegate -------------------------------------------------------- | |
325 | |
326 // When the selection in the table view changes, we need to adjust buttons. | |
327 - (void)tableViewSelectionDidChange:(NSNotification*)aNotification { | |
328 [self adjustEditingButtons]; | |
329 } | |
330 | |
331 // Disallow selection of the group header rows. | |
332 - (BOOL)tableView:(NSTableView*)table shouldSelectRow:(NSInteger)row { | |
333 return ![self tableView:table isGroupRow:row]; | |
334 } | |
335 | |
336 - (BOOL)tableView:(NSTableView*)table isGroupRow:(NSInteger)row { | |
337 int otherGroupRow = | |
338 controller_->table_model()->last_search_engine_index() + 1; | |
339 return (row == 0 || row == otherGroupRow); | |
340 } | |
341 | |
342 - (NSCell*)tableView:(NSTableView*)tableView | |
343 dataCellForTableColumn:(NSTableColumn*)tableColumn | |
344 row:(NSInteger)row { | |
345 static const CGFloat kCellFontSize = 12.0; | |
346 | |
347 // Check to see if we are a grouped row. | |
348 if ([self tableView:tableView isGroupRow:row]) { | |
349 DCHECK(!tableColumn); // This would violate the group row contract. | |
350 return groupCell_.get(); | |
351 } | |
352 | |
353 NSCell* cell = [tableColumn dataCellForRow:row]; | |
354 int offsetRow = [self indexInModelForRow:row]; | |
355 | |
356 // Set the favicon and title for the search engine in the name column. | |
357 if ([[tableColumn identifier] isEqualToString:@"name"]) { | |
358 DCHECK([cell isKindOfClass:[NSButtonCell class]]); | |
359 NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell); | |
360 string16 title = controller_->table_model()->GetText(offsetRow, | |
361 IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN); | |
362 [buttonCell setTitle:base::SysUTF16ToNSString(title)]; | |
363 [buttonCell setImage:observer_->GetImageForRow(offsetRow)]; | |
364 [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button. | |
365 [buttonCell setHighlightsBy:NSNoCellMask]; | |
366 } | |
367 | |
368 // The default search engine should be in bold font. | |
369 const TemplateURL* defaultEngine = | |
370 controller_->url_model()->GetDefaultSearchProvider(); | |
371 int rowIndex = controller_->table_model()->IndexOfTemplateURL(defaultEngine); | |
372 if (rowIndex == offsetRow) { | |
373 [cell setFont:[NSFont boldSystemFontOfSize:kCellFontSize]]; | |
374 } else { | |
375 [cell setFont:[NSFont systemFontOfSize:kCellFontSize]]; | |
376 } | |
377 return cell; | |
378 } | |
379 | |
380 // Private -------------------------------------------------------------------- | |
381 | |
382 // This function appropriately sets the enabled states on the table's editing | |
383 // buttons. | |
384 - (void)adjustEditingButtons { | |
385 NSIndexSet* selection = [tableView_ selectedRowIndexes]; | |
386 BOOL canRemove = ([selection count] > 0); | |
387 NSUInteger index = [selection firstIndex]; | |
388 | |
389 // Delete button. | |
390 while (canRemove && index != NSNotFound) { | |
391 int modelIndex = [self indexInModelForRow:index]; | |
392 const TemplateURL& url = | |
393 controller_->table_model()->GetTemplateURL(modelIndex); | |
394 if (!controller_->CanRemove(&url)) | |
395 canRemove = NO; | |
396 index = [selection indexGreaterThanIndex:index]; | |
397 } | |
398 [removeButton_ setEnabled:canRemove]; | |
399 | |
400 // Make default button. | |
401 if ([selection count] != 1) { | |
402 [makeDefaultButton_ setEnabled:NO]; | |
403 } else { | |
404 int row = [self indexInModelForRow:[selection firstIndex]]; | |
405 const TemplateURL& url = | |
406 controller_->table_model()->GetTemplateURL(row); | |
407 [makeDefaultButton_ setEnabled:controller_->CanMakeDefault(&url)]; | |
408 } | |
409 } | |
410 | |
411 // This converts a row index in our table view to an index in the model by | |
412 // computing the group offsets. | |
413 - (int)indexInModelForRow:(NSUInteger)row { | |
414 DCHECK_GT(row, 0U); | |
415 unsigned otherGroupId = | |
416 controller_->table_model()->last_search_engine_index() + 1; | |
417 DCHECK_NE(row, otherGroupId); | |
418 if (row >= otherGroupId) { | |
419 return row - 2; // Other group. | |
420 } else { | |
421 return row - 1; // Default group. | |
422 } | |
423 } | |
424 | |
425 @end | |
OLD | NEW |