| 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/options/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 #import "chrome/browser/ui/cocoa/options/edit_search_engine_cocoa_controller.h" | |
| 17 #import "chrome/browser/ui/cocoa/window_size_autosaver.h" | |
| 18 #include "chrome/browser/ui/search_engines/template_url_table_model.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 |