summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa/content_exceptions_window_controller.mm
blob: e58926120154c2b9359403bca76a5442d2cac911 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
// 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/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;
- (size_t)menuItemCount;
- (NSString*)titleForIndex:(size_t)index;
- (ContentSetting)settingForIndex:(size_t)index;
- (size_t)indexForSetting:(ContentSetting)setting;
@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;

// The settings shown in the combobox for plug-ins;
const ContentSetting kPluginSettings[] = { CONTENT_SETTING_ALLOW,
                                           CONTENT_SETTING_ASK,
                                           CONTENT_SETTING_BLOCK };

// The settings shown in the combobox if showSession_ is false;
const ContentSetting kNoSessionSettings[] = { CONTENT_SETTING_ALLOW,
                                              CONTENT_SETTING_BLOCK };

// The settings shown in the combobox if showSession_ is true;
const ContentSetting kSessionSettings[] = { CONTENT_SETTING_ALLOW,
                                            CONTENT_SETTING_SESSION_ONLY,
                                            CONTENT_SETTING_BLOCK };

}  // 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_));
    showSession_ = settingsType_ == CONTENT_SETTINGS_TYPE_COOKIES;
    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<NSMenu> menu([[NSMenu alloc] initWithTitle:@"exceptionMenu"]);
  for (size_t i = 0; i < [self menuItemCount]; ++i) {
    scoped_nsobject<NSMenuItem> allowItem([[NSMenuItem alloc]
        initWithTitle:[self titleForIndex:i] action:NULL keyEquivalent:@""]);
    [allowItem.get() setTag:[self 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<NSUInteger>(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:[self 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 = [self 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];
}

- (size_t)menuItemCount {
  if (settingsType_ == CONTENT_SETTINGS_TYPE_PLUGINS)
    return arraysize(kPluginSettings);
  return showSession_ ? arraysize(kSessionSettings)
                      : arraysize(kNoSessionSettings);
}

- (NSString*)titleForIndex:(size_t)index {
  switch ([self settingForIndex:index]) {
    case CONTENT_SETTING_ALLOW:
      return l10n_util::GetNSStringWithFixup(IDS_EXCEPTIONS_ALLOW_BUTTON);
    case CONTENT_SETTING_ASK:
      return l10n_util::GetNSStringWithFixup(IDS_EXCEPTIONS_ASK_BUTTON);
    case CONTENT_SETTING_BLOCK:
      return l10n_util::GetNSStringWithFixup(IDS_EXCEPTIONS_BLOCK_BUTTON);
    case CONTENT_SETTING_SESSION_ONLY:
      return l10n_util::GetNSStringWithFixup(
          IDS_EXCEPTIONS_SESSION_ONLY_BUTTON);
    default:
      NOTREACHED();
  }
  return @"";
}

- (ContentSetting)settingForIndex:(size_t)index {
  if (settingsType_ == CONTENT_SETTINGS_TYPE_PLUGINS)
    return kPluginSettings[index];
  return showSession_ ? kSessionSettings[index] : kNoSessionSettings[index];
}

- (size_t)indexForSetting:(ContentSetting)setting {
  for (size_t i = 0; i < [self menuItemCount]; ++i) {
    if ([self settingForIndex:i] == setting)
      return i;
  }
  NOTREACHED();
  return 0;
}

@end