| 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/content_exceptions_window_controller.h" | |
| 6 | |
| 7 #include "app/l10n_util.h" | |
| 8 #include "app/l10n_util_mac.h" | |
| 9 #include "base/command_line.h" | |
| 10 #import "base/mac/mac_util.h" | |
| 11 #import "base/scoped_nsobject.h" | |
| 12 #include "base/sys_string_conversions.h" | |
| 13 #include "chrome/browser/content_exceptions_table_model.h" | |
| 14 #include "chrome/browser/content_setting_combo_model.h" | |
| 15 #include "chrome/common/notification_registrar.h" | |
| 16 #include "chrome/common/notification_service.h" | |
| 17 #include "grit/generated_resources.h" | |
| 18 #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" | |
| 19 #include "ui/base/models/table_model_observer.h" | |
| 20 | |
| 21 @interface ContentExceptionsWindowController (Private) | |
| 22 - (id)initWithType:(ContentSettingsType)settingsType | |
| 23 settingsMap:(HostContentSettingsMap*)settingsMap | |
| 24 otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap; | |
| 25 - (void)updateRow:(NSInteger)row | |
| 26 withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry | |
| 27 forOtr:(BOOL)isOtr; | |
| 28 - (void)adjustEditingButtons; | |
| 29 - (void)modelDidChange; | |
| 30 - (NSString*)titleForIndex:(size_t)index; | |
| 31 @end | |
| 32 | |
| 33 //////////////////////////////////////////////////////////////////////////////// | |
| 34 // PatternFormatter | |
| 35 | |
| 36 // A simple formatter that accepts text that vaguely looks like a pattern. | |
| 37 @interface PatternFormatter : NSFormatter | |
| 38 @end | |
| 39 | |
| 40 @implementation PatternFormatter | |
| 41 - (NSString*)stringForObjectValue:(id)object { | |
| 42 if (![object isKindOfClass:[NSString class]]) | |
| 43 return nil; | |
| 44 return object; | |
| 45 } | |
| 46 | |
| 47 - (BOOL)getObjectValue:(id*)object | |
| 48 forString:(NSString*)string | |
| 49 errorDescription:(NSString**)error { | |
| 50 if ([string length]) { | |
| 51 if (ContentSettingsPattern( | |
| 52 base::SysNSStringToUTF8(string)).IsValid()) { | |
| 53 *object = string; | |
| 54 return YES; | |
| 55 } | |
| 56 } | |
| 57 if (error) | |
| 58 *error = @"Invalid pattern"; | |
| 59 return NO; | |
| 60 } | |
| 61 | |
| 62 - (NSAttributedString*)attributedStringForObjectValue:(id)object | |
| 63 withDefaultAttributes:(NSDictionary*)attribs { | |
| 64 return nil; | |
| 65 } | |
| 66 @end | |
| 67 | |
| 68 //////////////////////////////////////////////////////////////////////////////// | |
| 69 // UpdatingContentSettingsObserver | |
| 70 | |
| 71 // UpdatingContentSettingsObserver is a notification observer that tells a | |
| 72 // window controller to update its data on every notification. | |
| 73 class UpdatingContentSettingsObserver : public NotificationObserver { | |
| 74 public: | |
| 75 UpdatingContentSettingsObserver(ContentExceptionsWindowController* controller) | |
| 76 : controller_(controller) { | |
| 77 // One would think one could register a TableModelObserver to be notified of | |
| 78 // changes to ContentExceptionsTableModel. One would be wrong: The table | |
| 79 // model only sends out changes that are made through the model, not for | |
| 80 // changes made directly to its backing HostContentSettings object (that | |
| 81 // happens e.g. if the user uses the cookie confirmation dialog). Hence, | |
| 82 // observe the CONTENT_SETTINGS_CHANGED notification directly. | |
| 83 registrar_.Add(this, NotificationType::CONTENT_SETTINGS_CHANGED, | |
| 84 NotificationService::AllSources()); | |
| 85 } | |
| 86 virtual void Observe(NotificationType type, | |
| 87 const NotificationSource& source, | |
| 88 const NotificationDetails& details); | |
| 89 private: | |
| 90 NotificationRegistrar registrar_; | |
| 91 ContentExceptionsWindowController* controller_; | |
| 92 }; | |
| 93 | |
| 94 void UpdatingContentSettingsObserver::Observe( | |
| 95 NotificationType type, | |
| 96 const NotificationSource& source, | |
| 97 const NotificationDetails& details) { | |
| 98 [controller_ modelDidChange]; | |
| 99 } | |
| 100 | |
| 101 //////////////////////////////////////////////////////////////////////////////// | |
| 102 // Static functions | |
| 103 | |
| 104 namespace { | |
| 105 | |
| 106 NSString* GetWindowTitle(ContentSettingsType settingsType) { | |
| 107 switch (settingsType) { | |
| 108 case CONTENT_SETTINGS_TYPE_COOKIES: | |
| 109 return l10n_util::GetNSStringWithFixup(IDS_COOKIE_EXCEPTION_TITLE); | |
| 110 case CONTENT_SETTINGS_TYPE_IMAGES: | |
| 111 return l10n_util::GetNSStringWithFixup(IDS_IMAGES_EXCEPTION_TITLE); | |
| 112 case CONTENT_SETTINGS_TYPE_JAVASCRIPT: | |
| 113 return l10n_util::GetNSStringWithFixup(IDS_JS_EXCEPTION_TITLE); | |
| 114 case CONTENT_SETTINGS_TYPE_PLUGINS: | |
| 115 return l10n_util::GetNSStringWithFixup(IDS_PLUGINS_EXCEPTION_TITLE); | |
| 116 case CONTENT_SETTINGS_TYPE_POPUPS: | |
| 117 return l10n_util::GetNSStringWithFixup(IDS_POPUP_EXCEPTION_TITLE); | |
| 118 default: | |
| 119 NOTREACHED(); | |
| 120 } | |
| 121 return @""; | |
| 122 } | |
| 123 | |
| 124 const CGFloat kButtonBarHeight = 35.0; | |
| 125 | |
| 126 } // namespace | |
| 127 | |
| 128 //////////////////////////////////////////////////////////////////////////////// | |
| 129 // ContentExceptionsWindowController implementation | |
| 130 | |
| 131 static ContentExceptionsWindowController* | |
| 132 g_exceptionWindows[CONTENT_SETTINGS_NUM_TYPES] = { nil }; | |
| 133 | |
| 134 @implementation ContentExceptionsWindowController | |
| 135 | |
| 136 + (id)controllerForType:(ContentSettingsType)settingsType | |
| 137 settingsMap:(HostContentSettingsMap*)settingsMap | |
| 138 otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap { | |
| 139 if (!g_exceptionWindows[settingsType]) { | |
| 140 g_exceptionWindows[settingsType] = | |
| 141 [[ContentExceptionsWindowController alloc] | |
| 142 initWithType:settingsType | |
| 143 settingsMap:settingsMap | |
| 144 otrSettingsMap:otrSettingsMap]; | |
| 145 } | |
| 146 return g_exceptionWindows[settingsType]; | |
| 147 } | |
| 148 | |
| 149 - (id)initWithType:(ContentSettingsType)settingsType | |
| 150 settingsMap:(HostContentSettingsMap*)settingsMap | |
| 151 otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap { | |
| 152 NSString* nibpath = | |
| 153 [base::mac::MainAppBundle() pathForResource:@"ContentExceptionsWindow" | |
| 154 ofType:@"nib"]; | |
| 155 if ((self = [super initWithWindowNibPath:nibpath owner:self])) { | |
| 156 settingsType_ = settingsType; | |
| 157 settingsMap_ = settingsMap; | |
| 158 otrSettingsMap_ = otrSettingsMap; | |
| 159 model_.reset(new ContentExceptionsTableModel( | |
| 160 settingsMap_, otrSettingsMap_, settingsType_)); | |
| 161 popup_model_.reset(new ContentSettingComboModel(settingsType_)); | |
| 162 otrAllowed_ = otrSettingsMap != NULL; | |
| 163 tableObserver_.reset(new UpdatingContentSettingsObserver(self)); | |
| 164 updatesEnabled_ = YES; | |
| 165 | |
| 166 // TODO(thakis): autoremember window rect. | |
| 167 // TODO(thakis): sorting support. | |
| 168 } | |
| 169 return self; | |
| 170 } | |
| 171 | |
| 172 - (void)awakeFromNib { | |
| 173 DCHECK([self window]); | |
| 174 DCHECK_EQ(self, [[self window] delegate]); | |
| 175 DCHECK(tableView_); | |
| 176 DCHECK_EQ(self, [tableView_ dataSource]); | |
| 177 DCHECK_EQ(self, [tableView_ delegate]); | |
| 178 | |
| 179 [[self window] setTitle:GetWindowTitle(settingsType_)]; | |
| 180 | |
| 181 CGFloat minWidth = [[addButton_ superview] bounds].size.width + | |
| 182 [[doneButton_ superview] bounds].size.width; | |
| 183 [self setMinWidth:minWidth]; | |
| 184 | |
| 185 [self adjustEditingButtons]; | |
| 186 | |
| 187 // Initialize menu for the data cell in the "action" column. | |
| 188 scoped_nsobject<NSMenu> menu([[NSMenu alloc] initWithTitle:@"exceptionMenu"]); | |
| 189 for (int i = 0; i < popup_model_->GetItemCount(); ++i) { | |
| 190 NSString* title = | |
| 191 l10n_util::FixUpWindowsStyleLabel(popup_model_->GetItemAt(i)); | |
| 192 scoped_nsobject<NSMenuItem> allowItem( | |
| 193 [[NSMenuItem alloc] initWithTitle:title action:NULL keyEquivalent:@""]); | |
| 194 [allowItem.get() setTag:popup_model_->SettingForIndex(i)]; | |
| 195 [menu.get() addItem:allowItem.get()]; | |
| 196 } | |
| 197 NSCell* menuCell = | |
| 198 [[tableView_ tableColumnWithIdentifier:@"action"] dataCell]; | |
| 199 [menuCell setMenu:menu.get()]; | |
| 200 | |
| 201 NSCell* patternCell = | |
| 202 [[tableView_ tableColumnWithIdentifier:@"pattern"] dataCell]; | |
| 203 [patternCell setFormatter:[[[PatternFormatter alloc] init] autorelease]]; | |
| 204 | |
| 205 if (!otrAllowed_) { | |
| 206 [tableView_ | |
| 207 removeTableColumn:[tableView_ tableColumnWithIdentifier:@"otr"]]; | |
| 208 } | |
| 209 } | |
| 210 | |
| 211 - (void)setMinWidth:(CGFloat)minWidth { | |
| 212 NSWindow* window = [self window]; | |
| 213 [window setMinSize:NSMakeSize(minWidth, [window minSize].height)]; | |
| 214 if ([window frame].size.width < minWidth) { | |
| 215 NSRect frame = [window frame]; | |
| 216 frame.size.width = minWidth; | |
| 217 [window setFrame:frame display:NO]; | |
| 218 } | |
| 219 } | |
| 220 | |
| 221 - (void)windowWillClose:(NSNotification*)notification { | |
| 222 // Without this, some of the unit tests fail on 10.6: | |
| 223 [tableView_ setDataSource:nil]; | |
| 224 | |
| 225 g_exceptionWindows[settingsType_] = nil; | |
| 226 [self autorelease]; | |
| 227 } | |
| 228 | |
| 229 - (BOOL)editingNewException { | |
| 230 return newException_.get() != NULL; | |
| 231 } | |
| 232 | |
| 233 // Let esc cancel editing if the user is currently editing a pattern. Else, let | |
| 234 // esc close the window. | |
| 235 - (void)cancel:(id)sender { | |
| 236 if ([tableView_ currentEditor] != nil) { | |
| 237 [tableView_ abortEditing]; | |
| 238 [[self window] makeFirstResponder:tableView_]; // Re-gain focus. | |
| 239 | |
| 240 if ([tableView_ selectedRow] == model_->RowCount()) { | |
| 241 // Cancel addition of new row. | |
| 242 [self removeException:self]; | |
| 243 } | |
| 244 } else { | |
| 245 [self closeSheet:self]; | |
| 246 } | |
| 247 } | |
| 248 | |
| 249 - (void)keyDown:(NSEvent*)event { | |
| 250 NSString* chars = [event charactersIgnoringModifiers]; | |
| 251 if ([chars length] == 1) { | |
| 252 switch ([chars characterAtIndex:0]) { | |
| 253 case NSDeleteCharacter: | |
| 254 case NSDeleteFunctionKey: | |
| 255 // Delete deletes. | |
| 256 if ([[tableView_ selectedRowIndexes] count] > 0) | |
| 257 [self removeException:self]; | |
| 258 return; | |
| 259 case NSCarriageReturnCharacter: | |
| 260 case NSEnterCharacter: | |
| 261 // Return enters rename mode. | |
| 262 if ([[tableView_ selectedRowIndexes] count] == 1) { | |
| 263 [tableView_ editColumn:0 | |
| 264 row:[[tableView_ selectedRowIndexes] lastIndex] | |
| 265 withEvent:nil | |
| 266 select:YES]; | |
| 267 } | |
| 268 return; | |
| 269 } | |
| 270 } | |
| 271 [super keyDown:event]; | |
| 272 } | |
| 273 | |
| 274 - (void)attachSheetTo:(NSWindow*)window { | |
| 275 [NSApp beginSheet:[self window] | |
| 276 modalForWindow:window | |
| 277 modalDelegate:self | |
| 278 didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) | |
| 279 contextInfo:nil]; | |
| 280 } | |
| 281 | |
| 282 - (void)sheetDidEnd:(NSWindow*)sheet | |
| 283 returnCode:(NSInteger)returnCode | |
| 284 contextInfo:(void*)context { | |
| 285 [sheet close]; | |
| 286 [sheet orderOut:self]; | |
| 287 } | |
| 288 | |
| 289 - (IBAction)addException:(id)sender { | |
| 290 if (newException_.get()) { | |
| 291 // The invariant is that |newException_| is non-NULL exactly if the pattern | |
| 292 // of a new exception is currently being edited - so there's nothing to do | |
| 293 // in that case. | |
| 294 return; | |
| 295 } | |
| 296 newException_.reset(new HostContentSettingsMap::PatternSettingPair); | |
| 297 newException_->first = ContentSettingsPattern( | |
| 298 l10n_util::GetStringUTF8(IDS_EXCEPTIONS_SAMPLE_PATTERN)); | |
| 299 newException_->second = CONTENT_SETTING_BLOCK; | |
| 300 [tableView_ reloadData]; | |
| 301 | |
| 302 [self adjustEditingButtons]; | |
| 303 int index = model_->RowCount(); | |
| 304 NSIndexSet* selectedSet = [NSIndexSet indexSetWithIndex:index]; | |
| 305 [tableView_ selectRowIndexes:selectedSet byExtendingSelection:NO]; | |
| 306 [tableView_ editColumn:0 row:index withEvent:nil select:YES]; | |
| 307 } | |
| 308 | |
| 309 - (IBAction)removeException:(id)sender { | |
| 310 updatesEnabled_ = NO; | |
| 311 NSIndexSet* selection = [tableView_ selectedRowIndexes]; | |
| 312 [tableView_ deselectAll:self]; // Else we'll get a -setObjectValue: later. | |
| 313 DCHECK_GT([selection count], 0U); | |
| 314 NSUInteger index = [selection lastIndex]; | |
| 315 while (index != NSNotFound) { | |
| 316 if (index == static_cast<NSUInteger>(model_->RowCount())) | |
| 317 newException_.reset(); | |
| 318 else | |
| 319 model_->RemoveException(index); | |
| 320 index = [selection indexLessThanIndex:index]; | |
| 321 } | |
| 322 updatesEnabled_ = YES; | |
| 323 [self modelDidChange]; | |
| 324 } | |
| 325 | |
| 326 - (IBAction)removeAllExceptions:(id)sender { | |
| 327 updatesEnabled_ = NO; | |
| 328 [tableView_ deselectAll:self]; // Else we'll get a -setObjectValue: later. | |
| 329 newException_.reset(); | |
| 330 model_->RemoveAll(); | |
| 331 updatesEnabled_ = YES; | |
| 332 [self modelDidChange]; | |
| 333 } | |
| 334 | |
| 335 - (IBAction)closeSheet:(id)sender { | |
| 336 [NSApp endSheet:[self window]]; | |
| 337 } | |
| 338 | |
| 339 // Table View Data Source ----------------------------------------------------- | |
| 340 | |
| 341 - (NSInteger)numberOfRowsInTableView:(NSTableView*)table { | |
| 342 return model_->RowCount() + (newException_.get() ? 1 : 0); | |
| 343 } | |
| 344 | |
| 345 - (id)tableView:(NSTableView*)tv | |
| 346 objectValueForTableColumn:(NSTableColumn*)tableColumn | |
| 347 row:(NSInteger)row { | |
| 348 const HostContentSettingsMap::PatternSettingPair* entry; | |
| 349 int isOtr; | |
| 350 if (newException_.get() && row >= model_->RowCount()) { | |
| 351 entry = newException_.get(); | |
| 352 isOtr = 0; | |
| 353 } else { | |
| 354 entry = &model_->entry_at(row); | |
| 355 isOtr = model_->entry_is_off_the_record(row) ? 1 : 0; | |
| 356 } | |
| 357 | |
| 358 NSObject* result = nil; | |
| 359 NSString* identifier = [tableColumn identifier]; | |
| 360 if ([identifier isEqualToString:@"pattern"]) { | |
| 361 result = base::SysUTF8ToNSString(entry->first.AsString()); | |
| 362 } else if ([identifier isEqualToString:@"action"]) { | |
| 363 result = | |
| 364 [NSNumber numberWithInt:popup_model_->IndexForSetting(entry->second)]; | |
| 365 } else if ([identifier isEqualToString:@"otr"]) { | |
| 366 result = [NSNumber numberWithInt:isOtr]; | |
| 367 } else { | |
| 368 NOTREACHED(); | |
| 369 } | |
| 370 return result; | |
| 371 } | |
| 372 | |
| 373 // Updates exception at |row| to contain the data in |entry|. | |
| 374 - (void)updateRow:(NSInteger)row | |
| 375 withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry | |
| 376 forOtr:(BOOL)isOtr { | |
| 377 // TODO(thakis): This apparently moves an edited row to the back of the list. | |
| 378 // It's what windows and linux do, but it's kinda sucky. Fix. | |
| 379 // http://crbug.com/36904 | |
| 380 updatesEnabled_ = NO; | |
| 381 if (row < model_->RowCount()) | |
| 382 model_->RemoveException(row); | |
| 383 model_->AddException(entry.first, entry.second, isOtr); | |
| 384 updatesEnabled_ = YES; | |
| 385 [self modelDidChange]; | |
| 386 | |
| 387 // For now, at least re-select the edited element. | |
| 388 int newIndex = model_->IndexOfExceptionByPattern(entry.first, isOtr); | |
| 389 DCHECK(newIndex != -1); | |
| 390 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex] | |
| 391 byExtendingSelection:NO]; | |
| 392 } | |
| 393 | |
| 394 - (void) tableView:(NSTableView*)tv | |
| 395 setObjectValue:(id)object | |
| 396 forTableColumn:(NSTableColumn*)tableColumn | |
| 397 row:(NSInteger)row { | |
| 398 // -remove: and -removeAll: both call |tableView_|'s -deselectAll:, which | |
| 399 // calls this method if a cell is currently being edited. Do not commit edits | |
| 400 // of rows that are about to be deleted. | |
| 401 if (!updatesEnabled_) { | |
| 402 // If this method gets called, the pattern filed of the new exception can no | |
| 403 // longer be being edited. Reset |newException_| to keep the invariant true. | |
| 404 newException_.reset(); | |
| 405 return; | |
| 406 } | |
| 407 | |
| 408 // Get model object. | |
| 409 bool isNewRow = newException_.get() && row >= model_->RowCount(); | |
| 410 HostContentSettingsMap::PatternSettingPair originalEntry = | |
| 411 isNewRow ? *newException_ : model_->entry_at(row); | |
| 412 HostContentSettingsMap::PatternSettingPair entry = originalEntry; | |
| 413 bool isOtr = | |
| 414 isNewRow ? 0 : model_->entry_is_off_the_record(row); | |
| 415 bool wasOtr = isOtr; | |
| 416 | |
| 417 // Modify it. | |
| 418 NSString* identifier = [tableColumn identifier]; | |
| 419 if ([identifier isEqualToString:@"pattern"]) { | |
| 420 entry.first = ContentSettingsPattern(base::SysNSStringToUTF8(object)); | |
| 421 } | |
| 422 if ([identifier isEqualToString:@"action"]) { | |
| 423 int index = [object intValue]; | |
| 424 entry.second = popup_model_->SettingForIndex(index); | |
| 425 } | |
| 426 if ([identifier isEqualToString:@"otr"]) { | |
| 427 isOtr = [object intValue] != 0; | |
| 428 } | |
| 429 | |
| 430 // Commit modification, if any. | |
| 431 if (isNewRow) { | |
| 432 newException_.reset(); | |
| 433 if (![identifier isEqualToString:@"pattern"]) { | |
| 434 [tableView_ reloadData]; | |
| 435 [self adjustEditingButtons]; | |
| 436 return; // Commit new rows only when the pattern has been set. | |
| 437 } | |
| 438 int newIndex = model_->IndexOfExceptionByPattern(entry.first, false); | |
| 439 if (newIndex != -1) { | |
| 440 // The new pattern was already in the table. Focus existing row instead of | |
| 441 // overwriting it with a new one. | |
| 442 [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex] | |
| 443 byExtendingSelection:NO]; | |
| 444 [tableView_ reloadData]; | |
| 445 [self adjustEditingButtons]; | |
| 446 return; | |
| 447 } | |
| 448 } | |
| 449 if (entry != originalEntry || wasOtr != isOtr || isNewRow) | |
| 450 [self updateRow:row withEntry:entry forOtr:isOtr]; | |
| 451 } | |
| 452 | |
| 453 | |
| 454 // Table View Delegate -------------------------------------------------------- | |
| 455 | |
| 456 // When the selection in the table view changes, we need to adjust buttons. | |
| 457 - (void)tableViewSelectionDidChange:(NSNotification*)notification { | |
| 458 [self adjustEditingButtons]; | |
| 459 } | |
| 460 | |
| 461 // Private -------------------------------------------------------------------- | |
| 462 | |
| 463 // This method appropriately sets the enabled states on the table's editing | |
| 464 // buttons. | |
| 465 - (void)adjustEditingButtons { | |
| 466 NSIndexSet* selection = [tableView_ selectedRowIndexes]; | |
| 467 [removeButton_ setEnabled:([selection count] > 0)]; | |
| 468 [removeAllButton_ setEnabled:([tableView_ numberOfRows] > 0)]; | |
| 469 } | |
| 470 | |
| 471 - (void)modelDidChange { | |
| 472 // Some calls on |model_|, e.g. RemoveException(), change something on the | |
| 473 // backing content settings map object (which sends a notification) and then | |
| 474 // change more stuff in |model_|. If |model_| is deleted when the notification | |
| 475 // is sent, this second access causes a segmentation violation. Hence, disable | |
| 476 // resetting |model_| while updates can be in progress. | |
| 477 if (!updatesEnabled_) | |
| 478 return; | |
| 479 | |
| 480 // The model caches its data, meaning we need to recreate it on every change. | |
| 481 model_.reset(new ContentExceptionsTableModel( | |
| 482 settingsMap_, otrSettingsMap_, settingsType_)); | |
| 483 | |
| 484 [tableView_ reloadData]; | |
| 485 [self adjustEditingButtons]; | |
| 486 } | |
| 487 | |
| 488 @end | |
| OLD | NEW |