// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/cocoa/content_exceptions_window_controller.h" #include "app/l10n_util.h" #include "app/l10n_util_mac.h" #include "app/table_model_observer.h" #include "base/command_line.h" #import "base/mac_util.h" #import "base/scoped_nsobject.h" #include "base/sys_string_conversions.h" #include "chrome/browser/content_exceptions_table_model.h" #include "chrome/browser/content_setting_combo_model.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/notification_registrar.h" #include "chrome/common/notification_service.h" #include "grit/generated_resources.h" #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" @interface ContentExceptionsWindowController (Private) - (id)initWithType:(ContentSettingsType)settingsType settingsMap:(HostContentSettingsMap*)settingsMap otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap; - (void)updateRow:(NSInteger)row withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry forOtr:(BOOL)isOtr; - (void)adjustEditingButtons; - (void)modelDidChange; - (NSString*)titleForIndex:(size_t)index; @end //////////////////////////////////////////////////////////////////////////////// // PatternFormatter // A simple formatter that accepts text that vaguely looks like a pattern. @interface PatternFormatter : NSFormatter @end @implementation PatternFormatter - (NSString*)stringForObjectValue:(id)object { if (![object isKindOfClass:[NSString class]]) return nil; return object; } - (BOOL)getObjectValue:(id*)object forString:(NSString*)string errorDescription:(NSString**)error { if ([string length]) { if (HostContentSettingsMap::Pattern( base::SysNSStringToUTF8(string)).IsValid()) { *object = string; return YES; } } if (error) *error = @"Invalid pattern"; return NO; } - (NSAttributedString*)attributedStringForObjectValue:(id)object withDefaultAttributes:(NSDictionary*)attribs { return nil; } @end //////////////////////////////////////////////////////////////////////////////// // UpdatingContentSettingsObserver // UpdatingContentSettingsObserver is a notification observer that tells a // window controller to update its data on every notification. class UpdatingContentSettingsObserver : public NotificationObserver { public: UpdatingContentSettingsObserver(ContentExceptionsWindowController* controller) : controller_(controller) { // One would think one could register a TableModelObserver to be notified of // changes to ContentExceptionsTableModel. One would be wrong: The table // model only sends out changes that are made through the model, not for // changes made directly to its backing HostContentSettings object (that // happens e.g. if the user uses the cookie confirmation dialog). Hence, // observe the CONTENT_SETTINGS_CHANGED notification directly. registrar_.Add(this, NotificationType::CONTENT_SETTINGS_CHANGED, NotificationService::AllSources()); } virtual void Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details); private: NotificationRegistrar registrar_; ContentExceptionsWindowController* controller_; }; void UpdatingContentSettingsObserver::Observe( NotificationType type, const NotificationSource& source, const NotificationDetails& details) { [controller_ modelDidChange]; } //////////////////////////////////////////////////////////////////////////////// // Static functions namespace { NSString* GetWindowTitle(ContentSettingsType settingsType) { switch (settingsType) { case CONTENT_SETTINGS_TYPE_COOKIES: return l10n_util::GetNSStringWithFixup(IDS_COOKIE_EXCEPTION_TITLE); case CONTENT_SETTINGS_TYPE_IMAGES: return l10n_util::GetNSStringWithFixup(IDS_IMAGES_EXCEPTION_TITLE); case CONTENT_SETTINGS_TYPE_JAVASCRIPT: return l10n_util::GetNSStringWithFixup(IDS_JS_EXCEPTION_TITLE); case CONTENT_SETTINGS_TYPE_PLUGINS: return l10n_util::GetNSStringWithFixup(IDS_PLUGINS_EXCEPTION_TITLE); case CONTENT_SETTINGS_TYPE_POPUPS: return l10n_util::GetNSStringWithFixup(IDS_POPUP_EXCEPTION_TITLE); default: NOTREACHED(); } return @""; } const CGFloat kButtonBarHeight = 35.0; } // namespace //////////////////////////////////////////////////////////////////////////////// // ContentExceptionsWindowController implementation static ContentExceptionsWindowController* g_exceptionWindows[CONTENT_SETTINGS_NUM_TYPES] = { nil }; @implementation ContentExceptionsWindowController + (id)controllerForType:(ContentSettingsType)settingsType settingsMap:(HostContentSettingsMap*)settingsMap otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap { if (!g_exceptionWindows[settingsType]) { g_exceptionWindows[settingsType] = [[ContentExceptionsWindowController alloc] initWithType:settingsType settingsMap:settingsMap otrSettingsMap:otrSettingsMap]; } return g_exceptionWindows[settingsType]; } - (id)initWithType:(ContentSettingsType)settingsType settingsMap:(HostContentSettingsMap*)settingsMap otrSettingsMap:(HostContentSettingsMap*)otrSettingsMap { NSString* nibpath = [mac_util::MainAppBundle() pathForResource:@"ContentExceptionsWindow" ofType:@"nib"]; if ((self = [super initWithWindowNibPath:nibpath owner:self])) { settingsType_ = settingsType; settingsMap_ = settingsMap; otrSettingsMap_ = otrSettingsMap; model_.reset(new ContentExceptionsTableModel( settingsMap_, otrSettingsMap_, settingsType_)); popup_model_.reset(new ContentSettingComboModel(settingsType_)); otrAllowed_ = otrSettingsMap != NULL; tableObserver_.reset(new UpdatingContentSettingsObserver(self)); updatesEnabled_ = YES; // TODO(thakis): autoremember window rect. // TODO(thakis): sorting support. } return self; } - (void)awakeFromNib { DCHECK([self window]); DCHECK_EQ(self, [[self window] delegate]); DCHECK(tableView_); DCHECK_EQ(self, [tableView_ dataSource]); DCHECK_EQ(self, [tableView_ delegate]); [[self window] setTitle:GetWindowTitle(settingsType_)]; CGFloat minWidth = [[addButton_ superview] bounds].size.width + [[doneButton_ superview] bounds].size.width; [self setMinWidth:minWidth]; [self adjustEditingButtons]; // Initialize menu for the data cell in the "action" column. scoped_nsobject menu([[NSMenu alloc] initWithTitle:@"exceptionMenu"]); for (int i = 0; i < popup_model_->GetItemCount(); ++i) { NSString* title = l10n_util::FixUpWindowsStyleLabel(popup_model_->GetItemAt(i)); scoped_nsobject allowItem( [[NSMenuItem alloc] initWithTitle:title action:NULL keyEquivalent:@""]); [allowItem.get() setTag:popup_model_->SettingForIndex(i)]; [menu.get() addItem:allowItem.get()]; } NSCell* menuCell = [[tableView_ tableColumnWithIdentifier:@"action"] dataCell]; [menuCell setMenu:menu.get()]; NSCell* patternCell = [[tableView_ tableColumnWithIdentifier:@"pattern"] dataCell]; [patternCell setFormatter:[[[PatternFormatter alloc] init] autorelease]]; if (!otrAllowed_) { [tableView_ removeTableColumn:[tableView_ tableColumnWithIdentifier:@"otr"]]; } } - (void)setMinWidth:(CGFloat)minWidth { NSWindow* window = [self window]; [window setMinSize:NSMakeSize(minWidth, [window minSize].height)]; if ([window frame].size.width < minWidth) { NSRect frame = [window frame]; frame.size.width = minWidth; [window setFrame:frame display:NO]; } } - (void)windowWillClose:(NSNotification*)notification { // Without this, some of the unit tests fail on 10.6: [tableView_ setDataSource:nil]; g_exceptionWindows[settingsType_] = nil; [self autorelease]; } - (BOOL)editingNewException { return newException_.get() != NULL; } // Let esc cancel editing if the user is currently editing a pattern. Else, let // esc close the window. - (void)cancel:(id)sender { if ([tableView_ currentEditor] != nil) { [tableView_ abortEditing]; [[self window] makeFirstResponder:tableView_]; // Re-gain focus. if ([tableView_ selectedRow] == model_->RowCount()) { // Cancel addition of new row. [self removeException:self]; } } else { [self closeSheet:self]; } } - (void)keyDown:(NSEvent*)event { NSString* chars = [event charactersIgnoringModifiers]; if ([chars length] == 1) { switch ([chars characterAtIndex:0]) { case NSDeleteCharacter: case NSDeleteFunctionKey: // Delete deletes. if ([[tableView_ selectedRowIndexes] count] > 0) [self removeException:self]; return; case NSCarriageReturnCharacter: case NSEnterCharacter: // Return enters rename mode. if ([[tableView_ selectedRowIndexes] count] == 1) { [tableView_ editColumn:0 row:[[tableView_ selectedRowIndexes] lastIndex] withEvent:nil select:YES]; } return; } } [super keyDown:event]; } - (void)attachSheetTo:(NSWindow*)window { [NSApp beginSheet:[self window] modalForWindow:window modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil]; } - (void)sheetDidEnd:(NSWindow*)sheet returnCode:(NSInteger)returnCode contextInfo:(void*)context { [sheet close]; [sheet orderOut:self]; } - (IBAction)addException:(id)sender { if (newException_.get()) { // The invariant is that |newException_| is non-NULL exactly if the pattern // of a new exception is currently being edited - so there's nothing to do // in that case. return; } newException_.reset(new HostContentSettingsMap::PatternSettingPair); newException_->first = HostContentSettingsMap::Pattern( l10n_util::GetStringUTF8(IDS_EXCEPTIONS_SAMPLE_PATTERN)); newException_->second = CONTENT_SETTING_BLOCK; [tableView_ reloadData]; [self adjustEditingButtons]; int index = model_->RowCount(); NSIndexSet* selectedSet = [NSIndexSet indexSetWithIndex:index]; [tableView_ selectRowIndexes:selectedSet byExtendingSelection:NO]; [tableView_ editColumn:0 row:index withEvent:nil select:YES]; } - (IBAction)removeException:(id)sender { updatesEnabled_ = NO; NSIndexSet* selection = [tableView_ selectedRowIndexes]; [tableView_ deselectAll:self]; // Else we'll get a -setObjectValue: later. DCHECK_GT([selection count], 0U); NSUInteger index = [selection lastIndex]; while (index != NSNotFound) { if (index == static_cast(model_->RowCount())) newException_.reset(); else model_->RemoveException(index); index = [selection indexLessThanIndex:index]; } updatesEnabled_ = YES; [self modelDidChange]; } - (IBAction)removeAllExceptions:(id)sender { updatesEnabled_ = NO; [tableView_ deselectAll:self]; // Else we'll get a -setObjectValue: later. newException_.reset(); model_->RemoveAll(); updatesEnabled_ = YES; [self modelDidChange]; } - (IBAction)closeSheet:(id)sender { [NSApp endSheet:[self window]]; } // Table View Data Source ----------------------------------------------------- - (NSInteger)numberOfRowsInTableView:(NSTableView*)table { return model_->RowCount() + (newException_.get() ? 1 : 0); } - (id)tableView:(NSTableView*)tv objectValueForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row { const HostContentSettingsMap::PatternSettingPair* entry; int isOtr; if (newException_.get() && row >= model_->RowCount()) { entry = newException_.get(); isOtr = 0; } else { entry = &model_->entry_at(row); isOtr = model_->entry_is_off_the_record(row) ? 1 : 0; } NSObject* result = nil; NSString* identifier = [tableColumn identifier]; if ([identifier isEqualToString:@"pattern"]) { result = base::SysUTF8ToNSString(entry->first.AsString()); } else if ([identifier isEqualToString:@"action"]) { result = [NSNumber numberWithInt:popup_model_->IndexForSetting(entry->second)]; } else if ([identifier isEqualToString:@"otr"]) { result = [NSNumber numberWithInt:isOtr]; } else { NOTREACHED(); } return result; } // Updates exception at |row| to contain the data in |entry|. - (void)updateRow:(NSInteger)row withEntry:(const HostContentSettingsMap::PatternSettingPair&)entry forOtr:(BOOL)isOtr { // TODO(thakis): This apparently moves an edited row to the back of the list. // It's what windows and linux do, but it's kinda sucky. Fix. // http://crbug.com/36904 updatesEnabled_ = NO; if (row < model_->RowCount()) model_->RemoveException(row); model_->AddException(entry.first, entry.second, isOtr); updatesEnabled_ = YES; [self modelDidChange]; // For now, at least re-select the edited element. int newIndex = model_->IndexOfExceptionByPattern(entry.first, isOtr); DCHECK(newIndex != -1); [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex] byExtendingSelection:NO]; } - (void) tableView:(NSTableView*)tv setObjectValue:(id)object forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row { // -remove: and -removeAll: both call |tableView_|'s -deselectAll:, which // calls this method if a cell is currently being edited. Do not commit edits // of rows that are about to be deleted. if (!updatesEnabled_) { // If this method gets called, the pattern filed of the new exception can no // longer be being edited. Reset |newException_| to keep the invariant true. newException_.reset(); return; } // Get model object. bool isNewRow = newException_.get() && row >= model_->RowCount(); HostContentSettingsMap::PatternSettingPair originalEntry = isNewRow ? *newException_ : model_->entry_at(row); HostContentSettingsMap::PatternSettingPair entry = originalEntry; bool isOtr = isNewRow ? 0 : model_->entry_is_off_the_record(row); bool wasOtr = isOtr; // Modify it. NSString* identifier = [tableColumn identifier]; if ([identifier isEqualToString:@"pattern"]) { entry.first = HostContentSettingsMap::Pattern( base::SysNSStringToUTF8(object)); } if ([identifier isEqualToString:@"action"]) { int index = [object intValue]; entry.second = popup_model_->SettingForIndex(index); } if ([identifier isEqualToString:@"otr"]) { isOtr = [object intValue] != 0; } // Commit modification, if any. if (isNewRow) { newException_.reset(); if (![identifier isEqualToString:@"pattern"]) { [tableView_ reloadData]; [self adjustEditingButtons]; return; // Commit new rows only when the pattern has been set. } int newIndex = model_->IndexOfExceptionByPattern(entry.first, false); if (newIndex != -1) { // The new pattern was already in the table. Focus existing row instead of // overwriting it with a new one. [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex] byExtendingSelection:NO]; [tableView_ reloadData]; [self adjustEditingButtons]; return; } } if (entry != originalEntry || wasOtr != isOtr || isNewRow) [self updateRow:row withEntry:entry forOtr:isOtr]; } // Table View Delegate -------------------------------------------------------- // When the selection in the table view changes, we need to adjust buttons. - (void)tableViewSelectionDidChange:(NSNotification*)notification { [self adjustEditingButtons]; } // Private -------------------------------------------------------------------- // This method appropriately sets the enabled states on the table's editing // buttons. - (void)adjustEditingButtons { NSIndexSet* selection = [tableView_ selectedRowIndexes]; [removeButton_ setEnabled:([selection count] > 0)]; [removeAllButton_ setEnabled:([tableView_ numberOfRows] > 0)]; } - (void)modelDidChange { // Some calls on |model_|, e.g. RemoveException(), change something on the // backing content settings map object (which sends a notification) and then // change more stuff in |model_|. If |model_| is deleted when the notification // is sent, this second access causes a segmentation violation. Hence, disable // resetting |model_| while updates can be in progress. if (!updatesEnabled_) return; // The model caches its data, meaning we need to recreate it on every change. model_.reset(new ContentExceptionsTableModel( settingsMap_, otrSettingsMap_, settingsType_)); [tableView_ reloadData]; [self adjustEditingButtons]; } @end