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_setting_bubble_cocoa.h" | |
6 | |
7 #include "app/l10n_util.h" | |
8 #include "base/command_line.h" | |
9 #include "base/logging.h" | |
10 #include "base/sys_string_conversions.h" | |
11 #include "base/utf_string_conversions.h" | |
12 #include "chrome/browser/blocked_content_container.h" | |
13 #include "chrome/browser/content_setting_bubble_model.h" | |
14 #include "chrome/browser/content_settings/host_content_settings_map.h" | |
15 #include "chrome/browser/plugin_updater.h" | |
16 #import "chrome/browser/ui/cocoa/hyperlink_button_cell.h" | |
17 #import "chrome/browser/ui/cocoa/info_bubble_view.h" | |
18 #import "chrome/browser/ui/cocoa/l10n_util.h" | |
19 #import "chrome/browser/ui/cocoa/options/content_settings_dialog_controller.h" | |
20 #include "grit/generated_resources.h" | |
21 #include "skia/ext/skia_utils_mac.h" | |
22 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" | |
23 #include "webkit/glue/plugins/plugin_list.h" | |
24 | |
25 namespace { | |
26 | |
27 // Must match the tag of the unblock radio button in the xib files. | |
28 const int kAllowTag = 1; | |
29 | |
30 // Must match the tag of the block radio button in the xib files. | |
31 const int kBlockTag = 2; | |
32 | |
33 // Height of one link in the popup list. | |
34 const int kLinkHeight = 16; | |
35 | |
36 // Space between two popup links. | |
37 const int kLinkPadding = 4; | |
38 | |
39 // Space taken in total by one popup link. | |
40 const int kLinkLineHeight = kLinkHeight + kLinkPadding; | |
41 | |
42 // Space between popup list and surrounding UI elements. | |
43 const int kLinkOuterPadding = 8; | |
44 | |
45 // Height of each of the labels in the geolocation bubble. | |
46 const int kGeoLabelHeight = 14; | |
47 | |
48 // Height of the "Clear" button in the geolocation bubble. | |
49 const int kGeoClearButtonHeight = 17; | |
50 | |
51 // Padding between radio buttons and "Load all plugins" button | |
52 // in the plugin bubble. | |
53 const int kLoadAllPluginsButtonVerticalPadding = 8; | |
54 | |
55 // General padding between elements in the geolocation bubble. | |
56 const int kGeoPadding = 8; | |
57 | |
58 // Padding between host names in the geolocation bubble. | |
59 const int kGeoHostPadding = 4; | |
60 | |
61 // Minimal padding between "Manage" and "Done" buttons. | |
62 const int kManageDonePadding = 8; | |
63 | |
64 void SetControlSize(NSControl* control, NSControlSize controlSize) { | |
65 CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize]; | |
66 NSCell* cell = [control cell]; | |
67 NSFont* font = [NSFont fontWithName:[[cell font] fontName] size:fontSize]; | |
68 [cell setFont:font]; | |
69 [cell setControlSize:controlSize]; | |
70 } | |
71 | |
72 // Returns an autoreleased NSTextField that is configured to look like a Label | |
73 // looks in Interface Builder. | |
74 NSTextField* LabelWithFrame(NSString* text, const NSRect& frame) { | |
75 NSTextField* label = [[NSTextField alloc] initWithFrame:frame]; | |
76 [label setStringValue:text]; | |
77 [label setSelectable:NO]; | |
78 [label setBezeled:NO]; | |
79 return [label autorelease]; | |
80 } | |
81 | |
82 } // namespace | |
83 | |
84 @interface ContentSettingBubbleController(Private) | |
85 - (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel | |
86 parentWindow:(NSWindow*)parentWindow | |
87 anchoredAt:(NSPoint)anchoredAt; | |
88 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame | |
89 title:(NSString*)title | |
90 icon:(NSImage*)icon | |
91 referenceFrame:(NSRect)referenceFrame; | |
92 - (void)initializeBlockedPluginsList; | |
93 - (void)initializeTitle; | |
94 - (void)initializeRadioGroup; | |
95 - (void)initializePopupList; | |
96 - (void)initializeGeoLists; | |
97 - (void)sizeToFitLoadPluginsButton; | |
98 - (void)sizeToFitManageDoneButtons; | |
99 - (void)removeInfoButton; | |
100 - (void)popupLinkClicked:(id)sender; | |
101 - (void)clearGeolocationForCurrentHost:(id)sender; | |
102 @end | |
103 | |
104 @implementation ContentSettingBubbleController | |
105 | |
106 + (ContentSettingBubbleController*) | |
107 showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel | |
108 parentWindow:(NSWindow*)parentWindow | |
109 anchoredAt:(NSPoint)anchor { | |
110 // Autoreleases itself on bubble close. | |
111 return [[ContentSettingBubbleController alloc] | |
112 initWithModel:contentSettingBubbleModel | |
113 parentWindow:parentWindow | |
114 anchoredAt:anchor]; | |
115 } | |
116 | |
117 - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel | |
118 parentWindow:(NSWindow*)parentWindow | |
119 anchoredAt:(NSPoint)anchoredAt { | |
120 // This method takes ownership of |contentSettingBubbleModel| in all cases. | |
121 scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel); | |
122 DCHECK(model.get()); | |
123 | |
124 NSString* const nibPaths[] = { | |
125 @"ContentBlockedCookies", | |
126 @"ContentBlockedImages", | |
127 @"ContentBlockedJavaScript", | |
128 @"ContentBlockedPlugins", | |
129 @"ContentBlockedPopups", | |
130 @"ContentBubbleGeolocation", | |
131 @"", // Notifications do not have a bubble. | |
132 }; | |
133 COMPILE_ASSERT(arraysize(nibPaths) == CONTENT_SETTINGS_NUM_TYPES, | |
134 nibPaths_requires_an_entry_for_every_setting_type); | |
135 const int settingsType = model->content_type(); | |
136 // Nofifications do not have a bubble. | |
137 CHECK_NE(settingsType, CONTENT_SETTINGS_TYPE_NOTIFICATIONS); | |
138 DCHECK_LT(settingsType, CONTENT_SETTINGS_NUM_TYPES); | |
139 if ((self = [super initWithWindowNibPath:nibPaths[settingsType] | |
140 parentWindow:parentWindow | |
141 anchoredAt:anchoredAt])) { | |
142 contentSettingBubbleModel_.reset(model.release()); | |
143 [self showWindow:nil]; | |
144 } | |
145 return self; | |
146 } | |
147 | |
148 - (void)initializeTitle { | |
149 if (!titleLabel_) | |
150 return; | |
151 | |
152 NSString* label = base::SysUTF8ToNSString( | |
153 contentSettingBubbleModel_->bubble_content().title); | |
154 [titleLabel_ setStringValue:label]; | |
155 | |
156 // Layout title post-localization. | |
157 CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker | |
158 sizeToFitFixedWidthTextField:titleLabel_]; | |
159 NSRect windowFrame = [[self window] frame]; | |
160 windowFrame.size.height += deltaY; | |
161 [[self window] setFrame:windowFrame display:NO]; | |
162 NSRect titleFrame = [titleLabel_ frame]; | |
163 titleFrame.origin.y -= deltaY; | |
164 [titleLabel_ setFrame:titleFrame]; | |
165 } | |
166 | |
167 - (void)initializeRadioGroup { | |
168 // Configure the radio group. For now, only deal with the | |
169 // strictly needed case of group containing 2 radio buttons. | |
170 const ContentSettingBubbleModel::RadioGroup& radio_group = | |
171 contentSettingBubbleModel_->bubble_content().radio_group; | |
172 | |
173 // Select appropriate radio button. | |
174 [allowBlockRadioGroup_ selectCellWithTag: | |
175 radio_group.default_item == 0 ? kAllowTag : kBlockTag]; | |
176 | |
177 const ContentSettingBubbleModel::RadioItems& radio_items = | |
178 radio_group.radio_items; | |
179 DCHECK_EQ(2u, radio_items.size()) << "Only 2 radio items per group supported"; | |
180 // Set radio group labels from model. | |
181 NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag:kAllowTag]; | |
182 [radioCell setTitle:base::SysUTF8ToNSString(radio_items[0])]; | |
183 | |
184 radioCell = [allowBlockRadioGroup_ cellWithTag:kBlockTag]; | |
185 [radioCell setTitle:base::SysUTF8ToNSString(radio_items[1])]; | |
186 | |
187 // Layout radio group labels post-localization. | |
188 [GTMUILocalizerAndLayoutTweaker | |
189 wrapRadioGroupForWidth:allowBlockRadioGroup_]; | |
190 CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker | |
191 sizeToFitView:allowBlockRadioGroup_].height; | |
192 NSRect windowFrame = [[self window] frame]; | |
193 windowFrame.size.height += radioDeltaY; | |
194 [[self window] setFrame:windowFrame display:NO]; | |
195 } | |
196 | |
197 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame | |
198 title:(NSString*)title | |
199 icon:(NSImage*)icon | |
200 referenceFrame:(NSRect)referenceFrame { | |
201 scoped_nsobject<HyperlinkButtonCell> cell([[HyperlinkButtonCell alloc] | |
202 initTextCell:title]); | |
203 [cell.get() setAlignment:NSNaturalTextAlignment]; | |
204 if (icon) { | |
205 [cell.get() setImagePosition:NSImageLeft]; | |
206 [cell.get() setImage:icon]; | |
207 } else { | |
208 [cell.get() setImagePosition:NSNoImage]; | |
209 } | |
210 [cell.get() setControlSize:NSSmallControlSize]; | |
211 | |
212 NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease]; | |
213 // Cell must be set immediately after construction. | |
214 [button setCell:cell.get()]; | |
215 | |
216 // If the link text is too long, clamp it. | |
217 [button sizeToFit]; | |
218 int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame); | |
219 NSRect buttonFrame = [button frame]; | |
220 if (NSWidth(buttonFrame) > maxWidth) { | |
221 buttonFrame.size.width = maxWidth; | |
222 [button setFrame:buttonFrame]; | |
223 } | |
224 | |
225 [button setTarget:self]; | |
226 [button setAction:@selector(popupLinkClicked:)]; | |
227 return button; | |
228 } | |
229 | |
230 - (void)initializeBlockedPluginsList { | |
231 NSMutableArray* pluginArray = [NSMutableArray array]; | |
232 const std::set<std::string>& plugins = | |
233 contentSettingBubbleModel_->bubble_content().resource_identifiers; | |
234 if (plugins.empty()) { | |
235 int delta = NSMinY([titleLabel_ frame]) - | |
236 NSMinY([blockedResourcesField_ frame]); | |
237 [blockedResourcesField_ removeFromSuperview]; | |
238 NSRect frame = [[self window] frame]; | |
239 frame.size.height -= delta; | |
240 [[self window] setFrame:frame display:NO]; | |
241 } else { | |
242 for (std::set<std::string>::iterator it = plugins.begin(); | |
243 it != plugins.end(); ++it) { | |
244 NSString* name = SysUTF16ToNSString( | |
245 NPAPI::PluginList::Singleton()->GetPluginGroupName(*it)); | |
246 if ([name length] == 0) | |
247 name = base::SysUTF8ToNSString(*it); | |
248 [pluginArray addObject:name]; | |
249 } | |
250 [blockedResourcesField_ | |
251 setStringValue:[pluginArray componentsJoinedByString:@"\n"]]; | |
252 [GTMUILocalizerAndLayoutTweaker | |
253 sizeToFitFixedWidthTextField:blockedResourcesField_]; | |
254 } | |
255 } | |
256 | |
257 - (void)initializePopupList { | |
258 // I didn't put the buttons into a NSMatrix because then they are only one | |
259 // entity in the key view loop. This way, one can tab through all of them. | |
260 const ContentSettingBubbleModel::PopupItems& popupItems = | |
261 contentSettingBubbleModel_->bubble_content().popup_items; | |
262 | |
263 // Get the pre-resize frame of the radio group. Its origin is where the | |
264 // popup list should go. | |
265 NSRect radioFrame = [allowBlockRadioGroup_ frame]; | |
266 | |
267 // Make room for the popup list. The bubble view and its subviews autosize | |
268 // themselves when the window is enlarged. | |
269 // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib, | |
270 // so only 1 * kLinkOuterPadding more is needed. | |
271 int delta = popupItems.size() * kLinkLineHeight - kLinkPadding + | |
272 kLinkOuterPadding; | |
273 NSSize deltaSize = NSMakeSize(0, delta); | |
274 deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil]; | |
275 NSRect windowFrame = [[self window] frame]; | |
276 windowFrame.size.height += deltaSize.height; | |
277 [[self window] setFrame:windowFrame display:NO]; | |
278 | |
279 // Create popup list. | |
280 int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight; | |
281 int row = 0; | |
282 for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator | |
283 it(popupItems.begin()); it != popupItems.end(); ++it, ++row) { | |
284 const SkBitmap& icon = it->bitmap; | |
285 NSImage* image = nil; | |
286 if (!icon.empty()) | |
287 image = gfx::SkBitmapToNSImage(icon); | |
288 | |
289 std::string title(it->title); | |
290 // The popup may not have committed a load yet, in which case it won't | |
291 // have a URL or title. | |
292 if (title.empty()) | |
293 title = l10n_util::GetStringUTF8(IDS_TAB_LOADING_TITLE); | |
294 | |
295 NSRect linkFrame = | |
296 NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row, | |
297 200, kLinkHeight); | |
298 NSButton* button = [self | |
299 hyperlinkButtonWithFrame:linkFrame | |
300 title:base::SysUTF8ToNSString(title) | |
301 icon:image | |
302 referenceFrame:radioFrame]; | |
303 [[self bubble] addSubview:button]; | |
304 popupLinks_[button] = row; | |
305 } | |
306 } | |
307 | |
308 - (void)initializeGeoLists { | |
309 // Cocoa has its origin in the lower left corner. This means elements are | |
310 // added from bottom to top, which explains why loops run backwards and the | |
311 // order of operations is the other way than on Linux/Windows. | |
312 const ContentSettingBubbleModel::BubbleContent& content = | |
313 contentSettingBubbleModel_->bubble_content(); | |
314 NSRect containerFrame = [contentsContainer_ frame]; | |
315 NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight); | |
316 | |
317 // "Clear" button / text field. | |
318 if (!content.custom_link.empty()) { | |
319 scoped_nsobject<NSControl> control; | |
320 if(content.custom_link_enabled) { | |
321 NSRect buttonFrame = NSMakeRect(0, 0, | |
322 NSWidth(containerFrame), | |
323 kGeoClearButtonHeight); | |
324 NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame]; | |
325 control.reset(button); | |
326 [button setTitle:base::SysUTF8ToNSString(content.custom_link)]; | |
327 [button setTarget:self]; | |
328 [button setAction:@selector(clearGeolocationForCurrentHost:)]; | |
329 [button setBezelStyle:NSRoundRectBezelStyle]; | |
330 SetControlSize(button, NSSmallControlSize); | |
331 [button sizeToFit]; | |
332 } else { | |
333 // Add the notification that settings will be cleared on next reload. | |
334 control.reset([LabelWithFrame( | |
335 base::SysUTF8ToNSString(content.custom_link), frame) retain]); | |
336 SetControlSize(control.get(), NSSmallControlSize); | |
337 } | |
338 | |
339 // If the new control is wider than the container, widen the window. | |
340 CGFloat controlWidth = NSWidth([control frame]); | |
341 if (controlWidth > NSWidth(containerFrame)) { | |
342 NSRect windowFrame = [[self window] frame]; | |
343 windowFrame.size.width += controlWidth - NSWidth(containerFrame); | |
344 [[self window] setFrame:windowFrame display:NO]; | |
345 // Fetch the updated sizes. | |
346 containerFrame = [contentsContainer_ frame]; | |
347 frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight); | |
348 } | |
349 | |
350 DCHECK(control); | |
351 [contentsContainer_ addSubview:control]; | |
352 frame.origin.y = NSMaxY([control frame]) + kGeoPadding; | |
353 } | |
354 | |
355 typedef | |
356 std::vector<ContentSettingBubbleModel::DomainList>::const_reverse_iterator | |
357 GeolocationGroupIterator; | |
358 for (GeolocationGroupIterator i = content.domain_lists.rbegin(); | |
359 i != content.domain_lists.rend(); ++i) { | |
360 // Add all hosts in the current domain list. | |
361 for (std::set<std::string>::const_reverse_iterator j = i->hosts.rbegin(); | |
362 j != i->hosts.rend(); ++j) { | |
363 NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame); | |
364 SetControlSize(title, NSSmallControlSize); | |
365 [contentsContainer_ addSubview:title]; | |
366 | |
367 frame.origin.y = NSMaxY(frame) + kGeoHostPadding + | |
368 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; | |
369 } | |
370 if (!i->hosts.empty()) | |
371 frame.origin.y += kGeoPadding - kGeoHostPadding; | |
372 | |
373 // Add the domain list's title. | |
374 NSTextField* title = | |
375 LabelWithFrame(base::SysUTF8ToNSString(i->title), frame); | |
376 SetControlSize(title, NSSmallControlSize); | |
377 [contentsContainer_ addSubview:title]; | |
378 | |
379 frame.origin.y = NSMaxY(frame) + kGeoPadding + | |
380 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; | |
381 } | |
382 | |
383 CGFloat containerHeight = frame.origin.y; | |
384 // Undo last padding. | |
385 if (!content.domain_lists.empty()) | |
386 containerHeight -= kGeoPadding; | |
387 | |
388 // Resize container to fit its subviews, and window to fit the container. | |
389 NSRect windowFrame = [[self window] frame]; | |
390 windowFrame.size.height += containerHeight - NSHeight(containerFrame); | |
391 [[self window] setFrame:windowFrame display:NO]; | |
392 containerFrame.size.height = containerHeight; | |
393 [contentsContainer_ setFrame:containerFrame]; | |
394 } | |
395 | |
396 - (void)sizeToFitLoadPluginsButton { | |
397 const ContentSettingBubbleModel::BubbleContent& content = | |
398 contentSettingBubbleModel_->bubble_content(); | |
399 [loadAllPluginsButton_ setEnabled:content.custom_link_enabled]; | |
400 | |
401 // Resize horizontally to fit button if necessary. | |
402 NSRect windowFrame = [[self window] frame]; | |
403 int widthNeeded = NSWidth([loadAllPluginsButton_ frame]) + | |
404 2 * NSMinX([loadAllPluginsButton_ frame]); | |
405 if (NSWidth(windowFrame) < widthNeeded) { | |
406 windowFrame.size.width = widthNeeded; | |
407 [[self window] setFrame:windowFrame display:NO]; | |
408 } | |
409 } | |
410 | |
411 - (void)sizeToFitManageDoneButtons { | |
412 CGFloat actualWidth = NSWidth([[[self window] contentView] frame]); | |
413 CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding + | |
414 NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]); | |
415 if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_) | |
416 return; | |
417 | |
418 // Resize window, autoresizing takes care of the rest. | |
419 NSSize size = NSMakeSize(requiredWidth - actualWidth, 0); | |
420 size = [[[self window] contentView] convertSize:size toView:nil]; | |
421 NSRect frame = [[self window] frame]; | |
422 frame.origin.x -= size.width; | |
423 frame.size.width += size.width; | |
424 [[self window] setFrame:frame display:NO]; | |
425 } | |
426 | |
427 - (void)awakeFromNib { | |
428 [super awakeFromNib]; | |
429 | |
430 [[self bubble] setArrowLocation:info_bubble::kTopRight]; | |
431 | |
432 // Adapt window size to bottom buttons. Do this before all other layouting. | |
433 [self sizeToFitManageDoneButtons]; | |
434 | |
435 [self initializeTitle]; | |
436 | |
437 ContentSettingsType type = contentSettingBubbleModel_->content_type(); | |
438 if (type == CONTENT_SETTINGS_TYPE_PLUGINS) { | |
439 [self sizeToFitLoadPluginsButton]; | |
440 [self initializeBlockedPluginsList]; | |
441 } | |
442 if (allowBlockRadioGroup_) // not bound in cookie bubble xib | |
443 [self initializeRadioGroup]; | |
444 | |
445 if (type == CONTENT_SETTINGS_TYPE_POPUPS) | |
446 [self initializePopupList]; | |
447 if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION) | |
448 [self initializeGeoLists]; | |
449 } | |
450 | |
451 /////////////////////////////////////////////////////////////////////////////// | |
452 // Actual application logic | |
453 | |
454 - (IBAction)allowBlockToggled:(id)sender { | |
455 NSButtonCell *selectedCell = [sender selectedCell]; | |
456 contentSettingBubbleModel_->OnRadioClicked( | |
457 [selectedCell tag] == kAllowTag ? 0 : 1); | |
458 } | |
459 | |
460 - (void)popupLinkClicked:(id)sender { | |
461 content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender)); | |
462 DCHECK(i != popupLinks_.end()); | |
463 contentSettingBubbleModel_->OnPopupClicked(i->second); | |
464 } | |
465 | |
466 - (void)clearGeolocationForCurrentHost:(id)sender { | |
467 contentSettingBubbleModel_->OnCustomLinkClicked(); | |
468 [self close]; | |
469 } | |
470 | |
471 - (IBAction)showMoreInfo:(id)sender { | |
472 contentSettingBubbleModel_->OnCustomLinkClicked(); | |
473 [self close]; | |
474 } | |
475 | |
476 - (IBAction)loadAllPlugins:(id)sender { | |
477 contentSettingBubbleModel_->OnCustomLinkClicked(); | |
478 [self close]; | |
479 } | |
480 | |
481 - (IBAction)manageBlocking:(id)sender { | |
482 contentSettingBubbleModel_->OnManageLinkClicked(); | |
483 } | |
484 | |
485 - (IBAction)closeBubble:(id)sender { | |
486 [self close]; | |
487 } | |
488 | |
489 @end // ContentSettingBubbleController | |
OLD | NEW |