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 |