Home | History | Annotate | Download | only in bubble
      1 // Copyright (c) 2012 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 #include "ui/views/bubble/bubble_frame_view.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "ui/base/hit_test.h"
     10 #include "ui/base/resource/resource_bundle.h"
     11 #include "ui/gfx/path.h"
     12 #include "ui/gfx/screen.h"
     13 #include "ui/gfx/skia_util.h"
     14 #include "ui/native_theme/native_theme.h"
     15 #include "ui/resources/grit/ui_resources.h"
     16 #include "ui/views/bubble/bubble_border.h"
     17 #include "ui/views/controls/button/label_button.h"
     18 #include "ui/views/widget/widget.h"
     19 #include "ui/views/widget/widget_delegate.h"
     20 #include "ui/views/window/client_view.h"
     21 
     22 namespace {
     23 
     24 // Insets for the title bar views in pixels.
     25 const int kTitleTopInset = 12;
     26 const int kTitleLeftInset = 19;
     27 const int kTitleBottomInset = 12;
     28 const int kTitleRightInset = 7;
     29 
     30 // Get the |vertical| or horizontal amount that |available_bounds| overflows
     31 // |window_bounds|.
     32 int GetOffScreenLength(const gfx::Rect& available_bounds,
     33                        const gfx::Rect& window_bounds,
     34                        bool vertical) {
     35   if (available_bounds.IsEmpty() || available_bounds.Contains(window_bounds))
     36     return 0;
     37 
     38   //  window_bounds
     39   //  +---------------------------------+
     40   //  |             top                 |
     41   //  |      +------------------+       |
     42   //  | left | available_bounds | right |
     43   //  |      +------------------+       |
     44   //  |            bottom               |
     45   //  +---------------------------------+
     46   if (vertical)
     47     return std::max(0, available_bounds.y() - window_bounds.y()) +
     48            std::max(0, window_bounds.bottom() - available_bounds.bottom());
     49   return std::max(0, available_bounds.x() - window_bounds.x()) +
     50          std::max(0, window_bounds.right() - available_bounds.right());
     51 }
     52 
     53 }  // namespace
     54 
     55 namespace views {
     56 
     57 // static
     58 const char BubbleFrameView::kViewClassName[] = "BubbleFrameView";
     59 
     60 // static
     61 gfx::Insets BubbleFrameView::GetTitleInsets() {
     62   return gfx::Insets(kTitleTopInset, kTitleLeftInset,
     63                      kTitleBottomInset, kTitleRightInset);
     64 }
     65 
     66 BubbleFrameView::BubbleFrameView(const gfx::Insets& content_margins)
     67     : bubble_border_(NULL),
     68       content_margins_(content_margins),
     69       title_(NULL),
     70       close_(NULL),
     71       titlebar_extra_view_(NULL) {
     72   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
     73   title_ = new Label(base::string16(),
     74                      rb.GetFontList(ui::ResourceBundle::MediumFont));
     75   title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
     76   AddChildView(title_);
     77 
     78   close_ = new LabelButton(this, base::string16());
     79   close_->SetImage(CustomButton::STATE_NORMAL,
     80                    *rb.GetImageNamed(IDR_CLOSE_DIALOG).ToImageSkia());
     81   close_->SetImage(CustomButton::STATE_HOVERED,
     82                    *rb.GetImageNamed(IDR_CLOSE_DIALOG_H).ToImageSkia());
     83   close_->SetImage(CustomButton::STATE_PRESSED,
     84                    *rb.GetImageNamed(IDR_CLOSE_DIALOG_P).ToImageSkia());
     85   close_->SetBorder(scoped_ptr<Border>());
     86   close_->SetSize(close_->GetPreferredSize());
     87   close_->SetVisible(false);
     88   AddChildView(close_);
     89 }
     90 
     91 BubbleFrameView::~BubbleFrameView() {}
     92 
     93 gfx::Rect BubbleFrameView::GetBoundsForClientView() const {
     94   gfx::Rect client_bounds = GetLocalBounds();
     95   client_bounds.Inset(GetInsets());
     96   client_bounds.Inset(bubble_border_->GetInsets());
     97   return client_bounds;
     98 }
     99 
    100 gfx::Rect BubbleFrameView::GetWindowBoundsForClientBounds(
    101     const gfx::Rect& client_bounds) const {
    102   return const_cast<BubbleFrameView*>(this)->GetUpdatedWindowBounds(
    103       gfx::Rect(), client_bounds.size(), false);
    104 }
    105 
    106 int BubbleFrameView::NonClientHitTest(const gfx::Point& point) {
    107   if (!bounds().Contains(point))
    108     return HTNOWHERE;
    109   if (close_->visible() && close_->GetMirroredBounds().Contains(point))
    110     return HTCLOSE;
    111 
    112   // Allow dialogs to show the system menu and be dragged.
    113   if (GetWidget()->widget_delegate()->AsDialogDelegate()) {
    114     gfx::Rect sys_rect(0, 0, title_->x(), title_->y());
    115     sys_rect.set_origin(gfx::Point(GetMirroredXForRect(sys_rect), 0));
    116     if (sys_rect.Contains(point))
    117       return HTSYSMENU;
    118     if (point.y() < title_->bounds().bottom())
    119       return HTCAPTION;
    120   }
    121 
    122   return GetWidget()->client_view()->NonClientHitTest(point);
    123 }
    124 
    125 void BubbleFrameView::GetWindowMask(const gfx::Size& size,
    126                                     gfx::Path* window_mask) {
    127   // NOTE: this only provides implementations for the types used by dialogs.
    128   if ((bubble_border_->arrow() != BubbleBorder::NONE &&
    129        bubble_border_->arrow() != BubbleBorder::FLOAT) ||
    130       (bubble_border_->shadow() != BubbleBorder::SMALL_SHADOW &&
    131        bubble_border_->shadow() != BubbleBorder::NO_SHADOW_OPAQUE_BORDER))
    132     return;
    133 
    134   // Use a window mask roughly matching the border in the image assets.
    135   static const int kBorderStrokeSize = 1;
    136   static const SkScalar kCornerRadius = SkIntToScalar(6);
    137   const gfx::Insets border_insets = bubble_border_->GetInsets();
    138   SkRect rect = { SkIntToScalar(border_insets.left() - kBorderStrokeSize),
    139                   SkIntToScalar(border_insets.top() - kBorderStrokeSize),
    140                   SkIntToScalar(size.width() - border_insets.right() +
    141                                 kBorderStrokeSize),
    142                   SkIntToScalar(size.height() - border_insets.bottom() +
    143                                 kBorderStrokeSize) };
    144   if (bubble_border_->shadow() == BubbleBorder::NO_SHADOW_OPAQUE_BORDER) {
    145     window_mask->addRoundRect(rect, kCornerRadius, kCornerRadius);
    146   } else {
    147     static const int kBottomBorderShadowSize = 2;
    148     rect.fBottom += SkIntToScalar(kBottomBorderShadowSize);
    149     window_mask->addRect(rect);
    150   }
    151 }
    152 
    153 void BubbleFrameView::ResetWindowControls() {
    154   close_->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton());
    155 }
    156 
    157 void BubbleFrameView::UpdateWindowIcon() {}
    158 
    159 void BubbleFrameView::UpdateWindowTitle() {
    160   title_->SetText(GetWidget()->widget_delegate()->ShouldShowWindowTitle() ?
    161       GetWidget()->widget_delegate()->GetWindowTitle() : base::string16());
    162   // Update the close button visibility too, otherwise it's not intialized.
    163   ResetWindowControls();
    164 }
    165 
    166 void BubbleFrameView::SizeConstraintsChanged() {}
    167 
    168 void BubbleFrameView::SetTitleFontList(const gfx::FontList& font_list) {
    169   title_->SetFontList(font_list);
    170 }
    171 
    172 gfx::Insets BubbleFrameView::GetInsets() const {
    173   gfx::Insets insets = content_margins_;
    174   const int title_height = title_->text().empty() ? 0 :
    175       title_->GetPreferredSize().height() + kTitleTopInset + kTitleBottomInset;
    176   const int close_height = close_->visible() ? close_->height() : 0;
    177   insets += gfx::Insets(std::max(title_height, close_height), 0, 0, 0);
    178   return insets;
    179 }
    180 
    181 gfx::Size BubbleFrameView::GetPreferredSize() const {
    182   return GetSizeForClientSize(GetWidget()->client_view()->GetPreferredSize());
    183 }
    184 
    185 gfx::Size BubbleFrameView::GetMinimumSize() const {
    186   return GetSizeForClientSize(GetWidget()->client_view()->GetMinimumSize());
    187 }
    188 
    189 void BubbleFrameView::Layout() {
    190   gfx::Rect bounds(GetContentsBounds());
    191   bounds.Inset(GetTitleInsets());
    192   if (bounds.IsEmpty())
    193     return;
    194 
    195   // The close button top inset is actually smaller than the title top inset.
    196   close_->SetPosition(gfx::Point(bounds.right() - close_->width(),
    197                                  bounds.y() - 5));
    198 
    199   gfx::Size title_size(title_->GetPreferredSize());
    200   const int title_width = std::max(0, close_->x() - bounds.x());
    201   title_size.SetToMin(gfx::Size(title_width, title_size.height()));
    202   bounds.set_size(title_size);
    203   title_->SetBoundsRect(bounds);
    204 
    205   if (titlebar_extra_view_) {
    206     const int extra_width = close_->x() - title_->bounds().right();
    207     gfx::Size size = titlebar_extra_view_->GetPreferredSize();
    208     size.SetToMin(gfx::Size(std::max(0, extra_width), size.height()));
    209     gfx::Rect titlebar_extra_view_bounds(
    210         close_->x() - size.width(),
    211         bounds.y(),
    212         size.width(),
    213         bounds.height());
    214     titlebar_extra_view_bounds.Subtract(bounds);
    215     titlebar_extra_view_->SetBoundsRect(titlebar_extra_view_bounds);
    216   }
    217 }
    218 
    219 const char* BubbleFrameView::GetClassName() const {
    220   return kViewClassName;
    221 }
    222 
    223 void BubbleFrameView::ChildPreferredSizeChanged(View* child) {
    224   if (child == titlebar_extra_view_ || child == title_)
    225     Layout();
    226 }
    227 
    228 void BubbleFrameView::OnThemeChanged() {
    229   UpdateWindowTitle();
    230   ResetWindowControls();
    231   UpdateWindowIcon();
    232 }
    233 
    234 void BubbleFrameView::OnNativeThemeChanged(const ui::NativeTheme* theme) {
    235   if (bubble_border_ && bubble_border_->use_theme_background_color()) {
    236     bubble_border_->set_background_color(GetNativeTheme()->
    237         GetSystemColor(ui::NativeTheme::kColorId_DialogBackground));
    238     SchedulePaint();
    239   }
    240 }
    241 
    242 void BubbleFrameView::ButtonPressed(Button* sender, const ui::Event& event) {
    243   if (sender == close_)
    244     GetWidget()->Close();
    245 }
    246 
    247 void BubbleFrameView::SetBubbleBorder(scoped_ptr<BubbleBorder> border) {
    248   bubble_border_ = border.get();
    249   SetBorder(border.PassAs<Border>());
    250 
    251   // Update the background, which relies on the border.
    252   set_background(new views::BubbleBackground(bubble_border_));
    253 }
    254 
    255 void BubbleFrameView::SetTitlebarExtraView(View* view) {
    256   DCHECK(view);
    257   DCHECK(!titlebar_extra_view_);
    258   AddChildView(view);
    259   titlebar_extra_view_ = view;
    260 }
    261 
    262 gfx::Rect BubbleFrameView::GetUpdatedWindowBounds(const gfx::Rect& anchor_rect,
    263                                                   gfx::Size client_size,
    264                                                   bool adjust_if_offscreen) {
    265   gfx::Size size(GetSizeForClientSize(client_size));
    266 
    267   const BubbleBorder::Arrow arrow = bubble_border_->arrow();
    268   if (adjust_if_offscreen && BubbleBorder::has_arrow(arrow)) {
    269     // Try to mirror the anchoring if the bubble does not fit on the screen.
    270     if (!bubble_border_->is_arrow_at_center(arrow)) {
    271       MirrorArrowIfOffScreen(true, anchor_rect, size);
    272       MirrorArrowIfOffScreen(false, anchor_rect, size);
    273     } else {
    274       const bool mirror_vertical = BubbleBorder::is_arrow_on_horizontal(arrow);
    275       MirrorArrowIfOffScreen(mirror_vertical, anchor_rect, size);
    276       OffsetArrowIfOffScreen(anchor_rect, size);
    277     }
    278   }
    279 
    280   // Calculate the bounds with the arrow in its updated location and offset.
    281   return bubble_border_->GetBounds(anchor_rect, size);
    282 }
    283 
    284 gfx::Rect BubbleFrameView::GetAvailableScreenBounds(const gfx::Rect& rect) {
    285   // The bubble attempts to fit within the current screen bounds.
    286   // TODO(scottmg): Native is wrong. http://crbug.com/133312
    287   return gfx::Screen::GetNativeScreen()->GetDisplayNearestPoint(
    288       rect.CenterPoint()).work_area();
    289 }
    290 
    291 void BubbleFrameView::MirrorArrowIfOffScreen(
    292     bool vertical,
    293     const gfx::Rect& anchor_rect,
    294     const gfx::Size& client_size) {
    295   // Check if the bounds don't fit on screen.
    296   gfx::Rect available_bounds(GetAvailableScreenBounds(anchor_rect));
    297   gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size));
    298   if (GetOffScreenLength(available_bounds, window_bounds, vertical) > 0) {
    299     BubbleBorder::Arrow arrow = bubble_border()->arrow();
    300     // Mirror the arrow and get the new bounds.
    301     bubble_border_->set_arrow(
    302         vertical ? BubbleBorder::vertical_mirror(arrow) :
    303                    BubbleBorder::horizontal_mirror(arrow));
    304     gfx::Rect mirror_bounds =
    305         bubble_border_->GetBounds(anchor_rect, client_size);
    306     // Restore the original arrow if mirroring doesn't show more of the bubble.
    307     // Otherwise it should invoke parent's Layout() to layout the content based
    308     // on the new bubble border.
    309     if (GetOffScreenLength(available_bounds, mirror_bounds, vertical) >=
    310         GetOffScreenLength(available_bounds, window_bounds, vertical))
    311       bubble_border_->set_arrow(arrow);
    312     else if (parent())
    313       parent()->Layout();
    314   }
    315 }
    316 
    317 void BubbleFrameView::OffsetArrowIfOffScreen(const gfx::Rect& anchor_rect,
    318                                              const gfx::Size& client_size) {
    319   BubbleBorder::Arrow arrow = bubble_border()->arrow();
    320   DCHECK(BubbleBorder::is_arrow_at_center(arrow));
    321 
    322   // Get the desired bubble bounds without adjustment.
    323   bubble_border_->set_arrow_offset(0);
    324   gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size));
    325 
    326   gfx::Rect available_bounds(GetAvailableScreenBounds(anchor_rect));
    327   if (available_bounds.IsEmpty() || available_bounds.Contains(window_bounds))
    328     return;
    329 
    330   // Calculate off-screen adjustment.
    331   const bool is_horizontal = BubbleBorder::is_arrow_on_horizontal(arrow);
    332   int offscreen_adjust = 0;
    333   if (is_horizontal) {
    334     if (window_bounds.x() < available_bounds.x())
    335       offscreen_adjust = available_bounds.x() - window_bounds.x();
    336     else if (window_bounds.right() > available_bounds.right())
    337       offscreen_adjust = available_bounds.right() - window_bounds.right();
    338   } else {
    339     if (window_bounds.y() < available_bounds.y())
    340       offscreen_adjust = available_bounds.y() - window_bounds.y();
    341     else if (window_bounds.bottom() > available_bounds.bottom())
    342       offscreen_adjust = available_bounds.bottom() - window_bounds.bottom();
    343   }
    344 
    345   // For center arrows, arrows are moved in the opposite direction of
    346   // |offscreen_adjust|, e.g. positive |offscreen_adjust| means bubble
    347   // window needs to be moved to the right and that means we need to move arrow
    348   // to the left, and that means negative offset.
    349   bubble_border_->set_arrow_offset(
    350       bubble_border_->GetArrowOffset(window_bounds.size()) - offscreen_adjust);
    351   if (offscreen_adjust)
    352     SchedulePaint();
    353 }
    354 
    355 gfx::Size BubbleFrameView::GetSizeForClientSize(
    356     const gfx::Size& client_size) const {
    357   // Accommodate the width of the title bar elements.
    358   int title_bar_width = GetInsets().width() + border()->GetInsets().width();
    359   if (!title_->text().empty())
    360     title_bar_width += kTitleLeftInset + title_->GetPreferredSize().width();
    361   if (close_->visible())
    362     title_bar_width += close_->width() + 1;
    363   if (titlebar_extra_view_ != NULL)
    364     title_bar_width += titlebar_extra_view_->GetPreferredSize().width();
    365   gfx::Size size(client_size);
    366   size.SetToMax(gfx::Size(title_bar_width, 0));
    367   const gfx::Insets insets(GetInsets());
    368   size.Enlarge(insets.width(), insets.height());
    369   return size;
    370 }
    371 
    372 }  // namespace views
    373