// Copyright (c) 2011 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 "speech_input_window_controller.h" #include "base/logging.h" #include "base/sys_string_conversions.h" #include "chrome/browser/ui/cocoa/info_bubble_view.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "media/audio/audio_manager.h" #import "skia/ext/skia_utils_mac.h" #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" #include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image.h" const int kBubbleControlVerticalSpacing = 10; // Space between controls. const int kBubbleHorizontalMargin = 5; // Space on either sides of controls. const int kInstructionLabelMaxWidth = 150; @interface SpeechInputWindowController (Private) - (NSSize)calculateContentSize; - (void)layout:(NSSize)size; @end @implementation SpeechInputWindowController - (id)initWithParentWindow:(NSWindow*)parentWindow delegate:(SpeechInputBubbleDelegate*)delegate anchoredAt:(NSPoint)anchoredAt { anchoredAt.y += info_bubble::kBubbleArrowHeight / 2.0; if ((self = [super initWithWindowNibPath:@"SpeechInputBubble" parentWindow:parentWindow anchoredAt:anchoredAt])) { DCHECK(delegate); delegate_ = delegate; displayMode_ = SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP; } return self; } - (void)awakeFromNib { [super awakeFromNib]; [[self bubble] setArrowLocation:info_bubble::kTopLeft]; } - (IBAction)cancel:(id)sender { delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_CANCEL); } - (IBAction)tryAgain:(id)sender { delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_TRY_AGAIN); } - (IBAction)micSettings:(id)sender { [[NSWorkspace sharedWorkspace] openFile: @"/System/Library/PreferencePanes/Sound.prefPane"]; } // Calculate the window dimensions to reflect the sum height and max width of // all controls, with appropriate spacing between and around them. The returned // size is in view coordinates. - (NSSize)calculateContentSize { [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_]; [GTMUILocalizerAndLayoutTweaker sizeToFitView:tryAgainButton_]; [GTMUILocalizerAndLayoutTweaker sizeToFitView:micSettingsButton_]; NSSize cancelSize = [cancelButton_ bounds].size; NSSize tryAgainSize = [tryAgainButton_ bounds].size; CGFloat newHeight = cancelSize.height + kBubbleControlVerticalSpacing; CGFloat newWidth = cancelSize.width; if (![tryAgainButton_ isHidden]) newWidth += tryAgainSize.width; // The size of the bubble in warm up mode is fixed to be the same as in // recording mode, so from warm up it can transition to recording without any // UI jank. bool isWarmUp = (displayMode_ == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP); if (![iconImage_ isHidden]) { NSSize size = [[iconImage_ image] size]; if (isWarmUp) { NSImage* volumeIcon = ResourceBundle::GetSharedInstance().GetNativeImageNamed( IDR_SPEECH_INPUT_MIC_EMPTY); size = [volumeIcon size]; } newHeight += size.height; newWidth = std::max(newWidth, size.width + 2 * kBubbleHorizontalMargin); } if (![instructionLabel_ isHidden] || isWarmUp) { [instructionLabel_ sizeToFit]; NSSize textSize = [[instructionLabel_ cell] cellSize]; NSRect boundsRect = NSMakeRect(0, 0, kInstructionLabelMaxWidth, CGFLOAT_MAX); NSSize multiLineSize = [[instructionLabel_ cell] cellSizeForBounds:boundsRect]; if (textSize.width > multiLineSize.width) textSize = multiLineSize; newHeight += textSize.height + kBubbleControlVerticalSpacing; newWidth = std::max(newWidth, textSize.width); } if (![micSettingsButton_ isHidden]) { NSSize size = [micSettingsButton_ bounds].size; newHeight += size.height; newWidth = std::max(newWidth, size.width); } return NSMakeSize(newWidth + 2 * kBubbleHorizontalMargin, newHeight + 3 * kBubbleControlVerticalSpacing); } // Position the controls within the given content area bounds. - (void)layout:(NSSize)size { int y = kBubbleControlVerticalSpacing; NSRect cancelRect = [cancelButton_ bounds]; if ([tryAgainButton_ isHidden]) { cancelRect.origin.x = (size.width - NSWidth(cancelRect)) / 2; } else { NSRect tryAgainRect = [tryAgainButton_ bounds]; cancelRect.origin.x = (size.width - NSWidth(cancelRect) - NSWidth(tryAgainRect)) / 2; tryAgainRect.origin.x = cancelRect.origin.x + NSWidth(cancelRect); tryAgainRect.origin.y = y; [tryAgainButton_ setFrame:tryAgainRect]; } cancelRect.origin.y = y; if (![cancelButton_ isHidden]) { [cancelButton_ setFrame:cancelRect]; y += NSHeight(cancelRect) + kBubbleControlVerticalSpacing; } NSRect rect; if (![micSettingsButton_ isHidden]) { rect = [micSettingsButton_ bounds]; rect.origin.x = (size.width - NSWidth(rect)) / 2; rect.origin.y = y; [micSettingsButton_ setFrame:rect]; y += rect.size.height + kBubbleControlVerticalSpacing; } if (![instructionLabel_ isHidden]) { int spaceForIcon = 0; if (![iconImage_ isHidden]) { spaceForIcon = [[iconImage_ image] size].height + kBubbleControlVerticalSpacing; } rect = NSMakeRect(0, y, size.width, size.height - y - spaceForIcon - kBubbleControlVerticalSpacing * 2); [instructionLabel_ setFrame:rect]; y = size.height - spaceForIcon - kBubbleControlVerticalSpacing; } if (![iconImage_ isHidden]) { rect.size = [[iconImage_ image] size]; // In warm-up mode only the icon gets displayed so center it vertically. if (displayMode_ == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP) y = (size.height - rect.size.height) / 2; rect.origin.x = (size.width - NSWidth(rect)) / 2; rect.origin.y = y; [iconImage_ setFrame:rect]; } } - (void)updateLayout:(SpeechInputBubbleBase::DisplayMode)mode messageText:(const string16&)messageText iconImage:(NSImage*)iconImage { // The very first time this method is called, the child views would still be // uninitialized and null. So we invoke [self window] first and that sets up // the child views properly so we can do the layout calculations below. NSWindow* window = [self window]; displayMode_ = mode; BOOL is_message = (mode == SpeechInputBubbleBase::DISPLAY_MODE_MESSAGE); BOOL is_recording = (mode == SpeechInputBubbleBase::DISPLAY_MODE_RECORDING); BOOL is_warm_up = (mode == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP); [iconImage_ setHidden:is_message]; [tryAgainButton_ setHidden:!is_message]; [micSettingsButton_ setHidden:!is_message]; [instructionLabel_ setHidden:!is_message && !is_recording]; [cancelButton_ setHidden:is_warm_up]; // Get the right set of controls to be visible. if (is_message) { [instructionLabel_ setStringValue:base::SysUTF16ToNSString(messageText)]; } else { [iconImage_ setImage:iconImage]; [instructionLabel_ setStringValue:l10n_util::GetNSString( IDS_SPEECH_INPUT_BUBBLE_HEADING)]; } NSSize newSize = [self calculateContentSize]; [[self bubble] setFrameSize:newSize]; NSSize windowDelta = [[window contentView] convertSize:newSize toView:nil]; NSRect newFrame = [window frame]; newFrame.origin.y -= windowDelta.height - newFrame.size.height; newFrame.size = windowDelta; [window setFrame:newFrame display:YES]; [self layout:newSize]; // Layout all the child controls. } - (void)windowWillClose:(NSNotification*)notification { delegate_->InfoBubbleFocusChanged(); } - (void)show { [self showWindow:nil]; } - (void)hide { [[self window] orderOut:nil]; } - (void)setImage:(NSImage*)image { [iconImage_ setImage:image]; } @end // implementation SpeechInputWindowController