| // Copyright 2013 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 "ash/wm/caption_buttons/maximize_bubble_controller.h" |
| |
| #include "ash/shell.h" |
| #include "ash/shell_delegate.h" |
| #include "ash/shell_window_ids.h" |
| #include "ash/wm/caption_buttons/frame_maximize_button.h" |
| #include "ash/wm/window_animations.h" |
| #include "base/timer/timer.h" |
| #include "grit/ash_resources.h" |
| #include "grit/ash_strings.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/animation/animation.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/path.h" |
| #include "ui/gfx/screen.h" |
| #include "ui/views/bubble/bubble_delegate.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/mouse_watcher.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace { |
| |
| // The spacing between two buttons. |
| const int kLayoutSpacing = -1; |
| |
| // The background color. |
| const SkColor kBubbleBackgroundColor = 0xFF141414; |
| |
| // The text color within the bubble. |
| const SkColor kBubbleTextColor = SK_ColorWHITE; |
| |
| // The line width of the bubble. |
| const int kLineWidth = 1; |
| |
| // The spacing for the top and bottom of the info label. |
| const int kLabelSpacing = 4; |
| |
| // The pixel dimensions of the arrow. |
| const int kArrowHeight = 10; |
| const int kArrowWidth = 20; |
| |
| // The animation offset in y for the bubble when appearing. |
| const int kBubbleAnimationOffsetY = 5; |
| |
| class MaximizeBubbleBorder : public views::BubbleBorder { |
| public: |
| MaximizeBubbleBorder(views::View* content_view, views::View* anchor); |
| |
| virtual ~MaximizeBubbleBorder() {} |
| |
| // Get the mouse active area of the window. |
| void GetMask(gfx::Path* mask); |
| |
| // Overridden from views::BubbleBorder to match the design specs. |
| virtual gfx::Rect GetBounds(const gfx::Rect& position_relative_to, |
| const gfx::Size& contents_size) const OVERRIDE; |
| |
| // Overridden from views::Border. |
| virtual void Paint(const views::View& view, gfx::Canvas* canvas) OVERRIDE; |
| |
| private: |
| // Note: Animations can continue after then main window frame was destroyed. |
| // To avoid this problem, the owning screen metrics get extracted upon |
| // creation. |
| gfx::Size anchor_size_; |
| gfx::Point anchor_screen_origin_; |
| views::View* content_view_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MaximizeBubbleBorder); |
| }; |
| |
| MaximizeBubbleBorder::MaximizeBubbleBorder(views::View* content_view, |
| views::View* anchor) |
| : views::BubbleBorder(views::BubbleBorder::TOP_RIGHT, |
| views::BubbleBorder::NO_SHADOW, |
| kBubbleBackgroundColor), |
| anchor_size_(anchor->size()), |
| anchor_screen_origin_(0, 0), |
| content_view_(content_view) { |
| views::View::ConvertPointToScreen(anchor, &anchor_screen_origin_); |
| set_alignment(views::BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE); |
| } |
| |
| void MaximizeBubbleBorder::GetMask(gfx::Path* mask) { |
| gfx::Insets inset = GetInsets(); |
| // Note: Even though the tip could be added as activatable, it is left out |
| // since it would not change the action behavior in any way plus it makes |
| // more sense to keep the focus on the underlying button for clicks. |
| int left = inset.left() - kLineWidth; |
| int right = inset.left() + content_view_->width() + kLineWidth; |
| int top = inset.top() - kLineWidth; |
| int bottom = inset.top() + content_view_->height() + kLineWidth; |
| mask->moveTo(left, top); |
| mask->lineTo(right, top); |
| mask->lineTo(right, bottom); |
| mask->lineTo(left, bottom); |
| mask->lineTo(left, top); |
| mask->close(); |
| } |
| |
| gfx::Rect MaximizeBubbleBorder::GetBounds( |
| const gfx::Rect& position_relative_to, |
| const gfx::Size& contents_size) const { |
| gfx::Size border_size(contents_size); |
| gfx::Insets insets = GetInsets(); |
| border_size.Enlarge(insets.width(), insets.height()); |
| |
| // Position the bubble to center the box on the anchor. |
| int x = (-border_size.width() + anchor_size_.width()) / 2; |
| // Position the bubble under the anchor, overlapping the arrow with it. |
| int y = anchor_size_.height() - insets.top(); |
| |
| gfx::Point view_origin(x + anchor_screen_origin_.x(), |
| y + anchor_screen_origin_.y()); |
| |
| return gfx::Rect(view_origin, border_size); |
| } |
| |
| void MaximizeBubbleBorder::Paint(const views::View& view, gfx::Canvas* canvas) { |
| gfx::Insets inset = GetInsets(); |
| |
| // Draw the border line around everything. |
| int y = inset.top(); |
| // Top |
| canvas->FillRect(gfx::Rect(inset.left(), |
| y - kLineWidth, |
| content_view_->width(), |
| kLineWidth), |
| kBubbleBackgroundColor); |
| // Bottom |
| canvas->FillRect(gfx::Rect(inset.left(), |
| y + content_view_->height(), |
| content_view_->width(), |
| kLineWidth), |
| kBubbleBackgroundColor); |
| // Left |
| canvas->FillRect(gfx::Rect(inset.left() - kLineWidth, |
| y - kLineWidth, |
| kLineWidth, |
| content_view_->height() + 2 * kLineWidth), |
| kBubbleBackgroundColor); |
| // Right |
| canvas->FillRect(gfx::Rect(inset.left() + content_view_->width(), |
| y - kLineWidth, |
| kLineWidth, |
| content_view_->height() + 2 * kLineWidth), |
| kBubbleBackgroundColor); |
| |
| // Draw the arrow afterwards covering the border. |
| SkPath path; |
| path.incReserve(4); |
| // The center of the tip should be in the middle of the button. |
| int tip_x = inset.left() + content_view_->width() / 2; |
| int left_base_x = tip_x - kArrowWidth / 2; |
| int left_base_y = y; |
| int tip_y = left_base_y - kArrowHeight; |
| path.moveTo(SkIntToScalar(left_base_x), SkIntToScalar(left_base_y)); |
| path.lineTo(SkIntToScalar(tip_x), SkIntToScalar(tip_y)); |
| path.lineTo(SkIntToScalar(left_base_x + kArrowWidth), |
| SkIntToScalar(left_base_y)); |
| |
| SkPaint paint; |
| paint.setStyle(SkPaint::kFill_Style); |
| paint.setColor(kBubbleBackgroundColor); |
| canvas->DrawPath(path, paint); |
| } |
| |
| } // namespace |
| |
| namespace ash { |
| |
| class BubbleContentsButtonRow; |
| class BubbleContentsView; |
| class BubbleDialogButton; |
| |
| // The mouse watcher host which makes sure that the bubble does not get closed |
| // while the mouse cursor is over the maximize button or the balloon content. |
| // Note: This object gets destroyed when the MouseWatcher gets destroyed. |
| class BubbleMouseWatcherHost: public views::MouseWatcherHost { |
| public: |
| explicit BubbleMouseWatcherHost(MaximizeBubbleController::Bubble* bubble) |
| : bubble_(bubble) {} |
| virtual ~BubbleMouseWatcherHost() {} |
| |
| // Implementation of MouseWatcherHost. |
| virtual bool Contains(const gfx::Point& screen_point, |
| views::MouseWatcherHost::MouseEventType type) OVERRIDE; |
| private: |
| MaximizeBubbleController::Bubble* bubble_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BubbleMouseWatcherHost); |
| }; |
| |
| // The class which creates and manages the bubble menu element. |
| // It creates a 'bubble border' and the content accordingly. |
| // Note: Since the SnapSizer will show animations on top of the maximize button |
| // this menu gets created as a separate window and the SnapSizer will be |
| // created underneath this window. |
| class MaximizeBubbleController::Bubble : public views::BubbleDelegateView, |
| public views::MouseWatcherListener { |
| public: |
| Bubble(MaximizeBubbleController* owner, |
| int appearance_delay_ms, |
| SnapType initial_snap_type); |
| virtual ~Bubble() {} |
| |
| // The window of the menu under which the SnapSizer will get created. |
| aura::Window* GetBubbleWindow(); |
| |
| // Overridden from views::BubbleDelegateView. |
| virtual gfx::Rect GetAnchorRect() OVERRIDE; |
| virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE; |
| virtual bool CanActivate() const OVERRIDE { return false; } |
| |
| // Overridden from views::WidgetDelegateView. |
| virtual bool WidgetHasHitTestMask() const OVERRIDE; |
| virtual void GetWidgetHitTestMask(gfx::Path* mask) const OVERRIDE; |
| |
| // Implementation of MouseWatcherListener. |
| virtual void MouseMovedOutOfHost() OVERRIDE; |
| |
| // Implementation of MouseWatcherHost. |
| virtual bool Contains(const gfx::Point& screen_point, |
| views::MouseWatcherHost::MouseEventType type); |
| |
| // Overridden from views::View. |
| virtual gfx::Size GetPreferredSize() OVERRIDE; |
| |
| // Overridden from views::Widget::Observer. |
| virtual void OnWidgetDestroying(views::Widget* widget) OVERRIDE; |
| |
| // Called from the controller class to indicate that the menu should get |
| // destroyed. |
| virtual void ControllerRequestsCloseAndDelete(); |
| |
| // Called from the owning class to change the menu content to the given |
| // |snap_type| so that the user knows what is selected. |
| void SetSnapType(SnapType snap_type); |
| |
| // Get the owning MaximizeBubbleController. This might return NULL in case |
| // of an asynchronous shutdown. |
| MaximizeBubbleController* controller() const { return owner_; } |
| |
| // Added for unit test: Retrieve the button for an action. |
| // |state| can be either SNAP_LEFT, SNAP_RIGHT or SNAP_MINIMIZE. |
| views::CustomButton* GetButtonForUnitTest(SnapType state); |
| |
| private: |
| // True if the shut down has been initiated. |
| bool shutting_down_; |
| |
| // Our owning class. |
| MaximizeBubbleController* owner_; |
| |
| // The widget which contains our menu and the bubble border. |
| views::Widget* bubble_widget_; |
| |
| // The content accessor of the menu. |
| BubbleContentsView* contents_view_; |
| |
| // The bubble border. |
| MaximizeBubbleBorder* bubble_border_; |
| |
| // The rectangle before the animation starts. |
| gfx::Rect initial_position_; |
| |
| // The mouse watcher which takes care of out of window hover events. |
| scoped_ptr<views::MouseWatcher> mouse_watcher_; |
| |
| // The fade delay - if 0 it will show / hide immediately. |
| const int appearance_delay_ms_; |
| |
| DISALLOW_COPY_AND_ASSIGN(Bubble); |
| }; |
| |
| // A class that creates all buttons and put them into a view. |
| class BubbleContentsButtonRow : public views::View, |
| public views::ButtonListener { |
| public: |
| explicit BubbleContentsButtonRow(MaximizeBubbleController::Bubble* bubble); |
| |
| virtual ~BubbleContentsButtonRow() {} |
| |
| // Overridden from ButtonListener. |
| virtual void ButtonPressed(views::Button* sender, |
| const ui::Event& event) OVERRIDE; |
| // Called from BubbleDialogButton. |
| void ButtonHovered(BubbleDialogButton* sender); |
| |
| // Added for unit test: Retrieve the button for an action. |
| // |state| can be either SNAP_LEFT, SNAP_RIGHT or SNAP_MINIMIZE. |
| views::CustomButton* GetButtonForUnitTest(SnapType state); |
| |
| MaximizeBubbleController::Bubble* bubble() { return bubble_; } |
| |
| private: |
| // Functions to add the left and right maximize / restore buttons. |
| void AddMaximizeLeftButton(); |
| void AddMaximizeRightButton(); |
| void AddMinimizeButton(); |
| |
| // The owning object which gets notifications. |
| MaximizeBubbleController::Bubble* bubble_; |
| |
| // The created buttons for our menu. |
| BubbleDialogButton* left_button_; |
| BubbleDialogButton* minimize_button_; |
| BubbleDialogButton* right_button_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BubbleContentsButtonRow); |
| }; |
| |
| // A class which creates the content of the bubble: The buttons, and the label. |
| class BubbleContentsView : public views::View { |
| public: |
| BubbleContentsView(MaximizeBubbleController::Bubble* bubble, |
| SnapType initial_snap_type); |
| |
| virtual ~BubbleContentsView() {} |
| |
| // Set the label content to reflect the currently selected |snap_type|. |
| // This function can be executed through the frame maximize button as well as |
| // through hover operations. |
| void SetSnapType(SnapType snap_type); |
| |
| // Added for unit test: Retrieve the button for an action. |
| // |state| can be either SNAP_LEFT, SNAP_RIGHT or SNAP_MINIMIZE. |
| views::CustomButton* GetButtonForUnitTest(SnapType state) { |
| return buttons_view_->GetButtonForUnitTest(state); |
| } |
| |
| private: |
| // The owning class. |
| MaximizeBubbleController::Bubble* bubble_; |
| |
| // The object which owns all the buttons. |
| BubbleContentsButtonRow* buttons_view_; |
| |
| // The label object which shows the user the selected action. |
| views::Label* label_view_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BubbleContentsView); |
| }; |
| |
| // The image button gets overridden to be able to capture mouse hover events. |
| // The constructor also assigns all button states and |
| class BubbleDialogButton : public views::ImageButton { |
| public: |
| explicit BubbleDialogButton( |
| BubbleContentsButtonRow* button_row_listener, |
| int normal_image, |
| int hovered_image, |
| int pressed_image); |
| virtual ~BubbleDialogButton() {} |
| |
| // CustomButton overrides: |
| virtual void OnMouseCaptureLost() OVERRIDE; |
| virtual void OnMouseEntered(const ui::MouseEvent& event) OVERRIDE; |
| virtual void OnMouseExited(const ui::MouseEvent& event) OVERRIDE; |
| virtual bool OnMouseDragged(const ui::MouseEvent& event) OVERRIDE; |
| |
| private: |
| // The creating class which needs to get notified in case of a hover event. |
| BubbleContentsButtonRow* button_row_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BubbleDialogButton); |
| }; |
| |
| MaximizeBubbleController::Bubble::Bubble( |
| MaximizeBubbleController* owner, |
| int appearance_delay_ms, |
| SnapType initial_snap_type) |
| : views::BubbleDelegateView(owner->frame_maximize_button(), |
| views::BubbleBorder::TOP_RIGHT), |
| shutting_down_(false), |
| owner_(owner), |
| bubble_widget_(NULL), |
| contents_view_(NULL), |
| bubble_border_(NULL), |
| appearance_delay_ms_(appearance_delay_ms) { |
| set_margins(gfx::Insets()); |
| |
| // The window needs to be owned by the root so that the SnapSizer does not |
| // cover it upon animation. |
| aura::Window* parent = Shell::GetContainer( |
| Shell::GetTargetRootWindow(), |
| internal::kShellWindowId_ShelfContainer); |
| set_parent_window(parent); |
| |
| set_notify_enter_exit_on_child(true); |
| set_adjust_if_offscreen(false); |
| SetPaintToLayer(true); |
| set_color(kBubbleBackgroundColor); |
| set_close_on_deactivate(false); |
| set_background( |
| views::Background::CreateSolidBackground(kBubbleBackgroundColor)); |
| |
| SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kVertical, 0, 0, kLayoutSpacing)); |
| |
| contents_view_ = new BubbleContentsView(this, initial_snap_type); |
| AddChildView(contents_view_); |
| |
| // Note that the returned widget has an observer which points to our |
| // functions. |
| bubble_widget_ = views::BubbleDelegateView::CreateBubble(this); |
| bubble_widget_->set_focus_on_creation(false); |
| |
| SetAlignment(views::BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE); |
| bubble_widget_->non_client_view()->frame_view()->set_background(NULL); |
| |
| bubble_border_ = new MaximizeBubbleBorder(this, GetAnchorView()); |
| GetBubbleFrameView()->SetBubbleBorder(bubble_border_); |
| GetBubbleFrameView()->set_background(NULL); |
| |
| // Recalculate size with new border. |
| SizeToContents(); |
| |
| if (!appearance_delay_ms_) |
| GetWidget()->Show(); |
| else |
| StartFade(true); |
| |
| ash::Shell::GetInstance()->delegate()->RecordUserMetricsAction( |
| ash::UMA_WINDOW_MAXIMIZE_BUTTON_SHOW_BUBBLE); |
| |
| mouse_watcher_.reset(new views::MouseWatcher( |
| new BubbleMouseWatcherHost(this), |
| this)); |
| mouse_watcher_->Start(); |
| } |
| |
| bool BubbleMouseWatcherHost::Contains( |
| const gfx::Point& screen_point, |
| views::MouseWatcherHost::MouseEventType type) { |
| return bubble_->Contains(screen_point, type); |
| } |
| |
| aura::Window* MaximizeBubbleController::Bubble::GetBubbleWindow() { |
| return bubble_widget_ ? bubble_widget_->GetNativeWindow() : NULL; |
| } |
| |
| gfx::Rect MaximizeBubbleController::Bubble::GetAnchorRect() { |
| if (!owner_) |
| return gfx::Rect(); |
| |
| gfx::Rect anchor_rect = |
| owner_->frame_maximize_button()->GetBoundsInScreen(); |
| return anchor_rect; |
| } |
| |
| void MaximizeBubbleController::Bubble::AnimationProgressed( |
| const gfx::Animation* animation) { |
| // First do everything needed for the fade by calling the base function. |
| BubbleDelegateView::AnimationProgressed(animation); |
| // When fading in we are done. |
| if (!shutting_down_) |
| return; |
| // Upon fade out an additional shift is required. |
| int shift = animation->CurrentValueBetween(kBubbleAnimationOffsetY, 0); |
| gfx::Rect rect = initial_position_; |
| |
| rect.set_y(rect.y() + shift); |
| bubble_widget_->GetNativeWindow()->SetBounds(rect); |
| } |
| |
| bool MaximizeBubbleController::Bubble::WidgetHasHitTestMask() const { |
| return bubble_border_ != NULL; |
| } |
| |
| void MaximizeBubbleController::Bubble::GetWidgetHitTestMask( |
| gfx::Path* mask) const { |
| DCHECK(mask); |
| DCHECK(bubble_border_); |
| bubble_border_->GetMask(mask); |
| } |
| |
| void MaximizeBubbleController::Bubble::MouseMovedOutOfHost() { |
| if (!owner_ || shutting_down_) |
| return; |
| // When we leave the bubble, we might be still be in gesture mode or over |
| // the maximize button. So only close if none of the other cases apply. |
| if (!owner_->frame_maximize_button()->is_snap_enabled()) { |
| gfx::Point screen_location = Shell::GetScreen()->GetCursorScreenPoint(); |
| if (!owner_->frame_maximize_button()->GetBoundsInScreen().Contains( |
| screen_location)) { |
| owner_->RequestDestructionThroughOwner(); |
| } |
| } |
| } |
| |
| bool MaximizeBubbleController::Bubble::Contains( |
| const gfx::Point& screen_point, |
| views::MouseWatcherHost::MouseEventType type) { |
| if (!owner_ || shutting_down_) |
| return false; |
| bool inside_button = |
| owner_->frame_maximize_button()->GetBoundsInScreen().Contains( |
| screen_point); |
| if (!owner_->frame_maximize_button()->is_snap_enabled() && inside_button) { |
| SetSnapType(controller()->maximize_type() == FRAME_STATE_FULL ? |
| SNAP_RESTORE : SNAP_MAXIMIZE); |
| return true; |
| } |
| // Check if either a gesture is taking place (=> bubble stays no matter what |
| // the mouse does) or the mouse is over the maximize button or the bubble |
| // content. |
| return (owner_->frame_maximize_button()->is_snap_enabled() || |
| inside_button || |
| contents_view_->GetBoundsInScreen().Contains(screen_point)); |
| } |
| |
| gfx::Size MaximizeBubbleController::Bubble::GetPreferredSize() { |
| return contents_view_->GetPreferredSize(); |
| } |
| |
| void MaximizeBubbleController::Bubble::OnWidgetDestroying( |
| views::Widget* widget) { |
| if (bubble_widget_ == widget) { |
| mouse_watcher_->Stop(); |
| |
| if (owner_) { |
| // If the bubble destruction was triggered by some other external |
| // influence then ourselves, the owner needs to be informed that the menu |
| // is gone. |
| shutting_down_ = true; |
| owner_->RequestDestructionThroughOwner(); |
| owner_ = NULL; |
| } |
| } |
| BubbleDelegateView::OnWidgetDestroying(widget); |
| } |
| |
| void MaximizeBubbleController::Bubble::ControllerRequestsCloseAndDelete() { |
| // This only gets called from the owning base class once it is deleted. |
| if (shutting_down_) |
| return; |
| shutting_down_ = true; |
| owner_ = NULL; |
| |
| // Close the widget asynchronously after the hide animation is finished. |
| initial_position_ = bubble_widget_->GetNativeWindow()->bounds(); |
| if (!appearance_delay_ms_) |
| bubble_widget_->CloseNow(); |
| else |
| StartFade(false); |
| } |
| |
| void MaximizeBubbleController::Bubble::SetSnapType(SnapType snap_type) { |
| if (contents_view_) |
| contents_view_->SetSnapType(snap_type); |
| } |
| |
| views::CustomButton* MaximizeBubbleController::Bubble::GetButtonForUnitTest( |
| SnapType state) { |
| return contents_view_->GetButtonForUnitTest(state); |
| } |
| |
| BubbleContentsButtonRow::BubbleContentsButtonRow( |
| MaximizeBubbleController::Bubble* bubble) |
| : bubble_(bubble), |
| left_button_(NULL), |
| minimize_button_(NULL), |
| right_button_(NULL) { |
| SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kHorizontal, 0, 0, kLayoutSpacing)); |
| set_background( |
| views::Background::CreateSolidBackground(kBubbleBackgroundColor)); |
| |
| if (base::i18n::IsRTL()) { |
| AddMaximizeRightButton(); |
| AddMinimizeButton(); |
| AddMaximizeLeftButton(); |
| } else { |
| AddMaximizeLeftButton(); |
| AddMinimizeButton(); |
| AddMaximizeRightButton(); |
| } |
| } |
| |
| // Overridden from ButtonListener. |
| void BubbleContentsButtonRow::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| // While shutting down, the connection to the owner might already be broken. |
| if (!bubble_->controller()) |
| return; |
| if (sender == left_button_) |
| bubble_->controller()->OnButtonClicked( |
| bubble_->controller()->maximize_type() == FRAME_STATE_SNAP_LEFT ? |
| SNAP_RESTORE : SNAP_LEFT); |
| else if (sender == minimize_button_) |
| bubble_->controller()->OnButtonClicked(SNAP_MINIMIZE); |
| else if (sender == right_button_) |
| bubble_->controller()->OnButtonClicked( |
| bubble_->controller()->maximize_type() == FRAME_STATE_SNAP_RIGHT ? |
| SNAP_RESTORE : SNAP_RIGHT); |
| else |
| NOTREACHED() << "Unknown button pressed."; |
| } |
| |
| // Called from BubbleDialogButton. |
| void BubbleContentsButtonRow::ButtonHovered(BubbleDialogButton* sender) { |
| // While shutting down, the connection to the owner might already be broken. |
| if (!bubble_->controller()) |
| return; |
| if (sender == left_button_) |
| bubble_->controller()->OnButtonHover( |
| bubble_->controller()->maximize_type() == FRAME_STATE_SNAP_LEFT ? |
| SNAP_RESTORE : SNAP_LEFT); |
| else if (sender == minimize_button_) |
| bubble_->controller()->OnButtonHover(SNAP_MINIMIZE); |
| else if (sender == right_button_) |
| bubble_->controller()->OnButtonHover( |
| bubble_->controller()->maximize_type() == FRAME_STATE_SNAP_RIGHT ? |
| SNAP_RESTORE : SNAP_RIGHT); |
| else |
| bubble_->controller()->OnButtonHover(SNAP_NONE); |
| } |
| |
| views::CustomButton* BubbleContentsButtonRow::GetButtonForUnitTest( |
| SnapType state) { |
| switch (state) { |
| case SNAP_LEFT: |
| return left_button_; |
| case SNAP_MINIMIZE: |
| return minimize_button_; |
| case SNAP_RIGHT: |
| return right_button_; |
| default: |
| NOTREACHED(); |
| return NULL; |
| } |
| } |
| |
| void BubbleContentsButtonRow::AddMaximizeLeftButton() { |
| if (bubble_->controller()->maximize_type() == FRAME_STATE_SNAP_LEFT) { |
| left_button_ = new BubbleDialogButton( |
| this, |
| IDR_AURA_WINDOW_POSITION_LEFT_RESTORE, |
| IDR_AURA_WINDOW_POSITION_LEFT_RESTORE_H, |
| IDR_AURA_WINDOW_POSITION_LEFT_RESTORE_P); |
| } else { |
| left_button_ = new BubbleDialogButton( |
| this, |
| IDR_AURA_WINDOW_POSITION_LEFT, |
| IDR_AURA_WINDOW_POSITION_LEFT_H, |
| IDR_AURA_WINDOW_POSITION_LEFT_P); |
| } |
| } |
| |
| void BubbleContentsButtonRow::AddMaximizeRightButton() { |
| if (bubble_->controller()->maximize_type() == FRAME_STATE_SNAP_RIGHT) { |
| right_button_ = new BubbleDialogButton( |
| this, |
| IDR_AURA_WINDOW_POSITION_RIGHT_RESTORE, |
| IDR_AURA_WINDOW_POSITION_RIGHT_RESTORE_H, |
| IDR_AURA_WINDOW_POSITION_RIGHT_RESTORE_P); |
| } else { |
| right_button_ = new BubbleDialogButton( |
| this, |
| IDR_AURA_WINDOW_POSITION_RIGHT, |
| IDR_AURA_WINDOW_POSITION_RIGHT_H, |
| IDR_AURA_WINDOW_POSITION_RIGHT_P); |
| } |
| } |
| |
| void BubbleContentsButtonRow::AddMinimizeButton() { |
| minimize_button_ = new BubbleDialogButton( |
| this, |
| IDR_AURA_WINDOW_POSITION_MIDDLE, |
| IDR_AURA_WINDOW_POSITION_MIDDLE_H, |
| IDR_AURA_WINDOW_POSITION_MIDDLE_P); |
| } |
| |
| BubbleContentsView::BubbleContentsView( |
| MaximizeBubbleController::Bubble* bubble, |
| SnapType initial_snap_type) |
| : bubble_(bubble), |
| buttons_view_(NULL), |
| label_view_(NULL) { |
| SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kVertical, 0, 0, kLayoutSpacing)); |
| set_background( |
| views::Background::CreateSolidBackground(kBubbleBackgroundColor)); |
| |
| buttons_view_ = new BubbleContentsButtonRow(bubble); |
| AddChildView(buttons_view_); |
| |
| label_view_ = new views::Label(); |
| SetSnapType(initial_snap_type); |
| label_view_->SetBackgroundColor(kBubbleBackgroundColor); |
| label_view_->SetEnabledColor(kBubbleTextColor); |
| label_view_->set_border(views::Border::CreateEmptyBorder( |
| kLabelSpacing, 0, kLabelSpacing, 0)); |
| AddChildView(label_view_); |
| } |
| |
| // Set the label content to reflect the currently selected |snap_type|. |
| // This function can be executed through the frame maximize button as well as |
| // through hover operations. |
| void BubbleContentsView::SetSnapType(SnapType snap_type) { |
| if (!bubble_->controller()) |
| return; |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| int id = 0; |
| switch (snap_type) { |
| case SNAP_LEFT: |
| id = IDS_ASH_SNAP_WINDOW_LEFT; |
| break; |
| case SNAP_RIGHT: |
| id = IDS_ASH_SNAP_WINDOW_RIGHT; |
| break; |
| case SNAP_MAXIMIZE: |
| DCHECK_NE(FRAME_STATE_FULL, bubble_->controller()->maximize_type()); |
| id = IDS_ASH_MAXIMIZE_WINDOW; |
| break; |
| case SNAP_MINIMIZE: |
| id = IDS_ASH_MINIMIZE_WINDOW; |
| break; |
| case SNAP_RESTORE: |
| DCHECK_NE(FRAME_STATE_NONE, bubble_->controller()->maximize_type()); |
| id = IDS_ASH_RESTORE_WINDOW; |
| break; |
| default: |
| // If nothing is selected, we automatically select the click operation. |
| id = bubble_->controller()->maximize_type() == FRAME_STATE_FULL ? |
| IDS_ASH_RESTORE_WINDOW : IDS_ASH_MAXIMIZE_WINDOW; |
| break; |
| } |
| label_view_->SetText(rb.GetLocalizedString(id)); |
| } |
| |
| MaximizeBubbleController::MaximizeBubbleController( |
| FrameMaximizeButton* frame_maximize_button, |
| MaximizeBubbleFrameState maximize_type, |
| int appearance_delay_ms) |
| : frame_maximize_button_(frame_maximize_button), |
| bubble_(NULL), |
| maximize_type_(maximize_type), |
| snap_type_for_creation_(SNAP_NONE), |
| appearance_delay_ms_(appearance_delay_ms) { |
| // Create the task which will create the bubble delayed. |
| base::OneShotTimer<MaximizeBubbleController>* new_timer = |
| new base::OneShotTimer<MaximizeBubbleController>(); |
| // Note: Even if there was no delay time given, we need to have a timer. |
| new_timer->Start( |
| FROM_HERE, |
| base::TimeDelta::FromMilliseconds( |
| appearance_delay_ms_ ? appearance_delay_ms_ : 10), |
| this, |
| &MaximizeBubbleController::CreateBubble); |
| timer_.reset(new_timer); |
| if (!appearance_delay_ms_) |
| CreateBubble(); |
| } |
| |
| MaximizeBubbleController::~MaximizeBubbleController() { |
| // Note: The destructor only gets initiated through the owner. |
| timer_.reset(); |
| if (bubble_) { |
| bubble_->ControllerRequestsCloseAndDelete(); |
| bubble_ = NULL; |
| } |
| } |
| |
| void MaximizeBubbleController::SetSnapType(SnapType snap_type) { |
| if (bubble_) { |
| bubble_->SetSnapType(snap_type); |
| } else { |
| // The bubble has not been created yet. This can occur if bubble creation is |
| // delayed. |
| snap_type_for_creation_ = snap_type; |
| } |
| } |
| |
| aura::Window* MaximizeBubbleController::GetBubbleWindow() { |
| return bubble_ ? bubble_->GetBubbleWindow() : NULL; |
| } |
| |
| void MaximizeBubbleController::DelayCreation() { |
| if (timer_.get() && timer_->IsRunning()) |
| timer_->Reset(); |
| } |
| |
| void MaximizeBubbleController::OnButtonClicked(SnapType snap_type) { |
| frame_maximize_button_->ExecuteSnapAndCloseMenu(snap_type); |
| } |
| |
| void MaximizeBubbleController::OnButtonHover(SnapType snap_type) { |
| frame_maximize_button_->SnapButtonHovered(snap_type); |
| } |
| |
| views::CustomButton* MaximizeBubbleController::GetButtonForUnitTest( |
| SnapType state) { |
| return bubble_ ? bubble_->GetButtonForUnitTest(state) : NULL; |
| } |
| |
| void MaximizeBubbleController::RequestDestructionThroughOwner() { |
| // Tell the parent to destroy us (if this didn't happen yet). |
| if (timer_) { |
| timer_.reset(NULL); |
| // Informs the owner that the menu is gone and requests |this| destruction. |
| frame_maximize_button_->DestroyMaximizeMenu(); |
| // Note: After this call |this| is destroyed. |
| } |
| } |
| |
| void MaximizeBubbleController::CreateBubble() { |
| if (!bubble_) |
| bubble_ = new Bubble(this, appearance_delay_ms_, snap_type_for_creation_); |
| |
| timer_->Stop(); |
| } |
| |
| BubbleDialogButton::BubbleDialogButton( |
| BubbleContentsButtonRow* button_row, |
| int normal_image, |
| int hovered_image, |
| int pressed_image) |
| : views::ImageButton(button_row), |
| button_row_(button_row) { |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| SetImage(views::CustomButton::STATE_NORMAL, |
| rb.GetImageSkiaNamed(normal_image)); |
| SetImage(views::CustomButton::STATE_HOVERED, |
| rb.GetImageSkiaNamed(hovered_image)); |
| SetImage(views::CustomButton::STATE_PRESSED, |
| rb.GetImageSkiaNamed(pressed_image)); |
| button_row->AddChildView(this); |
| } |
| |
| void BubbleDialogButton::OnMouseCaptureLost() { |
| button_row_->ButtonHovered(NULL); |
| views::ImageButton::OnMouseCaptureLost(); |
| } |
| |
| void BubbleDialogButton::OnMouseEntered(const ui::MouseEvent& event) { |
| button_row_->ButtonHovered(this); |
| views::ImageButton::OnMouseEntered(event); |
| } |
| |
| void BubbleDialogButton::OnMouseExited(const ui::MouseEvent& event) { |
| button_row_->ButtonHovered(NULL); |
| views::ImageButton::OnMouseExited(event); |
| } |
| |
| bool BubbleDialogButton::OnMouseDragged(const ui::MouseEvent& event) { |
| if (!button_row_->bubble()->controller()) |
| return false; |
| |
| // Remove the phantom window when we leave the button. |
| gfx::Point screen_location(event.location()); |
| View::ConvertPointToScreen(this, &screen_location); |
| if (!GetBoundsInScreen().Contains(screen_location)) |
| button_row_->ButtonHovered(NULL); |
| else |
| button_row_->ButtonHovered(this); |
| |
| // Pass the event on to the normal handler. |
| return views::ImageButton::OnMouseDragged(event); |
| } |
| |
| } // namespace ash |