OLD | NEW |
| (Empty) |
1 // Copyright 2016 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/chooser_content_view_cocoa.h" | |
6 | |
7 #include <algorithm> | |
8 | |
9 #include "base/macros.h" | |
10 #include "base/strings/sys_string_conversions.h" | |
11 #include "chrome/browser/chooser_controller/chooser_controller.h" | |
12 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h" | |
13 #include "chrome/browser/ui/cocoa/spinner_view.h" | |
14 #include "chrome/grit/generated_resources.h" | |
15 #include "skia/ext/skia_utils_mac.h" | |
16 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTw
eaker.h" | |
17 #import "ui/base/cocoa/controls/hyperlink_button_cell.h" | |
18 #include "ui/base/l10n/l10n_util_mac.h" | |
19 #include "ui/base/resource/resource_bundle.h" | |
20 #include "ui/gfx/color_palette.h" | |
21 #include "ui/gfx/image/image_skia_util_mac.h" | |
22 #include "ui/gfx/paint_vector_icon.h" | |
23 #include "ui/gfx/vector_icons_public.h" | |
24 #include "ui/resources/grit/ui_resources.h" | |
25 | |
26 namespace { | |
27 | |
28 // Chooser width. | |
29 const CGFloat kChooserWidth = 390.0f; | |
30 | |
31 // Chooser height. | |
32 const CGFloat kChooserHeight = 330.0f; | |
33 | |
34 // Row view image size. | |
35 const CGFloat kRowViewImageSize = 20.0f; | |
36 | |
37 // Table row view height. | |
38 const CGFloat kTableRowViewOneLineHeight = 23.0f; | |
39 const CGFloat kTableRowViewTwoLinesHeight = 39.0f; | |
40 | |
41 // Spinner size. | |
42 const CGFloat kSpinnerSize = 24.0f; | |
43 | |
44 // Distance between the chooser border and the view that is closest to the | |
45 // border. | |
46 const CGFloat kMarginX = 20.0f; | |
47 const CGFloat kMarginY = 20.0f; | |
48 | |
49 // Distance between two views inside the chooser. | |
50 const CGFloat kHorizontalPadding = 10.0f; | |
51 const CGFloat kVerticalPadding = 10.0f; | |
52 | |
53 // Separator alpha value. | |
54 const CGFloat kSeparatorAlphaValue = 0.6f; | |
55 | |
56 // Separator height. | |
57 const CGFloat kSeparatorHeight = 1.0f; | |
58 | |
59 // Distance between two views inside the table row view. | |
60 const CGFloat kTableRowViewHorizontalPadding = 5.0f; | |
61 const CGFloat kTableRowViewVerticalPadding = 1.0f; | |
62 | |
63 // Distance between the adapter off help link and the scroll view boundaries. | |
64 const CGFloat kAdapterOffHelpLinkPadding = 5.0f; | |
65 | |
66 // The lookup table for signal strength level image. | |
67 const int kSignalStrengthLevelImageIds[5] = {IDR_SIGNAL_0_BAR, IDR_SIGNAL_1_BAR, | |
68 IDR_SIGNAL_2_BAR, IDR_SIGNAL_3_BAR, | |
69 IDR_SIGNAL_4_BAR}; | |
70 const int kSignalStrengthLevelImageSelectedIds[5] = { | |
71 IDR_SIGNAL_0_BAR_SELECTED, IDR_SIGNAL_1_BAR_SELECTED, | |
72 IDR_SIGNAL_2_BAR_SELECTED, IDR_SIGNAL_3_BAR_SELECTED, | |
73 IDR_SIGNAL_4_BAR_SELECTED}; | |
74 | |
75 // Creates a label with |text|. | |
76 base::scoped_nsobject<NSTextField> CreateLabel(NSString* text) { | |
77 base::scoped_nsobject<NSTextField> label( | |
78 [[NSTextField alloc] initWithFrame:NSZeroRect]); | |
79 [label setDrawsBackground:NO]; | |
80 [label setBezeled:NO]; | |
81 [label setEditable:NO]; | |
82 [label setSelectable:NO]; | |
83 [label setStringValue:text]; | |
84 [label setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]]; | |
85 [label setTextColor:[NSColor blackColor]]; | |
86 [label sizeToFit]; | |
87 return label; | |
88 } | |
89 | |
90 } // namespace | |
91 | |
92 // A table row view that contains one line of text, and optionally contains an | |
93 // image in front of the text. | |
94 @interface ChooserContentTableRowView : NSView { | |
95 @private | |
96 base::scoped_nsobject<NSImageView> image_; | |
97 base::scoped_nsobject<NSTextField> text_; | |
98 base::scoped_nsobject<NSTextField> pairedStatus_; | |
99 } | |
100 | |
101 // Designated initializer. | |
102 // This initializer is used when the chooser needs to show the no-devices-found | |
103 // message. | |
104 - (instancetype)initWithText:(NSString*)text; | |
105 | |
106 // Designated initializer. | |
107 - (instancetype)initWithText:(NSString*)text | |
108 signalStrengthLevel:(NSInteger)level | |
109 isConnected:(bool)isConnected | |
110 isPaired:(bool)isPaired | |
111 rowHeight:(CGFloat)rowHeight | |
112 textNeedIndentation:(bool)textNeedIndentation; | |
113 | |
114 // Gets the image in front of the text. | |
115 - (NSImageView*)image; | |
116 | |
117 // Gets the text. | |
118 - (NSTextField*)text; | |
119 | |
120 // Gets the paired status. | |
121 - (NSTextField*)pairedStatus; | |
122 | |
123 @end | |
124 | |
125 @implementation ChooserContentTableRowView | |
126 | |
127 - (instancetype)initWithText:(NSString*)text { | |
128 if ((self = [super initWithFrame:NSZeroRect])) { | |
129 text_ = CreateLabel(text); | |
130 CGFloat textHeight = NSHeight([text_ frame]); | |
131 CGFloat textOriginX = kTableRowViewHorizontalPadding; | |
132 CGFloat textOriginY = (kTableRowViewOneLineHeight - textHeight) / 2; | |
133 [text_ setFrameOrigin:NSMakePoint(textOriginX, textOriginY)]; | |
134 [self addSubview:text_]; | |
135 } | |
136 | |
137 return self; | |
138 } | |
139 | |
140 - (instancetype)initWithText:(NSString*)text | |
141 signalStrengthLevel:(NSInteger)level | |
142 isConnected:(bool)isConnected | |
143 isPaired:(bool)isPaired | |
144 rowHeight:(CGFloat)rowHeight | |
145 textNeedIndentation:(bool)textNeedIndentation { | |
146 if ((self = [super initWithFrame:NSZeroRect])) { | |
147 // Create the views. | |
148 // Image. | |
149 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
150 NSImage* image = nullptr; | |
151 if (isConnected) { | |
152 image = gfx::NSImageFromImageSkia(gfx::CreateVectorIcon( | |
153 gfx::VectorIconId::BLUETOOTH_CONNECTED, gfx::kChromeIconGrey)); | |
154 } else if (level != -1) { | |
155 DCHECK_GE(level, 0); | |
156 DCHECK_LT(level, base::checked_cast<NSInteger>( | |
157 arraysize(kSignalStrengthLevelImageIds))); | |
158 image = rb.GetNativeImageNamed(kSignalStrengthLevelImageIds[level]) | |
159 .ToNSImage(); | |
160 } | |
161 | |
162 CGFloat imageOriginX = kTableRowViewHorizontalPadding; | |
163 CGFloat imageOriginY = (rowHeight - kRowViewImageSize) / 2; | |
164 if (image) { | |
165 image_.reset([[NSImageView alloc] | |
166 initWithFrame:NSMakeRect(imageOriginX, imageOriginY, | |
167 kRowViewImageSize, kRowViewImageSize)]); | |
168 [image_ setImage:image]; | |
169 [self addSubview:image_]; | |
170 } | |
171 | |
172 // Text. | |
173 text_ = CreateLabel(text); | |
174 CGFloat textHeight = NSHeight([text_ frame]); | |
175 | |
176 // Paired status. | |
177 CGFloat pairedStatusHeight = 0.0f; | |
178 if (isPaired) { | |
179 pairedStatus_ = CreateLabel( | |
180 l10n_util::GetNSString(IDS_DEVICE_CHOOSER_PAIRED_STATUS_TEXT)); | |
181 [pairedStatus_ | |
182 setTextColor:skia::SkColorToCalibratedNSColor(gfx::kGoogleGreen700)]; | |
183 pairedStatusHeight = NSHeight([pairedStatus_ frame]); | |
184 } | |
185 | |
186 // Lay out the views. | |
187 // Text. | |
188 CGFloat textOriginX = kTableRowViewHorizontalPadding; | |
189 if (textNeedIndentation) | |
190 textOriginX += imageOriginX + kRowViewImageSize; | |
191 CGFloat textOriginY; | |
192 if (isPaired) { | |
193 textOriginY = pairedStatusHeight + | |
194 (rowHeight - textHeight - pairedStatusHeight - | |
195 kTableRowViewVerticalPadding) / | |
196 2 + | |
197 kTableRowViewVerticalPadding; | |
198 } else { | |
199 textOriginY = (rowHeight - textHeight) / 2; | |
200 } | |
201 | |
202 [text_ setFrameOrigin:NSMakePoint(textOriginX, textOriginY)]; | |
203 [self addSubview:text_]; | |
204 | |
205 // Paired status. | |
206 if (isPaired) { | |
207 CGFloat pairedStatusOriginX = textOriginX; | |
208 CGFloat pairedStatusOriginY = | |
209 (rowHeight - textHeight - pairedStatusHeight - | |
210 kTableRowViewVerticalPadding) / | |
211 2; | |
212 [pairedStatus_ | |
213 setFrameOrigin:NSMakePoint(pairedStatusOriginX, pairedStatusOriginY)]; | |
214 [self addSubview:pairedStatus_]; | |
215 } | |
216 } | |
217 | |
218 return self; | |
219 } | |
220 | |
221 - (NSImageView*)image { | |
222 return image_.get(); | |
223 } | |
224 | |
225 - (NSTextField*)text { | |
226 return text_.get(); | |
227 } | |
228 | |
229 - (NSTextField*)pairedStatus { | |
230 return pairedStatus_.get(); | |
231 } | |
232 | |
233 @end | |
234 | |
235 class ChooserContentViewController : public ChooserController::View { | |
236 public: | |
237 ChooserContentViewController(ChooserController* chooser_controller, | |
238 NSButton* adapter_off_help_button, | |
239 NSTextField* adapter_off_message, | |
240 NSTableView* table_view, | |
241 SpinnerView* spinner, | |
242 NSTextField* scanning_message, | |
243 NSTextField* word_connector, | |
244 NSButton* rescan_button); | |
245 ~ChooserContentViewController() override; | |
246 | |
247 // ChooserController::View: | |
248 void OnOptionsInitialized() override; | |
249 void OnOptionAdded(size_t index) override; | |
250 void OnOptionRemoved(size_t index) override; | |
251 void OnOptionUpdated(size_t index) override; | |
252 void OnAdapterEnabledChanged(bool enabled) override; | |
253 void OnRefreshStateChanged(bool refreshing) override; | |
254 | |
255 void UpdateTableView(); | |
256 | |
257 private: | |
258 ChooserController* chooser_controller_; | |
259 NSButton* adapter_off_help_button_; | |
260 NSTextField* adapter_off_message_; | |
261 NSTableView* table_view_; | |
262 SpinnerView* spinner_; | |
263 NSTextField* scanning_message_; | |
264 NSTextField* word_connector_; | |
265 NSButton* rescan_button_; | |
266 | |
267 DISALLOW_COPY_AND_ASSIGN(ChooserContentViewController); | |
268 }; | |
269 | |
270 ChooserContentViewController::ChooserContentViewController( | |
271 ChooserController* chooser_controller, | |
272 NSButton* adapter_off_help_button, | |
273 NSTextField* adapter_off_message, | |
274 NSTableView* table_view, | |
275 SpinnerView* spinner, | |
276 NSTextField* scanning_message, | |
277 NSTextField* word_connector, | |
278 NSButton* rescan_button) | |
279 : chooser_controller_(chooser_controller), | |
280 adapter_off_help_button_(adapter_off_help_button), | |
281 adapter_off_message_(adapter_off_message), | |
282 table_view_(table_view), | |
283 spinner_(spinner), | |
284 scanning_message_(scanning_message), | |
285 word_connector_(word_connector), | |
286 rescan_button_(rescan_button) { | |
287 DCHECK(chooser_controller_); | |
288 DCHECK(adapter_off_help_button_); | |
289 DCHECK(adapter_off_message_); | |
290 DCHECK(table_view_); | |
291 DCHECK(spinner_); | |
292 DCHECK(scanning_message_); | |
293 DCHECK(word_connector_); | |
294 DCHECK(rescan_button_); | |
295 chooser_controller_->set_view(this); | |
296 } | |
297 | |
298 ChooserContentViewController::~ChooserContentViewController() { | |
299 chooser_controller_->set_view(nullptr); | |
300 } | |
301 | |
302 void ChooserContentViewController::OnOptionsInitialized() { | |
303 UpdateTableView(); | |
304 } | |
305 | |
306 void ChooserContentViewController::OnOptionAdded(size_t index) { | |
307 UpdateTableView(); | |
308 [table_view_ setHidden:NO]; | |
309 [spinner_ setHidden:YES]; | |
310 } | |
311 | |
312 void ChooserContentViewController::OnOptionRemoved(size_t index) { | |
313 // |table_view_| will automatically select the removed item's next item. | |
314 // So here it tracks if the removed item is the item that was currently | |
315 // selected, if so, deselect it. Also if the removed item is before the | |
316 // currently selected item, the currently selected item's index needs to | |
317 // be adjusted by one. | |
318 NSIndexSet* selected_rows = [table_view_ selectedRowIndexes]; | |
319 NSMutableIndexSet* updated_selected_rows = [NSMutableIndexSet indexSet]; | |
320 NSUInteger row = [selected_rows firstIndex]; | |
321 while (row != NSNotFound) { | |
322 if (row < index) | |
323 [updated_selected_rows addIndex:row]; | |
324 else if (row > index) | |
325 [updated_selected_rows addIndex:row - 1]; | |
326 row = [selected_rows indexGreaterThanIndex:row]; | |
327 } | |
328 | |
329 [table_view_ selectRowIndexes:updated_selected_rows byExtendingSelection:NO]; | |
330 | |
331 UpdateTableView(); | |
332 } | |
333 | |
334 void ChooserContentViewController::OnOptionUpdated(size_t index) { | |
335 UpdateTableView(); | |
336 } | |
337 | |
338 void ChooserContentViewController::OnAdapterEnabledChanged(bool enabled) { | |
339 // No row is selected since the adapter status has changed. | |
340 // This will also disable the OK button if it was enabled because | |
341 // of a previously selected row. | |
342 [table_view_ deselectAll:nil]; | |
343 UpdateTableView(); | |
344 [table_view_ setHidden:enabled ? NO : YES]; | |
345 [adapter_off_help_button_ setHidden:enabled ? YES : NO]; | |
346 [adapter_off_message_ setHidden:enabled ? YES : NO]; | |
347 | |
348 [spinner_ setHidden:YES]; | |
349 | |
350 [scanning_message_ setHidden:YES]; | |
351 // When adapter is enabled, show |word_connector_| and |rescan_button_|; | |
352 // otherwise hide them. | |
353 [word_connector_ setHidden:enabled ? NO : YES]; | |
354 [rescan_button_ setHidden:enabled ? NO : YES]; | |
355 } | |
356 | |
357 void ChooserContentViewController::OnRefreshStateChanged(bool refreshing) { | |
358 if (refreshing) { | |
359 // No row is selected since the chooser is refreshing. | |
360 // This will also disable the OK button if it was enabled because | |
361 // of a previously selected row. | |
362 [table_view_ deselectAll:nil]; | |
363 UpdateTableView(); | |
364 } | |
365 | |
366 // When refreshing and no option available yet, hide |table_view_| and show | |
367 // |spinner_|. Otherwise show |table_view_| and hide |spinner_|. | |
368 bool table_view_hidden = | |
369 refreshing && (chooser_controller_->NumOptions() == 0); | |
370 [table_view_ setHidden:table_view_hidden ? YES : NO]; | |
371 [spinner_ setHidden:table_view_hidden ? NO : YES]; | |
372 | |
373 // When refreshing, show |scanning_message_| and hide |word_connector_| and | |
374 // |rescan_button_|. | |
375 // When complete, show |word_connector_| and |rescan_button_| and hide | |
376 // |scanning_message_|. | |
377 [scanning_message_ setHidden:refreshing ? NO : YES]; | |
378 [word_connector_ setHidden:refreshing ? YES : NO]; | |
379 [rescan_button_ setHidden:refreshing ? YES : NO]; | |
380 } | |
381 | |
382 void ChooserContentViewController::UpdateTableView() { | |
383 [table_view_ setEnabled:chooser_controller_->NumOptions() > 0]; | |
384 // For NSView-based table views, calling reloadData will deselect the | |
385 // currently selected row, so |selected_rows| stores the currently selected | |
386 // rows in order to select them again. | |
387 NSIndexSet* selected_rows = [table_view_ selectedRowIndexes]; | |
388 [table_view_ reloadData]; | |
389 [table_view_ selectRowIndexes:selected_rows byExtendingSelection:NO]; | |
390 } | |
391 | |
392 @implementation ChooserContentViewCocoa | |
393 | |
394 // TODO(juncai): restructure this function to be some smaller methods to | |
395 // create the pieces for the view. By doing so, the methods that calculate | |
396 // the frame and origins can be moved into those methods, rather than as | |
397 // helper functions. | |
398 - (instancetype)initWithChooserTitle:(NSString*)chooserTitle | |
399 chooserController: | |
400 (std::unique_ptr<ChooserController>)chooserController { | |
401 // ------------------------------------ | |
402 // | Chooser title | | |
403 // | -------------------------------- | | |
404 // | | option 0 | | | |
405 // | | option 1 | | | |
406 // | | option 2 | | | |
407 // | | | | | |
408 // | | | | | |
409 // | | | | | |
410 // | -------------------------------- | | |
411 // | [ Connect ] [ Cancel ] | | |
412 // |----------------------------------| | |
413 // | Get help | | |
414 // ------------------------------------ | |
415 | |
416 // Determine the dimensions of the chooser. | |
417 // Once the height and width are set, the buttons and permission menus can | |
418 // be laid out correctly. | |
419 NSRect chooserFrame = NSMakeRect(0, 0, kChooserWidth, kChooserHeight); | |
420 | |
421 if ((self = [super initWithFrame:chooserFrame])) { | |
422 chooserController_ = std::move(chooserController); | |
423 | |
424 // Create the views. | |
425 // Title. | |
426 titleView_ = [self createChooserTitle:chooserTitle]; | |
427 CGFloat titleHeight = NSHeight([titleView_ frame]); | |
428 | |
429 // Adapter turned off help button. | |
430 adapterOffHelpButton_ = [self | |
431 createHyperlinkButtonWithText: | |
432 l10n_util::GetNSString( | |
433 IDS_BLUETOOTH_DEVICE_CHOOSER_TURN_ON_BLUETOOTH_LINK_TEXT)]; | |
434 CGFloat adapterOffHelpButtonWidth = NSWidth([adapterOffHelpButton_ frame]); | |
435 CGFloat adapterOffHelpButtonHeight = | |
436 NSHeight([adapterOffHelpButton_ frame]); | |
437 | |
438 // Adapter turned off message. | |
439 adapterOffMessage_ = CreateLabel(l10n_util::GetNSStringF( | |
440 IDS_BLUETOOTH_DEVICE_CHOOSER_TURN_ADAPTER_OFF, base::string16())); | |
441 | |
442 // Connect button. | |
443 connectButton_ = [self createConnectButton]; | |
444 CGFloat connectButtonWidth = NSWidth([connectButton_ frame]); | |
445 CGFloat connectButtonHeight = NSHeight([connectButton_ frame]); | |
446 | |
447 // Cancel button. | |
448 cancelButton_ = [self createCancelButton]; | |
449 CGFloat cancelButtonWidth = NSWidth([cancelButton_ frame]); | |
450 | |
451 // Separator. | |
452 separator_ = [self createSeparator]; | |
453 | |
454 // Help button. | |
455 CGFloat helpButtonWidth = 0.0f; | |
456 CGFloat helpButtonHeight = 0.0f; | |
457 if (chooserController_->ShouldShowFootnoteView()) { | |
458 helpButton_ = | |
459 [self createHyperlinkButtonWithText: | |
460 l10n_util::GetNSStringF( | |
461 IDS_DEVICE_CHOOSER_GET_HELP_LINK_WITH_SCANNING_STATUS, | |
462 base::string16())]; | |
463 helpButtonWidth = NSWidth([helpButton_ frame]); | |
464 helpButtonHeight = NSHeight([helpButton_ frame]); | |
465 } | |
466 | |
467 // Scanning message. | |
468 scanningMessage_ = CreateLabel( | |
469 l10n_util::GetNSString(IDS_BLUETOOTH_DEVICE_CHOOSER_SCANNING)); | |
470 | |
471 // Word connector. | |
472 wordConnector_ = CreateLabel(l10n_util::GetNSStringF( | |
473 IDS_DEVICE_CHOOSER_GET_HELP_LINK_WITH_RE_SCAN_LINK, base::string16(), | |
474 base::string16())); | |
475 CGFloat wordConnectorWidth = NSWidth([wordConnector_ frame]); | |
476 | |
477 // Re-scan button. | |
478 rescanButton_ = | |
479 [self createHyperlinkButtonWithText: | |
480 l10n_util::GetNSString(IDS_BLUETOOTH_DEVICE_CHOOSER_RE_SCAN)]; | |
481 | |
482 // ScollView embedding with TableView. | |
483 CGFloat scrollViewWidth = kChooserWidth - 2 * kMarginX; | |
484 CGFloat scrollViewHeight = | |
485 kChooserHeight - 2 * kMarginY - | |
486 (chooserController_->ShouldShowFootnoteView() ? 4 * kVerticalPadding | |
487 : 2 * kVerticalPadding) - | |
488 titleHeight - connectButtonHeight - helpButtonHeight; | |
489 CGFloat scrollViewOriginX = kMarginX; | |
490 CGFloat scrollViewOriginY = | |
491 kMarginY + helpButtonHeight + | |
492 (chooserController_->ShouldShowFootnoteView() ? 3 * kVerticalPadding | |
493 : kVerticalPadding) + | |
494 connectButtonHeight; | |
495 NSRect scrollFrame = NSMakeRect(scrollViewOriginX, scrollViewOriginY, | |
496 scrollViewWidth, scrollViewHeight); | |
497 scrollView_.reset([[NSScrollView alloc] initWithFrame:scrollFrame]); | |
498 [scrollView_ setBorderType:NSBezelBorder]; | |
499 [scrollView_ setHasVerticalScroller:YES]; | |
500 [scrollView_ setHasHorizontalScroller:YES]; | |
501 [scrollView_ setAutohidesScrollers:YES]; | |
502 [scrollView_ setDrawsBackground:NO]; | |
503 | |
504 // TableView. | |
505 tableView_.reset([[NSTableView alloc] initWithFrame:NSZeroRect]); | |
506 tableColumn_.reset([[NSTableColumn alloc] initWithIdentifier:@""]); | |
507 [tableColumn_ setWidth:(scrollViewWidth - kMarginX)]; | |
508 [tableView_ addTableColumn:tableColumn_]; | |
509 // Make the column title invisible. | |
510 [tableView_ setHeaderView:nil]; | |
511 [tableView_ setFocusRingType:NSFocusRingTypeNone]; | |
512 [tableView_ | |
513 setAllowsMultipleSelection:chooserController_->AllowMultipleSelection() | |
514 ? YES | |
515 : NO]; | |
516 | |
517 // Spinner. | |
518 // Set the spinner in the center of the scroll view. | |
519 CGFloat spinnerOriginX = | |
520 scrollViewOriginX + (scrollViewWidth - kSpinnerSize) / 2; | |
521 CGFloat spinnerOriginY = | |
522 scrollViewOriginY + (scrollViewHeight - kSpinnerSize) / 2; | |
523 spinner_.reset([[SpinnerView alloc] | |
524 initWithFrame:NSMakeRect(spinnerOriginX, spinnerOriginY, kSpinnerSize, | |
525 kSpinnerSize)]); | |
526 | |
527 // Lay out the views. | |
528 // Title. | |
529 CGFloat titleOriginX = kMarginX; | |
530 CGFloat titleOriginY = kChooserHeight - kMarginY - titleHeight; | |
531 [titleView_ setFrameOrigin:NSMakePoint(titleOriginX, titleOriginY)]; | |
532 [self addSubview:titleView_]; | |
533 | |
534 // Adapter turned off help button. | |
535 CGFloat adapterOffHelpButtonOriginX = kMarginX + kAdapterOffHelpLinkPadding; | |
536 CGFloat adapterOffHelpButtonOriginY = titleOriginY - kVerticalPadding - | |
537 adapterOffHelpButtonHeight - | |
538 kAdapterOffHelpLinkPadding; | |
539 [adapterOffHelpButton_ | |
540 setFrameOrigin:NSMakePoint(adapterOffHelpButtonOriginX, | |
541 adapterOffHelpButtonOriginY)]; | |
542 [adapterOffHelpButton_ setTarget:self]; | |
543 [adapterOffHelpButton_ setAction:@selector(onAdapterOffHelp:)]; | |
544 [adapterOffHelpButton_ setHidden:YES]; | |
545 [self addSubview:adapterOffHelpButton_]; | |
546 | |
547 // Adapter turned off message. | |
548 CGFloat adapterOffMessageOriginX = adapterOffHelpButtonOriginX + | |
549 adapterOffHelpButtonWidth - | |
550 kHorizontalPadding / 2; | |
551 CGFloat adapterOffMessageOriginY = adapterOffHelpButtonOriginY; | |
552 [adapterOffMessage_ setFrameOrigin:NSMakePoint(adapterOffMessageOriginX, | |
553 adapterOffMessageOriginY)]; | |
554 [adapterOffMessage_ setHidden:YES]; | |
555 [self addSubview:adapterOffMessage_]; | |
556 | |
557 // ScollView and Spinner. Only one of them is shown. | |
558 [scrollView_ setDocumentView:tableView_]; | |
559 [self addSubview:scrollView_]; | |
560 [spinner_ setHidden:YES]; | |
561 [self addSubview:spinner_]; | |
562 | |
563 // Connect button. | |
564 CGFloat connectButtonOriginX = kChooserWidth - kMarginX - | |
565 kHorizontalPadding - connectButtonWidth - | |
566 cancelButtonWidth; | |
567 CGFloat connectButtonOriginY = | |
568 kMarginY + helpButtonHeight + | |
569 (chooserController_->ShouldShowFootnoteView() ? 2 * kVerticalPadding | |
570 : 0.0f); | |
571 [connectButton_ | |
572 setFrameOrigin:NSMakePoint(connectButtonOriginX, connectButtonOriginY)]; | |
573 [connectButton_ setEnabled:NO]; | |
574 [self addSubview:connectButton_]; | |
575 | |
576 // Cancel button. | |
577 CGFloat cancelButtonOriginX = kChooserWidth - kMarginX - cancelButtonWidth; | |
578 CGFloat cancelButtonOriginY = connectButtonOriginY; | |
579 [cancelButton_ | |
580 setFrameOrigin:NSMakePoint(cancelButtonOriginX, cancelButtonOriginY)]; | |
581 [self addSubview:cancelButton_]; | |
582 | |
583 if (chooserController_->ShouldShowFootnoteView()) { | |
584 // Separator. | |
585 CGFloat separatorOriginX = 0.0f; | |
586 separatorOriginY_ = kMarginY + helpButtonHeight + kVerticalPadding; | |
587 [separator_ | |
588 setFrameOrigin:NSMakePoint(separatorOriginX, separatorOriginY_)]; | |
589 [self addSubview:separator_]; | |
590 | |
591 // Help button. | |
592 CGFloat helpButtonOriginX = kMarginX; | |
593 CGFloat helpButtonOriginY = (kVerticalPadding + kMarginY) / 2; | |
594 [helpButton_ | |
595 setFrameOrigin:NSMakePoint(helpButtonOriginX, helpButtonOriginY)]; | |
596 [helpButton_ setTarget:self]; | |
597 [helpButton_ setAction:@selector(onHelpPressed:)]; | |
598 [self addSubview:helpButton_]; | |
599 | |
600 // Scanning message. | |
601 CGFloat scanningMessageOriginX = | |
602 kMarginX + helpButtonWidth - kHorizontalPadding / 2; | |
603 CGFloat scanningMessageOriginY = helpButtonOriginY; | |
604 [scanningMessage_ setFrameOrigin:NSMakePoint(scanningMessageOriginX, | |
605 scanningMessageOriginY)]; | |
606 [self addSubview:scanningMessage_]; | |
607 [scanningMessage_ setHidden:YES]; | |
608 | |
609 // Word connector. | |
610 CGFloat wordConnectorOriginX = | |
611 kMarginX + helpButtonWidth - kHorizontalPadding / 2; | |
612 CGFloat wordConnectorOriginY = helpButtonOriginY; | |
613 [wordConnector_ setFrameOrigin:NSMakePoint(wordConnectorOriginX, | |
614 wordConnectorOriginY)]; | |
615 [self addSubview:wordConnector_]; | |
616 [wordConnector_ setHidden:YES]; | |
617 | |
618 // Re-scan button. | |
619 CGFloat reScanButtonOriginX = | |
620 kMarginX + helpButtonWidth + wordConnectorWidth - kHorizontalPadding; | |
621 CGFloat reScanButtonOriginY = helpButtonOriginY; | |
622 [rescanButton_ | |
623 setFrameOrigin:NSMakePoint(reScanButtonOriginX, reScanButtonOriginY)]; | |
624 [rescanButton_ setTarget:self]; | |
625 [rescanButton_ setAction:@selector(onRescan:)]; | |
626 [self addSubview:rescanButton_]; | |
627 [rescanButton_ setHidden:YES]; | |
628 } | |
629 | |
630 chooserContentViewController_.reset(new ChooserContentViewController( | |
631 chooserController_.get(), adapterOffHelpButton_.get(), | |
632 adapterOffMessage_.get(), tableView_.get(), spinner_.get(), | |
633 scanningMessage_.get(), wordConnector_.get(), rescanButton_.get())); | |
634 } | |
635 | |
636 return self; | |
637 } | |
638 | |
639 - (base::scoped_nsobject<NSTextField>)createChooserTitle:(NSString*)title { | |
640 base::scoped_nsobject<NSTextField> titleView( | |
641 [[NSTextField alloc] initWithFrame:NSZeroRect]); | |
642 [titleView setDrawsBackground:NO]; | |
643 [titleView setBezeled:NO]; | |
644 [titleView setEditable:NO]; | |
645 [titleView setSelectable:NO]; | |
646 [titleView setStringValue:title]; | |
647 [titleView setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]]; | |
648 // The height is arbitrary as it will be adjusted later. | |
649 [titleView setFrameSize:NSMakeSize(kChooserWidth - 2 * kMarginX, 0.0f)]; | |
650 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleView]; | |
651 return titleView; | |
652 } | |
653 | |
654 - (base::scoped_nsobject<NSView>)createTableRowView:(NSInteger)row { | |
655 NSInteger level = -1; | |
656 bool isConnected = false; | |
657 bool isPaired = false; | |
658 size_t numOptions = chooserController_->NumOptions(); | |
659 base::scoped_nsobject<NSView> tableRowView; | |
660 if (numOptions == 0) { | |
661 DCHECK_EQ(0, row); | |
662 tableRowView.reset([[ChooserContentTableRowView alloc] | |
663 initWithText:[self optionAtIndex:row]]); | |
664 } else { | |
665 DCHECK_GE(row, 0); | |
666 DCHECK_LT(row, base::checked_cast<NSInteger>(numOptions)); | |
667 size_t rowIndex = base::checked_cast<size_t>(row); | |
668 if (chooserController_->ShouldShowIconBeforeText()) { | |
669 level = base::checked_cast<NSInteger>( | |
670 chooserController_->GetSignalStrengthLevel(rowIndex)); | |
671 } | |
672 isConnected = chooserController_->IsConnected(rowIndex); | |
673 isPaired = chooserController_->IsPaired(rowIndex); | |
674 bool textNeedIndentation = false; | |
675 for (size_t i = 0; i < numOptions; ++i) { | |
676 if (chooserController_->GetSignalStrengthLevel(i) != -1 || | |
677 chooserController_->IsConnected(i)) { | |
678 textNeedIndentation = true; | |
679 break; | |
680 } | |
681 } | |
682 tableRowView.reset([[ChooserContentTableRowView alloc] | |
683 initWithText:[self optionAtIndex:row] | |
684 signalStrengthLevel:level | |
685 isConnected:isConnected | |
686 isPaired:isPaired | |
687 rowHeight:[self tableRowViewHeight:row] | |
688 textNeedIndentation:textNeedIndentation]); | |
689 } | |
690 | |
691 return tableRowView; | |
692 } | |
693 | |
694 - (CGFloat)tableRowViewHeight:(NSInteger)row { | |
695 size_t numOptions = chooserController_->NumOptions(); | |
696 if (numOptions == 0) { | |
697 DCHECK_EQ(0, row); | |
698 return kTableRowViewOneLineHeight; | |
699 } | |
700 | |
701 DCHECK_GE(row, 0); | |
702 DCHECK_LT(row, base::checked_cast<NSInteger>(numOptions)); | |
703 size_t rowIndex = base::checked_cast<size_t>(row); | |
704 return chooserController_->IsPaired(rowIndex) ? kTableRowViewTwoLinesHeight | |
705 : kTableRowViewOneLineHeight; | |
706 } | |
707 | |
708 - (base::scoped_nsobject<NSButton>)createButtonWithTitle:(NSString*)title { | |
709 base::scoped_nsobject<NSButton> button( | |
710 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]); | |
711 [button setButtonType:NSMomentaryPushInButton]; | |
712 [button setTitle:title]; | |
713 [button sizeToFit]; | |
714 return button; | |
715 } | |
716 | |
717 - (base::scoped_nsobject<NSButton>)createConnectButton { | |
718 NSString* connectTitle = | |
719 base::SysUTF16ToNSString(chooserController_->GetOkButtonLabel()); | |
720 return [self createButtonWithTitle:connectTitle]; | |
721 } | |
722 | |
723 - (base::scoped_nsobject<NSButton>)createCancelButton { | |
724 NSString* cancelTitle = | |
725 l10n_util::GetNSString(IDS_DEVICE_CHOOSER_CANCEL_BUTTON_TEXT); | |
726 return [self createButtonWithTitle:cancelTitle]; | |
727 } | |
728 | |
729 - (base::scoped_nsobject<NSBox>)createSeparator { | |
730 base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:NSZeroRect]); | |
731 [spacer setBoxType:NSBoxSeparator]; | |
732 [spacer setBorderType:NSLineBorder]; | |
733 [spacer setAlphaValue:kSeparatorAlphaValue]; | |
734 [spacer setFrameSize:NSMakeSize(kChooserWidth, kSeparatorHeight)]; | |
735 return spacer; | |
736 } | |
737 | |
738 - (base::scoped_nsobject<NSButton>)createHyperlinkButtonWithText: | |
739 (NSString*)text { | |
740 base::scoped_nsobject<NSButton> button( | |
741 [[NSButton alloc] initWithFrame:NSZeroRect]); | |
742 base::scoped_nsobject<HyperlinkButtonCell> cell( | |
743 [[HyperlinkButtonCell alloc] initTextCell:text]); | |
744 [button setCell:cell.get()]; | |
745 [button sizeToFit]; | |
746 return button; | |
747 } | |
748 | |
749 - (NSButton*)adapterOffHelpButton { | |
750 return adapterOffHelpButton_.get(); | |
751 } | |
752 | |
753 - (NSTableView*)tableView { | |
754 return tableView_.get(); | |
755 } | |
756 | |
757 - (SpinnerView*)spinner { | |
758 return spinner_.get(); | |
759 } | |
760 | |
761 - (NSButton*)connectButton { | |
762 return connectButton_.get(); | |
763 } | |
764 | |
765 - (NSButton*)cancelButton { | |
766 return cancelButton_.get(); | |
767 } | |
768 | |
769 - (NSButton*)helpButton { | |
770 return helpButton_.get(); | |
771 } | |
772 | |
773 - (NSTextField*)scanningMessage { | |
774 return scanningMessage_.get(); | |
775 } | |
776 | |
777 - (NSTextField*)wordConnector { | |
778 return wordConnector_.get(); | |
779 } | |
780 | |
781 - (NSButton*)rescanButton { | |
782 return rescanButton_.get(); | |
783 } | |
784 | |
785 - (NSInteger)numberOfOptions { | |
786 // When there are no devices, the table contains a message saying there are | |
787 // no devices, so the number of rows is always at least 1. | |
788 return std::max(static_cast<NSInteger>(chooserController_->NumOptions()), | |
789 static_cast<NSInteger>(1)); | |
790 } | |
791 | |
792 // When this function is called with numOptions == 0, it is to show the | |
793 // message saying there are no devices. | |
794 - (NSString*)optionAtIndex:(NSInteger)index { | |
795 NSInteger numOptions = | |
796 static_cast<NSInteger>(chooserController_->NumOptions()); | |
797 if (numOptions == 0) { | |
798 DCHECK_EQ(0, index); | |
799 return base::SysUTF16ToNSString(chooserController_->GetNoOptionsText()); | |
800 } | |
801 | |
802 DCHECK_GE(index, 0); | |
803 DCHECK_LT(index, numOptions); | |
804 | |
805 return base::SysUTF16ToNSString( | |
806 chooserController_->GetOption(static_cast<size_t>(index))); | |
807 } | |
808 | |
809 - (void)updateTableView { | |
810 chooserContentViewController_->UpdateTableView(); | |
811 } | |
812 | |
813 - (void)accept { | |
814 NSIndexSet* selectedRows = [tableView_ selectedRowIndexes]; | |
815 NSUInteger index = [selectedRows firstIndex]; | |
816 std::vector<size_t> indices; | |
817 while (index != NSNotFound) { | |
818 indices.push_back(index); | |
819 index = [selectedRows indexGreaterThanIndex:index]; | |
820 } | |
821 chooserController_->Select(indices); | |
822 } | |
823 | |
824 - (void)cancel { | |
825 chooserController_->Cancel(); | |
826 } | |
827 | |
828 - (void)close { | |
829 chooserController_->Close(); | |
830 } | |
831 | |
832 - (void)onAdapterOffHelp:(id)sender { | |
833 chooserController_->OpenAdapterOffHelpUrl(); | |
834 } | |
835 | |
836 - (void)onRescan:(id)sender { | |
837 chooserController_->RefreshOptions(); | |
838 } | |
839 | |
840 - (void)onHelpPressed:(id)sender { | |
841 chooserController_->OpenHelpCenterUrl(); | |
842 } | |
843 | |
844 - (void)updateContentRowColor { | |
845 NSIndexSet* selectedRows = [tableView_ selectedRowIndexes]; | |
846 NSInteger numOptions = | |
847 base::checked_cast<NSInteger>(chooserController_->NumOptions()); | |
848 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
849 for (NSInteger rowIndex = 0; rowIndex < numOptions; ++rowIndex) { | |
850 BOOL isSelected = [selectedRows containsIndex:rowIndex]; | |
851 // Update the color of the text. | |
852 [[self tableRowViewText:rowIndex] | |
853 setTextColor:(isSelected ? [NSColor whiteColor] | |
854 : [NSColor blackColor])]; | |
855 | |
856 // Update the color of the image. | |
857 if (chooserController_->ShouldShowIconBeforeText()) { | |
858 if (chooserController_->IsConnected(rowIndex)) { | |
859 [[self tableRowViewImage:rowIndex] | |
860 setImage:gfx::NSImageFromImageSkia(gfx::CreateVectorIcon( | |
861 gfx::VectorIconId::BLUETOOTH_CONNECTED, | |
862 isSelected ? SK_ColorWHITE : gfx::kChromeIconGrey))]; | |
863 } else { | |
864 int signalStrengthLevel = | |
865 chooserController_->GetSignalStrengthLevel(rowIndex); | |
866 if (signalStrengthLevel != -1) { | |
867 int imageId = | |
868 isSelected | |
869 ? kSignalStrengthLevelImageSelectedIds[signalStrengthLevel] | |
870 : kSignalStrengthLevelImageIds[signalStrengthLevel]; | |
871 [[self tableRowViewImage:rowIndex] | |
872 setImage:rb.GetNativeImageNamed(imageId).ToNSImage()]; | |
873 } | |
874 } | |
875 } | |
876 | |
877 // Update the color of paired status. | |
878 NSTextField* pairedStatusText = [self tableRowViewPairedStatus:rowIndex]; | |
879 if (pairedStatusText) { | |
880 [pairedStatusText setTextColor:(skia::SkColorToCalibratedNSColor( | |
881 isSelected ? gfx::kGoogleGreen300 | |
882 : gfx::kGoogleGreen700))]; | |
883 } | |
884 } | |
885 } | |
886 | |
887 - (NSImageView*)tableRowViewImage:(NSInteger)row { | |
888 ChooserContentTableRowView* tableRowView = | |
889 [tableView_ viewAtColumn:0 row:row makeIfNecessary:YES]; | |
890 return [tableRowView image]; | |
891 } | |
892 | |
893 - (NSTextField*)tableRowViewText:(NSInteger)row { | |
894 ChooserContentTableRowView* tableRowView = | |
895 [tableView_ viewAtColumn:0 row:row makeIfNecessary:YES]; | |
896 return [tableRowView text]; | |
897 } | |
898 | |
899 - (NSTextField*)tableRowViewPairedStatus:(NSInteger)row { | |
900 ChooserContentTableRowView* tableRowView = | |
901 [tableView_ viewAtColumn:0 row:row makeIfNecessary:YES]; | |
902 return [tableRowView pairedStatus]; | |
903 } | |
904 | |
905 - (void)drawRect:(NSRect)rect { | |
906 [[NSColor colorWithCalibratedWhite:245.0f / 255.0f alpha:1.0f] setFill]; | |
907 NSRect footnoteFrame = | |
908 NSMakeRect(0.0f, 0.0f, kChooserWidth, separatorOriginY_); | |
909 NSRectFill(footnoteFrame); | |
910 [super drawRect:footnoteFrame]; | |
911 } | |
912 | |
913 @end | |
OLD | NEW |