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