Home | History | Annotate | Download | only in cocoa
      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 
     19 const int kBubbleControlVerticalSpacing = 10;  // Space between controls.
     20 const int kBubbleHorizontalMargin = 5;  // Space on either sides of controls.
     21 const 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