| // Copyright (c) 2012 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. |
| |
| #include "ui/views/bubble/bubble_frame_view.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "build/build_config.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "ui/base/default_style.h" |
| #include "ui/base/hit_test.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/material_design/material_design_controller.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/compositor/paint_recorder.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/geometry/vector2d.h" |
| #include "ui/gfx/path.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/resources/grit/ui_resources.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/bubble/bubble_border.h" |
| #include "ui/views/bubble/bubble_dialog_delegate.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/button/image_button_factory.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/layout_provider.h" |
| #include "ui/views/paint_info.h" |
| #include "ui/views/resources/grit/views_resources.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| #include "ui/views/window/client_view.h" |
| #include "ui/views/window/dialog_delegate.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // Get the |vertical| or horizontal amount that |available_bounds| overflows |
| // |window_bounds|. |
| int GetOffScreenLength(const gfx::Rect& available_bounds, |
| const gfx::Rect& window_bounds, |
| bool vertical) { |
| if (available_bounds.IsEmpty() || available_bounds.Contains(window_bounds)) |
| return 0; |
| |
| // window_bounds |
| // +---------------------------------+ |
| // | top | |
| // | +------------------+ | |
| // | left | available_bounds | right | |
| // | +------------------+ | |
| // | bottom | |
| // +---------------------------------+ |
| if (vertical) |
| return std::max(0, available_bounds.y() - window_bounds.y()) + |
| std::max(0, window_bounds.bottom() - available_bounds.bottom()); |
| return std::max(0, available_bounds.x() - window_bounds.x()) + |
| std::max(0, window_bounds.right() - available_bounds.right()); |
| } |
| |
| } // namespace |
| |
| // A container that changes visibility with its contents. |
| class FootnoteContainerView : public View { |
| public: |
| FootnoteContainerView() {} |
| |
| // View: |
| void ChildVisibilityChanged(View* child) override { |
| DCHECK_EQ(child_count(), 1); |
| SetVisible(child->visible()); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(FootnoteContainerView); |
| }; |
| |
| // static |
| const char BubbleFrameView::kViewClassName[] = "BubbleFrameView"; |
| |
| BubbleFrameView::BubbleFrameView(const gfx::Insets& title_margins, |
| const gfx::Insets& content_margins) |
| : bubble_border_(nullptr), |
| title_margins_(title_margins), |
| content_margins_(content_margins), |
| footnote_margins_(content_margins_), |
| title_icon_(new views::ImageView()), |
| default_title_(CreateDefaultTitleLabel(base::string16()).release()), |
| custom_title_(nullptr), |
| close_(nullptr), |
| footnote_container_(nullptr), |
| close_button_clicked_(false) { |
| AddChildView(title_icon_); |
| |
| default_title_->SetVisible(false); |
| AddChildView(default_title_); |
| |
| close_ = CreateCloseButton(this); |
| close_->SetVisible(false); |
| #if defined(OS_WIN) |
| // Windows will automatically create a tooltip for the close button based on |
| // the HTCLOSE result from NonClientHitTest(). |
| close_->SetTooltipText(base::string16()); |
| #endif |
| AddChildView(close_); |
| } |
| |
| BubbleFrameView::~BubbleFrameView() {} |
| |
| // static |
| std::unique_ptr<Label> BubbleFrameView::CreateDefaultTitleLabel( |
| const base::string16& title_text) { |
| auto title = std::make_unique<Label>(title_text, style::CONTEXT_DIALOG_TITLE); |
| title->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| title->set_collapse_when_hidden(true); |
| title->SetMultiLine(true); |
| return title; |
| } |
| |
| // static |
| Button* BubbleFrameView::CreateCloseButton(ButtonListener* listener) { |
| ImageButton* close_button = nullptr; |
| if (ui::MaterialDesignController::IsSecondaryUiMaterial()) { |
| close_button = CreateVectorImageButton(listener); |
| SetImageFromVectorIcon(close_button, vector_icons::kCloseRoundedIcon); |
| } else { |
| ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); |
| close_button = new ImageButton(listener); |
| close_button->SetImage(Button::STATE_NORMAL, |
| *rb->GetImageNamed(IDR_CLOSE_DIALOG).ToImageSkia()); |
| close_button->SetImage( |
| Button::STATE_HOVERED, |
| *rb->GetImageNamed(IDR_CLOSE_DIALOG_H).ToImageSkia()); |
| close_button->SetImage( |
| Button::STATE_PRESSED, |
| *rb->GetImageNamed(IDR_CLOSE_DIALOG_P).ToImageSkia()); |
| } |
| close_button->SetTooltipText(l10n_util::GetStringUTF16(IDS_APP_CLOSE)); |
| close_button->SizeToPreferredSize(); |
| |
| // Remove the close button from tab traversal on all platforms. Note this does |
| // not affect screen readers' ability to focus the close button. Keyboard |
| // access to the close button when not using a screen reader is done via the |
| // ESC key handler in DialogClientView. |
| close_button->SetFocusBehavior(View::FocusBehavior::NEVER); |
| return close_button; |
| } |
| |
| gfx::Rect BubbleFrameView::GetBoundsForClientView() const { |
| // When NonClientView asks for this, the size of the frame view has been set |
| // (i.e. |this|), but not the client view bounds. |
| gfx::Rect client_bounds = GetContentsBounds(); |
| client_bounds.Inset(GetClientInsetsForFrameWidth(client_bounds.width())); |
| // Only account for footnote_container_'s height if it's visible, because |
| // content_margins_ adds extra padding even if all child views are invisible. |
| if (footnote_container_ && footnote_container_->visible()) { |
| client_bounds.set_height(client_bounds.height() - |
| footnote_container_->height()); |
| } |
| return client_bounds; |
| } |
| |
| gfx::Rect BubbleFrameView::GetWindowBoundsForClientBounds( |
| const gfx::Rect& client_bounds) const { |
| gfx::Size size(GetFrameSizeForClientSize(client_bounds.size())); |
| return bubble_border_->GetBounds(gfx::Rect(), size); |
| } |
| |
| bool BubbleFrameView::GetClientMask(const gfx::Size& size, |
| gfx::Path* path) const { |
| // NonClientView calls this after setting the client view size from the return |
| // of GetBoundsForClientView(); feeding it back in |size|. |
| DCHECK(GetBoundsForClientView().size() == size); |
| DCHECK(GetWidget()->client_view()->size() == size); |
| |
| const int radius = bubble_border_->GetBorderCornerRadius(); |
| gfx::Insets content_insets = GetInsets(); |
| // If the client bounds don't touch the edges, no need to mask. |
| if (std::min({content_insets.top(), content_insets.left(), |
| content_insets.bottom(), content_insets.right()}) > radius) { |
| return false; |
| } |
| gfx::RectF rect((gfx::Rect(size))); |
| path->addRoundRect(gfx::RectFToSkRect(rect), radius, radius); |
| return true; |
| } |
| |
| int BubbleFrameView::NonClientHitTest(const gfx::Point& point) { |
| if (!bounds().Contains(point)) |
| return HTNOWHERE; |
| if (close_->visible() && close_->GetMirroredBounds().Contains(point)) |
| return HTCLOSE; |
| |
| // Allow dialogs to show the system menu and be dragged. |
| if (GetWidget()->widget_delegate()->AsDialogDelegate() && |
| !GetWidget()->widget_delegate()->AsBubbleDialogDelegate()) { |
| gfx::Rect bounds(GetContentsBounds()); |
| bounds.Inset(title_margins_); |
| gfx::Rect sys_rect(0, 0, bounds.x(), bounds.y()); |
| sys_rect.set_origin(gfx::Point(GetMirroredXForRect(sys_rect), 0)); |
| if (sys_rect.Contains(point)) |
| return HTSYSMENU; |
| if (point.y() < title()->bounds().bottom()) |
| return HTCAPTION; |
| } |
| |
| return GetWidget()->client_view()->NonClientHitTest(point); |
| } |
| |
| void BubbleFrameView::GetWindowMask(const gfx::Size& size, |
| gfx::Path* window_mask) { |
| if (bubble_border_->shadow() != BubbleBorder::SMALL_SHADOW && |
| bubble_border_->shadow() != BubbleBorder::NO_SHADOW_OPAQUE_BORDER && |
| bubble_border_->shadow() != BubbleBorder::NO_ASSETS) |
| return; |
| |
| // We don't return a mask for windows with arrows unless they use |
| // BubbleBorder::NO_ASSETS. |
| if (bubble_border_->shadow() != BubbleBorder::NO_ASSETS && |
| bubble_border_->arrow() != BubbleBorder::NONE && |
| bubble_border_->arrow() != BubbleBorder::FLOAT) |
| return; |
| |
| // Use a window mask roughly matching the border in the image assets. |
| const int kBorderStrokeSize = |
| bubble_border_->shadow() == BubbleBorder::NO_ASSETS ? 0 : 1; |
| const SkScalar kCornerRadius = |
| SkIntToScalar(bubble_border_->GetBorderCornerRadius()); |
| const gfx::Insets border_insets = bubble_border_->GetInsets(); |
| SkRect rect = { |
| SkIntToScalar(border_insets.left() - kBorderStrokeSize), |
| SkIntToScalar(border_insets.top() - kBorderStrokeSize), |
| SkIntToScalar(size.width() - border_insets.right() + kBorderStrokeSize), |
| SkIntToScalar(size.height() - border_insets.bottom() + |
| kBorderStrokeSize)}; |
| |
| if (bubble_border_->shadow() == BubbleBorder::NO_SHADOW_OPAQUE_BORDER || |
| bubble_border_->shadow() == BubbleBorder::NO_ASSETS) { |
| window_mask->addRoundRect(rect, kCornerRadius, kCornerRadius); |
| } else { |
| static const int kBottomBorderShadowSize = 2; |
| rect.fBottom += SkIntToScalar(kBottomBorderShadowSize); |
| window_mask->addRect(rect); |
| } |
| gfx::Path arrow_path; |
| if (bubble_border_->GetArrowPath(gfx::Rect(size), &arrow_path)) |
| window_mask->addPath(arrow_path, 0, 0); |
| } |
| |
| void BubbleFrameView::ResetWindowControls() { |
| close_->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton()); |
| } |
| |
| void BubbleFrameView::UpdateWindowIcon() { |
| gfx::ImageSkia image; |
| if (GetWidget()->widget_delegate()->ShouldShowWindowIcon()) |
| image = GetWidget()->widget_delegate()->GetWindowIcon(); |
| title_icon_->SetImage(&image); |
| } |
| |
| void BubbleFrameView::UpdateWindowTitle() { |
| if (default_title_) { |
| const WidgetDelegate* delegate = GetWidget()->widget_delegate(); |
| default_title_->SetVisible(delegate->ShouldShowWindowTitle() && |
| !delegate->GetWindowTitle().empty()); |
| default_title_->SetText(delegate->GetWindowTitle()); |
| } // custom_title_'s updates are handled by its creator. |
| Layout(); |
| } |
| |
| void BubbleFrameView::SizeConstraintsChanged() {} |
| |
| void BubbleFrameView::SetTitleView(std::unique_ptr<View> title_view) { |
| DCHECK(title_view); |
| delete default_title_; |
| default_title_ = nullptr; |
| delete custom_title_; |
| custom_title_ = title_view.get(); |
| // Keep the title after the icon for focus order. |
| AddChildViewAt(title_view.release(), 1); |
| } |
| |
| const char* BubbleFrameView::GetClassName() const { |
| return kViewClassName; |
| } |
| |
| gfx::Insets BubbleFrameView::GetInsets() const { |
| return GetClientInsetsForFrameWidth(GetContentsBounds().width()); |
| } |
| |
| gfx::Size BubbleFrameView::CalculatePreferredSize() const { |
| // Get the preferred size of the client area. |
| gfx::Size client_size = GetWidget()->client_view()->GetPreferredSize(); |
| // Expand it to include the bubble border and space for the arrow. |
| return GetWindowBoundsForClientBounds(gfx::Rect(client_size)).size(); |
| } |
| |
| gfx::Size BubbleFrameView::GetMinimumSize() const { |
| // Get the minimum size of the client area. |
| gfx::Size client_size = GetWidget()->client_view()->GetMinimumSize(); |
| // Expand it to include the bubble border and space for the arrow. |
| return GetWindowBoundsForClientBounds(gfx::Rect(client_size)).size(); |
| } |
| |
| gfx::Size BubbleFrameView::GetMaximumSize() const { |
| #if defined(OS_WIN) |
| // On Windows, this causes problems, so do not set a maximum size (it doesn't |
| // take the drop shadow area into account, resulting in a too-small window; |
| // see http://crbug.com/506206). This isn't necessary on Windows anyway, since |
| // the OS doesn't give the user controls to resize a bubble. |
| return gfx::Size(); |
| #else |
| #if defined(OS_MACOSX) |
| // Allow BubbleFrameView dialogs to be resizable on Mac. |
| if (GetWidget()->widget_delegate()->CanResize()) { |
| gfx::Size client_size = GetWidget()->client_view()->GetMaximumSize(); |
| if (client_size.IsEmpty()) |
| return client_size; |
| return GetWindowBoundsForClientBounds(gfx::Rect(client_size)).size(); |
| } |
| #endif // OS_MACOSX |
| // Non-dialog bubbles should be non-resizable, so its max size is its |
| // preferred size. |
| return GetPreferredSize(); |
| #endif |
| } |
| |
| void BubbleFrameView::Layout() { |
| // The title margins may not be set, but make sure that's only the case when |
| // there's no title. |
| DCHECK(!title_margins_.IsEmpty() || |
| (!custom_title_ && !default_title_->visible())); |
| |
| const gfx::Rect contents_bounds = GetContentsBounds(); |
| gfx::Rect bounds = contents_bounds; |
| bounds.Inset(title_margins_); |
| if (bounds.IsEmpty()) |
| return; |
| |
| int title_label_right = bounds.right(); |
| if (close_->visible()) { |
| // The close button is positioned somewhat closer to the edge of the bubble. |
| const int close_margin = |
| LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN); |
| close_->SetPosition( |
| gfx::Point(contents_bounds.right() - close_margin - close_->width(), |
| contents_bounds.y() + close_margin)); |
| title_label_right = std::min(title_label_right, close_->x() - close_margin); |
| } |
| |
| gfx::Size title_icon_pref_size(title_icon_->GetPreferredSize()); |
| const int title_icon_padding = |
| title_icon_pref_size.width() > 0 ? title_margins_.left() : 0; |
| const int title_label_x = |
| bounds.x() + title_icon_pref_size.width() + title_icon_padding; |
| |
| // TODO(tapted): Layout() should skip more surrounding code when !HasTitle(). |
| // Currently DCHECKs fail since title_insets is 0 when there is no title. |
| if (DCHECK_IS_ON() && HasTitle()) { |
| gfx::Insets title_insets = GetTitleLabelInsetsFromFrame(); |
| if (border()) |
| title_insets += border()->GetInsets(); |
| DCHECK_EQ(title_insets.left(), title_label_x); |
| DCHECK_EQ(title_insets.right(), width() - title_label_right); |
| } |
| |
| const int title_available_width = |
| std::max(1, title_label_right - title_label_x); |
| const int title_preferred_height = |
| title()->GetHeightForWidth(title_available_width); |
| const int title_height = |
| std::max(title_icon_pref_size.height(), title_preferred_height); |
| title()->SetBounds(title_label_x, |
| bounds.y() + (title_height - title_preferred_height) / 2, |
| title_available_width, title_preferred_height); |
| |
| title_icon_->SetBounds(bounds.x(), bounds.y(), title_icon_pref_size.width(), |
| title_height); |
| |
| // Only account for footnote_container_'s height if it's visible, because |
| // content_margins_ adds extra padding even if all child views are invisible. |
| if (footnote_container_ && footnote_container_->visible()) { |
| const int width = contents_bounds.width(); |
| const int height = footnote_container_->GetHeightForWidth(width); |
| footnote_container_->SetBounds( |
| contents_bounds.x(), contents_bounds.bottom() - height, width, height); |
| } |
| } |
| |
| void BubbleFrameView::OnThemeChanged() { |
| UpdateWindowTitle(); |
| ResetWindowControls(); |
| UpdateWindowIcon(); |
| } |
| |
| void BubbleFrameView::OnNativeThemeChanged(const ui::NativeTheme* theme) { |
| if (bubble_border_ && bubble_border_->use_theme_background_color()) { |
| bubble_border_->set_background_color(GetNativeTheme()-> |
| GetSystemColor(ui::NativeTheme::kColorId_DialogBackground)); |
| SchedulePaint(); |
| } |
| } |
| |
| void BubbleFrameView::ViewHierarchyChanged( |
| const ViewHierarchyChangedDetails& details) { |
| if (details.is_add && details.child == this) |
| OnThemeChanged(); |
| |
| if (!details.is_add && details.parent == footnote_container_ && |
| footnote_container_->child_count() == 1 && |
| details.child == footnote_container_->child_at(0)) { |
| // Setting the footnote_container_ to be hidden and null it. This will |
| // remove update the bubble to have no placeholder for the footnote and |
| // enable the destructor to delete the footnote_container_ later. |
| footnote_container_->SetVisible(false); |
| footnote_container_ = nullptr; |
| } |
| } |
| |
| void BubbleFrameView::OnPaint(gfx::Canvas* canvas) { |
| OnPaintBackground(canvas); |
| // Border comes after children. |
| } |
| |
| void BubbleFrameView::PaintChildren(const PaintInfo& paint_info) { |
| NonClientFrameView::PaintChildren(paint_info); |
| |
| ui::PaintCache paint_cache; |
| ui::PaintRecorder recorder( |
| paint_info.context(), paint_info.paint_recording_size(), |
| paint_info.paint_recording_scale_x(), |
| paint_info.paint_recording_scale_y(), &paint_cache); |
| OnPaintBorder(recorder.canvas()); |
| } |
| |
| void BubbleFrameView::ButtonPressed(Button* sender, const ui::Event& event) { |
| if (sender == close_) { |
| close_button_clicked_ = true; |
| GetWidget()->Close(); |
| } |
| } |
| |
| void BubbleFrameView::SetBubbleBorder(std::unique_ptr<BubbleBorder> border) { |
| bubble_border_ = border.get(); |
| SetBorder(std::move(border)); |
| |
| // Update the background, which relies on the border. |
| SetBackground(std::make_unique<views::BubbleBackground>(bubble_border_)); |
| } |
| |
| void BubbleFrameView::SetFootnoteView(View* view) { |
| if (!view) |
| return; |
| |
| DCHECK(!footnote_container_); |
| footnote_container_ = new FootnoteContainerView(); |
| footnote_container_->SetLayoutManager( |
| std::make_unique<BoxLayout>(BoxLayout::kVertical, footnote_margins_, 0)); |
| footnote_container_->SetBackground( |
| CreateSolidBackground(gfx::kGoogleGrey050)); |
| footnote_container_->SetBorder( |
| CreateSolidSidedBorder(1, 0, 0, 0, gfx::kGoogleGrey200)); |
| footnote_container_->AddChildView(view); |
| footnote_container_->SetVisible(view->visible()); |
| AddChildView(footnote_container_); |
| } |
| |
| gfx::Rect BubbleFrameView::GetUpdatedWindowBounds(const gfx::Rect& anchor_rect, |
| const gfx::Size& client_size, |
| bool adjust_if_offscreen) { |
| gfx::Size size(GetFrameSizeForClientSize(client_size)); |
| |
| const BubbleBorder::Arrow arrow = bubble_border_->arrow(); |
| if (adjust_if_offscreen && BubbleBorder::has_arrow(arrow)) { |
| // Try to mirror the anchoring if the bubble does not fit on the screen. |
| if (!bubble_border_->is_arrow_at_center(arrow)) { |
| MirrorArrowIfOffScreen(true, anchor_rect, size); |
| MirrorArrowIfOffScreen(false, anchor_rect, size); |
| } else { |
| const bool mirror_vertical = BubbleBorder::is_arrow_on_horizontal(arrow); |
| MirrorArrowIfOffScreen(mirror_vertical, anchor_rect, size); |
| OffsetArrowIfOffScreen(anchor_rect, size); |
| } |
| } |
| |
| // Calculate the bounds with the arrow in its updated location and offset. |
| return bubble_border_->GetBounds(anchor_rect, size); |
| } |
| |
| gfx::Rect BubbleFrameView::GetAvailableScreenBounds( |
| const gfx::Rect& rect) const { |
| // The bubble attempts to fit within the current screen bounds. |
| return display::Screen::GetScreen() |
| ->GetDisplayNearestPoint(rect.CenterPoint()) |
| .work_area(); |
| } |
| |
| bool BubbleFrameView::ExtendClientIntoTitle() const { |
| return false; |
| } |
| |
| bool BubbleFrameView::IsCloseButtonVisible() const { |
| return close_->visible(); |
| } |
| |
| gfx::Rect BubbleFrameView::GetCloseButtonMirroredBounds() const { |
| return close_->GetMirroredBounds(); |
| } |
| |
| void BubbleFrameView::MirrorArrowIfOffScreen( |
| bool vertical, |
| const gfx::Rect& anchor_rect, |
| const gfx::Size& client_size) { |
| // Check if the bounds don't fit on screen. |
| gfx::Rect available_bounds(GetAvailableScreenBounds(anchor_rect)); |
| gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size)); |
| if (GetOffScreenLength(available_bounds, window_bounds, vertical) > 0) { |
| BubbleBorder::Arrow arrow = bubble_border()->arrow(); |
| // Mirror the arrow and get the new bounds. |
| bubble_border_->set_arrow( |
| vertical ? BubbleBorder::vertical_mirror(arrow) : |
| BubbleBorder::horizontal_mirror(arrow)); |
| gfx::Rect mirror_bounds = |
| bubble_border_->GetBounds(anchor_rect, client_size); |
| // Restore the original arrow if mirroring doesn't show more of the bubble. |
| // Otherwise it should invoke parent's Layout() to layout the content based |
| // on the new bubble border. |
| if (GetOffScreenLength(available_bounds, mirror_bounds, vertical) >= |
| GetOffScreenLength(available_bounds, window_bounds, vertical)) { |
| bubble_border_->set_arrow(arrow); |
| } else { |
| if (parent()) |
| parent()->Layout(); |
| SchedulePaint(); |
| } |
| } |
| } |
| |
| void BubbleFrameView::OffsetArrowIfOffScreen(const gfx::Rect& anchor_rect, |
| const gfx::Size& client_size) { |
| BubbleBorder::Arrow arrow = bubble_border()->arrow(); |
| DCHECK(BubbleBorder::is_arrow_at_center(arrow)); |
| |
| // Get the desired bubble bounds without adjustment. |
| bubble_border_->set_arrow_offset(0); |
| gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size)); |
| |
| gfx::Rect available_bounds(GetAvailableScreenBounds(anchor_rect)); |
| if (available_bounds.IsEmpty() || available_bounds.Contains(window_bounds)) |
| return; |
| |
| // Calculate off-screen adjustment. |
| const bool is_horizontal = BubbleBorder::is_arrow_on_horizontal(arrow); |
| int offscreen_adjust = 0; |
| if (is_horizontal) { |
| if (window_bounds.x() < available_bounds.x()) |
| offscreen_adjust = available_bounds.x() - window_bounds.x(); |
| else if (window_bounds.right() > available_bounds.right()) |
| offscreen_adjust = available_bounds.right() - window_bounds.right(); |
| } else { |
| if (window_bounds.y() < available_bounds.y()) |
| offscreen_adjust = available_bounds.y() - window_bounds.y(); |
| else if (window_bounds.bottom() > available_bounds.bottom()) |
| offscreen_adjust = available_bounds.bottom() - window_bounds.bottom(); |
| } |
| |
| // For center arrows, arrows are moved in the opposite direction of |
| // |offscreen_adjust|, e.g. positive |offscreen_adjust| means bubble |
| // window needs to be moved to the right and that means we need to move arrow |
| // to the left, and that means negative offset. |
| bubble_border_->set_arrow_offset( |
| bubble_border_->GetArrowOffset(window_bounds.size()) - offscreen_adjust); |
| if (offscreen_adjust) |
| SchedulePaint(); |
| } |
| |
| int BubbleFrameView::GetFrameWidthForClientWidth(int client_width) const { |
| // Note that GetMinimumSize() for multiline Labels is typically 0. |
| const int title_bar_width = title()->GetMinimumSize().width() + |
| GetTitleLabelInsetsFromFrame().width(); |
| const int client_area_width = client_width + content_margins_.width(); |
| const int frame_width = std::max(title_bar_width, client_area_width); |
| DialogDelegate* dialog_delegate = |
| GetWidget()->widget_delegate()->AsDialogDelegate(); |
| return dialog_delegate && dialog_delegate->ShouldSnapFrameWidth() |
| ? LayoutProvider::Get()->GetSnappedDialogWidth(frame_width) |
| : frame_width; |
| } |
| |
| gfx::Size BubbleFrameView::GetFrameSizeForClientSize( |
| const gfx::Size& client_size) const { |
| const int frame_width = GetFrameWidthForClientWidth(client_size.width()); |
| const gfx::Insets client_insets = GetClientInsetsForFrameWidth(frame_width); |
| DCHECK_GE(frame_width, client_size.width()); |
| gfx::Size size(frame_width, client_size.height() + client_insets.height()); |
| |
| // Only account for footnote_container_'s height if it's visible, because |
| // content_margins_ adds extra padding even if all child views are invisible. |
| if (footnote_container_ && footnote_container_->visible()) |
| size.Enlarge(0, footnote_container_->GetHeightForWidth(size.width())); |
| |
| return size; |
| } |
| |
| bool BubbleFrameView::HasTitle() const { |
| return (custom_title_ != nullptr && |
| GetWidget()->widget_delegate()->ShouldShowWindowTitle()) || |
| (default_title_ != nullptr && |
| default_title_->GetPreferredSize().height() > 0) || |
| title_icon_->GetPreferredSize().height() > 0; |
| } |
| |
| gfx::Insets BubbleFrameView::GetTitleLabelInsetsFromFrame() const { |
| int insets_right = 0; |
| if (GetWidget()->widget_delegate()->ShouldShowCloseButton()) { |
| const int close_margin = |
| LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN); |
| insets_right = 2 * close_margin + close_->width(); |
| } |
| if (!HasTitle()) |
| return gfx::Insets(0, 0, 0, insets_right); |
| |
| insets_right = std::max(insets_right, title_margins_.right()); |
| const gfx::Size title_icon_pref_size = title_icon_->GetPreferredSize(); |
| const int title_icon_padding = |
| title_icon_pref_size.width() > 0 ? title_margins_.left() : 0; |
| const int insets_left = |
| title_margins_.left() + title_icon_pref_size.width() + title_icon_padding; |
| return gfx::Insets(title_margins_.top(), insets_left, title_margins_.bottom(), |
| insets_right); |
| } |
| |
| gfx::Insets BubbleFrameView::GetClientInsetsForFrameWidth( |
| int frame_width) const { |
| int close_height = 0; |
| if (!ExtendClientIntoTitle() && |
| GetWidget()->widget_delegate()->ShouldShowCloseButton()) { |
| const int close_margin = |
| LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN); |
| // Note: |close_margin| is not applied on the bottom of the icon. |
| close_height = close_margin + close_->height(); |
| } |
| if (!HasTitle()) |
| return content_margins_ + gfx::Insets(close_height, 0, 0, 0); |
| |
| const int icon_height = title_icon_->GetPreferredSize().height(); |
| const int label_height = title()->GetHeightForWidth( |
| frame_width - GetTitleLabelInsetsFromFrame().width()); |
| const int title_height = |
| std::max(icon_height, label_height) + title_margins_.height(); |
| return content_margins_ + |
| gfx::Insets(std::max(title_height, close_height), 0, 0, 0); |
| } |
| |
| } // namespace views |