1// Copyright (c) 2011 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 "speech_input_window_controller.h"
6
7#include "base/logging.h"
8#include "base/sys_string_conversions.h"
9#include "chrome/browser/ui/cocoa/info_bubble_view.h"
10#include "grit/generated_resources.h"
11#include "grit/theme_resources.h"
12#include "media/audio/audio_manager.h"
13#import "skia/ext/skia_utils_mac.h"
14#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
15#include "ui/base/l10n/l10n_util_mac.h"
16#include "ui/base/resource/resource_bundle.h"
17#include "ui/gfx/image.h"
18
19const int kBubbleControlVerticalSpacing = 10;  // Space between controls.
20const int kBubbleHorizontalMargin = 5;  // Space on either sides of controls.
21const int kInstructionLabelMaxWidth = 150;
22
23@interface SpeechInputWindowController (Private)
24- (NSSize)calculateContentSize;
25- (void)layout:(NSSize)size;
26@end
27
28@implementation SpeechInputWindowController
29
30- (id)initWithParentWindow:(NSWindow*)parentWindow
31                  delegate:(SpeechInputBubbleDelegate*)delegate
32              anchoredAt:(NSPoint)anchoredAt {
33  anchoredAt.y += info_bubble::kBubbleArrowHeight / 2.0;
34  if ((self = [super initWithWindowNibPath:@"SpeechInputBubble"
35                              parentWindow:parentWindow
36                                anchoredAt:anchoredAt])) {
37    DCHECK(delegate);
38    delegate_ = delegate;
39    displayMode_ = SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP;
40  }
41  return self;
42}
43
44- (void)awakeFromNib {
45  [super awakeFromNib];
46  [[self bubble] setArrowLocation:info_bubble::kTopLeft];
47}
48
49- (IBAction)cancel:(id)sender {
50  delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_CANCEL);
51}
52
53- (IBAction)tryAgain:(id)sender {
54  delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_TRY_AGAIN);
55}
56
57- (IBAction)micSettings:(id)sender {
58  [[NSWorkspace sharedWorkspace] openFile:
59       @"/System/Library/PreferencePanes/Sound.prefPane"];
60}
61
62// Calculate the window dimensions to reflect the sum height and max width of
63// all controls, with appropriate spacing between and around them. The returned
64// size is in view coordinates.
65- (NSSize)calculateContentSize {
66  [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
67  [GTMUILocalizerAndLayoutTweaker sizeToFitView:tryAgainButton_];
68  [GTMUILocalizerAndLayoutTweaker sizeToFitView:micSettingsButton_];
69  NSSize cancelSize = [cancelButton_ bounds].size;
70  NSSize tryAgainSize = [tryAgainButton_ bounds].size;
71  CGFloat newHeight = cancelSize.height + kBubbleControlVerticalSpacing;
72  CGFloat newWidth = cancelSize.width;
73  if (![tryAgainButton_ isHidden])
74    newWidth += tryAgainSize.width;
75
76  // The size of the bubble in warm up mode is fixed to be the same as in
77  // recording mode, so from warm up it can transition to recording without any
78  // UI jank.
79  bool isWarmUp = (displayMode_ ==
80                   SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP);
81
82  if (![iconImage_ isHidden]) {
83    NSSize size = [[iconImage_ image] size];
84    if (isWarmUp) {
85      NSImage* volumeIcon =
86          ResourceBundle::GetSharedInstance().GetNativeImageNamed(
87              IDR_SPEECH_INPUT_MIC_EMPTY);
88      size = [volumeIcon size];
89    }
90    newHeight += size.height;
91    newWidth = std::max(newWidth, size.width + 2 * kBubbleHorizontalMargin);
92  }
93
94  if (![instructionLabel_ isHidden] || isWarmUp) {
95    [instructionLabel_ sizeToFit];
96    NSSize textSize = [[instructionLabel_ cell] cellSize];
97    NSRect boundsRect = NSMakeRect(0, 0, kInstructionLabelMaxWidth,
98                                   CGFLOAT_MAX);
99    NSSize multiLineSize =
100        [[instructionLabel_ cell] cellSizeForBounds:boundsRect];
101    if (textSize.width > multiLineSize.width)
102      textSize = multiLineSize;
103    newHeight += textSize.height + kBubbleControlVerticalSpacing;
104    newWidth = std::max(newWidth, textSize.width);
105  }
106
107  if (![micSettingsButton_ isHidden]) {
108    NSSize size = [micSettingsButton_ bounds].size;
109    newHeight += size.height;
110    newWidth = std::max(newWidth, size.width);
111  }
112
113  return NSMakeSize(newWidth + 2 * kBubbleHorizontalMargin,
114                    newHeight + 3 * kBubbleControlVerticalSpacing);
115}
116
117// Position the controls within the given content area bounds.
118- (void)layout:(NSSize)size {
119  int y = kBubbleControlVerticalSpacing;
120
121  NSRect cancelRect = [cancelButton_ bounds];
122
123  if ([tryAgainButton_ isHidden]) {
124    cancelRect.origin.x = (size.width - NSWidth(cancelRect)) / 2;
125  } else {
126    NSRect tryAgainRect = [tryAgainButton_ bounds];
127    cancelRect.origin.x = (size.width - NSWidth(cancelRect) -
128                           NSWidth(tryAgainRect)) / 2;
129    tryAgainRect.origin.x = cancelRect.origin.x + NSWidth(cancelRect);
130    tryAgainRect.origin.y = y;
131    [tryAgainButton_ setFrame:tryAgainRect];
132  }
133  cancelRect.origin.y = y;
134
135  if (![cancelButton_ isHidden]) {
136    [cancelButton_ setFrame:cancelRect];
137    y += NSHeight(cancelRect) + kBubbleControlVerticalSpacing;
138  }
139
140  NSRect rect;
141  if (![micSettingsButton_ isHidden]) {
142    rect = [micSettingsButton_ bounds];
143    rect.origin.x = (size.width - NSWidth(rect)) / 2;
144    rect.origin.y = y;
145    [micSettingsButton_ setFrame:rect];
146    y += rect.size.height + kBubbleControlVerticalSpacing;
147  }
148
149  if (![instructionLabel_ isHidden]) {
150    int spaceForIcon = 0;
151    if (![iconImage_ isHidden]) {
152      spaceForIcon = [[iconImage_ image] size].height +
153                     kBubbleControlVerticalSpacing;
154    }
155
156    rect = NSMakeRect(0, y, size.width, size.height - y - spaceForIcon -
157                      kBubbleControlVerticalSpacing * 2);
158    [instructionLabel_ setFrame:rect];
159    y = size.height - spaceForIcon - kBubbleControlVerticalSpacing;
160  }
161
162  if (![iconImage_ isHidden]) {
163    rect.size = [[iconImage_ image] size];
164    // In warm-up mode only the icon gets displayed so center it vertically.
165    if (displayMode_ == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP)
166      y = (size.height - rect.size.height) / 2;
167    rect.origin.x = (size.width - NSWidth(rect)) / 2;
168    rect.origin.y = y;
169    [iconImage_ setFrame:rect];
170  }
171}
172
173- (void)updateLayout:(SpeechInputBubbleBase::DisplayMode)mode
174         messageText:(const string16&)messageText
175           iconImage:(NSImage*)iconImage {
176  // The very first time this method is called, the child views would still be
177  // uninitialized and null. So we invoke [self window] first and that sets up
178  // the child views properly so we can do the layout calculations below.
179  NSWindow* window = [self window];
180  displayMode_ = mode;
181  BOOL is_message = (mode == SpeechInputBubbleBase::DISPLAY_MODE_MESSAGE);
182  BOOL is_recording = (mode == SpeechInputBubbleBase::DISPLAY_MODE_RECORDING);
183  BOOL is_warm_up = (mode == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP);
184  [iconImage_ setHidden:is_message];
185  [tryAgainButton_ setHidden:!is_message];
186  [micSettingsButton_ setHidden:!is_message];
187  [instructionLabel_ setHidden:!is_message && !is_recording];
188  [cancelButton_ setHidden:is_warm_up];
189
190  // Get the right set of controls to be visible.
191  if (is_message) {
192    [instructionLabel_ setStringValue:base::SysUTF16ToNSString(messageText)];
193  } else {
194    [iconImage_ setImage:iconImage];
195    [instructionLabel_ setStringValue:l10n_util::GetNSString(
196        IDS_SPEECH_INPUT_BUBBLE_HEADING)];
197  }
198
199  NSSize newSize = [self calculateContentSize];
200  [[self bubble] setFrameSize:newSize];
201
202  NSSize windowDelta = [[window contentView] convertSize:newSize toView:nil];
203  NSRect newFrame = [window frame];
204  newFrame.origin.y -= windowDelta.height - newFrame.size.height;
205  newFrame.size = windowDelta;
206  [window setFrame:newFrame display:YES];
207
208  [self layout:newSize];  // Layout all the child controls.
209}
210
211- (void)windowWillClose:(NSNotification*)notification {
212  delegate_->InfoBubbleFocusChanged();
213}
214
215- (void)show {
216  [self showWindow:nil];
217}
218
219- (void)hide {
220  [[self window] orderOut:nil];
221}
222
223- (void)setImage:(NSImage*)image {
224  [iconImage_ setImage:image];
225}
226
227@end  // implementation SpeechInputWindowController
228