diff options
author | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-18 21:58:23 +0000 |
---|---|---|
committer | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-18 21:58:23 +0000 |
commit | b1ed27851db7f86d7f71dfe4c897c796a953fc25 (patch) | |
tree | b9d972e7a6770b4c9055507fe2e9d329b0974106 /ui | |
parent | cee34b707ac41e83ff9d9045c0ffda49ec3f556f (diff) | |
download | chromium_src-b1ed27851db7f86d7f71dfe4c897c796a953fc25.zip chromium_src-b1ed27851db7f86d7f71dfe4c897c796a953fc25.tar.gz chromium_src-b1ed27851db7f86d7f71dfe4c897c796a953fc25.tar.bz2 |
views: Move bubble, events, focus and layout to ui/views/.
Left stub files that will be removed in a follow up patch after updating
the files to point to the new location.
BUG=104039
R=ben@chromium.org
Review URL: http://codereview.chromium.org/8588064
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@110761 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui')
49 files changed, 9350 insertions, 0 deletions
diff --git a/ui/views/bubble/border_contents_view.cc b/ui/views/bubble/border_contents_view.cc new file mode 100644 index 0000000..67c893a --- /dev/null +++ b/ui/views/bubble/border_contents_view.cc @@ -0,0 +1,185 @@ +// Copyright (c) 2011 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 "views/bubble/border_contents_view.h" + +#include <algorithm> + +#include "ui/gfx/screen.h" + +static const int kTopMargin = 6; +static const int kLeftMargin = 6; +static const int kBottomMargin = 6; +static const int kRightMargin = 6; + +namespace { + +// Computes how much |window_bounds| is off-screen of the monitor bounds +// |monitor_bounds| and puts the values in |offscreen_insets|. +// Returns false if |window_bounds| is actually contained in |monitor_bounds|, +// in which case |offscreen_insets| is not modified. +bool ComputeOffScreenInsets(const gfx::Rect& monitor_bounds, + const gfx::Rect& window_bounds, + gfx::Insets* offscreen_insets) { + if (monitor_bounds.Contains(window_bounds)) + return false; + + if (!offscreen_insets) + return true; + + // window_bounds + // +-------------------------------+ + // | top | + // | +----------------+ | + // | left | monitor_bounds | right | + // | +----------------+ | + // | bottom | + // +-------------------------------+ + int top = std::max(0, monitor_bounds.y() - window_bounds.y()); + int left = std::max(0, monitor_bounds.x() - window_bounds.x()); + int bottom = std::max(0, window_bounds.bottom() - monitor_bounds.bottom()); + int right = std::max(0, window_bounds.right() - monitor_bounds.right()); + + offscreen_insets->Set(top, left, bottom, right); + return true; +} + +// Convenience method that returns the height of |insets| if |vertical| is +// true, its width otherwise. +int GetInsetsLength(const gfx::Insets& insets, bool vertical) { + return vertical ? insets.height() : insets.width(); +} + +} // namespace + +namespace views { + +BorderContentsView::BorderContentsView() + : bubble_border_(NULL), + content_margins_(kTopMargin, kLeftMargin, kBottomMargin, kRightMargin) { +} + +BorderContentsView::BorderContentsView(int top_margin, + int left_margin, + int bottom_margin, + int right_margin) + : bubble_border_(NULL), + content_margins_(top_margin, left_margin, bottom_margin, right_margin) { +} + +BorderContentsView::~BorderContentsView() {} + +void BorderContentsView::Init() { + // Default arrow location. + BubbleBorder::ArrowLocation arrow_location = + BubbleBorder::TOP_LEFT; + if (base::i18n::IsRTL()) + arrow_location = BubbleBorder::horizontal_mirror(arrow_location); + DCHECK(!bubble_border_); + + // TODO(alicet): Expose the shadow option in BorderContentsView when we make + // the fullscreen exit bubble use the new bubble code. + bubble_border_ = new BubbleBorder(arrow_location, + views::BubbleBorder::NO_SHADOW); + set_border(bubble_border_); + set_background(new BubbleBackground(bubble_border_)); +} + +void BorderContentsView::SetBackgroundColor(SkColor color) { + bubble_border_->set_background_color(color); +} + +void BorderContentsView::SetAlignment( + views::BubbleBorder::BubbleAlignment alignment) { + bubble_border_->set_alignment(alignment); +} + +void BorderContentsView::SizeAndGetBounds( + const gfx::Rect& position_relative_to, + BubbleBorder::ArrowLocation arrow_location, + bool allow_bubble_offscreen, + const gfx::Size& contents_size, + gfx::Rect* contents_bounds, + gfx::Rect* window_bounds) { + if (base::i18n::IsRTL()) + arrow_location = BubbleBorder::horizontal_mirror(arrow_location); + bubble_border_->set_arrow_location(arrow_location); + // Set the border. + set_border(bubble_border_); + + // Give the contents a margin. + gfx::Size local_contents_size(contents_size); + local_contents_size.Enlarge(content_margins_.width(), + content_margins_.height()); + + // Try putting the arrow in its initial location, and calculating the bounds. + *window_bounds = + bubble_border_->GetBounds(position_relative_to, local_contents_size); + if (!allow_bubble_offscreen) { + gfx::Rect monitor_bounds = GetMonitorBounds(position_relative_to); + if (!monitor_bounds.IsEmpty()) { + // Try to resize vertically if this does not fit on the screen. + MirrorArrowIfOffScreen(true, // |vertical|. + position_relative_to, monitor_bounds, + local_contents_size, &arrow_location, + window_bounds); + // Then try to resize horizontally if it still does not fit on the screen. + MirrorArrowIfOffScreen(false, // |vertical|. + position_relative_to, monitor_bounds, + local_contents_size, &arrow_location, + window_bounds); + } + } + + // Calculate the bounds of the contained contents (in window coordinates) by + // subtracting the border dimensions and margin amounts. + *contents_bounds = gfx::Rect(gfx::Point(), window_bounds->size()); + gfx::Insets insets; + bubble_border_->GetInsets(&insets); + insets += content_margins_; + contents_bounds->Inset(insets); +} + +gfx::Rect BorderContentsView::GetMonitorBounds(const gfx::Rect& rect) { + return gfx::Screen::GetMonitorWorkAreaNearestPoint(rect.CenterPoint()); +} + +void BorderContentsView::MirrorArrowIfOffScreen( + bool vertical, + const gfx::Rect& position_relative_to, + const gfx::Rect& monitor_bounds, + const gfx::Size& local_contents_size, + BubbleBorder::ArrowLocation* arrow_location, + gfx::Rect* window_bounds) { + // If the bounds don't fit, move the arrow to its mirrored position to see if + // it improves things. + gfx::Insets offscreen_insets; + if (ComputeOffScreenInsets(monitor_bounds, *window_bounds, + &offscreen_insets) && + GetInsetsLength(offscreen_insets, vertical) > 0) { + BubbleBorder::ArrowLocation original_arrow_location = + *arrow_location; + *arrow_location = + vertical ? BubbleBorder::vertical_mirror(*arrow_location) : + BubbleBorder::horizontal_mirror(*arrow_location); + + // Change the arrow and get the new bounds. + bubble_border_->set_arrow_location(*arrow_location); + *window_bounds = bubble_border_->GetBounds(position_relative_to, + local_contents_size); + gfx::Insets new_offscreen_insets; + // If there is more of the window offscreen, we'll keep the old arrow. + if (ComputeOffScreenInsets(monitor_bounds, *window_bounds, + &new_offscreen_insets) && + GetInsetsLength(new_offscreen_insets, vertical) >= + GetInsetsLength(offscreen_insets, vertical)) { + *arrow_location = original_arrow_location; + bubble_border_->set_arrow_location(*arrow_location); + *window_bounds = bubble_border_->GetBounds(position_relative_to, + local_contents_size); + } + } +} + +} // namespace views diff --git a/ui/views/bubble/border_contents_view.h b/ui/views/bubble/border_contents_view.h new file mode 100644 index 0000000..696dd26 --- /dev/null +++ b/ui/views/bubble/border_contents_view.h @@ -0,0 +1,88 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_BUBBLE_BORDER_CONTENTS_VIEW_H_ +#define UI_VIEWS_BUBBLE_BORDER_CONTENTS_VIEW_H_ +#pragma once + +#include "views/bubble/bubble_border.h" +#include "third_party/skia/include/core/SkColor.h" +#include "views/view.h" + +namespace views { + +// This is used to paint the border and background of the Bubble. +class VIEWS_EXPORT BorderContentsView : public View { + public: + BorderContentsView(); + BorderContentsView(int top_margin, + int left_margin, + int bottom_margin, + int right_margin); + + // Must be called before this object can be used. + void Init(); + + // Sets the background color. + void SetBackgroundColor(SkColor color); + + // Sets the bubble alignment. + void SetAlignment(views::BubbleBorder::BubbleAlignment alignment); + + // Given the size of the contents and the rect to point at, returns the bounds + // of both the border and the contents inside the bubble. + // |arrow_location| specifies the preferred location for the arrow + // anchor. If the bubble does not fit on the monitor and + // |allow_bubble_offscreen| is false, the arrow location may change so the + // bubble shows entirely. + virtual void SizeAndGetBounds( + const gfx::Rect& position_relative_to, // In screen coordinates + BubbleBorder::ArrowLocation arrow_location, + bool allow_bubble_offscreen, + const gfx::Size& contents_size, + gfx::Rect* contents_bounds, // Returned in window coordinates + gfx::Rect* window_bounds); // Returned in screen coordinates + + // Sets content margins. + void set_content_margins(const gfx::Insets& margins) { + content_margins_ = margins; + } + + // Accessor for |content_margins_|. + const gfx::Insets& content_margins() const { + return content_margins_; + } + + protected: + virtual ~BorderContentsView(); + + // Returns the bounds for the monitor showing the specified |rect|. + virtual gfx::Rect GetMonitorBounds(const gfx::Rect& rect); + + BubbleBorder* bubble_border() const { return bubble_border_; } + + private: + // Changes |arrow_location| to its mirrored version, vertically if |vertical| + // is true, horizontally otherwise, if |window_bounds| don't fit in + // |monitor_bounds|. + void MirrorArrowIfOffScreen( + bool vertical, + const gfx::Rect& position_relative_to, + const gfx::Rect& monitor_bounds, + const gfx::Size& local_contents_size, + BubbleBorder::ArrowLocation* arrow_location, + gfx::Rect* window_bounds); + + // The bubble border. + BubbleBorder* bubble_border_; + + // Margins between the content and the inside of the border, in pixels. + gfx::Insets content_margins_; + + DISALLOW_COPY_AND_ASSIGN(BorderContentsView); +}; + +} // namespace views + +#endif // UI_VIEWS_BUBBLE_BORDER_CONTENTS_VIEW_H_ diff --git a/ui/views/bubble/bubble_border.cc b/ui/views/bubble/bubble_border.cc new file mode 100644 index 0000000..368b0be --- /dev/null +++ b/ui/views/bubble/bubble_border.cc @@ -0,0 +1,511 @@ +// Copyright (c) 2011 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 "views/bubble/bubble_border.h" + +#include <algorithm> // for std::max + +#include "base/logging.h" +#include "grit/ui_resources.h" +#include "grit/ui_resources_standard.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/path.h" + +namespace views { + +struct BubbleBorder::BorderImages { + BorderImages() + : left(NULL), + top_left(NULL), + top(NULL), + top_right(NULL), + right(NULL), + bottom_right(NULL), + bottom(NULL), + bottom_left(NULL), + left_arrow(NULL), + top_arrow(NULL), + right_arrow(NULL), + bottom_arrow(NULL) { + } + + SkBitmap* left; + SkBitmap* top_left; + SkBitmap* top; + SkBitmap* top_right; + SkBitmap* right; + SkBitmap* bottom_right; + SkBitmap* bottom; + SkBitmap* bottom_left; + SkBitmap* left_arrow; + SkBitmap* top_arrow; + SkBitmap* right_arrow; + SkBitmap* bottom_arrow; +}; + +// static +struct BubbleBorder::BorderImages* BubbleBorder::normal_images_ = NULL; +struct BubbleBorder::BorderImages* BubbleBorder::shadow_images_ = NULL; + + +// The height inside the arrow image, in pixels. +static const int kArrowInteriorHeight = 7; + +BubbleBorder::BubbleBorder(ArrowLocation arrow_location, Shadow shadow) + : override_arrow_offset_(0), + arrow_location_(arrow_location), + alignment_(ALIGN_ARROW_TO_MID_ANCHOR), + background_color_(SK_ColorWHITE) { + images_ = GetBorderImages(shadow); + + // Calculate horizontal and vertical insets for arrow by ensuring that + // the widest arrow and corner images will have enough room to avoid overlap + int offset_x = + (std::max(images_->top_arrow->width(), + images_->bottom_arrow->width()) / 2) + + std::max(std::max(images_->top_left->width(), + images_->top_right->width()), + std::max(images_->bottom_left->width(), + images_->bottom_right->width())); + int offset_y = + (std::max(images_->left_arrow->height(), + images_->right_arrow->height()) / 2) + + std::max(std::max(images_->top_left->height(), + images_->top_right->height()), + std::max(images_->bottom_left->height(), + images_->bottom_right->height())); + arrow_offset_ = std::max(offset_x, offset_y); +} + +gfx::Rect BubbleBorder::GetBounds(const gfx::Rect& position_relative_to, + const gfx::Size& contents_size) const { + // Desired size is size of contents enlarged by the size of the border images. + gfx::Size border_size(contents_size); + gfx::Insets insets; + GetInsets(&insets); + border_size.Enlarge(insets.left() + insets.right(), + insets.top() + insets.bottom()); + + // Screen position depends on the arrow location. + // The arrow should overlap the target by some amount since there is space + // for shadow between arrow tip and bitmap bounds. + const int kArrowOverlap = 3; + int x = position_relative_to.x(); + int y = position_relative_to.y(); + int w = position_relative_to.width(); + int h = position_relative_to.height(); + int arrow_offset = override_arrow_offset_ ? override_arrow_offset_ : + arrow_offset_; + + // Calculate bubble x coordinate. + switch (arrow_location_) { + case TOP_LEFT: + case BOTTOM_LEFT: + x += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? w / 2 - arrow_offset : + -kArrowOverlap; + break; + + case TOP_RIGHT: + case BOTTOM_RIGHT: + x += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? + w / 2 + arrow_offset - border_size.width() + 1 : + w - border_size.width() + kArrowOverlap; + break; + + case LEFT_TOP: + case LEFT_BOTTOM: + x += w - kArrowOverlap; + break; + + case RIGHT_TOP: + case RIGHT_BOTTOM: + x += kArrowOverlap - border_size.width(); + break; + + case NONE: + case FLOAT: + x += w / 2 - border_size.width() / 2; + break; + } + + // Calculate bubble y coordinate. + switch (arrow_location_) { + case TOP_LEFT: + case TOP_RIGHT: + y += h - kArrowOverlap; + break; + + case BOTTOM_LEFT: + case BOTTOM_RIGHT: + y += kArrowOverlap - border_size.height(); + break; + + case LEFT_TOP: + case RIGHT_TOP: + y += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? h / 2 - arrow_offset : + -kArrowOverlap; + break; + + case LEFT_BOTTOM: + case RIGHT_BOTTOM: + y += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? + h / 2 + arrow_offset - border_size.height() + 1 : + h - border_size.height() + kArrowOverlap; + break; + + case NONE: + y += h; + break; + + case FLOAT: + y += h / 2 - border_size.height() / 2; + break; + } + + return gfx::Rect(x, y, border_size.width(), border_size.height()); +} + +void BubbleBorder::GetInsets(gfx::Insets* insets) const { + int top = images_->top->height(); + int bottom = images_->bottom->height(); + int left = images_->left->width(); + int right = images_->right->width(); + switch (arrow_location_) { + case TOP_LEFT: + case TOP_RIGHT: + top = std::max(top, images_->top_arrow->height()); + break; + + case BOTTOM_LEFT: + case BOTTOM_RIGHT: + bottom = std::max(bottom, images_->bottom_arrow->height()); + break; + + case LEFT_TOP: + case LEFT_BOTTOM: + left = std::max(left, images_->left_arrow->width()); + break; + + case RIGHT_TOP: + case RIGHT_BOTTOM: + right = std::max(right, images_->right_arrow->width()); + break; + + case NONE: + case FLOAT: + // Nothing to do. + break; + } + insets->Set(top, left, bottom, right); +} + +int BubbleBorder::SetArrowOffset(int offset, const gfx::Size& contents_size) { + gfx::Size border_size(contents_size); + gfx::Insets insets; + GetInsets(&insets); + border_size.Enlarge(insets.left() + insets.right(), + insets.top() + insets.bottom()); + offset = std::max(arrow_offset_, + std::min(offset, (is_arrow_on_horizontal(arrow_location_) ? + border_size.width() : border_size.height()) - arrow_offset_)); + override_arrow_offset_ = offset; + return override_arrow_offset_; +} + +// static +BubbleBorder::BorderImages* BubbleBorder::GetBorderImages(Shadow shadow) { + if (shadow == SHADOW && shadow_images_ == NULL) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + shadow_images_ = new BorderImages(); + shadow_images_->left = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_L); + shadow_images_->top_left = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_TL); + shadow_images_->top = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_T); + shadow_images_->top_right = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_TR); + shadow_images_->right = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_R); + shadow_images_->bottom_right = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_BR); + shadow_images_->bottom = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_B); + shadow_images_->bottom_left = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_BL); + shadow_images_->left_arrow = new SkBitmap(); + shadow_images_->top_arrow = new SkBitmap(); + shadow_images_->right_arrow = new SkBitmap(); + shadow_images_->bottom_arrow = new SkBitmap(); + } else if (shadow == NO_SHADOW && normal_images_ == NULL) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + normal_images_ = new BorderImages(); + normal_images_->left = rb.GetBitmapNamed(IDR_BUBBLE_L); + normal_images_->top_left = rb.GetBitmapNamed(IDR_BUBBLE_TL); + normal_images_->top = rb.GetBitmapNamed(IDR_BUBBLE_T); + normal_images_->top_right = rb.GetBitmapNamed(IDR_BUBBLE_TR); + normal_images_->right = rb.GetBitmapNamed(IDR_BUBBLE_R); + normal_images_->bottom_right = rb.GetBitmapNamed(IDR_BUBBLE_BR); + normal_images_->bottom = rb.GetBitmapNamed(IDR_BUBBLE_B); + normal_images_->bottom_left = rb.GetBitmapNamed(IDR_BUBBLE_BL); + normal_images_->left_arrow = rb.GetBitmapNamed(IDR_BUBBLE_L_ARROW); + normal_images_->top_arrow = rb.GetBitmapNamed(IDR_BUBBLE_T_ARROW); + normal_images_->right_arrow = rb.GetBitmapNamed(IDR_BUBBLE_R_ARROW); + normal_images_->bottom_arrow = rb.GetBitmapNamed(IDR_BUBBLE_B_ARROW); + } + return shadow == SHADOW ? shadow_images_ : normal_images_; +} + +BubbleBorder::~BubbleBorder() {} + +void BubbleBorder::Paint(const views::View& view, gfx::Canvas* canvas) const { + // Convenience shorthand variables. + const int tl_width = images_->top_left->width(); + const int tl_height = images_->top_left->height(); + const int t_height = images_->top->height(); + const int tr_width = images_->top_right->width(); + const int tr_height = images_->top_right->height(); + const int l_width = images_->left->width(); + const int r_width = images_->right->width(); + const int br_width = images_->bottom_right->width(); + const int br_height = images_->bottom_right->height(); + const int b_height = images_->bottom->height(); + const int bl_width = images_->bottom_left->width(); + const int bl_height = images_->bottom_left->height(); + + gfx::Insets insets; + GetInsets(&insets); + const int top = insets.top() - t_height; + const int bottom = view.height() - insets.bottom() + b_height; + const int left = insets.left() - l_width; + const int right = view.width() - insets.right() + r_width; + const int height = bottom - top; + const int width = right - left; + + // |arrow_offset| is offset of arrow from the begining of the edge. + int arrow_offset = arrow_offset_; + if (override_arrow_offset_) + arrow_offset = override_arrow_offset_; + else if (is_arrow_on_horizontal(arrow_location_) && + !is_arrow_on_left(arrow_location_)) { + arrow_offset = view.width() - arrow_offset - 1; + } else if (!is_arrow_on_horizontal(arrow_location_) && + !is_arrow_on_top(arrow_location_)) { + arrow_offset = view.height() - arrow_offset - 1; + } + + // Left edge. + if (arrow_location_ == LEFT_TOP || arrow_location_ == LEFT_BOTTOM) { + int start_y = top + tl_height; + int before_arrow = + arrow_offset - start_y - images_->left_arrow->height() / 2; + int after_arrow = height - tl_height - bl_height - + images_->left_arrow->height() - before_arrow; + int tip_y = start_y + before_arrow + images_->left_arrow->height() / 2; + DrawArrowInterior(canvas, + false, + images_->left_arrow->width() - kArrowInteriorHeight, + tip_y, + kArrowInteriorHeight, + images_->left_arrow->height() / 2 - 1); + DrawEdgeWithArrow(canvas, + false, + images_->left, + images_->left_arrow, + left, + start_y, + before_arrow, + after_arrow, + images_->left->width() - images_->left_arrow->width()); + } else { + canvas->TileImageInt(*images_->left, left, top + tl_height, l_width, + height - tl_height - bl_height); + } + + // Top left corner. + canvas->DrawBitmapInt(*images_->top_left, left, top); + + // Top edge. + if (arrow_location_ == TOP_LEFT || arrow_location_ == TOP_RIGHT) { + int start_x = left + tl_width; + int before_arrow = arrow_offset - start_x - images_->top_arrow->width() / 2; + int after_arrow = width - tl_width - tr_width - + images_->top_arrow->width() - before_arrow; + DrawArrowInterior(canvas, + true, + start_x + before_arrow + images_->top_arrow->width() / 2, + images_->top_arrow->height() - kArrowInteriorHeight, + 1 - images_->top_arrow->width() / 2, + kArrowInteriorHeight); + DrawEdgeWithArrow(canvas, + true, + images_->top, + images_->top_arrow, + start_x, + top, + before_arrow, + after_arrow, + images_->top->height() - images_->top_arrow->height()); + } else { + canvas->TileImageInt(*images_->top, left + tl_width, top, + width - tl_width - tr_width, t_height); + } + + // Top right corner. + canvas->DrawBitmapInt(*images_->top_right, right - tr_width, top); + + // Right edge. + if (arrow_location_ == RIGHT_TOP || arrow_location_ == RIGHT_BOTTOM) { + int start_y = top + tr_height; + int before_arrow = + arrow_offset - start_y - images_->right_arrow->height() / 2; + int after_arrow = height - tl_height - bl_height - + images_->right_arrow->height() - before_arrow; + int tip_y = start_y + before_arrow + images_->right_arrow->height() / 2; + DrawArrowInterior(canvas, + false, + right - r_width + kArrowInteriorHeight, + tip_y, + -kArrowInteriorHeight, + images_->right_arrow->height() / 2 - 1); + DrawEdgeWithArrow(canvas, + false, + images_->right, + images_->right_arrow, + right - r_width, + start_y, + before_arrow, + after_arrow, + 0); + } else { + canvas->TileImageInt(*images_->right, right - r_width, top + tr_height, + r_width, height - tr_height - br_height); + } + + // Bottom right corner. + canvas->DrawBitmapInt(*images_->bottom_right, + right - br_width, + bottom - br_height); + + // Bottom edge. + if (arrow_location_ == BOTTOM_LEFT || arrow_location_ == BOTTOM_RIGHT) { + int start_x = left + bl_width; + int before_arrow = + arrow_offset - start_x - images_->bottom_arrow->width() / 2; + int after_arrow = width - bl_width - br_width - + images_->bottom_arrow->width() - before_arrow; + int tip_x = start_x + before_arrow + images_->bottom_arrow->width() / 2; + DrawArrowInterior(canvas, + true, + tip_x, + bottom - b_height + kArrowInteriorHeight, + 1 - images_->bottom_arrow->width() / 2, + -kArrowInteriorHeight); + DrawEdgeWithArrow(canvas, + true, + images_->bottom, + images_->bottom_arrow, + start_x, + bottom - b_height, + before_arrow, + after_arrow, + 0); + } else { + canvas->TileImageInt(*images_->bottom, left + bl_width, bottom - b_height, + width - bl_width - br_width, b_height); + } + + // Bottom left corner. + canvas->DrawBitmapInt(*images_->bottom_left, left, bottom - bl_height); +} + +void BubbleBorder::DrawEdgeWithArrow(gfx::Canvas* canvas, + bool is_horizontal, + SkBitmap* edge, + SkBitmap* arrow, + int start_x, + int start_y, + int before_arrow, + int after_arrow, + int offset) const { + /* Here's what the parameters mean: + * start_x + * . + * . ┌───┐ ┬ offset + * start_y..........┌────┬────────┤ ▲ ├────────┬────┐ + * │ / │--------│∙ ∙│--------│ \ │ + * │ / ├────────┴───┴────────┤ \ │ + * ├───┬┘ └┬───┤ + * └───┬────┘ └───┬────┘ + * before_arrow ─┘ └─ after_arrow + */ + if (before_arrow) { + canvas->TileImageInt(*edge, start_x, start_y, + is_horizontal ? before_arrow : edge->width(), + is_horizontal ? edge->height() : before_arrow); + } + + canvas->DrawBitmapInt(*arrow, + start_x + (is_horizontal ? before_arrow : offset), + start_y + (is_horizontal ? offset : before_arrow)); + + if (after_arrow) { + start_x += (is_horizontal ? before_arrow + arrow->width() : 0); + start_y += (is_horizontal ? 0 : before_arrow + arrow->height()); + canvas->TileImageInt(*edge, start_x, start_y, + is_horizontal ? after_arrow : edge->width(), + is_horizontal ? edge->height() : after_arrow); + } +} + +void BubbleBorder::DrawArrowInterior(gfx::Canvas* canvas, + bool is_horizontal, + int tip_x, + int tip_y, + int shift_x, + int shift_y) const { + /* This function fills the interior of the arrow with background color. + * It draws isosceles triangle under semitransparent arrow tip. + * + * Here's what the parameters mean: + * + * ┌──────── |tip_x| + * ┌─────┐ + * │ ▲ │ ──── |tip y| + * │∙∙∙∙∙│ ┐ + * └─────┘ └─── |shift_x| (offset from tip to vertexes of isosceles triangle) + * └────────── |shift_y| + */ + SkPaint paint; + paint.setStyle(SkPaint::kFill_Style); + paint.setColor(background_color_); + gfx::Path path; + path.incReserve(4); + path.moveTo(SkIntToScalar(tip_x), SkIntToScalar(tip_y)); + path.lineTo(SkIntToScalar(tip_x + shift_x), + SkIntToScalar(tip_y + shift_y)); + if (is_horizontal) + path.lineTo(SkIntToScalar(tip_x - shift_x), SkIntToScalar(tip_y + shift_y)); + else + path.lineTo(SkIntToScalar(tip_x + shift_x), SkIntToScalar(tip_y - shift_y)); + path.close(); + canvas->GetSkCanvas()->drawPath(path, paint); +} + +///////////////////////// + +void BubbleBackground::Paint(gfx::Canvas* canvas, views::View* view) const { + // The border of this view creates an anti-aliased round-rect region for the + // contents, which we need to fill with the background color. + // NOTE: This doesn't handle an arrow location of "NONE", which has square top + // corners. + SkPaint paint; + paint.setAntiAlias(true); + paint.setStyle(SkPaint::kFill_Style); + paint.setColor(border_->background_color()); + gfx::Path path; + gfx::Rect bounds(view->GetContentsBounds()); + SkRect rect; + rect.set(SkIntToScalar(bounds.x()), SkIntToScalar(bounds.y()), + SkIntToScalar(bounds.right()), SkIntToScalar(bounds.bottom())); + SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius()); + path.addRoundRect(rect, radius, radius); + canvas->GetSkCanvas()->drawPath(path, paint); +} + +} // namespace views diff --git a/ui/views/bubble/bubble_border.h b/ui/views/bubble/bubble_border.h new file mode 100644 index 0000000..09e20bb --- /dev/null +++ b/ui/views/bubble/bubble_border.h @@ -0,0 +1,183 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_BUBBLE_BUBBLE_BORDER_H_ +#define UI_VIEWS_BUBBLE_BUBBLE_BORDER_H_ +#pragma once + +#include "views/background.h" +#include "views/border.h" + +class SkBitmap; + +namespace views { + +// Renders a border, with optional arrow, and a custom dropshadow. +// This can be used to produce floating "bubble" objects with rounded corners. +class VIEWS_EXPORT BubbleBorder : public views::Border { + public: + // Possible locations for the (optional) arrow. + // 0 bit specifies left or right. + // 1 bit specifies top or bottom. + // 2 bit specifies horizontal or vertical. + enum ArrowLocation { + TOP_LEFT = 0, + TOP_RIGHT = 1, + BOTTOM_LEFT = 2, + BOTTOM_RIGHT = 3, + LEFT_TOP = 4, + RIGHT_TOP = 5, + LEFT_BOTTOM = 6, + RIGHT_BOTTOM = 7, + NONE = 8, // No arrow. Positioned under the supplied rect. + FLOAT = 9 // No arrow. Centered over the supplied rect. + }; + + enum Shadow { + SHADOW = 0, + NO_SHADOW = 1 + }; + + // The position of the bubble in relation to the anchor. + enum BubbleAlignment { + // The tip of the arrow points to the middle of the anchor. + ALIGN_ARROW_TO_MID_ANCHOR, + // The edge nearest to the arrow is lined up with the edge of the anchor. + ALIGN_EDGE_TO_ANCHOR_EDGE + }; + + BubbleBorder(ArrowLocation arrow_location, Shadow shadow); + + // Returns the radius of the corner of the border. + static int GetCornerRadius() { + // We can't safely calculate a border radius by comparing the sizes of the + // side and corner images, because either may have been extended in various + // directions in order to do more subtle dropshadow fading or other effects. + // So we hardcode the most accurate value. + return 4; + } + + // Sets the location for the arrow. + void set_arrow_location(ArrowLocation arrow_location) { + arrow_location_ = arrow_location; + } + ArrowLocation arrow_location() const { return arrow_location_; } + + // Sets the alignment. + void set_alignment(BubbleAlignment alignment) { alignment_ = alignment; } + BubbleAlignment alignment() const { return alignment_; } + + static ArrowLocation horizontal_mirror(ArrowLocation loc) { + return loc >= NONE ? loc : static_cast<ArrowLocation>(loc ^ 1); + } + + static ArrowLocation vertical_mirror(ArrowLocation loc) { + return loc >= NONE ? loc : static_cast<ArrowLocation>(loc ^ 2); + } + + static bool has_arrow(ArrowLocation loc) { + return loc >= NONE ? false : true; + } + + static bool is_arrow_on_left(ArrowLocation loc) { + return loc >= NONE ? false : !(loc & 1); + } + + static bool is_arrow_on_top(ArrowLocation loc) { + return loc >= NONE ? false : !(loc & 2); + } + + static bool is_arrow_on_horizontal(ArrowLocation loc) { + return loc >= NONE ? false : !(loc & 4); + } + + // Sets the background color for the arrow body. This is irrelevant if you do + // not also set the arrow location to something other than NONE. + void set_background_color(SkColor background_color) { + background_color_ = background_color; + } + SkColor background_color() const { return background_color_; } + + // For borders with an arrow, gives the desired bounds (in screen coordinates) + // given the rect to point to and the size of the contained contents. This + // depends on the arrow location, so if you change that, you should call this + // again to find out the new coordinates. + gfx::Rect GetBounds(const gfx::Rect& position_relative_to, + const gfx::Size& contents_size) const; + + // Sets a fixed offset for the arrow from the beginning of corresponding edge. + // The arrow will still point to the same location but the bubble will shift + // location to make that happen. Returns actuall arrow offset, in case of + // overflow it differ from desired. + int SetArrowOffset(int offset, const gfx::Size& contents_size); + + // Overridden from views::Border: + virtual void GetInsets(gfx::Insets* insets) const; + + private: + struct BorderImages; + + // Loads images if necessary. + static BorderImages* GetBorderImages(Shadow shadow); + + virtual ~BubbleBorder(); + + // Overridden from views::Border: + virtual void Paint(const views::View& view, gfx::Canvas* canvas) const; + + void DrawEdgeWithArrow(gfx::Canvas* canvas, + bool is_horizontal, + SkBitmap* edge, + SkBitmap* arrow, + int start_x, + int start_y, + int before_arrow, + int after_arrow, + int offset) const; + + void DrawArrowInterior(gfx::Canvas* canvas, + bool is_horizontal, + int tip_x, + int tip_y, + int shift_x, + int shift_y) const; + + // Border graphics. + struct BorderImages* images_; + + // Image bundles. + static struct BorderImages* normal_images_; + static struct BorderImages* shadow_images_; + + // Minimal offset of the arrow from the closet edge of bounding rect. + int arrow_offset_; + + // If specified, overrides the pre-calculated |arrow_offset_| of the arrow. + int override_arrow_offset_; + + ArrowLocation arrow_location_; + BubbleAlignment alignment_; + SkColor background_color_; + + DISALLOW_COPY_AND_ASSIGN(BubbleBorder); +}; + +// A Background that clips itself to the specified BubbleBorder and uses +// the background color of the BubbleBorder. +class VIEWS_EXPORT BubbleBackground : public views::Background { + public: + explicit BubbleBackground(BubbleBorder* border) : border_(border) {} + + // Background overrides. + virtual void Paint(gfx::Canvas* canvas, views::View* view) const; + + private: + BubbleBorder* border_; + + DISALLOW_COPY_AND_ASSIGN(BubbleBackground); +}; + +} // namespace views + +#endif // UI_VIEWS_BUBBLE_BUBBLE_BORDER_H_ diff --git a/ui/views/bubble/bubble_delegate.cc b/ui/views/bubble/bubble_delegate.cc new file mode 100644 index 0000000..b3baea4 --- /dev/null +++ b/ui/views/bubble/bubble_delegate.cc @@ -0,0 +1,287 @@ +// Copyright (c) 2011 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 "views/bubble/bubble_delegate.h" + +#include "ui/base/animation/slide_animation.h" +#include "views/bubble/bubble_frame_view.h" +#include "views/widget/widget.h" + +// The duration of the fade animation in milliseconds. +static const int kHideFadeDurationMS = 200; + +namespace views { + +namespace { + +// Create a widget to host the bubble. +Widget* CreateBubbleWidget(BubbleDelegateView* bubble, Widget* parent) { + Widget* bubble_widget = new Widget(); + Widget::InitParams bubble_params(Widget::InitParams::TYPE_BUBBLE); + bubble_params.delegate = bubble; + bubble_params.transparent = true; + bubble_params.parent_widget = parent; +#if defined(OS_WIN) && !defined(USE_AURA) + bubble_params.type = Widget::InitParams::TYPE_WINDOW_FRAMELESS; + bubble_params.transparent = false; +#endif + bubble_widget->Init(bubble_params); + return bubble_widget; +} + +#if defined(OS_WIN) && !defined(USE_AURA) +// The border widget's delegate, needed for transparent Windows native controls. +// TODO(msw): Remove this when Windows native controls are no longer needed. +class VIEWS_EXPORT BubbleBorderDelegateView : public WidgetDelegateView { + public: + explicit BubbleBorderDelegateView(BubbleDelegateView* bubble) + : bubble_(bubble) {} + virtual ~BubbleBorderDelegateView() {} + + // WidgetDelegateView overrides: + virtual bool CanActivate() const OVERRIDE; + virtual NonClientFrameView* CreateNonClientFrameView() OVERRIDE; + + private: + BubbleDelegateView* bubble_; + + DISALLOW_COPY_AND_ASSIGN(BubbleBorderDelegateView); +}; + +bool BubbleBorderDelegateView::CanActivate() const { return false; } + +NonClientFrameView* BubbleBorderDelegateView::CreateNonClientFrameView() { + return bubble_->CreateNonClientFrameView(); +} + +// Create a widget to host the bubble's border. +Widget* CreateBorderWidget(BubbleDelegateView* bubble, Widget* parent) { + Widget* border_widget = new Widget(); + Widget::InitParams border_params(Widget::InitParams::TYPE_BUBBLE); + border_params.delegate = new BubbleBorderDelegateView(bubble); + border_params.transparent = true; + border_params.parent_widget = parent; + if (!border_params.parent_widget) + border_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + border_widget->Init(border_params); + return border_widget; +} +#endif + +} // namespace + +BubbleDelegateView::BubbleDelegateView() + : close_on_esc_(true), + close_on_deactivate_(true), + allow_bubble_offscreen_(false), + anchor_view_(NULL), + arrow_location_(BubbleBorder::TOP_LEFT), + color_(SK_ColorWHITE), + border_widget_(NULL), + use_focusless_(false) { + set_background(views::Background::CreateSolidBackground(color_)); + AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, 0)); +} + +BubbleDelegateView::BubbleDelegateView( + View* anchor_view, + BubbleBorder::ArrowLocation arrow_location, + const SkColor& color) + : close_on_esc_(true), + close_on_deactivate_(true), + allow_bubble_offscreen_(false), + anchor_view_(anchor_view), + arrow_location_(arrow_location), + color_(color), + original_opacity_(255), + border_widget_(NULL), + use_focusless_(false) { + set_background(views::Background::CreateSolidBackground(color_)); + AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, 0)); +} + +BubbleDelegateView::~BubbleDelegateView() { + if (border_widget_) + border_widget_->Close(); +} + +// static +Widget* BubbleDelegateView::CreateBubble(BubbleDelegateView* bubble_delegate) { + bubble_delegate->Init(); + Widget* parent = bubble_delegate->anchor_view() ? + bubble_delegate->anchor_view()->GetWidget() : NULL; + Widget* bubble_widget = CreateBubbleWidget(bubble_delegate, parent); + +#if defined(OS_WIN) && !defined(USE_AURA) + // First set the contents view to initialize view bounds for widget sizing. + bubble_widget->SetContentsView(bubble_delegate->GetContentsView()); + bubble_delegate->border_widget_ = CreateBorderWidget(bubble_delegate, parent); +#endif + + bubble_delegate->SizeToContents(); + bubble_widget->AddObserver(bubble_delegate); + if (parent && parent->GetTopLevelWidget()) + parent->GetTopLevelWidget()->DisableInactiveRendering(); + return bubble_widget; +} + +View* BubbleDelegateView::GetInitiallyFocusedView() { + return this; +} + +BubbleDelegateView* BubbleDelegateView::AsBubbleDelegate() { + return this; +} + +View* BubbleDelegateView::GetContentsView() { + return this; +} + +NonClientFrameView* BubbleDelegateView::CreateNonClientFrameView() { + return new BubbleFrameView(GetArrowLocation(), + GetPreferredSize(), + GetColor(), + allow_bubble_offscreen_); +} + +void BubbleDelegateView::OnWidgetActivationChanged(Widget* widget, + bool active) { + if (close_on_deactivate() && widget == GetWidget() && !active) { + GetWidget()->RemoveObserver(this); + GetWidget()->Close(); + } +} + +gfx::Point BubbleDelegateView::GetAnchorPoint() { + if (!anchor_view()) + return gfx::Point(); + + BubbleBorder::ArrowLocation location = GetArrowLocation(); + gfx::Point anchor(anchor_view()->bounds().CenterPoint()); + // By default, pick the middle of |anchor_view_|'s edge opposite the arrow. + if (BubbleBorder::is_arrow_on_horizontal(location)) { + anchor.SetPoint(anchor_view()->width() / 2, + BubbleBorder::is_arrow_on_top(location) ? anchor_view()->height() : 0); + } else if (BubbleBorder::has_arrow(location)) { + anchor.SetPoint( + BubbleBorder::is_arrow_on_left(location) ? anchor_view()->width() : 0, + anchor_view_->height() / 2); + } + View::ConvertPointToScreen(anchor_view(), &anchor); + return anchor; +} + +BubbleBorder::ArrowLocation BubbleDelegateView::GetArrowLocation() const { + return arrow_location_; +} + +SkColor BubbleDelegateView::GetColor() const { + return color_; +} + +void BubbleDelegateView::Show() { + if (border_widget_) + border_widget_->Show(); + GetWidget()->Show(); + GetFocusManager()->SetFocusedView(GetInitiallyFocusedView()); +} + +void BubbleDelegateView::StartFade(bool fade_in) { + fade_animation_.reset(new ui::SlideAnimation(this)); + fade_animation_->SetSlideDuration(kHideFadeDurationMS); + fade_animation_->Reset(fade_in ? 0.0 : 1.0); + if (fade_in) { + original_opacity_ = 0; + if (border_widget_) + border_widget_->SetOpacity(original_opacity_); + GetWidget()->SetOpacity(original_opacity_); + Show(); + fade_animation_->Show(); + } else { + original_opacity_ = 255; + fade_animation_->Hide(); + } +} + +void BubbleDelegateView::ResetFade() { + fade_animation_.reset(); + if (border_widget_) + border_widget_->SetOpacity(original_opacity_); + GetWidget()->SetOpacity(original_opacity_); +} + +bool BubbleDelegateView::AcceleratorPressed( + const ui::Accelerator& accelerator) { + if (!close_on_esc() || accelerator.key_code() != ui::VKEY_ESCAPE) + return false; + if (fade_animation_.get()) + fade_animation_->Reset(); + GetWidget()->Close(); + return true; +} + +void BubbleDelegateView::AnimationEnded(const ui::Animation* animation) { + if (animation != fade_animation_.get()) + return; + bool closed = fade_animation_->GetCurrentValue() == 0; + fade_animation_->Reset(); + if (closed) + GetWidget()->Close(); +} + +void BubbleDelegateView::AnimationProgressed(const ui::Animation* animation) { + if (animation != fade_animation_.get()) + return; + DCHECK(fade_animation_->is_animating()); + unsigned char opacity = fade_animation_->GetCurrentValue() * 255; +#if defined(OS_WIN) && !defined(USE_AURA) + // Explicitly set the content Widget's layered style and set transparency via + // SetLayeredWindowAttributes. This is done because initializing the Widget as + // transparent and setting opacity via UpdateLayeredWindow doesn't support + // hosting child native Windows controls. + const HWND hwnd = GetWidget()->GetNativeView(); + const DWORD style = GetWindowLong(hwnd, GWL_EXSTYLE); + if ((opacity == 255) == !!(style & WS_EX_LAYERED)) + SetWindowLong(hwnd, GWL_EXSTYLE, style ^ WS_EX_LAYERED); + SetLayeredWindowAttributes(hwnd, 0, opacity, LWA_ALPHA); + // Update the border widget's opacity. + border_widget_->SetOpacity(opacity); + border_widget_->non_client_view()->SchedulePaint(); +#endif + GetWidget()->SetOpacity(opacity); + SchedulePaint(); +} + +void BubbleDelegateView::Init() {} + +void BubbleDelegateView::SizeToContents() { +#if defined(OS_WIN) && !defined(USE_AURA) + border_widget_->SetBounds(GetBubbleBounds()); + GetWidget()->SetBounds(GetBubbleClientBounds()); +#else + GetWidget()->SetBounds(GetBubbleBounds()); +#endif +} + +BubbleFrameView* BubbleDelegateView::GetBubbleFrameView() const { + const Widget* widget = border_widget_ ? border_widget_ : GetWidget(); + return static_cast<BubbleFrameView*>(widget->non_client_view()->frame_view()); +} + +gfx::Rect BubbleDelegateView::GetBubbleBounds() { + // The argument rect has its origin at the bubble's arrow anchor point; + // its size is the preferred size of the bubble's client view (this view). + return GetBubbleFrameView()->GetWindowBoundsForClientBounds( + gfx::Rect(GetAnchorPoint(), GetPreferredSize())); +} + +#if defined(OS_WIN) && !defined(USE_AURA) +gfx::Rect BubbleDelegateView::GetBubbleClientBounds() const { + gfx::Rect client_bounds(GetBubbleFrameView()->GetBoundsForClientView()); + client_bounds.Offset(border_widget_->GetWindowScreenBounds().origin()); + return client_bounds; +} +#endif + +} // namespace views diff --git a/ui/views/bubble/bubble_delegate.h b/ui/views/bubble/bubble_delegate.h new file mode 100644 index 0000000..a21bfab --- /dev/null +++ b/ui/views/bubble/bubble_delegate.h @@ -0,0 +1,153 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_BUBBLE_BUBBLE_DELEGATE_H_ +#define UI_VIEWS_BUBBLE_BUBBLE_DELEGATE_H_ +#pragma once + +#include "base/gtest_prod_util.h" +#include "ui/base/animation/animation_delegate.h" +#include "views/bubble/bubble_border.h" +#include "views/widget/widget.h" +#include "views/widget/widget_delegate.h" + +namespace ui { +class SlideAnimation; +} // namespace ui + +namespace views { + +class BubbleFrameView; + +// BubbleDelegateView creates frame and client views for bubble Widgets. +// BubbleDelegateView itself is the client's contents view. +// +/////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT BubbleDelegateView : public WidgetDelegateView, + public ui::AnimationDelegate, + public Widget::Observer { + public: + BubbleDelegateView(); + BubbleDelegateView(View* anchor_view, + BubbleBorder::ArrowLocation arrow_location, + const SkColor& color); + virtual ~BubbleDelegateView(); + + // Create and initialize the bubble Widget(s) with proper bounds. + static Widget* CreateBubble(BubbleDelegateView* bubble_delegate); + + // WidgetDelegate overrides: + virtual View* GetInitiallyFocusedView() OVERRIDE; + virtual BubbleDelegateView* AsBubbleDelegate() OVERRIDE; + virtual View* GetContentsView() OVERRIDE; + virtual NonClientFrameView* CreateNonClientFrameView() OVERRIDE; + + // Widget::Observer overrides: + virtual void OnWidgetActivationChanged(Widget* widget, bool active) OVERRIDE; + + bool close_on_esc() const { return close_on_esc_; } + void set_close_on_esc(bool close_on_esc) { close_on_esc_ = close_on_esc; } + + bool close_on_deactivate() const { return close_on_deactivate_; } + void set_close_on_deactivate(bool close_on_deactivate) { + close_on_deactivate_ = close_on_deactivate; + } + + bool allow_bubble_offscreen() const { return allow_bubble_offscreen_; } + void set_allow_bubble_offscreen(bool allow_bubble_offscreen) { + allow_bubble_offscreen_ = allow_bubble_offscreen; + } + + View* anchor_view() const { return anchor_view_; } + + bool use_focusless() const { return use_focusless_; } + void set_use_focusless(bool use_focusless) { + use_focusless_ = use_focusless; + } + + // Get the arrow's anchor point in screen space. + virtual gfx::Point GetAnchorPoint(); + + // Get the arrow's location on the bubble. + virtual BubbleBorder::ArrowLocation GetArrowLocation() const; + + // Get the color used for the background and border. + virtual SkColor GetColor() const; + + // Show the bubble's widget (and |border_widget_| on Windows). + void Show(); + + // Fade the bubble in or out via Widget transparency. + // Fade in calls Widget::Show; fade out calls Widget::Close upon completion. + void StartFade(bool fade_in); + + // Reset fade and opacity of bubble. Restore the opacity of the + // bubble to the setting before StartFade() was called. + void ResetFade(); + + protected: + // View overrides: + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) OVERRIDE; + + // ui::AnimationDelegate overrides: + virtual void AnimationEnded(const ui::Animation* animation) OVERRIDE; + virtual void AnimationProgressed(const ui::Animation* animation) OVERRIDE; + + // Perform view initialization on the contents for bubble sizing. + virtual void Init(); + + // Resizes and potentially moves the Bubble to best accommodate the + // contents preferred size. + void SizeToContents(); + + private: + FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewBasicTest, NonClientHitTest); + FRIEND_TEST_ALL_PREFIXES(BubbleDelegateTest, CreateDelegate); + + BubbleFrameView* GetBubbleFrameView() const; + + // Get bubble bounds from the anchor point and client view's preferred size. + gfx::Rect GetBubbleBounds(); + +#if defined(OS_WIN) && !defined(USE_AURA) + // Get bounds for the Windows-only widget that hosts the bubble's contents. + gfx::Rect GetBubbleClientBounds() const; +#endif + + // Fade animation for bubble. + scoped_ptr<ui::SlideAnimation> fade_animation_; + + // Flags controlling bubble closure on the escape key and deactivation. + bool close_on_esc_; + bool close_on_deactivate_; + + // Whether the bubble is allowed to be displayed offscreen, or if auto + // re-positioning should be performed. + bool allow_bubble_offscreen_; + + // The view hosting this bubble; the arrow is anchored to this view. + View* anchor_view_; + + // The arrow's location on the bubble. + BubbleBorder::ArrowLocation arrow_location_; + + // The background color of the bubble. + SkColor color_; + + // Original opacity of the bubble. + int original_opacity_; + + // The widget hosting the border for this bubble (non-Aura Windows only). + Widget* border_widget_; + + // Create a popup window for focusless bubbles on Linux/ChromeOS. + // These bubbles are not interactive and should not gain focus. + bool use_focusless_; + + DISALLOW_COPY_AND_ASSIGN(BubbleDelegateView); +}; + +} // namespace views + +#endif // UI_VIEWS_BUBBLE_BUBBLE_DELEGATE_H_ diff --git a/ui/views/bubble/bubble_delegate_unittest.cc b/ui/views/bubble/bubble_delegate_unittest.cc new file mode 100644 index 0000000..f6d256a --- /dev/null +++ b/ui/views/bubble/bubble_delegate_unittest.cc @@ -0,0 +1,31 @@ +// Copyright (c) 2011 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/test/views_test_base.h" +#include "views/bubble/bubble_delegate.h" +#include "views/bubble/bubble_frame_view.h" +#include "views/widget/widget.h" + +namespace views { + +typedef ViewsTestBase BubbleDelegateTest; + +TEST_F(BubbleDelegateTest, CreateDelegate) { + BubbleDelegateView* bubble_delegate = new BubbleDelegateView( + NULL, BubbleBorder::NONE, SK_ColorGREEN); + Widget* bubble_widget( + BubbleDelegateView::CreateBubble(bubble_delegate)); + EXPECT_EQ(bubble_delegate, bubble_widget->widget_delegate()); + EXPECT_EQ(bubble_widget, bubble_delegate->GetWidget()); + + BubbleBorder* border = + bubble_delegate->GetBubbleFrameView()->bubble_border(); + EXPECT_EQ(bubble_delegate->GetArrowLocation(), border->arrow_location()); + EXPECT_EQ(bubble_delegate->GetColor(), border->background_color()); + + bubble_widget->CloseNow(); + RunPendingMessages(); +} + +} // namespace views diff --git a/ui/views/bubble/bubble_frame_view.cc b/ui/views/bubble/bubble_frame_view.cc new file mode 100644 index 0000000..85e1f6e --- /dev/null +++ b/ui/views/bubble/bubble_frame_view.cc @@ -0,0 +1,78 @@ +// Copyright (c) 2011 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 "views/bubble/bubble_frame_view.h" + +#include <algorithm> + +#include "ui/views/window/client_view.h" +#include "views/bubble/border_contents_view.h" +#include "views/bubble/bubble_border.h" +#include "views/layout/fill_layout.h" +#include "views/widget/widget.h" + +namespace views { + +BubbleFrameView::BubbleFrameView(BubbleBorder::ArrowLocation location, + const gfx::Size& client_size, + SkColor color, + bool allow_bubble_offscreen) + : border_contents_(new BorderContentsView()), + location_(location), + allow_bubble_offscreen_(allow_bubble_offscreen) { + border_contents_->Init(); + bubble_border()->set_arrow_location(location_); + bubble_border()->set_background_color(color); + SetLayoutManager(new views::FillLayout()); + AddChildView(border_contents_); + gfx::Rect bounds(gfx::Point(), client_size); + gfx::Rect windows_bounds = GetWindowBoundsForClientBounds(bounds); + border_contents_->SetBoundsRect( + gfx::Rect(gfx::Point(), windows_bounds.size())); + SetBoundsRect(windows_bounds); +} + +BubbleFrameView::~BubbleFrameView() {} + +gfx::Rect BubbleFrameView::GetBoundsForClientView() const { + gfx::Insets margin; + bubble_border()->GetInsets(&margin); + margin += border_contents_->content_margins(); + return gfx::Rect(margin.left(), + margin.top(), + std::max(width() - margin.width(), 0), + std::max(height() - margin.height(), 0)); +} + +gfx::Rect BubbleFrameView::GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const { + // The |client_bounds| origin is the bubble arrow anchor point. + gfx::Rect position_relative_to(client_bounds.origin(), gfx::Size()); + // The |client_bounds| size is the bubble client view size. + gfx::Rect content_bounds; + gfx::Rect window_bounds; + border_contents_->SizeAndGetBounds(position_relative_to, + location_, + allow_bubble_offscreen_, + client_bounds.size(), + &content_bounds, + &window_bounds); + return window_bounds; +} + +int BubbleFrameView::NonClientHitTest(const gfx::Point& point) { + return GetWidget()->client_view()->NonClientHitTest(point); +} + +gfx::Size BubbleFrameView::GetPreferredSize() { + Widget* widget = GetWidget(); + gfx::Rect rect(gfx::Point(), widget->client_view()->GetPreferredSize()); + return widget->non_client_view()->GetWindowBoundsForClientBounds(rect).size(); +} + +BubbleBorder* BubbleFrameView::bubble_border() const { + return static_cast<BubbleBorder*>(border_contents_->border()); +} + +} // namespace views diff --git a/ui/views/bubble/bubble_frame_view.h b/ui/views/bubble/bubble_frame_view.h new file mode 100644 index 0000000..f134725 --- /dev/null +++ b/ui/views/bubble/bubble_frame_view.h @@ -0,0 +1,57 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_BUBBLE_BUBBLE_FRAME_VIEW_H_ +#define UI_VIEWS_BUBBLE_BUBBLE_FRAME_VIEW_H_ +#pragma once + +#include "base/gtest_prod_util.h" +#include "ui/views/window/non_client_view.h" +#include "views/bubble/bubble_border.h" + +namespace views { + +class BorderContentsView; + +// BubbleFrameView to render BubbleBorder. +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView { + public: + BubbleFrameView(BubbleBorder::ArrowLocation location, + const gfx::Size& client_size, + SkColor color, + bool allow_bubble_offscreen); + virtual ~BubbleFrameView(); + + // NonClientFrameView overrides: + virtual gfx::Rect GetBoundsForClientView() const OVERRIDE; + virtual gfx::Rect GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const OVERRIDE; + virtual int NonClientHitTest(const gfx::Point& point) OVERRIDE; + virtual void GetWindowMask(const gfx::Size& size, + gfx::Path* window_mask) OVERRIDE {} + virtual void EnableClose(bool enable) OVERRIDE {} + virtual void ResetWindowControls() OVERRIDE {} + virtual void UpdateWindowIcon() OVERRIDE {} + + // View overrides: + virtual gfx::Size GetPreferredSize() OVERRIDE; + + // Accessor for bubble border inside border contents. + BubbleBorder* bubble_border() const; + + private: + FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewBasicTest, GetBoundsForClientView); + + BorderContentsView* border_contents_; + BubbleBorder::ArrowLocation location_; + bool allow_bubble_offscreen_; + + DISALLOW_COPY_AND_ASSIGN(BubbleFrameView); +}; + +} // namespace views + +#endif // UI_VIEWS_BUBBLE_BUBBLE_FRAME_VIEW_H_ diff --git a/ui/views/bubble/bubble_frame_view_unittest.cc b/ui/views/bubble/bubble_frame_view_unittest.cc new file mode 100644 index 0000000..6699bfd --- /dev/null +++ b/ui/views/bubble/bubble_frame_view_unittest.cc @@ -0,0 +1,68 @@ +// Copyright (c) 2011 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/base/hit_test.h" +#include "ui/views/test/views_test_base.h" +#include "views/bubble/border_contents_view.h" +#include "views/bubble/bubble_border.h" +#include "views/bubble/bubble_delegate.h" +#include "views/bubble/bubble_frame_view.h" +#include "views/widget/widget.h" + +namespace views { + +typedef ViewsTestBase BubbleFrameViewBasicTest; + +const BubbleBorder::ArrowLocation kArrow = BubbleBorder::TOP_LEFT; +const gfx::Rect kRect(10, 10, 200, 200); +const SkColor kBackgroundColor = SK_ColorRED; +const bool kAllowBubbleOffscreen = true; + +TEST_F(BubbleFrameViewBasicTest, GetBoundsForClientView) { + BubbleFrameView frame(kArrow, kRect.size(), kBackgroundColor, + kAllowBubbleOffscreen); + EXPECT_EQ(frame.GetWindowBoundsForClientBounds(kRect).size(), frame.size()); + EXPECT_EQ(kArrow, frame.bubble_border()->arrow_location()); + EXPECT_EQ(kBackgroundColor, frame.bubble_border()->background_color()); + + int margin_x = frame.border_contents_->content_margins().left(); + int margin_y = frame.border_contents_->content_margins().top(); + gfx::Insets insets; + frame.bubble_border()->GetInsets(&insets); + EXPECT_EQ(insets.left() + margin_x, frame.GetBoundsForClientView().x()); + EXPECT_EQ(insets.top() + margin_y, frame.GetBoundsForClientView().y()); +} + +namespace { + +class SizedBubbleDelegateView : public BubbleDelegateView { + public: + SizedBubbleDelegateView() {} + virtual ~SizedBubbleDelegateView() {} + + // View overrides: + virtual gfx::Size GetPreferredSize() OVERRIDE; + + private: + DISALLOW_COPY_AND_ASSIGN(SizedBubbleDelegateView); +}; + +gfx::Size SizedBubbleDelegateView::GetPreferredSize() { return kRect.size(); } + +} // namespace + +TEST_F(BubbleFrameViewBasicTest, NonClientHitTest) { + BubbleDelegateView* delegate = new SizedBubbleDelegateView(); + Widget* widget(BubbleDelegateView::CreateBubble(delegate)); + delegate->Show(); + gfx::Point kPtInBound(100, 100); + gfx::Point kPtOutsideBound(1000, 1000); + BubbleFrameView* bubble_frame_view = delegate->GetBubbleFrameView(); + EXPECT_EQ(HTCLIENT, bubble_frame_view->NonClientHitTest(kPtInBound)); + EXPECT_EQ(HTNOWHERE, bubble_frame_view->NonClientHitTest(kPtOutsideBound)); + widget->CloseNow(); + RunPendingMessages(); +} + +} // namespace views diff --git a/ui/views/events/event.cc b/ui/views/events/event.cc new file mode 100644 index 0000000..67d165e --- /dev/null +++ b/ui/views/events/event.cc @@ -0,0 +1,200 @@ +// Copyright (c) 2011 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 "views/events/event.h" + +#include "base/logging.h" +#include "ui/base/keycodes/keyboard_code_conversion.h" +#include "views/view.h" +#include "views/widget/root_view.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// Event, protected: + +Event::Event(ui::EventType type, int flags) + : type_(type), + time_stamp_(base::Time::NowFromSystemTime()), + flags_(flags) { + // Safely initialize the pointer/struct to null/empty. + memset(&native_event_, 0, sizeof(native_event_)); +#if defined(TOOLKIT_USES_GTK) + gdk_event_ = NULL; +#endif +} + +Event::Event(const NativeEvent& native_event, ui::EventType type, int flags) + : native_event_(native_event), + type_(type), + time_stamp_(base::Time::NowFromSystemTime()), + flags_(flags) { +#if defined(TOOLKIT_USES_GTK) + gdk_event_ = NULL; +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// LocatedEvent, protected: + +#if !defined(USE_AURA) +LocatedEvent::LocatedEvent(const NativeEvent& native_event) + : Event(native_event, + ui::EventTypeFromNative(native_event), + ui::EventFlagsFromNative(native_event)), + location_(ui::EventLocationFromNative(native_event)) { +} +#endif + +// TODO(msw): Kill this legacy constructor when we update uses. +LocatedEvent::LocatedEvent(ui::EventType type, + const gfx::Point& location, + int flags) + : Event(type, flags), + location_(location) { +} + +LocatedEvent::LocatedEvent(const LocatedEvent& model, + View* source, + View* target) + : Event(model), + location_(model.location_) { + if (target && target != source) + View::ConvertPointToView(source, target, &location_); +} + +LocatedEvent::LocatedEvent(const LocatedEvent& model, View* root) + : Event(model), + location_(model.location_) { + View::ConvertPointFromWidget(root, &location_); +} + +//////////////////////////////////////////////////////////////////////////////// +// KeyEvent, public: + +#if !defined(USE_AURA) +KeyEvent::KeyEvent(const NativeEvent& native_event) + : Event(native_event, + ui::EventTypeFromNative(native_event), + ui::EventFlagsFromNative(native_event)), + key_code_(ui::KeyboardCodeFromNative(native_event)), + character_(0), + unmodified_character_(0) { +} +#endif + +KeyEvent::KeyEvent(ui::EventType type, + ui::KeyboardCode key_code, + int event_flags) + : Event(type, event_flags), + key_code_(key_code), + character_(ui::GetCharacterFromKeyCode(key_code, event_flags)), + unmodified_character_(0) { +} + +//////////////////////////////////////////////////////////////////////////////// +// MouseEvent, public: + +MouseEvent::MouseEvent(const NativeEvent& native_event) + : LocatedEvent(native_event) { +} + +MouseEvent::MouseEvent(const MouseEvent& model, View* source, View* target) + : LocatedEvent(model, source, target) { +} + +MouseEvent::MouseEvent(const TouchEvent& touch) + : LocatedEvent(touch.native_event()) { + // The location of the event is correctly extracted from the native event. But + // it is necessary to update the event type. + ui::EventType mtype = ui::ET_UNKNOWN; + switch (touch.type()) { + case ui::ET_TOUCH_RELEASED: + mtype = ui::ET_MOUSE_RELEASED; + break; + case ui::ET_TOUCH_PRESSED: + mtype = ui::ET_MOUSE_PRESSED; + break; + case ui::ET_TOUCH_MOVED: + mtype = ui::ET_MOUSE_MOVED; + break; + default: + NOTREACHED() << "Invalid mouse event."; + } + set_type(mtype); + + // It may not be possible to extract the button-information necessary for a + // MouseEvent from the native event for a TouchEvent, so the flags are + // explicitly updated as well. The button is approximated from the touchpoint + // identity. + int new_flags = flags() & ~(ui::EF_LEFT_BUTTON_DOWN | + ui::EF_RIGHT_BUTTON_DOWN | + ui::EF_MIDDLE_BUTTON_DOWN); + int button = ui::EF_LEFT_BUTTON_DOWN; + if (touch.identity() == 1) + button = ui::EF_RIGHT_BUTTON_DOWN; + else if (touch.identity() == 2) + button = ui::EF_MIDDLE_BUTTON_DOWN; + set_flags(new_flags | button); +} + +//////////////////////////////////////////////////////////////////////////////// +// MouseWheelEvent, public: + +#if !defined(USE_AURA) +MouseWheelEvent::MouseWheelEvent(const NativeEvent& native_event) + : MouseEvent(native_event), + offset_(ui::GetMouseWheelOffset(native_event)) { +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// TouchEvent, public: + +TouchEvent::TouchEvent(ui::EventType type, + int x, + int y, + int flags, + int touch_id, + float radius_x, + float radius_y, + float angle, + float force) + : LocatedEvent(type, gfx::Point(x, y), flags), + touch_id_(touch_id), + radius_x_(radius_x), + radius_y_(radius_y), + rotation_angle_(angle), + force_(force) { +} + +TouchEvent::TouchEvent(const TouchEvent& model, View* source, View* target) + : LocatedEvent(model, source, target), + touch_id_(model.touch_id_), + radius_x_(model.radius_x_), + radius_y_(model.radius_y_), + rotation_angle_(model.rotation_angle_), + force_(model.force_) { +} + +//////////////////////////////////////////////////////////////////////////////// +// TouchEvent, private: + +TouchEvent::TouchEvent(const TouchEvent& model, View* root) + : LocatedEvent(model, root), + touch_id_(model.touch_id_), + radius_x_(model.radius_x_), + radius_y_(model.radius_y_), + rotation_angle_(model.rotation_angle_), + force_(model.force_) { +} + +//////////////////////////////////////////////////////////////////////////////// +// MouseWheelEvent, public: + +// This value matches windows WHEEL_DELTA. +// static +const int MouseWheelEvent::kWheelDelta = 120; + +} // namespace views diff --git a/ui/views/events/event.h b/ui/views/events/event.h new file mode 100644 index 0000000..bcf57c5 --- /dev/null +++ b/ui/views/events/event.h @@ -0,0 +1,415 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_EVENTS_EVENT_H_ +#define UI_VIEWS_EVENTS_EVENT_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/time.h" +#include "ui/base/events.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/gfx/point.h" +#include "views/views_export.h" + +namespace ui { +class OSExchangeData; +} + +#if defined(USE_AURA) +namespace aura { +class Event; +} +#endif + +// TODO(msw): Remove GTK support from views event code when possible. +#if defined(TOOLKIT_USES_GTK) +typedef union _GdkEvent GdkEvent; +#endif + +namespace views { + +#if defined(USE_AURA) +typedef aura::Event* NativeEvent; +#else +typedef base::NativeEvent NativeEvent; +#endif + +class View; + +namespace internal { +class NativeWidgetView; +class RootView; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Event class +// +// An event encapsulates an input event that can be propagated into view +// hierarchies. An event has a type, some flags and a time stamp. +// +// Each major event type has a corresponding Event subclass. +// +// Events are immutable but support copy +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT Event { + public: + const NativeEvent& native_event() const { return native_event_; } +#if defined(TOOLKIT_USES_GTK) + GdkEvent* gdk_event() const { return gdk_event_; } +#endif + ui::EventType type() const { return type_; } + const base::Time& time_stamp() const { return time_stamp_; } + int flags() const { return flags_; } + void set_flags(int flags) { flags_ = flags; } + + // The following methods return true if the respective keys were pressed at + // the time the event was created. + bool IsShiftDown() const { return (flags_ & ui::EF_SHIFT_DOWN) != 0; } + bool IsControlDown() const { return (flags_ & ui::EF_CONTROL_DOWN) != 0; } + bool IsCapsLockDown() const { return (flags_ & ui::EF_CAPS_LOCK_DOWN) != 0; } + bool IsAltDown() const { return (flags_ & ui::EF_ALT_DOWN) != 0; } + + bool IsMouseEvent() const { + return type_ == ui::ET_MOUSE_PRESSED || + type_ == ui::ET_MOUSE_DRAGGED || + type_ == ui::ET_MOUSE_RELEASED || + type_ == ui::ET_MOUSE_MOVED || + type_ == ui::ET_MOUSE_ENTERED || + type_ == ui::ET_MOUSE_EXITED || + type_ == ui::ET_MOUSEWHEEL; + } + + bool IsTouchEvent() const { + return type_ == ui::ET_TOUCH_RELEASED || + type_ == ui::ET_TOUCH_PRESSED || + type_ == ui::ET_TOUCH_MOVED || + type_ == ui::ET_TOUCH_STATIONARY || + type_ == ui::ET_TOUCH_CANCELLED; + } + + protected: + Event(ui::EventType type, int flags); + Event(const NativeEvent& native_event, ui::EventType type, int flags); +#if defined(TOOLKIT_USES_GTK) + Event(GdkEvent* gdk_event, ui::EventType type, int flags); +#endif + + Event(const Event& model) + : native_event_(model.native_event()), +#if defined(TOOLKIT_USES_GTK) + gdk_event_(model.gdk_event_), +#endif + type_(model.type()), + time_stamp_(model.time_stamp()), + flags_(model.flags()) { + } + + void set_type(ui::EventType type) { type_ = type; } + + private: + void operator=(const Event&); + + NativeEvent native_event_; +#if defined(TOOLKIT_USES_GTK) + GdkEvent* gdk_event_; +#endif + ui::EventType type_; + base::Time time_stamp_; + int flags_; +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// LocatedEvent class +// +// A generic event that is used for any events that is located at a specific +// position in the screen. +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT LocatedEvent : public Event { + public: + int x() const { return location_.x(); } + int y() const { return location_.y(); } + const gfx::Point& location() const { return location_; } + + protected: + explicit LocatedEvent(const NativeEvent& native_event); +#if defined(TOOLKIT_USES_GTK) + explicit LocatedEvent(GdkEvent* gdk_event); +#endif + + // TODO(msw): Kill this legacy constructor when we update uses. + // Simple initialization from cracked metadata. + LocatedEvent(ui::EventType type, const gfx::Point& location, int flags); + + // Create a new LocatedEvent which is identical to the provided model. + // If source / target views are provided, the model location will be converted + // from |source| coordinate system to |target| coordinate system. + LocatedEvent(const LocatedEvent& model, View* source, View* target); + + // This constructor is to allow converting the location of an event from the + // widget's coordinate system to the RootView's coordinate system. + LocatedEvent(const LocatedEvent& model, View* root); + + gfx::Point location_; +}; + +class TouchEvent; + +//////////////////////////////////////////////////////////////////////////////// +// +// MouseEvent class +// +// A mouse event is used for any input event related to the mouse. +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT MouseEvent : public LocatedEvent { + public: + explicit MouseEvent(const NativeEvent& native_event); +#if defined(TOOLKIT_USES_GTK) + explicit MouseEvent(GdkEvent* gdk_event); +#endif + + // Create a new MouseEvent which is identical to the provided model. + // If source / target views are provided, the model location will be converted + // from |source| coordinate system to |target| coordinate system. + MouseEvent(const MouseEvent& model, View* source, View* target); + + // Creates a new MouseEvent from a TouchEvent. The location of the TouchEvent + // is the same as the MouseEvent. Other attributes (e.g. type, flags) are + // mapped from the TouchEvent to appropriate MouseEvent attributes. + // GestureManager uses this to convert TouchEvents that are not handled by any + // view. + explicit MouseEvent(const TouchEvent& touch); + + // TODO(msw): Kill this legacy constructor when we update uses. + // Create a new mouse event + MouseEvent(ui::EventType type, int x, int y, int flags) + : LocatedEvent(type, gfx::Point(x, y), flags) { + } + + // Conveniences to quickly test what button is down + bool IsOnlyLeftMouseButton() const { + return (flags() & ui::EF_LEFT_BUTTON_DOWN) && + !(flags() & (ui::EF_MIDDLE_BUTTON_DOWN | ui::EF_RIGHT_BUTTON_DOWN)); + } + + bool IsLeftMouseButton() const { + return (flags() & ui::EF_LEFT_BUTTON_DOWN) != 0; + } + + bool IsOnlyMiddleMouseButton() const { + return (flags() & ui::EF_MIDDLE_BUTTON_DOWN) && + !(flags() & (ui::EF_LEFT_BUTTON_DOWN | ui::EF_RIGHT_BUTTON_DOWN)); + } + + bool IsMiddleMouseButton() const { + return (flags() & ui::EF_MIDDLE_BUTTON_DOWN) != 0; + } + + bool IsOnlyRightMouseButton() const { + return (flags() & ui::EF_RIGHT_BUTTON_DOWN) && + !(flags() & (ui::EF_LEFT_BUTTON_DOWN | ui::EF_MIDDLE_BUTTON_DOWN)); + } + + bool IsRightMouseButton() const { + return (flags() & ui::EF_RIGHT_BUTTON_DOWN) != 0; + } + + protected: + MouseEvent(const MouseEvent& model, View* root) + : LocatedEvent(model, root) { + } + + private: + friend class internal::NativeWidgetView; + friend class internal::RootView; + + DISALLOW_COPY_AND_ASSIGN(MouseEvent); +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// TouchEvent class +// +// A touch event is generated by touch screen and advanced track +// pad devices. There is a deliberate direct correspondence between +// TouchEvent and PlatformTouchPoint. +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT TouchEvent : public LocatedEvent { + public: + explicit TouchEvent(const NativeEvent& native_event); + + // Create a new touch event. + TouchEvent(ui::EventType type, + int x, + int y, + int flags, + int touch_id, + float radius_x, + float radius_y, + float angle, + float force); + + // Create a new TouchEvent which is identical to the provided model. + // If source / target views are provided, the model location will be converted + // from |source| coordinate system to |target| coordinate system. + TouchEvent(const TouchEvent& model, View* source, View* target); + + int identity() const { return touch_id_; } + + float radius_x() const { return radius_x_; } + float radius_y() const { return radius_y_; } + float rotation_angle() const { return rotation_angle_; } + float force() const { return force_; } + + private: + friend class internal::NativeWidgetView; + friend class internal::RootView; + + TouchEvent(const TouchEvent& model, View* root); + + // The identity (typically finger) of the touch starting at 0 and incrementing + // for each separable additional touch that the hardware can detect. + const int touch_id_; + + // Radius of the X (major) axis of the touch ellipse. 1.0 if unknown. + const float radius_x_; + + // Radius of the Y (minor) axis of the touch ellipse. 1.0 if unknown. + const float radius_y_; + + // Angle of the major axis away from the X axis. Default 0.0. + const float rotation_angle_; + + // Force (pressure) of the touch. Normalized to be [0, 1]. Default to be 0.0. + const float force_; + + DISALLOW_COPY_AND_ASSIGN(TouchEvent); +}; + +//////////////////////////////////////////////////////////////////////////////// +// KeyEvent class +// +// KeyEvent encapsulates keyboard input events - key press and release. +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT KeyEvent : public Event { + public: + explicit KeyEvent(const NativeEvent& native_event); +#if defined(TOOLKIT_USES_GTK) + explicit KeyEvent(GdkEvent* gdk_event); +#endif + + // Creates a new KeyEvent synthetically (i.e. not in response to an input + // event from the host environment). This is typically only used in testing as + // some metadata obtainable from the underlying native event is not present. + // It's also used by input methods to fabricate keyboard events. + KeyEvent(ui::EventType type, + ui::KeyboardCode key_code, + int event_flags); + + ui::KeyboardCode key_code() const { return key_code_; } + + // These setters allow an I18N virtual keyboard to fabricate a keyboard event + // which does not have a corresponding ui::KeyboardCode (example: U+00E1 Latin + // small letter A with acute, U+0410 Cyrillic capital letter A.) + // GetCharacter() and GetUnmodifiedCharacter() return the character. + void set_character(uint16 character) { character_ = character; } + void set_unmodified_character(uint16 unmodified_character) { + unmodified_character_ = unmodified_character; + } + + // Gets the character generated by this key event. It only supports Unicode + // BMP characters. + uint16 GetCharacter() const; + + // Gets the character generated by this key event ignoring concurrently-held + // modifiers (except shift). + uint16 GetUnmodifiedCharacter() const; + + private: + ui::KeyboardCode key_code_; + + uint16 character_; + uint16 unmodified_character_; + + DISALLOW_COPY_AND_ASSIGN(KeyEvent); +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// MouseWheelEvent class +// +// A MouseWheelEvent is used to propagate mouse wheel user events. +// Note: e.GetOffset() > 0 means scroll up / left. +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT MouseWheelEvent : public MouseEvent { + public: + // See |offset| for details. + static const int kWheelDelta; + + explicit MouseWheelEvent(const NativeEvent& native_event); +#if defined(TOOLKIT_USES_GTK) + explicit MouseWheelEvent(GdkEvent* gdk_event); +#endif + + // The amount to scroll. This is in multiples of kWheelDelta. + int offset() const { return offset_; } + + private: + friend class internal::RootView; + friend class internal::NativeWidgetView; + + MouseWheelEvent(const MouseWheelEvent& model, View* root) + : MouseEvent(model, root), + offset_(model.offset_) { + } + + int offset_; + + DISALLOW_COPY_AND_ASSIGN(MouseWheelEvent); +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// DropTargetEvent class +// +// A DropTargetEvent is sent to the view the mouse is over during a drag and +// drop operation. +// +//////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT DropTargetEvent : public LocatedEvent { + public: + DropTargetEvent(const ui::OSExchangeData& data, + int x, + int y, + int source_operations) + : LocatedEvent(ui::ET_DROP_TARGET_EVENT, gfx::Point(x, y), 0), + data_(data), + source_operations_(source_operations) { + // TODO(msw): Hook up key state flags for CTRL + drag and drop, etc. + } + + const ui::OSExchangeData& data() const { return data_; } + int source_operations() const { return source_operations_; } + + private: + // Data associated with the drag/drop session. + const ui::OSExchangeData& data_; + + // Bitmask of supported ui::DragDropTypes::DragOperation by the source. + int source_operations_; + + DISALLOW_COPY_AND_ASSIGN(DropTargetEvent); +}; + +} // namespace views + +#endif // UI_VIEWS_EVENTS_EVENT_H_ diff --git a/ui/views/events/event_aura.cc b/ui/views/events/event_aura.cc new file mode 100644 index 0000000..a4d3169 --- /dev/null +++ b/ui/views/events/event_aura.cc @@ -0,0 +1,62 @@ +// Copyright (c) 2011 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 "views/events/event.h" + +#include "base/logging.h" +#include "ui/aura/event.h" +#include "ui/base/keycodes/keyboard_code_conversion.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// LocatedEvent, protected: + +LocatedEvent::LocatedEvent(const NativeEvent& native_event) + : Event(native_event, native_event->type(), native_event->flags()), + location_(static_cast<aura::LocatedEvent*>(native_event)->location()) { +} + +//////////////////////////////////////////////////////////////////////////////// +// TouchEvent, public: + +TouchEvent::TouchEvent(const NativeEvent& event) + : LocatedEvent(event), + touch_id_(static_cast<aura::TouchEvent*>(event)->touch_id()), + radius_x_(static_cast<aura::TouchEvent*>(event)->radius_x()), + radius_y_(static_cast<aura::TouchEvent*>(event)->radius_y()), + rotation_angle_(static_cast<aura::TouchEvent*>(event)->rotation_angle()), + force_(static_cast<aura::TouchEvent*>(event)->force()) { +} + +//////////////////////////////////////////////////////////////////////////////// +// KeyEvent, public: + +KeyEvent::KeyEvent(const NativeEvent& native_event) + : Event(native_event, native_event->type(), native_event->flags()), + key_code_(static_cast<aura::KeyEvent*>(native_event)->key_code()), + character_(ui::GetCharacterFromKeyCode(key_code_, flags())), + unmodified_character_(0) { +} + +uint16 KeyEvent::GetCharacter() const { + return character_; +} + +uint16 KeyEvent::GetUnmodifiedCharacter() const { + if (unmodified_character_) + return unmodified_character_; + + return ui::GetCharacterFromKeyCode(key_code_, flags() & ui::EF_SHIFT_DOWN); +} + +//////////////////////////////////////////////////////////////////////////////// +// MouseWheelEvent, public: + +MouseWheelEvent::MouseWheelEvent(const NativeEvent& native_event) + : MouseEvent(native_event), + offset_(ui::GetMouseWheelOffset(native_event->native_event())) { +} + +} // namespace views diff --git a/ui/views/events/event_gtk.cc b/ui/views/events/event_gtk.cc new file mode 100644 index 0000000..fb0c88a --- /dev/null +++ b/ui/views/events/event_gtk.cc @@ -0,0 +1,183 @@ +// Copyright (c) 2011 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 "views/events/event.h" + +#include <gdk/gdk.h> + +#include "base/logging.h" +#include "ui/base/keycodes/keyboard_code_conversion_gtk.h" +#include "ui/gfx/point.h" + +namespace { + +unsigned int GetGdkStateFromNative(GdkEvent* gdk_event) { + switch (gdk_event->type) { + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + return gdk_event->key.state; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + return gdk_event->button.state; + case GDK_SCROLL: + return gdk_event->scroll.state; + case GDK_MOTION_NOTIFY: + return gdk_event->motion.state; + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + return gdk_event->crossing.state; + default: + NOTREACHED(); + break; + } + return 0; +} + +int GetFlagsFromGdkState(unsigned int state) { + int flags = 0; + flags |= (state & GDK_LOCK_MASK) ? ui::EF_CAPS_LOCK_DOWN : 0; + flags |= (state & GDK_CONTROL_MASK) ? ui::EF_CONTROL_DOWN : 0; + flags |= (state & GDK_SHIFT_MASK) ? ui::EF_SHIFT_DOWN : 0; + flags |= (state & GDK_MOD1_MASK) ? ui::EF_ALT_DOWN : 0; + flags |= (state & GDK_BUTTON1_MASK) ? ui::EF_LEFT_BUTTON_DOWN : 0; + flags |= (state & GDK_BUTTON2_MASK) ? ui::EF_MIDDLE_BUTTON_DOWN : 0; + flags |= (state & GDK_BUTTON3_MASK) ? ui::EF_RIGHT_BUTTON_DOWN : 0; + return flags; +} + +//////////////////////////////////////////////////////////////////////////////// +// These functions mirror ui/base/events.h, but GTK is in the midst of removal. + +ui::EventType EventTypeFromNative(GdkEvent* gdk_event) { + // Add new event types as necessary. + switch (gdk_event->type) { + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_PRESS: + return ui::ET_MOUSE_PRESSED; + case GDK_BUTTON_RELEASE: + return ui::ET_MOUSE_RELEASED; + case GDK_DRAG_MOTION: + return ui::ET_MOUSE_DRAGGED; + case GDK_ENTER_NOTIFY: + return ui::ET_MOUSE_ENTERED; + case GDK_KEY_PRESS: + return ui::ET_KEY_PRESSED; + case GDK_KEY_RELEASE: + return ui::ET_KEY_RELEASED; + case GDK_LEAVE_NOTIFY: + return ui::ET_MOUSE_EXITED; + case GDK_MOTION_NOTIFY: + if (gdk_event->motion.state & GDK_BUTTON1_MASK || + gdk_event->motion.state & GDK_BUTTON2_MASK || + gdk_event->motion.state & GDK_BUTTON3_MASK || + gdk_event->motion.state & GDK_BUTTON4_MASK || + gdk_event->motion.state & GDK_BUTTON5_MASK) { + return ui::ET_MOUSE_DRAGGED; + } + return ui::ET_MOUSE_MOVED; + case GDK_SCROLL: + return ui::ET_MOUSEWHEEL; + default: + NOTREACHED(); + break; + } + return ui::ET_UNKNOWN; +} + +int EventFlagsFromNative(GdkEvent* gdk_event) { + int flags = GetFlagsFromGdkState(GetGdkStateFromNative(gdk_event)); + if (gdk_event->type == GDK_2BUTTON_PRESS) + flags |= ui::EF_IS_DOUBLE_CLICK; + if (gdk_event->type == GDK_BUTTON_PRESS || + gdk_event->type == GDK_2BUTTON_PRESS || + gdk_event->type == GDK_3BUTTON_PRESS || + gdk_event->type == GDK_BUTTON_RELEASE) { + switch (gdk_event->button.button) { + case 1: + return flags | ui::EF_LEFT_BUTTON_DOWN; + case 2: + return flags | ui::EF_MIDDLE_BUTTON_DOWN; + case 3: + return flags | ui::EF_RIGHT_BUTTON_DOWN; + } + } + return flags; +} + +gfx::Point EventLocationFromNative(GdkEvent* gdk_event) { + double x = 0, y = 0; + if (gdk_event_get_coords(gdk_event, &x, &y)) + return gfx::Point(static_cast<int>(x), static_cast<int>(y)); + return gfx::Point(); +} + +ui::KeyboardCode KeyboardCodeFromNative(GdkEvent* gdk_event) { + DCHECK(gdk_event->type == GDK_KEY_PRESS || + gdk_event->type == GDK_KEY_RELEASE); + return ui::KeyboardCodeFromGdkEventKey(&gdk_event->key); +} + +int GetMouseWheelOffset(GdkEvent* gdk_event) { + DCHECK(gdk_event->type == GDK_SCROLL); + int offset = (gdk_event->scroll.direction == GDK_SCROLL_UP || + gdk_event->scroll.direction == GDK_SCROLL_LEFT) ? 1 : -1; + return offset; +} + +} // namespace + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// Event, protected: + +Event::Event(GdkEvent* gdk_event, ui::EventType type, int flags) + : native_event_(NULL), + gdk_event_(gdk_event), + type_(type), + time_stamp_(base::Time::NowFromSystemTime()), + flags_(flags) { +} + +//////////////////////////////////////////////////////////////////////////////// +// LocatedEvent, protected: + +LocatedEvent::LocatedEvent(GdkEvent* gdk_event) + : Event(gdk_event, + EventTypeFromNative(gdk_event), + EventFlagsFromNative(gdk_event)), + location_(EventLocationFromNative(gdk_event)) { +} + +//////////////////////////////////////////////////////////////////////////////// +// KeyEvent, public: + +KeyEvent::KeyEvent(GdkEvent* gdk_event) + : Event(gdk_event, + EventTypeFromNative(gdk_event), + EventFlagsFromNative(gdk_event)), + key_code_(KeyboardCodeFromNative(gdk_event)), + character_(0), + unmodified_character_(0) { +} + +//////////////////////////////////////////////////////////////////////////////// +// MouseEvent, public: + +MouseEvent::MouseEvent(GdkEvent* gdk_event) + : LocatedEvent(gdk_event) { +} + +//////////////////////////////////////////////////////////////////////////////// +// MouseWheelEvent, public: + +MouseWheelEvent::MouseWheelEvent(GdkEvent* gdk_event) + : MouseEvent(gdk_event), + offset_(kWheelDelta * GetMouseWheelOffset(gdk_event)) { +} + +} // namespace views diff --git a/ui/views/events/event_unittest.cc b/ui/views/events/event_unittest.cc new file mode 100644 index 0000000..211877d --- /dev/null +++ b/ui/views/events/event_unittest.cc @@ -0,0 +1,128 @@ +// Copyright (c) 2011 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 "testing/gtest/include/gtest/gtest.h" + +#include "base/basictypes.h" +#include "views/events/event.h" + +// Bug 99129. +#if defined(USE_AURA) +#define MAYBE_KeyEvent FAILS_KeyEvent +#define MAYBE_KeyEventDirectUnicode FAILS_KeyEventDirectUnicode +#else +#define MAYBE_KeyEvent KeyEvent +#define MAYBE_KeyEventDirectUnicode KeyEventDirectUnicode +#endif + +namespace views { + +class EventTest : public testing::Test { + public: + EventTest() { + } + + private: + DISALLOW_COPY_AND_ASSIGN(EventTest); +}; + +TEST_F(EventTest, MAYBE_KeyEvent) { + static const struct { + ui::KeyboardCode key_code; + int flags; + uint16 character; + uint16 unmodified_character; + } kTestData[] = { + { ui::VKEY_A, 0, 'a', 'a' }, + { ui::VKEY_A, ui::EF_SHIFT_DOWN, 'A', 'A' }, + { ui::VKEY_A, ui::EF_CAPS_LOCK_DOWN, 'A', 'a' }, + { ui::VKEY_A, ui::EF_SHIFT_DOWN | ui::EF_CAPS_LOCK_DOWN, 'a', 'A' }, + { ui::VKEY_A, ui::EF_CONTROL_DOWN, 0x01, 'a' }, + { ui::VKEY_A, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\x01', 'A' }, + { ui::VKEY_Z, 0, 'z', 'z' }, + { ui::VKEY_Z, ui::EF_SHIFT_DOWN, 'Z', 'Z' }, + { ui::VKEY_Z, ui::EF_CAPS_LOCK_DOWN, 'Z', 'z' }, + { ui::VKEY_Z, ui::EF_SHIFT_DOWN | ui::EF_CAPS_LOCK_DOWN, 'z', 'Z' }, + { ui::VKEY_Z, ui::EF_CONTROL_DOWN, '\x1A', 'z' }, + { ui::VKEY_Z, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\x1A', 'Z' }, + + { ui::VKEY_2, ui::EF_CONTROL_DOWN, '\0', '2' }, + { ui::VKEY_2, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\0', '@' }, + { ui::VKEY_6, ui::EF_CONTROL_DOWN, '\0', '6' }, + { ui::VKEY_6, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\x1E', '^' }, + { ui::VKEY_OEM_MINUS, ui::EF_CONTROL_DOWN, '\0', '-' }, + { ui::VKEY_OEM_MINUS, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\x1F', '_'}, + { ui::VKEY_OEM_4, ui::EF_CONTROL_DOWN, '\x1B', '[' }, + { ui::VKEY_OEM_4, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\0', '{' }, + { ui::VKEY_OEM_5, ui::EF_CONTROL_DOWN, '\x1C', '\\' }, + { ui::VKEY_OEM_5, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\0', '|' }, + { ui::VKEY_OEM_6, ui::EF_CONTROL_DOWN, '\x1D', ']' }, + { ui::VKEY_OEM_6, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\0', '}' }, + { ui::VKEY_RETURN, ui::EF_CONTROL_DOWN, '\x0A', '\r' }, + + { ui::VKEY_0, 0, '0', '0' }, + { ui::VKEY_0, ui::EF_SHIFT_DOWN, ')', ')' }, + { ui::VKEY_0, ui::EF_SHIFT_DOWN | ui::EF_CAPS_LOCK_DOWN, ')', ')' }, + { ui::VKEY_0, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\0', ')' }, + + { ui::VKEY_9, 0, '9', '9' }, + { ui::VKEY_9, ui::EF_SHIFT_DOWN, '(', '(' }, + { ui::VKEY_9, ui::EF_SHIFT_DOWN | ui::EF_CAPS_LOCK_DOWN, '(', '(' }, + { ui::VKEY_9, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, '\0', '(' }, + + { ui::VKEY_NUMPAD0, ui::EF_CONTROL_DOWN, '\0', '0' }, + { ui::VKEY_NUMPAD0, ui::EF_SHIFT_DOWN, '0', '0' }, + + { ui::VKEY_NUMPAD9, ui::EF_CONTROL_DOWN, '\0', '9' }, + { ui::VKEY_NUMPAD9, ui::EF_SHIFT_DOWN, '9', '9' }, + + { ui::VKEY_TAB, ui::EF_CONTROL_DOWN, '\0', '\t' }, + { ui::VKEY_TAB, ui::EF_SHIFT_DOWN, '\t', '\t' }, + + { ui::VKEY_MULTIPLY, ui::EF_CONTROL_DOWN, '\0', '*' }, + { ui::VKEY_MULTIPLY, ui::EF_SHIFT_DOWN, '*', '*' }, + { ui::VKEY_ADD, ui::EF_CONTROL_DOWN, '\0', '+' }, + { ui::VKEY_ADD, ui::EF_SHIFT_DOWN, '+', '+' }, + { ui::VKEY_SUBTRACT, ui::EF_CONTROL_DOWN, '\0', '-' }, + { ui::VKEY_SUBTRACT, ui::EF_SHIFT_DOWN, '-', '-' }, + { ui::VKEY_DECIMAL, ui::EF_CONTROL_DOWN, '\0', '.' }, + { ui::VKEY_DECIMAL, ui::EF_SHIFT_DOWN, '.', '.' }, + { ui::VKEY_DIVIDE, ui::EF_CONTROL_DOWN, '\0', '/' }, + { ui::VKEY_DIVIDE, ui::EF_SHIFT_DOWN, '/', '/' }, + + { ui::VKEY_OEM_1, ui::EF_CONTROL_DOWN, '\0', ';' }, + { ui::VKEY_OEM_1, ui::EF_SHIFT_DOWN, ':', ':' }, + { ui::VKEY_OEM_PLUS, ui::EF_CONTROL_DOWN, '\0', '=' }, + { ui::VKEY_OEM_PLUS, ui::EF_SHIFT_DOWN, '+', '+' }, + { ui::VKEY_OEM_COMMA, ui::EF_CONTROL_DOWN, '\0', ',' }, + { ui::VKEY_OEM_COMMA, ui::EF_SHIFT_DOWN, '<', '<' }, + { ui::VKEY_OEM_PERIOD, ui::EF_CONTROL_DOWN, '\0', '.' }, + { ui::VKEY_OEM_PERIOD, ui::EF_SHIFT_DOWN, '>', '>' }, + { ui::VKEY_OEM_3, ui::EF_CONTROL_DOWN, '\0', '`' }, + { ui::VKEY_OEM_3, ui::EF_SHIFT_DOWN, '~', '~' }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestData); ++i) { + KeyEvent key(ui::ET_KEY_PRESSED, kTestData[i].key_code, kTestData[i].flags); + EXPECT_EQ(kTestData[i].character, key.GetCharacter()) + << " Index:" << i << " key_code:" << kTestData[i].key_code; + EXPECT_EQ(kTestData[i].unmodified_character, key.GetUnmodifiedCharacter()) + << " Index:" << i << " key_code:" << kTestData[i].key_code; + } +} + +TEST_F(EventTest, MAYBE_KeyEventDirectUnicode) { + KeyEvent key(ui::ET_KEY_PRESSED, ui::VKEY_UNKNOWN, ui::EF_SHIFT_DOWN); + key.set_character(0x1234U); + key.set_unmodified_character(0x4321U); + EXPECT_EQ(0x1234U, key.GetCharacter()); + EXPECT_EQ(0x4321U, key.GetUnmodifiedCharacter()); + KeyEvent key2(ui::ET_KEY_RELEASED, ui::VKEY_UNKNOWN, ui::EF_CONTROL_DOWN); + key2.set_character(0x4321U); + key2.set_unmodified_character(0x1234U); + EXPECT_EQ(0x4321U, key2.GetCharacter()); + EXPECT_EQ(0x1234U, key2.GetUnmodifiedCharacter()); +} + +} // namespace views diff --git a/ui/views/events/event_wayland.cc b/ui/views/events/event_wayland.cc new file mode 100644 index 0000000..44998e6 --- /dev/null +++ b/ui/views/events/event_wayland.cc @@ -0,0 +1,23 @@ +// Copyright (c) 2011 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 "views/events/event.h" + +#include "base/logging.h" +#include "ui/base/keycodes/keyboard_code_conversion.h" + +namespace views { + +////////////////////////////////////////////////////////////////////////////// +// KeyEvent, public: + +uint16 KeyEvent::GetCharacter() const { + return ui::GetCharacterFromKeyCode(key_code_, flags()); +} + +uint16 KeyEvent::GetUnmodifiedCharacter() const { + return ui::GetCharacterFromKeyCode(key_code_, flags() & ui::EF_SHIFT_DOWN); +} + +} // namespace views diff --git a/ui/views/events/event_win.cc b/ui/views/events/event_win.cc new file mode 100644 index 0000000..03b855e --- /dev/null +++ b/ui/views/events/event_win.cc @@ -0,0 +1,30 @@ +// Copyright (c) 2011 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 "views/events/event.h" + +#include "base/logging.h" +#include "ui/base/keycodes/keyboard_code_conversion.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// KeyEvent, public: + +uint16 KeyEvent::GetCharacter() const { + if (character_) + return character_; + return (native_event().message == WM_CHAR) ? key_code_ : + ui::GetCharacterFromKeyCode(key_code_, flags()); +} + +uint16 KeyEvent::GetUnmodifiedCharacter() const { + if (unmodified_character_) + return unmodified_character_; + // Looks like there is no way to get unmodified character on Windows. + return (native_event().message == WM_CHAR) ? key_code_ : + ui::GetCharacterFromKeyCode(key_code_, flags() & ui::EF_SHIFT_DOWN); +} + +} // namespace views diff --git a/ui/views/events/event_x.cc b/ui/views/events/event_x.cc new file mode 100644 index 0000000..1c9e330 --- /dev/null +++ b/ui/views/events/event_x.cc @@ -0,0 +1,152 @@ +// Copyright (c) 2011 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 "views/events/event.h" + +#include <gdk/gdk.h> +#include <gdk/gdkx.h> +#include <X11/extensions/XInput2.h> +#include <X11/Xlib.h> + +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "ui/base/events.h" +#include "ui/base/keycodes/keyboard_code_conversion.h" +#include "ui/base/keycodes/keyboard_code_conversion_x.h" +#include "ui/base/touch/touch_factory.h" +#include "views/widget/root_view.h" + +namespace views { + +namespace { + +// The following two functions are copied from event_gtk.cc. These will be +// removed when GTK dependency is removed. +#if defined(TOOLKIT_USES_GTK) +uint16 GetCharacterFromGdkKeyval(guint keyval) { + guint32 ch = gdk_keyval_to_unicode(keyval); + + // We only support BMP characters. + return ch < 0xFFFE ? static_cast<uint16>(ch) : 0; +} + +GdkEventKey* GetGdkEventKeyFromNative(GdkEvent* gdk_event) { + DCHECK(gdk_event->type == GDK_KEY_PRESS || + gdk_event->type == GDK_KEY_RELEASE); + return &gdk_event->key; +} +#endif + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// KeyEvent, public: + +uint16 KeyEvent::GetCharacter() const { + if (character_) + return character_; + + if (!native_event()) { +#if defined(TOOLKIT_USES_GTK) + // This event may have been created from a Gdk event. + if (!IsControlDown() && gdk_event()) { + uint16 ch = GetCharacterFromGdkKeyval( + GetGdkEventKeyFromNative(gdk_event())->keyval); + if (ch) + return ch; + } +#endif + return ui::GetCharacterFromKeyCode(key_code_, flags()); + } + + DCHECK(native_event()->type == KeyPress || + native_event()->type == KeyRelease); + + uint16 ch = ui::DefaultSymbolFromXEvent(native_event()); + return ch ? ch : ui::GetCharacterFromKeyCode(key_code_, flags()); +} + +uint16 KeyEvent::GetUnmodifiedCharacter() const { + if (unmodified_character_) + return unmodified_character_; + + if (!native_event()) { +#if defined(TOOLKIT_USES_GTK) + // This event may have been created from a Gdk event. + if (gdk_event()) { + GdkEventKey* key = GetGdkEventKeyFromNative(gdk_event()); + + static const guint kIgnoredModifiers = + GDK_CONTROL_MASK | GDK_LOCK_MASK | GDK_MOD1_MASK | GDK_MOD2_MASK | + GDK_MOD3_MASK | GDK_MOD4_MASK | GDK_MOD5_MASK | GDK_SUPER_MASK | + GDK_HYPER_MASK | GDK_META_MASK; + + // We can't use things like (key->state & GDK_SHIFT_MASK), as it may mask + // out bits used by X11 or Gtk internally. + GdkModifierType modifiers = + static_cast<GdkModifierType>(key->state & ~kIgnoredModifiers); + guint keyval = 0; + if (gdk_keymap_translate_keyboard_state(NULL, key->hardware_keycode, + modifiers, key->group, &keyval, NULL, NULL, NULL)) { + uint16 ch = GetCharacterFromGdkKeyval(keyval); + if (ch) + return ch; + } + } +#endif + return ui::GetCharacterFromKeyCode(key_code_, flags() & ui::EF_SHIFT_DOWN); + } + + DCHECK(native_event()->type == KeyPress || + native_event()->type == KeyRelease); + + XKeyEvent *key = &native_event()->xkey; + + static const unsigned int kIgnoredModifiers = ControlMask | LockMask | + Mod1Mask | Mod2Mask | Mod3Mask | Mod4Mask | Mod5Mask; + + // We can't use things like (key.state & ShiftMask), as it may mask out bits + // used by X11 internally. + key->state &= ~kIgnoredModifiers; + uint16 ch = ui::DefaultSymbolFromXEvent(native_event()); + return ch ? ch : + ui::GetCharacterFromKeyCode(key_code_, flags() & ui::EF_SHIFT_DOWN); +} + +//////////////////////////////////////////////////////////////////////////////// +// TouchEvent, public: + +TouchEvent::TouchEvent(const base::NativeEvent& native_event) + : LocatedEvent(native_event), + touch_id_(ui::GetTouchId(native_event)), + radius_x_(ui::GetTouchRadiusX(native_event)), + radius_y_(ui::GetTouchRadiusY(native_event)), + rotation_angle_(ui::GetTouchAngle(native_event)), + force_(ui::GetTouchForce(native_event)) { +#if defined(USE_XI2_MT) + if (type() == ui::ET_TOUCH_RELEASED) { + // NOTE: The slot is allocated by TouchFactory for each XI_TouchBegin + // event, which carries a new tracking ID to identify a new touch + // sequence. + ui::TouchFactory* factory = ui::TouchFactory::GetInstance(); + float tracking_id; + if (factory->ExtractTouchParam(*native_event, + ui::TouchFactory::TP_TRACKING_ID, + &tracking_id)) { + factory->ReleaseSlotForTrackingID(tracking_id); + } + } +#else + if (type() == ui::ET_TOUCH_PRESSED || type() == ui::ET_TOUCH_RELEASED) { + ui::TouchFactory* factory = ui::TouchFactory::GetInstance(); + float slot; + if (factory->ExtractTouchParam(*native_event, + ui::TouchFactory::TP_SLOT_ID, &slot)) { + factory->SetSlotUsed(slot, type() == ui::ET_TOUCH_PRESSED); + } + } +#endif +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler.h b/ui/views/focus/accelerator_handler.h new file mode 100644 index 0000000..c0f9591 --- /dev/null +++ b/ui/views/focus/accelerator_handler.h @@ -0,0 +1,61 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_FOCUS_ACCELERATOR_HANDLER_H_ +#define UI_VIEWS_FOCUS_ACCELERATOR_HANDLER_H_ +#pragma once + +#include "build/build_config.h" + +#if defined(TOOLKIT_USES_GTK) +#include <gdk/gdk.h> +#endif + +#include <set> +#include <vector> + +#include "base/message_loop.h" +#include "views/views_export.h" + +namespace views { + +#if defined(TOUCH_UI) || defined(USE_AURA) +#if defined(USE_X11) && !defined(USE_WAYLAND) +// Dispatch an XEvent to the RootView. Return true if the event was dispatched +// and handled, false otherwise. +bool VIEWS_EXPORT DispatchXEvent(XEvent* xevent); +#endif // USE_X11 && !USE_WAYLAND +#endif // TOUCH_UI || USE_AURA + +// This class delegates the key messages to the associated FocusManager class +// for the window that is receiving these messages for accelerator processing. +class VIEWS_EXPORT AcceleratorHandler : public MessageLoop::Dispatcher { + public: + AcceleratorHandler(); + + // Dispatcher method. This returns true if an accelerator was processed by the + // focus manager +#if defined(OS_WIN) + virtual bool Dispatch(const MSG& msg); +#elif defined(USE_WAYLAND) + virtual base::MessagePumpDispatcher::DispatchStatus Dispatch( + base::wayland::WaylandEvent* ev); +#elif defined(TOUCH_UI) || defined(USE_AURA) + virtual base::MessagePumpDispatcher::DispatchStatus Dispatch(XEvent* xev); +#else + virtual bool Dispatch(GdkEvent* event); +#endif + + private: +#if defined(OS_WIN) + // The keys currently pressed and consumed by the FocusManager. + std::set<WPARAM> pressed_keys_; +#endif + + DISALLOW_COPY_AND_ASSIGN(AcceleratorHandler); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_ACCELERATOR_HANDLER_H_ diff --git a/ui/views/focus/accelerator_handler_aura.cc b/ui/views/focus/accelerator_handler_aura.cc new file mode 100644 index 0000000..ab552899 --- /dev/null +++ b/ui/views/focus/accelerator_handler_aura.cc @@ -0,0 +1,29 @@ +// Copyright (c) 2011 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 "views/focus/accelerator_handler.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() { +} + +#if defined(OS_WIN) +bool AcceleratorHandler::Dispatch(const MSG& msg) { + TranslateMessage(&msg); + DispatchMessage(&msg); + return true; +} +#else +base::MessagePumpDispatcher::DispatchStatus AcceleratorHandler::Dispatch( + XEvent*) { + return base::MessagePumpDispatcher::EVENT_IGNORED; +} + +bool DispatchXEvent(XEvent* xev) { + return false; +} +#endif // defined(OS_WIN) + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_gtk.cc b/ui/views/focus/accelerator_handler_gtk.cc new file mode 100644 index 0000000..fab3b86 --- /dev/null +++ b/ui/views/focus/accelerator_handler_gtk.cc @@ -0,0 +1,22 @@ +// Copyright (c) 2011 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 "views/focus/accelerator_handler.h" + +#include <gtk/gtk.h> + +#include "views/focus/focus_manager.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() {} + +bool AcceleratorHandler::Dispatch(GdkEvent* event) { + // The logic for handling keyboard accelerators has been moved into + // NativeWidgetGtk::OnEventKey handler (views/widget/widget_gtk.cc). + gtk_main_do_event(event); + return true; +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_gtk_unittest.cc b/ui/views/focus/accelerator_handler_gtk_unittest.cc new file mode 100644 index 0000000..098325d --- /dev/null +++ b/ui/views/focus/accelerator_handler_gtk_unittest.cc @@ -0,0 +1,199 @@ +// Copyright (c) 2011 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 <gdk/gdkkeysyms.h> + +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/gfx/rect.h" +#include "ui/base/models/accelerator.h" +#include "views/focus/accelerator_handler.h" +#include "views/focus/focus_manager.h" +#include "views/view.h" +#include "views/widget/widget.h" +#include "views/widget/widget_delegate.h" + +namespace views { + +class AcceleratorHandlerGtkTest + : public testing::Test, + public WidgetDelegate, + public ui::AcceleratorTarget { + public: + AcceleratorHandlerGtkTest() + : kMenuAccelerator(ui::VKEY_MENU, false, false, false), + kHomepageAccelerator(ui::VKEY_HOME, false, false, true), + content_view_(NULL) { + } + + virtual void SetUp() { + window_ = Widget::CreateWindowWithBounds(this, gfx::Rect(0, 0, 500, 500)); + window_->Show(); + FocusManager* focus_manager = window_->GetFocusManager(); + focus_manager->RegisterAccelerator(kMenuAccelerator, this); + focus_manager->RegisterAccelerator(kHomepageAccelerator, this); + menu_pressed_ = false; + home_pressed_ = false; + } + + virtual void TearDown() { + window_->Close(); + + // Flush the message loop to make application verifiers happy. + message_loop_.RunAllPending(); + } + + GdkEventKey CreateKeyEvent(GdkEventType type, guint keyval, guint state) { + GdkEventKey evt; + memset(&evt, 0, sizeof(evt)); + evt.type = type; + evt.keyval = keyval; + // The keyval won't be a "correct" hardware keycode for any real hardware, + // but the code should never depend on exact hardware keycodes, just the + // fact that the code for presses and releases of the same key match. + evt.hardware_keycode = keyval; + evt.state = state; + GtkWidget* widget = GTK_WIDGET(window_->GetNativeWindow()); + evt.window = widget->window; + return evt; + } + + // AcceleratorTarget implementation. + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + if (accelerator == kMenuAccelerator) + menu_pressed_ = true; + else if (accelerator == kHomepageAccelerator) + home_pressed_ = true; + return true; + } + + // WidgetDelegate Implementation. + virtual View* GetContentsView() { + if (!content_view_) + content_view_ = new View(); + return content_view_; + } + virtual const views::Widget* GetWidget() const { + return content_view_->GetWidget(); + } + virtual views::Widget* GetWidget() { + return content_view_->GetWidget(); + } + + virtual void InitContentView() { + } + + protected: + bool menu_pressed_; + bool home_pressed_; + + private: + ui::Accelerator kMenuAccelerator; + ui::Accelerator kHomepageAccelerator; + Widget* window_; + View* content_view_; + MessageLoopForUI message_loop_; + DISALLOW_COPY_AND_ASSIGN(AcceleratorHandlerGtkTest); +}; + +// Test that the homepage accelerator (Alt+Home) is activated on key down +// and that the menu accelerator (Alt) is never activated. +TEST_F(AcceleratorHandlerGtkTest, TestHomepageAccelerator) { + AcceleratorHandler handler; + GdkEventKey evt; + + ASSERT_FALSE(menu_pressed_); + ASSERT_FALSE(home_pressed_); + + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + ASSERT_FALSE(home_pressed_); + + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Home, GDK_MOD1_MASK); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + ASSERT_TRUE(home_pressed_); + + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Home, GDK_MOD1_MASK); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + ASSERT_TRUE(home_pressed_); +} + +// Test that the menu accelerator is activated on key up and not key down. +TEST_F(AcceleratorHandlerGtkTest, TestMenuAccelerator) { + AcceleratorHandler handler; + GdkEventKey evt; + + ASSERT_FALSE(menu_pressed_); + + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_TRUE(menu_pressed_); +} + +// Test that the menu accelerator isn't confused by the interaction of the +// Alt and Shift keys. Try the following sequence on Linux: +// Press Alt +// Press Shift +// Release Alt +// Release Shift +// The key codes for pressing Alt and releasing Alt are different! This +// caused a bug in a previous version of the code, which is now fixed by +// keeping track of hardware keycodes, which are consistent. +TEST_F(AcceleratorHandlerGtkTest, TestAltShiftInteraction) { + AcceleratorHandler handler; + GdkEventKey evt; + + ASSERT_FALSE(menu_pressed_); + + // Press Shift. + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Shift_L, 0); + evt.hardware_keycode = 0x32; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + // Press Alt - but GDK calls this Meta when Shift is also down. + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Meta_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + // Release Shift. + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Shift_L, 0); + evt.hardware_keycode = 0x32; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + // Release Alt - with Shift not down, the keyval is now Alt, but + // the hardware keycode is unchanged. + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + + // Press Alt by itself. + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Alt_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + // This line fails if we don't keep track of hardware keycodes. + ASSERT_FALSE(menu_pressed_); + + // Release Alt - now this should trigger the menu shortcut. + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_TRUE(menu_pressed_); +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_touch.cc b/ui/views/focus/accelerator_handler_touch.cc new file mode 100644 index 0000000..1c4c9b3 --- /dev/null +++ b/ui/views/focus/accelerator_handler_touch.cc @@ -0,0 +1,187 @@ +// Copyright (c) 2011 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 "views/focus/accelerator_handler.h" + +#include <bitset> +#include <gtk/gtk.h> +#include <X11/extensions/XInput2.h> + +#include "ui/base/touch/touch_factory.h" +#include "ui/views/ime/input_method.h" +#include "views/events/event.h" +#include "views/focus/focus_manager.h" +#include "views/view.h" +#include "views/widget/native_widget.h" + +namespace views { + +namespace { + +Widget* FindWidgetForGdkWindow(GdkWindow* gdk_window) { + gpointer data = NULL; + gdk_window_get_user_data(gdk_window, &data); + GtkWidget* gtk_widget = reinterpret_cast<GtkWidget*>(data); + if (!gtk_widget || !GTK_IS_WIDGET(gtk_widget)) { + DLOG(WARNING) << "no GtkWidget found for that GdkWindow"; + return NULL; + } + Widget* widget = Widget::GetWidgetForNativeView(gtk_widget); + + if (!widget) { + DLOG(WARNING) << "no NativeWidgetGtk found for that GtkWidget"; + return NULL; + } + return widget; +} + +} // namespace + +bool DispatchX2Event(Widget* widget, XEvent* xev) { + XGenericEventCookie* cookie = &xev->xcookie; + switch (cookie->evtype) { + case XI_KeyPress: + case XI_KeyRelease: { + // TODO(sad): We don't capture XInput2 events from keyboard yet. + break; + } +#if defined(USE_XI2_MT) + case XI_TouchBegin: + case XI_TouchEnd: + case XI_TouchUpdate: { + // Hide the cursor when a touch event comes in. + ui::TouchFactory::GetInstance()->SetCursorVisible(false, false); + + // If the TouchEvent is processed by |widget|, then return. + TouchEvent touch(xev); + if (widget->OnTouchEvent(touch) != ui::TOUCH_STATUS_UNKNOWN) + return true; + + // We do not want to generate a mouse event for an unprocessed touch + // event here. That is already done by the gesture manager in + // RootView::OnTouchEvent. + return false; + } +#endif + case XI_ButtonPress: + case XI_ButtonRelease: + case XI_Motion: { + XIDeviceEvent* xievent = static_cast<XIDeviceEvent*>(cookie->data); + + // Scrolling the wheel generates press/release events with button id's 4 + // and 5. In case of a wheelscroll, we do not want to show the cursor. + if (xievent->detail == 4 || xievent->detail == 5) { + MouseWheelEvent wheelev(xev); + return widget->OnMouseEvent(wheelev); + } + + ui::TouchFactory* factory = ui::TouchFactory::GetInstance(); + // Is the event coming from a touch device? + if (factory->IsTouchDevice(xievent->sourceid)) { + // Hide the cursor when a touch event comes in. + factory->SetCursorVisible(false, false); + + // With XInput 2.0, XI_ButtonPress and XI_ButtonRelease events are + // ignored, as XI_Motion events contain enough data to detect finger + // press and release. See more notes in TouchFactory::TouchParam. + if ((cookie->evtype == XI_ButtonPress || + cookie->evtype == XI_ButtonRelease) && + factory->IsRealTouchDevice(xievent->sourceid)) + return false; + + // If the TouchEvent is processed by |widget|, then return. Otherwise + // let it fall through so it can be used as a MouseEvent, if desired. + TouchEvent touch(xev); + if (widget->OnTouchEvent(touch) != ui::TOUCH_STATUS_UNKNOWN) + return true; + + // We do not want to generate a mouse event for an unprocessed touch + // event here. That is already done by the gesture manager in + // RootView::OnTouchEvent. + return false; + } else { + MouseEvent mouseev(xev); + + // Show the cursor. Start a timer to hide the cursor after a delay on + // move (not drag) events, or if the only button pressed is released. + bool start_timer = mouseev.type() == ui::ET_MOUSE_MOVED; + start_timer |= mouseev.type() == ui::ET_MOUSE_RELEASED && + (mouseev.IsOnlyLeftMouseButton() || + mouseev.IsOnlyMiddleMouseButton() || + mouseev.IsOnlyRightMouseButton()); + factory->SetCursorVisible(true, start_timer); + + return widget->OnMouseEvent(mouseev); + } + } + } + return false; +} + +bool DispatchXEvent(XEvent* xev) { + GdkDisplay* gdisp = gdk_display_get_default(); + XID xwindow = xev->xany.window; + + if (xev->type == GenericEvent) { + if (!ui::TouchFactory::GetInstance()->ShouldProcessXI2Event(xev)) + return true; // Consume the event. + + XGenericEventCookie* cookie = &xev->xcookie; + if (cookie->evtype == XI_HierarchyChanged) { + ui::TouchFactory::GetInstance()->UpdateDeviceList(cookie->display); + return true; + } + + XIDeviceEvent* xiev = static_cast<XIDeviceEvent*>(cookie->data); + xwindow = xiev->event; + } + + GdkWindow* gwind = gdk_window_lookup_for_display(gdisp, xwindow); + Widget* widget = FindWidgetForGdkWindow(gwind); + if (widget) { + switch (xev->type) { + case KeyPress: + case KeyRelease: { + KeyEvent keyev(xev); + InputMethod* ime = widget->GetInputMethod(); + // Always dispatch key events to the input method first, to make sure + // that the input method's hotkeys work all time. + if (ime) { + ime->DispatchKeyEvent(keyev); + return true; + } + return widget->OnKeyEvent(keyev); + } + case ButtonPress: + case ButtonRelease: + if (xev->xbutton.button == 4 || xev->xbutton.button == 5) { + // Scrolling the wheel triggers button press/release events. + MouseWheelEvent wheelev(xev); + return widget->OnMouseEvent(wheelev); + } + // fallthrough + case MotionNotify: { + MouseEvent mouseev(xev); + return widget->OnMouseEvent(mouseev); + } + + case GenericEvent: { + return DispatchX2Event(widget, xev); + } + } + } + + return false; +} + +AcceleratorHandler::AcceleratorHandler() {} + +base::MessagePumpDispatcher::DispatchStatus + AcceleratorHandler::Dispatch(XEvent* xev) { + return DispatchXEvent(xev) ? + base::MessagePumpDispatcher::EVENT_PROCESSED : + base::MessagePumpDispatcher::EVENT_IGNORED; +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_wayland.cc b/ui/views/focus/accelerator_handler_wayland.cc new file mode 100644 index 0000000..a8963ee --- /dev/null +++ b/ui/views/focus/accelerator_handler_wayland.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2011 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 "views/focus/accelerator_handler.h" + +#include "views/focus/focus_manager.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() {} + +base::MessagePumpDispatcher::DispatchStatus + AcceleratorHandler::Dispatch(base::wayland::WaylandEvent* ev) { + return base::MessagePumpDispatcher::EVENT_IGNORED; +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_win.cc b/ui/views/focus/accelerator_handler_win.cc new file mode 100644 index 0000000..8f2c8e8 --- /dev/null +++ b/ui/views/focus/accelerator_handler_win.cc @@ -0,0 +1,60 @@ +// Copyright (c) 2011 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 "views/focus/accelerator_handler.h" + +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/keycodes/keyboard_code_conversion_win.h" +#include "views/events/event.h" +#include "views/focus/focus_manager.h" +#include "views/widget/widget.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() { +} + +bool AcceleratorHandler::Dispatch(const MSG& msg) { + bool process_message = true; + + if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST) { + Widget* widget = Widget::GetTopLevelWidgetForNativeView(msg.hwnd); + FocusManager* focus_manager = widget ? widget->GetFocusManager() : NULL; + if (focus_manager) { + switch (msg.message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: { + KeyEvent event(msg); + process_message = focus_manager->OnKeyEvent(event); + if (!process_message) { + // Record that this key is pressed so we can remember not to + // translate and dispatch the associated WM_KEYUP. + pressed_keys_.insert(msg.wParam); + } + break; + } + case WM_KEYUP: + case WM_SYSKEYUP: { + std::set<WPARAM>::iterator iter = pressed_keys_.find(msg.wParam); + if (iter != pressed_keys_.end()) { + // Don't translate/dispatch the KEYUP since we have eaten the + // associated KEYDOWN. + pressed_keys_.erase(iter); + return true; + } + break; + } + } + } + } + + if (process_message) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return true; +} + +} // namespace views diff --git a/ui/views/focus/external_focus_tracker.cc b/ui/views/focus/external_focus_tracker.cc new file mode 100644 index 0000000..193b57e --- /dev/null +++ b/ui/views/focus/external_focus_tracker.cc @@ -0,0 +1,72 @@ +// Copyright (c) 2011 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 "views/focus/external_focus_tracker.h" + +#include "base/logging.h" +#include "views/view.h" +#include "views/focus/view_storage.h" + +namespace views { + +ExternalFocusTracker::ExternalFocusTracker(View* parent_view, + FocusManager* focus_manager) + : focus_manager_(focus_manager), + parent_view_(parent_view) { + DCHECK(focus_manager); + DCHECK(parent_view); + view_storage_ = ViewStorage::GetInstance(); + last_focused_view_storage_id_ = view_storage_->CreateStorageID(); + // Store the view which is focused when we're created. + StartTracking(); +} + +ExternalFocusTracker::~ExternalFocusTracker() { + view_storage_->RemoveView(last_focused_view_storage_id_); + if (focus_manager_) + focus_manager_->RemoveFocusChangeListener(this); +} + +void ExternalFocusTracker::OnWillChangeFocus(View* focused_before, + View* focused_now) { + if (focused_now && !parent_view_->Contains(focused_now) && + parent_view_ != focused_now) { + // Store the newly focused view. + StoreLastFocusedView(focused_now); + } +} + +void ExternalFocusTracker::OnDidChangeFocus(View* focused_before, + View* focused_now) { +} + +void ExternalFocusTracker::FocusLastFocusedExternalView() { + View* last_focused_view = + view_storage_->RetrieveView(last_focused_view_storage_id_); + if (last_focused_view) + last_focused_view->RequestFocus(); +} + +void ExternalFocusTracker::SetFocusManager(FocusManager* focus_manager) { + if (focus_manager_) + focus_manager_->RemoveFocusChangeListener(this); + focus_manager_ = focus_manager; + if (focus_manager_) + StartTracking(); +} + +void ExternalFocusTracker::StoreLastFocusedView(View* view) { + view_storage_->RemoveView(last_focused_view_storage_id_); + // If the view is NULL, remove the last focused view from storage, but don't + // try to store NULL. + if (view != NULL) + view_storage_->StoreView(last_focused_view_storage_id_, view); +} + +void ExternalFocusTracker::StartTracking() { + StoreLastFocusedView(focus_manager_->GetFocusedView()); + focus_manager_->AddFocusChangeListener(this); +} + +} // namespace views diff --git a/ui/views/focus/external_focus_tracker.h b/ui/views/focus/external_focus_tracker.h new file mode 100644 index 0000000..0ea1479 --- /dev/null +++ b/ui/views/focus/external_focus_tracker.h @@ -0,0 +1,78 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ +#define UI_VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ +#pragma once + +#include "views/focus/focus_manager.h" + +namespace views { + +class View; +class ViewStorage; + +// ExternalFocusTracker tracks the last focused view which belongs to the +// provided focus manager and is not either the provided parent view or one of +// its descendants. This is generally used if the parent view want to return +// focus to some other view once it is dismissed. The parent view and the focus +// manager must exist for the duration of the tracking. If the focus manager +// must be deleted before this object is deleted, make sure to call +// SetFocusManager(NULL) first. +// +// Typical use: When a view is added to the view hierarchy, it instantiates an +// ExternalFocusTracker and passes in itself and its focus manager. Then, +// when that view wants to return focus to the last focused view which is not +// itself and not a descandant of itself, (usually when it is being closed) +// it calls FocusLastFocusedExternalView. +class VIEWS_EXPORT ExternalFocusTracker : public FocusChangeListener { + public: + ExternalFocusTracker(View* parent_view, FocusManager* focus_manager); + + virtual ~ExternalFocusTracker(); + // FocusChangeListener implementation. + virtual void OnWillChangeFocus(View* focused_before, View* focused_now); + virtual void OnDidChangeFocus(View* focused_before, View* focused_now); + + // Focuses last focused view which is not a child of parent view and is not + // parent view itself. Returns true if focus for a view was requested, false + // otherwise. + void FocusLastFocusedExternalView(); + + // Sets the focus manager whose focus we are tracking. |focus_manager| can + // be NULL, but no focus changes will be tracked. This is useful if the focus + // manager went away, but you might later want to start tracking with a new + // manager later, or call FocusLastFocusedExternalView to focus the previous + // view. + void SetFocusManager(FocusManager* focus_manager); + + private: + // Store the provided view. This view will be focused when + // FocusLastFocusedExternalView is called. + void StoreLastFocusedView(View* view); + + // Store the currently focused view for our view manager and register as a + // listener for future focus changes. + void StartTracking(); + + // Focus manager which we are a listener for. + FocusManager* focus_manager_; + + // ID of the last focused view, which we store in view_storage_. + int last_focused_view_storage_id_; + + // Used to store the last focused view which is not a child of + // ExternalFocusTracker. + ViewStorage* view_storage_; + + // The parent view of views which we should not track focus changes to. We + // also do not track changes to parent_view_ itself. + View* parent_view_; + + DISALLOW_COPY_AND_ASSIGN(ExternalFocusTracker); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ diff --git a/ui/views/focus/focus_manager.cc b/ui/views/focus/focus_manager.cc new file mode 100644 index 0000000..3ee0755 --- /dev/null +++ b/ui/views/focus/focus_manager.cc @@ -0,0 +1,413 @@ +// Copyright (c) 2011 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 "views/focus/focus_manager.h" + +#include <algorithm> + +#include "base/auto_reset.h" +#include "base/logging.h" +#include "build/build_config.h" +#include "ui/base/accelerator_manager.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/models/accelerator.h" +#include "views/focus/focus_search.h" +#include "views/focus/view_storage.h" +#include "views/focus/widget_focus_manager.h" +#include "views/view.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" + +namespace views { + +FocusManager::FocusManager(Widget* widget) + : widget_(widget), + focused_view_(NULL), + accelerator_manager_(new ui::AcceleratorManager), + focus_change_reason_(kReasonDirectFocusChange), + is_changing_focus_(false) { + DCHECK(widget_); + stored_focused_view_storage_id_ = + ViewStorage::GetInstance()->CreateStorageID(); +} + +FocusManager::~FocusManager() { +} + +bool FocusManager::OnKeyEvent(const KeyEvent& event) { +#if defined(OS_WIN) + // If the focused view wants to process the key event as is, let it be. + // On Linux we always dispatch key events to the focused view first, so + // we should not do this check here. See also NativeWidgetGtk::OnKeyEvent(). + if (focused_view_ && focused_view_->SkipDefaultKeyEventProcessing(event)) + return true; +#endif + + // Intercept Tab related messages for focus traversal. + // Note that we don't do focus traversal if the root window is not part of the + // active window hierarchy as this would mean we have no focused view and + // would focus the first focusable view. +#if defined(OS_WIN) && !defined(USE_AURA) + HWND top_window = widget_->GetNativeView(); + HWND active_window = ::GetActiveWindow(); + if ((active_window == top_window || ::IsChild(active_window, top_window)) && + IsTabTraversalKeyEvent(event)) { + AdvanceFocus(event.IsShiftDown()); + return false; + } +#else + if (IsTabTraversalKeyEvent(event)) { + AdvanceFocus(event.IsShiftDown()); + return false; + } +#endif + + // Intercept arrow key messages to switch between grouped views. + ui::KeyboardCode key_code = event.key_code(); + if (focused_view_ && focused_view_->GetGroup() != -1 && + (key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN || + key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT)) { + bool next = (key_code == ui::VKEY_RIGHT || key_code == ui::VKEY_DOWN); + View::Views views; + focused_view_->parent()->GetViewsInGroup(focused_view_->GetGroup(), &views); + View::Views::const_iterator i( + std::find(views.begin(), views.end(), focused_view_)); + DCHECK(i != views.end()); + int index = static_cast<int>(i - views.begin()); + index += next ? 1 : -1; + if (index < 0) { + index = static_cast<int>(views.size()) - 1; + } else if (index >= static_cast<int>(views.size())) { + index = 0; + } + SetFocusedViewWithReason(views[index], kReasonFocusTraversal); + return false; + } + + // Process keyboard accelerators. + // If the key combination matches an accelerator, the accelerator is + // triggered, otherwise the key event is processed as usual. + ui::Accelerator accelerator(event.key_code(), + event.IsShiftDown(), + event.IsControlDown(), + event.IsAltDown()); + if (ProcessAccelerator(accelerator)) { + // If a shortcut was activated for this keydown message, do not propagate + // the event further. + return false; + } + return true; +} + +void FocusManager::ValidateFocusedView() { + if (focused_view_) { + if (!ContainsView(focused_view_)) + ClearFocus(); + } +} + +// Tests whether a view is valid, whether it still belongs to the window +// hierarchy of the FocusManager. +bool FocusManager::ContainsView(View* view) { + Widget* widget = view->GetWidget(); + return widget ? widget->GetFocusManager() == this : false; +} + +void FocusManager::AdvanceFocus(bool reverse) { + View* v = GetNextFocusableView(focused_view_, reverse, false); + // Note: Do not skip this next block when v == focused_view_. If the user + // tabs past the last focusable element in a webpage, we'll get here, and if + // the TabContentsContainerView is the only focusable view (possible in + // fullscreen mode), we need to run this block in order to cycle around to the + // first element on the page. + if (v) { + v->AboutToRequestFocusFromTabTraversal(reverse); + SetFocusedViewWithReason(v, kReasonFocusTraversal); + } +} + +void FocusManager::ClearNativeFocus() { + // Keep the top root window focused so we get keyboard events. + widget_->ClearNativeFocus(); +} + +View* FocusManager::GetNextFocusableView(View* original_starting_view, + bool reverse, + bool dont_loop) { + FocusTraversable* focus_traversable = NULL; + + // Let's revalidate the focused view. + ValidateFocusedView(); + + View* starting_view = NULL; + if (original_starting_view) { + // Search up the containment hierarchy to see if a view is acting as + // a pane, and wants to implement its own focus traversable to keep + // the focus trapped within that pane. + View* pane_search = original_starting_view; + while (pane_search) { + focus_traversable = pane_search->GetPaneFocusTraversable(); + if (focus_traversable) { + starting_view = original_starting_view; + break; + } + pane_search = pane_search->parent(); + } + + if (!focus_traversable) { + if (!reverse) { + // If the starting view has a focus traversable, use it. + // This is the case with NativeWidgetWins for example. + focus_traversable = original_starting_view->GetFocusTraversable(); + + // Otherwise default to the root view. + if (!focus_traversable) { + focus_traversable = + original_starting_view->GetWidget()->GetFocusTraversable(); + starting_view = original_starting_view; + } + } else { + // When you are going back, starting view's FocusTraversable + // should not be used. + focus_traversable = + original_starting_view->GetWidget()->GetFocusTraversable(); + starting_view = original_starting_view; + } + } + } else { + focus_traversable = widget_->GetFocusTraversable(); + } + + // Traverse the FocusTraversable tree down to find the focusable view. + View* v = FindFocusableView(focus_traversable, starting_view, reverse); + if (v) { + return v; + } else { + // Let's go up in the FocusTraversable tree. + FocusTraversable* parent_focus_traversable = + focus_traversable->GetFocusTraversableParent(); + starting_view = focus_traversable->GetFocusTraversableParentView(); + while (parent_focus_traversable) { + FocusTraversable* new_focus_traversable = NULL; + View* new_starting_view = NULL; + // When we are going backward, the parent view might gain the next focus. + bool check_starting_view = reverse; + v = parent_focus_traversable->GetFocusSearch()->FindNextFocusableView( + starting_view, reverse, FocusSearch::UP, + check_starting_view, &new_focus_traversable, &new_starting_view); + + if (new_focus_traversable) { + DCHECK(!v); + + // There is a FocusTraversable, traverse it down. + v = FindFocusableView(new_focus_traversable, NULL, reverse); + } + + if (v) + return v; + + starting_view = focus_traversable->GetFocusTraversableParentView(); + parent_focus_traversable = + parent_focus_traversable->GetFocusTraversableParent(); + } + + // If we get here, we have reached the end of the focus hierarchy, let's + // loop. Make sure there was at least a view to start with, to prevent + // infinitely looping in empty windows. + if (!dont_loop && original_starting_view) { + // Easy, just clear the selection and press tab again. + // By calling with NULL as the starting view, we'll start from the + // top_root_view. + return GetNextFocusableView(NULL, reverse, true); + } + } + return NULL; +} + +void FocusManager::SetFocusedViewWithReason( + View* view, FocusChangeReason reason) { + if (focused_view_ == view) + return; + + AutoReset<bool> auto_changing_focus(&is_changing_focus_, true); + // Update the reason for the focus change (since this is checked by + // some listeners), then notify all listeners. + focus_change_reason_ = reason; + FOR_EACH_OBSERVER(FocusChangeListener, focus_change_listeners_, + OnWillChangeFocus(focused_view_, view)); + + View* old_focused_view = focused_view_; + focused_view_ = view; + if (old_focused_view) + old_focused_view->Blur(); + if (focused_view_) + focused_view_->Focus(); + + FOR_EACH_OBSERVER(FocusChangeListener, focus_change_listeners_, + OnDidChangeFocus(old_focused_view, focused_view_)); +} + +void FocusManager::ClearFocus() { + SetFocusedView(NULL); + ClearNativeFocus(); +} + +void FocusManager::StoreFocusedView() { + ViewStorage* view_storage = ViewStorage::GetInstance(); + if (!view_storage) { + // This should never happen but bug 981648 seems to indicate it could. + NOTREACHED(); + return; + } + + // TODO (jcampan): when a TabContents containing a popup is closed, the focus + // is stored twice causing an assert. We should find a better alternative than + // removing the view from the storage explicitly. + view_storage->RemoveView(stored_focused_view_storage_id_); + + if (!focused_view_) + return; + + view_storage->StoreView(stored_focused_view_storage_id_, focused_view_); + + View* v = focused_view_; + + { + // Temporarily disable notification. ClearFocus() will set the focus to the + // main browser window. This extra focus bounce which happens during + // deactivation can confuse registered WidgetFocusListeners, as the focus + // is not changing due to a user-initiated event. + AutoNativeNotificationDisabler local_notification_disabler; + ClearFocus(); + } + + if (v) + v->SchedulePaint(); // Remove focus border. +} + +void FocusManager::RestoreFocusedView() { + ViewStorage* view_storage = ViewStorage::GetInstance(); + if (!view_storage) { + // This should never happen but bug 981648 seems to indicate it could. + NOTREACHED(); + return; + } + + View* view = view_storage->RetrieveView(stored_focused_view_storage_id_); + if (view) { + if (ContainsView(view)) { + if (!view->IsFocusableInRootView() && + view->IsAccessibilityFocusableInRootView()) { + // RequestFocus would fail, but we want to restore focus to controls + // that had focus in accessibility mode. + SetFocusedViewWithReason(view, kReasonFocusRestore); + } else { + // This usually just sets the focus if this view is focusable, but + // let the view override RequestFocus if necessary. + view->RequestFocus(); + + // If it succeeded, the reason would be incorrect; set it to + // focus restore. + if (focused_view_ == view) + focus_change_reason_ = kReasonFocusRestore; + } + } + } +} + +void FocusManager::ClearStoredFocusedView() { + ViewStorage* view_storage = ViewStorage::GetInstance(); + if (!view_storage) { + // This should never happen but bug 981648 seems to indicate it could. + NOTREACHED(); + return; + } + view_storage->RemoveView(stored_focused_view_storage_id_); +} + +// Find the next (previous if reverse is true) focusable view for the specified +// FocusTraversable, starting at the specified view, traversing down the +// FocusTraversable hierarchy. +View* FocusManager::FindFocusableView(FocusTraversable* focus_traversable, + View* starting_view, + bool reverse) { + FocusTraversable* new_focus_traversable = NULL; + View* new_starting_view = NULL; + View* v = focus_traversable->GetFocusSearch()->FindNextFocusableView( + starting_view, + reverse, + FocusSearch::DOWN, + false, + &new_focus_traversable, + &new_starting_view); + + // Let's go down the FocusTraversable tree as much as we can. + while (new_focus_traversable) { + DCHECK(!v); + focus_traversable = new_focus_traversable; + starting_view = new_starting_view; + new_focus_traversable = NULL; + starting_view = NULL; + v = focus_traversable->GetFocusSearch()->FindNextFocusableView( + starting_view, + reverse, + FocusSearch::DOWN, + false, + &new_focus_traversable, + &new_starting_view); + } + return v; +} + +void FocusManager::RegisterAccelerator( + const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target) { + accelerator_manager_->Register(accelerator, target); +} + +void FocusManager::UnregisterAccelerator(const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target) { + accelerator_manager_->Unregister(accelerator, target); +} + +void FocusManager::UnregisterAccelerators(ui::AcceleratorTarget* target) { + accelerator_manager_->UnregisterAll(target); +} + +bool FocusManager::ProcessAccelerator(const ui::Accelerator& accelerator) { + return accelerator_manager_->Process(accelerator); +} + +ui::AcceleratorTarget* FocusManager::GetCurrentTargetForAccelerator( + const ui::Accelerator& accelerator) const { + return accelerator_manager_->GetCurrentTarget(accelerator); +} + +void FocusManager::FocusNativeView(gfx::NativeView native_view) { + widget_->FocusNativeView(native_view); +} + +// static +bool FocusManager::IsTabTraversalKeyEvent(const KeyEvent& key_event) { + return key_event.key_code() == ui::VKEY_TAB && !key_event.IsControlDown(); +} + +void FocusManager::ViewRemoved(View* removed) { + // If the view being removed contains (or is) the focused view, + // clear the focus. However, it's not safe to call ClearFocus() + // (and in turn ClearNativeFocus()) here because ViewRemoved() can + // be called while the top level widget is being destroyed. + if (focused_view_ && removed && removed->Contains(focused_view_)) + SetFocusedView(NULL); +} + +void FocusManager::AddFocusChangeListener(FocusChangeListener* listener) { + focus_change_listeners_.AddObserver(listener); +} + +void FocusManager::RemoveFocusChangeListener(FocusChangeListener* listener) { + focus_change_listeners_.RemoveObserver(listener); +} + +} // namespace views diff --git a/ui/views/focus/focus_manager.h b/ui/views/focus/focus_manager.h new file mode 100644 index 0000000..0fdecc1 --- /dev/null +++ b/ui/views/focus/focus_manager.h @@ -0,0 +1,283 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_FOCUS_FOCUS_MANAGER_H_ +#define UI_VIEWS_FOCUS_FOCUS_MANAGER_H_ +#pragma once + +#include <list> +#include <map> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/observer_list.h" +#include "ui/base/models/accelerator.h" +#include "ui/gfx/native_widget_types.h" +#include "views/views_export.h" +#include "views/events/event.h" + +// The FocusManager class is used to handle focus traversal, store/restore +// focused views and handle keyboard accelerators. +// +// There are 2 types of focus: +// - the native focus, which is the focus that an gfx::NativeView has. +// - the view focus, which is the focus that a views::View has. +// +// Each native view must register with their Focus Manager so the focus manager +// gets notified when they are focused (and keeps track of the native focus) and +// as well so that the tab key events can be intercepted. +// They can provide when they register a View that is kept in synch in term of +// focus. This is used in NativeControl for example, where a View wraps an +// actual native window. +// This is already done for you if you subclass the NativeControl class or if +// you use the NativeViewHost class. +// +// When creating a top window (derived from views::Widget) that is not a child +// window, it creates and owns a FocusManager to manage the focus for itself and +// all its child windows. +// +// The FocusTraversable interface exposes the methods a class should implement +// in order to be able to be focus traversed when tab key is pressed. +// RootViews implement FocusTraversable. +// The FocusManager contains a top FocusTraversable instance, which is the top +// RootView. +// +// If you just use views, then the focus traversal is handled for you by the +// RootView. The default traversal order is the order in which the views have +// been added to their container. You can modify this order by using the View +// method SetNextFocusableView(). +// +// If you are embedding a native view containing a nested RootView (for example +// by adding a NativeControl that contains a NativeWidgetWin as its native +// component), then you need to: +// - override the View::GetFocusTraversable() method in your outer component. +// It should return the RootView of the inner component. This is used when +// the focus traversal traverse down the focus hierarchy to enter the nested +// RootView. In the example mentioned above, the NativeControl overrides +// GetFocusTraversable() and returns hwnd_view_container_->GetRootView(). +// - call Widget::SetFocusTraversableParent() on the nested RootView and point +// it to the outer RootView. This is used when the focus goes out of the +// nested RootView. In the example: +// hwnd_view_container_->GetWidget()->SetFocusTraversableParent( +// native_control->GetRootView()); +// - call RootView::SetFocusTraversableParentView() on the nested RootView with +// the parent view that directly contains the native window. This is needed +// when traversing up from the nested RootView to know which view to start +// with when going to the next/previous view. +// In our example: +// hwnd_view_container_->GetWidget()->SetFocusTraversableParent( +// native_control); +// +// Note that FocusTraversable do not have to be RootViews: AccessibleToolbarView +// is FocusTraversable. + +namespace ui { +class AcceleratorTarget; +class AcceleratorManager; +} + +namespace views { + +class FocusSearch; +class RootView; +class View; +class Widget; + +// The FocusTraversable interface is used by components that want to process +// focus traversal events (due to Tab/Shift-Tab key events). +class VIEWS_EXPORT FocusTraversable { + public: + // Return a FocusSearch object that implements the algorithm to find + // the next or previous focusable view. + virtual FocusSearch* GetFocusSearch() = 0; + + // Should return the parent FocusTraversable. + // The top RootView which is the top FocusTraversable returns NULL. + virtual FocusTraversable* GetFocusTraversableParent() = 0; + + // This should return the View this FocusTraversable belongs to. + // It is used when walking up the view hierarchy tree to find which view + // should be used as the starting view for finding the next/previous view. + virtual View* GetFocusTraversableParentView() = 0; + + protected: + virtual ~FocusTraversable() {} +}; + +// This interface should be implemented by classes that want to be notified when +// the focus is about to change. See the Add/RemoveFocusChangeListener methods. +class VIEWS_EXPORT FocusChangeListener { + public: + // No change to focus state has occurred yet when this function is called. + virtual void OnWillChangeFocus(View* focused_before, View* focused_now) = 0; + + // Called after focus state has changed. + virtual void OnDidChangeFocus(View* focused_before, View* focused_now) = 0; + + protected: + virtual ~FocusChangeListener() {} +}; + +class VIEWS_EXPORT FocusManager { + public: + // The reason why the focus changed. + enum FocusChangeReason { + // The focus changed because the user traversed focusable views using + // keys like Tab or Shift+Tab. + kReasonFocusTraversal, + + // The focus changed due to restoring the focus. + kReasonFocusRestore, + + // The focus changed due to a click or a shortcut to jump directly to + // a particular view. + kReasonDirectFocusChange + }; + + explicit FocusManager(Widget* widget); + virtual ~FocusManager(); + + // Processes the passed key event for accelerators and tab traversal. + // Returns false if the event has been consumed and should not be processed + // further. + bool OnKeyEvent(const KeyEvent& event); + + // Returns true is the specified is part of the hierarchy of the window + // associated with this FocusManager. + bool ContainsView(View* view); + + // Advances the focus (backward if reverse is true). + void AdvanceFocus(bool reverse); + + // The FocusManager keeps track of the focused view within a RootView. + View* GetFocusedView() { return focused_view_; } + const View* GetFocusedView() const { return focused_view_; } + + // Low-level methods to force the focus to change (and optionally provide + // a reason). If the focus change should only happen if the view is + // currenty focusable, enabled, and visible, call view->RequestFocus(). + void SetFocusedViewWithReason(View* view, FocusChangeReason reason); + void SetFocusedView(View* view) { + SetFocusedViewWithReason(view, kReasonDirectFocusChange); + } + + // Get the reason why the focus most recently changed. + FocusChangeReason focus_change_reason() const { + return focus_change_reason_; + } + + // Clears the focused view. The window associated with the top root view gets + // the native focus (so we still get keyboard events). + void ClearFocus(); + + // Validates the focused view, clearing it if the window it belongs too is not + // attached to the window hierarchy anymore. + void ValidateFocusedView(); + + // Stores and restores the focused view. Used when the window becomes + // active/inactive. + void StoreFocusedView(); + void RestoreFocusedView(); + + // Clears the stored focused view. + void ClearStoredFocusedView(); + + // Returns true if in the process of changing the focused view. + bool is_changing_focus() const { return is_changing_focus_; } + + // Register a keyboard accelerator for the specified target. If multiple + // targets are registered for an accelerator, a target registered later has + // higher priority. + // Note that we are currently limited to accelerators that are either: + // - a key combination including Ctrl or Alt + // - the escape key + // - the enter key + // - any F key (F1, F2, F3 ...) + // - any browser specific keys (as available on special keyboards) + void RegisterAccelerator(const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target); + + // Unregister the specified keyboard accelerator for the specified target. + void UnregisterAccelerator(const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target); + + // Unregister all keyboard accelerator for the specified target. + void UnregisterAccelerators(ui::AcceleratorTarget* target); + + // Activate the target associated with the specified accelerator. + // First, AcceleratorPressed handler of the most recently registered target + // is called, and if that handler processes the event (i.e. returns true), + // this method immediately returns. If not, we do the same thing on the next + // target, and so on. + // Returns true if an accelerator was activated. + bool ProcessAccelerator(const ui::Accelerator& accelerator); + + // Called by a RootView when a view within its hierarchy is removed + // from its parent. This will only be called by a RootView in a + // hierarchy of Widgets that this FocusManager is attached to the + // parent Widget of. + void ViewRemoved(View* removed); + + // Adds/removes a listener. The FocusChangeListener is notified every time + // the focused view is about to change. + void AddFocusChangeListener(FocusChangeListener* listener); + void RemoveFocusChangeListener(FocusChangeListener* listener); + + // Returns the AcceleratorTarget that should be activated for the specified + // keyboard accelerator, or NULL if no view is registered for that keyboard + // accelerator. + ui::AcceleratorTarget* GetCurrentTargetForAccelerator( + const ui::Accelerator& accelertor) const; + + // Sets the focus to the specified native view. + virtual void FocusNativeView(gfx::NativeView native_view); + + // Clears the native view having the focus. + virtual void ClearNativeFocus(); + + // Convenience method that returns true if the passed |key_event| should + // trigger tab traversal (if it is a TAB key press with or without SHIFT + // pressed). + static bool IsTabTraversalKeyEvent(const KeyEvent& key_event); + + private: + // Returns the next focusable view. + View* GetNextFocusableView(View* starting_view, bool reverse, bool dont_loop); + + // Returns the focusable view found in the FocusTraversable specified starting + // at the specified view. This traverses down along the FocusTraversable + // hierarchy. + // Returns NULL if no focusable view were found. + View* FindFocusableView(FocusTraversable* focus_traversable, + View* starting_view, + bool reverse); + + // The top-level Widget this FocusManager is associated with. + Widget* widget_; + + // The view that currently is focused. + View* focused_view_; + + // The AcceleratorManager this FocusManager is associated with. + scoped_ptr<ui::AcceleratorManager> accelerator_manager_; + + // The storage id used in the ViewStorage to store/restore the view that last + // had focus. + int stored_focused_view_storage_id_; + + // The reason why the focus most recently changed. + FocusChangeReason focus_change_reason_; + + // The list of registered FocusChange listeners. + ObserverList<FocusChangeListener, true> focus_change_listeners_; + + // See description above getter. + bool is_changing_focus_; + + DISALLOW_COPY_AND_ASSIGN(FocusManager); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_FOCUS_MANAGER_H_ diff --git a/ui/views/focus/focus_manager_factory.cc b/ui/views/focus/focus_manager_factory.cc new file mode 100644 index 0000000..457f60b --- /dev/null +++ b/ui/views/focus/focus_manager_factory.cc @@ -0,0 +1,55 @@ +// Copyright (c) 2011 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 "views/focus/focus_manager_factory.h" + +#include "base/compiler_specific.h" +#include "views/focus/focus_manager.h" + +namespace { + +using views::FocusManager; + +class DefaultFocusManagerFactory : public views::FocusManagerFactory { + public: + DefaultFocusManagerFactory() : views::FocusManagerFactory() {} + virtual ~DefaultFocusManagerFactory() {} + + protected: + virtual FocusManager* CreateFocusManager(views::Widget* widget) OVERRIDE { + return new FocusManager(widget); + } + + private: + DISALLOW_COPY_AND_ASSIGN(DefaultFocusManagerFactory); +}; + +views::FocusManagerFactory* focus_manager_factory = NULL; + +} // namespace + +namespace views { + +FocusManagerFactory::FocusManagerFactory() { +} + +FocusManagerFactory::~FocusManagerFactory() { +} + +// static +FocusManager* FocusManagerFactory::Create(Widget* widget) { + if (!focus_manager_factory) + focus_manager_factory = new DefaultFocusManagerFactory(); + return focus_manager_factory->CreateFocusManager(widget); +} + +// static +void FocusManagerFactory::Install(FocusManagerFactory* f) { + if (f == focus_manager_factory) + return; + delete focus_manager_factory; + focus_manager_factory = f ? f : new DefaultFocusManagerFactory(); +} + +} // namespace views diff --git a/ui/views/focus/focus_manager_factory.h b/ui/views/focus/focus_manager_factory.h new file mode 100644 index 0000000..7aae11d --- /dev/null +++ b/ui/views/focus/focus_manager_factory.h @@ -0,0 +1,41 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_FOCUS_FOCUS_MANAGER_FACTORY_H_ +#define UI_VIEWS_FOCUS_FOCUS_MANAGER_FACTORY_H_ +#pragma once + +#include "base/basictypes.h" +#include "views/views_export.h" + +namespace views { + +class FocusManager; +class Widget; + +// A factory to create FocusManager. This is used in unit tests +// to inject a custom factory. +class VIEWS_EXPORT FocusManagerFactory { + public: + // Create a FocusManager for the given |widget| using installe Factory. + static FocusManager* Create(Widget* widget); + + // Installs FocusManagerFactory. If |factory| is NULL, it resets + // to the default factory which creates plain FocusManager. + static void Install(FocusManagerFactory* factory); + + protected: + FocusManagerFactory(); + virtual ~FocusManagerFactory(); + + // Create a FocusManager for the given |widget|. + virtual FocusManager* CreateFocusManager(Widget* widget) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(FocusManagerFactory); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_FOCUS_MANAGER_FACTORY_H_ diff --git a/ui/views/focus/focus_manager_unittest.cc b/ui/views/focus/focus_manager_unittest.cc new file mode 100644 index 0000000..d4372f2 --- /dev/null +++ b/ui/views/focus/focus_manager_unittest.cc @@ -0,0 +1,1763 @@ +// Copyright (c) 2011 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 "base/logging.h" +#include "base/string16.h" +#include "base/string_number_conversions.h" +#include "base/utf_string_conversions.h" +#include "third_party/skia/include/core/SkColor.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/models/accelerator.h" +#include "ui/base/models/combobox_model.h" +#include "ui/gfx/rect.h" +#include "ui/views/test/views_test_base.h" +#include "ui/views/window/non_client_view.h" +#include "views/background.h" +#include "views/border.h" +#include "views/controls/button/checkbox.h" +#include "views/controls/button/radio_button.h" +#include "views/controls/combobox/combobox.h" +#include "views/controls/combobox/native_combobox_wrapper.h" +#include "views/controls/label.h" +#include "views/controls/link.h" +#include "views/controls/native/native_view_host.h" +#include "views/controls/scroll_view.h" +#include "views/controls/tabbed_pane/native_tabbed_pane_wrapper.h" +#include "views/controls/tabbed_pane/tabbed_pane.h" +#include "views/controls/textfield/textfield.h" +#include "views/focus/accelerator_handler.h" +#include "views/focus/focus_manager_factory.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" +#include "views/widget/widget_delegate.h" + +#if defined(OS_LINUX) +#include "ui/base/keycodes/keyboard_code_conversion_gtk.h" +#endif + +namespace { +const int kWindowWidth = 600; +const int kWindowHeight = 500; + +int count = 1; + +const int kTopCheckBoxID = count++; // 1 +const int kLeftContainerID = count++; +const int kAppleLabelID = count++; +const int kAppleTextfieldID = count++; +const int kOrangeLabelID = count++; // 5 +const int kOrangeTextfieldID = count++; +const int kBananaLabelID = count++; +const int kBananaTextfieldID = count++; +const int kKiwiLabelID = count++; +const int kKiwiTextfieldID = count++; // 10 +const int kFruitButtonID = count++; +const int kFruitCheckBoxID = count++; +const int kComboboxID = count++; + +const int kRightContainerID = count++; +const int kAsparagusButtonID = count++; // 15 +const int kBroccoliButtonID = count++; +const int kCauliflowerButtonID = count++; + +const int kInnerContainerID = count++; +const int kScrollViewID = count++; +const int kRosettaLinkID = count++; // 20 +const int kStupeurEtTremblementLinkID = count++; +const int kDinerGameLinkID = count++; +const int kRidiculeLinkID = count++; +const int kClosetLinkID = count++; +const int kVisitingLinkID = count++; // 25 +const int kAmelieLinkID = count++; +const int kJoyeuxNoelLinkID = count++; +const int kCampingLinkID = count++; +const int kBriceDeNiceLinkID = count++; +const int kTaxiLinkID = count++; // 30 +const int kAsterixLinkID = count++; + +const int kOKButtonID = count++; +const int kCancelButtonID = count++; +const int kHelpButtonID = count++; + +const int kStyleContainerID = count++; // 35 +const int kBoldCheckBoxID = count++; +const int kItalicCheckBoxID = count++; +const int kUnderlinedCheckBoxID = count++; +const int kStyleHelpLinkID = count++; +const int kStyleTextEditID = count++; // 40 + +const int kSearchContainerID = count++; +const int kSearchTextfieldID = count++; +const int kSearchButtonID = count++; +const int kHelpLinkID = count++; + +const int kThumbnailContainerID = count++; // 45 +const int kThumbnailStarID = count++; +const int kThumbnailSuperStarID = count++; + +} // namespace + +namespace views { + +class FocusManagerTest : public ViewsTestBase, public WidgetDelegate { + public: + FocusManagerTest() + : window_(NULL), + content_view_(NULL), + focus_change_listener_(NULL) { + } + + ~FocusManagerTest() { + } + + virtual void SetUp() OVERRIDE { + ViewsTestBase::SetUp(); + window_ = Widget::CreateWindowWithBounds(this, bounds()); + InitContentView(); + window_->Show(); + } + + virtual void TearDown() OVERRIDE { + if (focus_change_listener_) + GetFocusManager()->RemoveFocusChangeListener(focus_change_listener_); + window_->Close(); + + // Flush the message loop to make application verifiers happy. + RunPendingMessages(); + ViewsTestBase::TearDown(); + } + + FocusManager* GetFocusManager() { + return window_->GetFocusManager(); + } + + void FocusNativeView(gfx::NativeView native_view) { +#if defined(USE_AURA) + NOTIMPLEMENTED(); +#elif defined(OS_WIN) + ::SendMessage(native_view, WM_SETFOCUS, NULL, NULL); +#else + gint return_val; + GdkEventFocus event; + event.type = GDK_FOCUS_CHANGE; + event.window = + gtk_widget_get_root_window(GTK_WIDGET(window_->GetNativeWindow())); + event.send_event = TRUE; + event.in = TRUE; + gtk_signal_emit_by_name(GTK_OBJECT(native_view), "focus-in-event", + &event, &return_val); +#endif + } + + // WidgetDelegate Implementation. + virtual View* GetContentsView() OVERRIDE { + if (!content_view_) + content_view_ = new View(); + return content_view_; + } + virtual Widget* GetWidget() OVERRIDE { + return content_view_->GetWidget(); + } + virtual const Widget* GetWidget() const OVERRIDE { + return content_view_->GetWidget(); + } + + virtual void InitContentView() { + } + + protected: + virtual gfx::Rect bounds() { + return gfx::Rect(0, 0, 500, 500); + } + + // Mocks activating/deactivating the window. + void SimulateActivateWindow() { +#if defined(USE_AURA) + NOTIMPLEMENTED(); +#elif defined(OS_WIN) + ::SendMessage(window_->GetNativeWindow(), WM_ACTIVATE, WA_ACTIVE, NULL); +#else + gboolean result; + g_signal_emit_by_name(G_OBJECT(window_->GetNativeWindow()), + "focus_in_event", 0, &result); +#endif + } + void SimulateDeactivateWindow() { +#if defined(USE_AURA) + NOTIMPLEMENTED(); +#elif defined(OS_WIN) + ::SendMessage(window_->GetNativeWindow(), WM_ACTIVATE, WA_INACTIVE, NULL); +#else + gboolean result; + g_signal_emit_by_name(G_OBJECT(window_->GetNativeWindow()), + "focus_out_event", 0, & result); +#endif + } + + Widget* window_; + View* content_view_; + + void AddFocusChangeListener(FocusChangeListener* listener) { + ASSERT_FALSE(focus_change_listener_); + focus_change_listener_ = listener; + GetFocusManager()->AddFocusChangeListener(listener); + } + +#if defined(USE_AURA) + void PostKeyDown(ui::KeyboardCode key_code) { + NOTIMPLEMENTED(); + } + + void PostKeyUp(ui::KeyboardCode key_code) { + NOTIMPLEMENTED(); + } +#elif defined(OS_WIN) + void PostKeyDown(ui::KeyboardCode key_code) { + ::PostMessage(window_->GetNativeWindow(), WM_KEYDOWN, key_code, 0); + } + + void PostKeyUp(ui::KeyboardCode key_code) { + ::PostMessage(window_->GetNativeWindow(), WM_KEYUP, key_code, 0); + } +#elif defined(OS_LINUX) + void PostKeyDown(ui::KeyboardCode key_code) { + PostKeyEvent(key_code, true); + } + + void PostKeyUp(ui::KeyboardCode key_code) { + PostKeyEvent(key_code, false); + } + + void PostKeyEvent(ui::KeyboardCode key_code, bool pressed) { + int keyval = GdkKeyCodeForWindowsKeyCode(key_code, false); + GdkKeymapKey* keys; + gint n_keys; + gdk_keymap_get_entries_for_keyval( + gdk_keymap_get_default(), + keyval, + &keys, + &n_keys); + GdkEvent* event = gdk_event_new(pressed ? GDK_KEY_PRESS : GDK_KEY_RELEASE); + GdkEventKey* key_event = reinterpret_cast<GdkEventKey*>(event); + int modifier = 0; + if (pressed) + key_event->state = modifier | GDK_KEY_PRESS_MASK; + else + key_event->state = modifier | GDK_KEY_RELEASE_MASK; + + key_event->window = GTK_WIDGET(window_->GetNativeWindow())->window; + DCHECK(key_event->window != NULL); + g_object_ref(key_event->window); + key_event->send_event = true; + key_event->time = GDK_CURRENT_TIME; + key_event->keyval = keyval; + key_event->hardware_keycode = keys[0].keycode; + key_event->group = keys[0].group; + + g_free(keys); + + gdk_event_put(event); + gdk_event_free(event); + } +#endif + + private: + FocusChangeListener* focus_change_listener_; + + DISALLOW_COPY_AND_ASSIGN(FocusManagerTest); +}; + +// BorderView is a view containing a native window with its own view hierarchy. +// It is interesting to test focus traversal from a view hierarchy to an inner +// view hierarchy. +class BorderView : public NativeViewHost { + public: + explicit BorderView(View* child) : child_(child), widget_(NULL) { + DCHECK(child); + set_focusable(false); + } + + virtual ~BorderView() {} + + virtual internal::RootView* GetContentsRootView() { + return static_cast<internal::RootView*>(widget_->GetRootView()); + } + + virtual FocusTraversable* GetFocusTraversable() { + return static_cast<internal::RootView*>(widget_->GetRootView()); + } + + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child) { + NativeViewHost::ViewHierarchyChanged(is_add, parent, child); + + if (child == this && is_add) { + if (!widget_) { + widget_ = new Widget; + Widget::InitParams params(Widget::InitParams::TYPE_CONTROL); +#if defined(OS_WIN) + params.parent = parent->GetWidget()->GetNativeView(); +#elif defined(TOOLKIT_USES_GTK) + params.parent = native_view(); +#else + NOTREACHED(); +#endif + widget_->Init(params); + widget_->SetFocusTraversableParentView(this); + widget_->SetContentsView(child_); + } + + // We have been added to a view hierarchy, attach the native view. + Attach(widget_->GetNativeView()); + // Also update the FocusTraversable parent so the focus traversal works. + static_cast<internal::RootView*>(widget_->GetRootView())-> + SetFocusTraversableParent(GetWidget()->GetFocusTraversable()); + } + } + + private: + View* child_; + Widget* widget_; + + DISALLOW_COPY_AND_ASSIGN(BorderView); +}; + +class DummyComboboxModel : public ui::ComboboxModel { + public: + virtual int GetItemCount() { return 10; } + + virtual string16 GetItemAt(int index) { + return ASCIIToUTF16("Item ") + base::IntToString16(index); + } +}; + +// A View that can act as a pane. +class PaneView : public View, public FocusTraversable { + public: + PaneView() : focus_search_(NULL) {} + + // If this method is called, this view will use GetPaneFocusTraversable to + // have this provided FocusSearch used instead of the default one, allowing + // you to trap focus within the pane. + void EnablePaneFocus(FocusSearch* focus_search) { + focus_search_ = focus_search; + } + + // Overridden from views::View: + virtual FocusTraversable* GetPaneFocusTraversable() { + if (focus_search_) + return this; + else + return NULL; + } + + // Overridden from views::FocusTraversable: + virtual views::FocusSearch* GetFocusSearch() { + return focus_search_; + } + virtual FocusTraversable* GetFocusTraversableParent() { + return NULL; + } + virtual View* GetFocusTraversableParentView() { + return NULL; + } + + private: + FocusSearch* focus_search_; +}; + +class FocusTraversalTest : public FocusManagerTest { + public: + ~FocusTraversalTest(); + + virtual void InitContentView(); + + protected: + FocusTraversalTest(); + + virtual gfx::Rect bounds() { + return gfx::Rect(0, 0, 600, 460); + } + + View* FindViewByID(int id) { + View* view = GetContentsView()->GetViewByID(id); + if (view) + return view; + if (style_tab_) + view = style_tab_->GetSelectedTab()->GetViewByID(id); + if (view) + return view; + view = search_border_view_->GetContentsRootView()->GetViewByID(id); + if (view) + return view; + return NULL; + } + + protected: + TabbedPane* style_tab_; + BorderView* search_border_view_; + DummyComboboxModel combobox_model_; + PaneView* left_container_; + PaneView* right_container_; + + DISALLOW_COPY_AND_ASSIGN(FocusTraversalTest); +}; + +//////////////////////////////////////////////////////////////////////////////// +// FocusTraversalTest +//////////////////////////////////////////////////////////////////////////////// + +FocusTraversalTest::FocusTraversalTest() + : style_tab_(NULL), + search_border_view_(NULL) { +} + +FocusTraversalTest::~FocusTraversalTest() { +} + +void FocusTraversalTest::InitContentView() { + // Create a complicated view hierarchy with lots of control types for + // use by all of the focus traversal tests. + // + // Class name, ID, and asterisk next to focusable views: + // + // View + // Checkbox * kTopCheckBoxID + // PaneView kLeftContainerID + // Label kAppleLabelID + // Textfield * kAppleTextfieldID + // Label kOrangeLabelID + // Textfield * kOrangeTextfieldID + // Label kBananaLabelID + // Textfield * kBananaTextfieldID + // Label kKiwiLabelID + // Textfield * kKiwiTextfieldID + // NativeButton * kFruitButtonID + // Checkbox * kFruitCheckBoxID + // Combobox * kComboboxID + // PaneView kRightContainerID + // RadioButton * kAsparagusButtonID + // RadioButton * kBroccoliButtonID + // RadioButton * kCauliflowerButtonID + // View kInnerContainerID + // ScrollView kScrollViewID + // View + // Link * kRosettaLinkID + // Link * kStupeurEtTremblementLinkID + // Link * kDinerGameLinkID + // Link * kRidiculeLinkID + // Link * kClosetLinkID + // Link * kVisitingLinkID + // Link * kAmelieLinkID + // Link * kJoyeuxNoelLinkID + // Link * kCampingLinkID + // Link * kBriceDeNiceLinkID + // Link * kTaxiLinkID + // Link * kAsterixLinkID + // NativeButton * kOKButtonID + // NativeButton * kCancelButtonID + // NativeButton * kHelpButtonID + // TabbedPane * kStyleContainerID + // View + // Checkbox * kBoldCheckBoxID + // Checkbox * kItalicCheckBoxID + // Checkbox * kUnderlinedCheckBoxID + // Link * kStyleHelpLinkID + // Textfield * kStyleTextEditID + // Other + // BorderView kSearchContainerID + // View + // Textfield * kSearchTextfieldID + // NativeButton * kSearchButtonID + // Link * kHelpLinkID + // View * kThumbnailContainerID + // NativeButton * kThumbnailStarID + // NativeButton * kThumbnailSuperStarID + + content_view_->set_background( + Background::CreateSolidBackground(SK_ColorWHITE)); + + Checkbox* cb = new Checkbox(ASCIIToUTF16("This is a checkbox")); + content_view_->AddChildView(cb); + // In this fast paced world, who really has time for non hard-coded layout? + cb->SetBounds(10, 10, 200, 20); + cb->set_id(kTopCheckBoxID); + + left_container_ = new PaneView(); + left_container_->set_border(Border::CreateSolidBorder(1, SK_ColorBLACK)); + left_container_->set_background( + Background::CreateSolidBackground(240, 240, 240)); + left_container_->set_id(kLeftContainerID); + content_view_->AddChildView(left_container_); + left_container_->SetBounds(10, 35, 250, 200); + + int label_x = 5; + int label_width = 50; + int label_height = 15; + int text_field_width = 150; + int y = 10; + int gap_between_labels = 10; + + Label* label = new Label(ASCIIToUTF16("Apple:")); + label->set_id(kAppleLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + Textfield* text_field = new Textfield(); + text_field->set_id(kAppleTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + label = new Label(ASCIIToUTF16("Orange:")); + label->set_id(kOrangeLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new Textfield(); + text_field->set_id(kOrangeTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + label = new Label(ASCIIToUTF16("Banana:")); + label->set_id(kBananaLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new Textfield(); + text_field->set_id(kBananaTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + label = new Label(ASCIIToUTF16("Kiwi:")); + label->set_id(kKiwiLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new Textfield(); + text_field->set_id(kKiwiTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + NativeTextButton* button = new NativeTextButton(NULL, + ASCIIToUTF16("Click me")); + button->SetBounds(label_x, y + 10, 80, 30); + button->set_id(kFruitButtonID); + left_container_->AddChildView(button); + y += 40; + + cb = new Checkbox(ASCIIToUTF16("This is another check box")); + cb->SetBounds(label_x + label_width + 5, y, 180, 20); + cb->set_id(kFruitCheckBoxID); + left_container_->AddChildView(cb); + y += 20; + + Combobox* combobox = new Combobox(&combobox_model_); + combobox->SetBounds(label_x + label_width + 5, y, 150, 30); + combobox->set_id(kComboboxID); + left_container_->AddChildView(combobox); + + right_container_ = new PaneView(); + right_container_->set_border(Border::CreateSolidBorder(1, SK_ColorBLACK)); + right_container_->set_background( + Background::CreateSolidBackground(240, 240, 240)); + right_container_->set_id(kRightContainerID); + content_view_->AddChildView(right_container_); + right_container_->SetBounds(270, 35, 300, 200); + + y = 10; + int radio_button_height = 18; + int gap_between_radio_buttons = 10; + RadioButton* radio_button = new RadioButton(ASCIIToUTF16("Asparagus"), 1); + radio_button->set_id(kAsparagusButtonID); + right_container_->AddChildView(radio_button); + radio_button->SetBounds(5, y, 70, radio_button_height); + radio_button->SetGroup(1); + y += radio_button_height + gap_between_radio_buttons; + radio_button = new RadioButton(ASCIIToUTF16("Broccoli"), 1); + radio_button->set_id(kBroccoliButtonID); + right_container_->AddChildView(radio_button); + radio_button->SetBounds(5, y, 70, radio_button_height); + radio_button->SetGroup(1); + RadioButton* radio_button_to_check = radio_button; + y += radio_button_height + gap_between_radio_buttons; + radio_button = new RadioButton(ASCIIToUTF16("Cauliflower"), 1); + radio_button->set_id(kCauliflowerButtonID); + right_container_->AddChildView(radio_button); + radio_button->SetBounds(5, y, 70, radio_button_height); + radio_button->SetGroup(1); + y += radio_button_height + gap_between_radio_buttons; + + View* inner_container = new View(); + inner_container->set_border(Border::CreateSolidBorder(1, SK_ColorBLACK)); + inner_container->set_background( + Background::CreateSolidBackground(230, 230, 230)); + inner_container->set_id(kInnerContainerID); + right_container_->AddChildView(inner_container); + inner_container->SetBounds(100, 10, 150, 180); + + ScrollView* scroll_view = new ScrollView(); + scroll_view->set_id(kScrollViewID); + inner_container->AddChildView(scroll_view); + scroll_view->SetBounds(1, 1, 148, 178); + + View* scroll_content = new View(); + scroll_content->SetBounds(0, 0, 200, 200); + scroll_content->set_background( + Background::CreateSolidBackground(200, 200, 200)); + scroll_view->SetContents(scroll_content); + + static const char* const kTitles[] = { + "Rosetta", "Stupeur et tremblement", "The diner game", + "Ridicule", "Le placard", "Les Visiteurs", "Amelie", + "Joyeux Noel", "Camping", "Brice de Nice", + "Taxi", "Asterix" + }; + + static const int kIDs[] = { + kRosettaLinkID, kStupeurEtTremblementLinkID, kDinerGameLinkID, + kRidiculeLinkID, kClosetLinkID, kVisitingLinkID, kAmelieLinkID, + kJoyeuxNoelLinkID, kCampingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID + }; + + DCHECK(arraysize(kTitles) == arraysize(kIDs)); + + y = 5; + for (size_t i = 0; i < arraysize(kTitles); ++i) { + Link* link = new Link(ASCIIToUTF16(kTitles[i])); + link->SetHorizontalAlignment(Label::ALIGN_LEFT); + link->set_id(kIDs[i]); + scroll_content->AddChildView(link); + link->SetBounds(5, y, 300, 15); + y += 15; + } + + y = 250; + int width = 60; + button = new NativeTextButton(NULL, ASCIIToUTF16("OK")); + button->set_id(kOKButtonID); + button->SetIsDefault(true); + + content_view_->AddChildView(button); + button->SetBounds(150, y, width, 30); + + button = new NativeTextButton(NULL, ASCIIToUTF16("Cancel")); + button->set_id(kCancelButtonID); + content_view_->AddChildView(button); + button->SetBounds(220, y, width, 30); + + button = new NativeTextButton(NULL, ASCIIToUTF16("Help")); + button->set_id(kHelpButtonID); + content_view_->AddChildView(button); + button->SetBounds(290, y, width, 30); + + y += 40; + + // Left bottom box with style checkboxes. + View* contents = new View(); + contents->set_background(Background::CreateSolidBackground(SK_ColorWHITE)); + cb = new Checkbox(ASCIIToUTF16("Bold")); + contents->AddChildView(cb); + cb->SetBounds(10, 10, 50, 20); + cb->set_id(kBoldCheckBoxID); + + cb = new Checkbox(ASCIIToUTF16("Italic")); + contents->AddChildView(cb); + cb->SetBounds(70, 10, 50, 20); + cb->set_id(kItalicCheckBoxID); + + cb = new Checkbox(ASCIIToUTF16("Underlined")); + contents->AddChildView(cb); + cb->SetBounds(130, 10, 70, 20); + cb->set_id(kUnderlinedCheckBoxID); + + Link* link = new Link(ASCIIToUTF16("Help")); + contents->AddChildView(link); + link->SetBounds(10, 35, 70, 10); + link->set_id(kStyleHelpLinkID); + + text_field = new Textfield(); + contents->AddChildView(text_field); + text_field->SetBounds(10, 50, 100, 20); + text_field->set_id(kStyleTextEditID); + + style_tab_ = new TabbedPane(); + style_tab_->set_id(kStyleContainerID); + content_view_->AddChildView(style_tab_); + style_tab_->SetBounds(10, y, 210, 100); + style_tab_->AddTab(ASCIIToUTF16("Style"), contents); + style_tab_->AddTab(ASCIIToUTF16("Other"), new View()); + + // Right bottom box with search. + contents = new View(); + contents->set_background(Background::CreateSolidBackground(SK_ColorWHITE)); + text_field = new Textfield(); + contents->AddChildView(text_field); + text_field->SetBounds(10, 10, 100, 20); + text_field->set_id(kSearchTextfieldID); + + button = new NativeTextButton(NULL, ASCIIToUTF16("Search")); + contents->AddChildView(button); + button->SetBounds(112, 5, 60, 30); + button->set_id(kSearchButtonID); + + link = new Link(ASCIIToUTF16("Help")); + link->SetHorizontalAlignment(Label::ALIGN_LEFT); + link->set_id(kHelpLinkID); + contents->AddChildView(link); + link->SetBounds(175, 10, 30, 20); + + search_border_view_ = new BorderView(contents); + search_border_view_->set_id(kSearchContainerID); + + content_view_->AddChildView(search_border_view_); + search_border_view_->SetBounds(300, y, 240, 50); + + y += 60; + + contents = new View(); + contents->set_focusable(true); + contents->set_background(Background::CreateSolidBackground(SK_ColorBLUE)); + contents->set_id(kThumbnailContainerID); + button = new NativeTextButton(NULL, ASCIIToUTF16("Star")); + contents->AddChildView(button); + button->SetBounds(5, 5, 50, 30); + button->set_id(kThumbnailStarID); + button = new NativeTextButton(NULL, ASCIIToUTF16("SuperStar")); + contents->AddChildView(button); + button->SetBounds(60, 5, 100, 30); + button->set_id(kThumbnailSuperStarID); + + content_view_->AddChildView(contents); + contents->SetBounds(250, y, 200, 50); + // We can only call RadioButton::SetChecked() on the radio-button is part of + // the view hierarchy. + radio_button_to_check->SetChecked(true); +} + +//////////////////////////////////////////////////////////////////////////////// +// The tests +//////////////////////////////////////////////////////////////////////////////// + +enum FocusTestEventType { + ON_FOCUS = 0, + ON_BLUR +}; + +struct FocusTestEvent { + FocusTestEvent(FocusTestEventType type, int view_id) + : type(type), + view_id(view_id) { + } + + FocusTestEventType type; + int view_id; +}; + +class SimpleTestView : public View { + public: + SimpleTestView(std::vector<FocusTestEvent>* event_list, int view_id) + : event_list_(event_list) { + set_focusable(true); + set_id(view_id); + } + + virtual void OnFocus() { + event_list_->push_back(FocusTestEvent(ON_FOCUS, id())); + } + + virtual void OnBlur() { + event_list_->push_back(FocusTestEvent(ON_BLUR, id())); + } + + private: + std::vector<FocusTestEvent>* event_list_; +}; + +// Tests that the appropriate Focus related methods are called when a View +// gets/loses focus. +TEST_F(FocusManagerTest, ViewFocusCallbacks) { + std::vector<FocusTestEvent> event_list; + const int kView1ID = 1; + const int kView2ID = 2; + + SimpleTestView* view1 = new SimpleTestView(&event_list, kView1ID); + SimpleTestView* view2 = new SimpleTestView(&event_list, kView2ID); + content_view_->AddChildView(view1); + content_view_->AddChildView(view2); + + view1->RequestFocus(); + ASSERT_EQ(1, static_cast<int>(event_list.size())); + EXPECT_EQ(ON_FOCUS, event_list[0].type); + EXPECT_EQ(kView1ID, event_list[0].view_id); + + event_list.clear(); + view2->RequestFocus(); + ASSERT_EQ(2, static_cast<int>(event_list.size())); + EXPECT_EQ(ON_BLUR, event_list[0].type); + EXPECT_EQ(kView1ID, event_list[0].view_id); + EXPECT_EQ(ON_FOCUS, event_list[1].type); + EXPECT_EQ(kView2ID, event_list[1].view_id); + + event_list.clear(); + GetFocusManager()->ClearFocus(); + ASSERT_EQ(1, static_cast<int>(event_list.size())); + EXPECT_EQ(ON_BLUR, event_list[0].type); + EXPECT_EQ(kView2ID, event_list[0].view_id); +} + +typedef std::pair<View*, View*> ViewPair; +class TestFocusChangeListener : public FocusChangeListener { + public: + virtual void OnWillChangeFocus(View* focused_before, View* focused_now) { + focus_changes_.push_back(ViewPair(focused_before, focused_now)); + } + virtual void OnDidChangeFocus(View* focused_before, View* focused_now) { + } + + const std::vector<ViewPair>& focus_changes() const { return focus_changes_; } + void ClearFocusChanges() { focus_changes_.clear(); } + + private: + // A vector of which views lost/gained focus. + std::vector<ViewPair> focus_changes_; +}; + +TEST_F(FocusManagerTest, FocusChangeListener) { + View* view1 = new View(); + view1->set_focusable(true); + View* view2 = new View(); + view2->set_focusable(true); + content_view_->AddChildView(view1); + content_view_->AddChildView(view2); + + TestFocusChangeListener listener; + AddFocusChangeListener(&listener); + + // Visual Studio 2010 has problems converting NULL to the null pointer for + // std::pair. See http://connect.microsoft.com/VisualStudio/feedback/details/520043/error-converting-from-null-to-a-pointer-type-in-std-pair + // It will work if we pass nullptr. +#if defined(_MSC_VER) && _MSC_VER >= 1600 + views::View* null_view = nullptr; +#else + views::View* null_view = NULL; +#endif + + view1->RequestFocus(); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, view1)); + listener.ClearFocusChanges(); + + view2->RequestFocus(); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(view1, view2)); + listener.ClearFocusChanges(); + + GetFocusManager()->ClearFocus(); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(view2, null_view)); +} + +class TestNativeButton : public NativeTextButton { + public: + explicit TestNativeButton(const string16& text) + : NativeTextButton(NULL, text) { + }; + virtual gfx::NativeView TestGetNativeControlView() { + return GetWidget()->GetNativeView(); + } +}; + +class TestCheckbox : public Checkbox { + public: + explicit TestCheckbox(const string16& text) : Checkbox(text) { + }; + virtual gfx::NativeView TestGetNativeControlView() { + return GetWidget()->GetNativeView(); + } +}; + +class TestRadioButton : public RadioButton { + public: + explicit TestRadioButton(const string16& text) + : RadioButton(text, 1) { + } + virtual gfx::NativeView TestGetNativeControlView() { + return GetWidget()->GetNativeView(); + } +}; + +class TestTextfield : public Textfield { + public: + TestTextfield() { } + virtual gfx::NativeView TestGetNativeControlView() { + return native_wrapper_->GetTestingHandle(); + } +}; + +class TestCombobox : public Combobox, public ui::ComboboxModel { + public: + TestCombobox() : Combobox(this) { } + virtual gfx::NativeView TestGetNativeControlView() { + return native_wrapper_->GetTestingHandle(); + } + virtual int GetItemCount() { + return 10; + } + virtual string16 GetItemAt(int index) { + return ASCIIToUTF16("Hello combo"); + } +}; + +class TestTabbedPane : public TabbedPane { + public: + TestTabbedPane() { } + virtual gfx::NativeView TestGetNativeControlView() { + return native_tabbed_pane_->GetTestingHandle(); + } +}; + +#if !defined(TOUCH_UI) +// TODO(oshima): replace TOUCH_UI with PURE_VIEWS + +// Tests that NativeControls do set the focus View appropriately on the +// FocusManager. +TEST_F(FocusManagerTest, FAILS_FocusNativeControls) { + TestTextfield* textfield = new TestTextfield(); + TestTabbedPane* tabbed_pane = new TestTabbedPane(); + TestTextfield* textfield2 = new TestTextfield(); + + content_view_->AddChildView(textfield); + content_view_->AddChildView(tabbed_pane); + + tabbed_pane->AddTab(ASCIIToUTF16("Awesome textfield"), textfield2); + + // Simulate the native view getting the native focus (such as by user click). + FocusNativeView(textfield->TestGetNativeControlView()); + EXPECT_EQ(textfield, GetFocusManager()->GetFocusedView()); + + FocusNativeView(tabbed_pane->TestGetNativeControlView()); + EXPECT_EQ(tabbed_pane, GetFocusManager()->GetFocusedView()); + + FocusNativeView(textfield2->TestGetNativeControlView()); + EXPECT_EQ(textfield2, GetFocusManager()->GetFocusedView()); +} +#endif + +// On linux, we don't store/restore focused view because gtk handles +// this (and pure views will be the same). +#if defined(OS_WIN) + +// Test that when activating/deactivating the top window, the focus is stored/ +// restored properly. +TEST_F(FocusManagerTest, FocusStoreRestore) { + // Simulate an activate, otherwise the deactivate isn't going to do anything. + SimulateActivateWindow(); + + NativeTextButton* button = new NativeTextButton(NULL, + ASCIIToUTF16("Press me")); + View* view = new View(); + view->set_focusable(true); + + content_view_->AddChildView(button); + button->SetBounds(10, 10, 200, 30); + content_view_->AddChildView(view); + RunPendingMessages(); + + TestFocusChangeListener listener; + AddFocusChangeListener(&listener); + + view->RequestFocus(); + RunPendingMessages(); + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + // Visual Studio 2010 has problems converting NULL to the null pointer for + // std::pair. See http://connect.microsoft.com/VisualStudio/feedback/details/520043/error-converting-from-null-to-a-pointer-type-in-std-pair + // It will work if we pass nullptr. +#if defined(_MSC_VER) && _MSC_VER >= 1600 + views::View* null_view = nullptr; +#else + views::View* null_view = NULL; +#endif + + // Deacivate the window, it should store its focus. + SimulateDeactivateWindow(); + EXPECT_EQ(NULL, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(2, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, view)); + EXPECT_TRUE(listener.focus_changes()[1] == ViewPair(view, null_view)); + listener.ClearFocusChanges(); + + // Reactivate, focus should come-back to the previously focused view. + SimulateActivateWindow(); + EXPECT_EQ(view, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, view)); + listener.ClearFocusChanges(); + + // Same test with a NativeControl. + button->RequestFocus(); + SimulateDeactivateWindow(); + EXPECT_EQ(NULL, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(2, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(view, button)); + EXPECT_TRUE(listener.focus_changes()[1] == ViewPair(button, null_view)); + listener.ClearFocusChanges(); + + SimulateActivateWindow(); + EXPECT_EQ(button, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, button)); + listener.ClearFocusChanges(); + + /* + // Now test that while the window is inactive we can change the focused view + // (we do that in several places). + SimulateDeactivateWindow(); + // TODO: would have to mock the window being inactive (with a TestWidgetWin + // that would return false on IsActive()). + GetFocusManager()->SetFocusedView(view); + ::SendMessage(window_->GetNativeWindow(), WM_ACTIVATE, WA_ACTIVE, NULL); + + EXPECT_EQ(view, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(2, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(button, null_view)); + EXPECT_TRUE(listener.focus_changes()[1] == ViewPair(null_view, view)); + */ +} +#endif + +#if !defined(TOUCH_UI) +// TODO(oshima): There is no tabbed pane in pure views. Replace it +// with different implementation. + +TEST_F(FocusManagerTest, ContainsView) { + View* view = new View(); + scoped_ptr<View> detached_view(new View()); + TabbedPane* tabbed_pane = new TabbedPane(); + TabbedPane* nested_tabbed_pane = new TabbedPane(); + NativeTextButton* tab_button = new NativeTextButton( + NULL, ASCIIToUTF16("tab button")); + + content_view_->AddChildView(view); + content_view_->AddChildView(tabbed_pane); + // Adding a View inside a TabbedPane to test the case of nested root view. + + tabbed_pane->AddTab(ASCIIToUTF16("Awesome tab"), nested_tabbed_pane); + nested_tabbed_pane->AddTab(ASCIIToUTF16("Awesomer tab"), tab_button); + + EXPECT_TRUE(GetFocusManager()->ContainsView(view)); + EXPECT_TRUE(GetFocusManager()->ContainsView(tabbed_pane)); + EXPECT_TRUE(GetFocusManager()->ContainsView(nested_tabbed_pane)); + EXPECT_TRUE(GetFocusManager()->ContainsView(tab_button)); + EXPECT_FALSE(GetFocusManager()->ContainsView(detached_view.get())); +} +#endif + +TEST_F(FocusTraversalTest, NormalTraversal) { + const int kTraversalIDs[] = { kTopCheckBoxID, kAppleTextfieldID, + kOrangeTextfieldID, kBananaTextfieldID, kKiwiTextfieldID, + kFruitButtonID, kFruitCheckBoxID, kComboboxID, kBroccoliButtonID, + kRosettaLinkID, kStupeurEtTremblementLinkID, + kDinerGameLinkID, kRidiculeLinkID, kClosetLinkID, kVisitingLinkID, + kAmelieLinkID, kJoyeuxNoelLinkID, kCampingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID, kOKButtonID, kCancelButtonID, kHelpButtonID, + kStyleContainerID, kBoldCheckBoxID, kItalicCheckBoxID, + kUnderlinedCheckBoxID, kStyleHelpLinkID, kStyleTextEditID, + kSearchTextfieldID, kSearchButtonID, kHelpLinkID, + kThumbnailContainerID, kThumbnailStarID, kThumbnailSuperStarID }; + + // Uncomment the following line if you want to test manually the UI of this + // test. + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + // Let's traverse the whole focus hierarchy (several times, to make sure it + // loops OK). + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } + + // Let's traverse in reverse order. + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } +} + +TEST_F(FocusTraversalTest, TraversalWithNonEnabledViews) { + const int kDisabledIDs[] = { + kBananaTextfieldID, kFruitCheckBoxID, kComboboxID, kAsparagusButtonID, + kCauliflowerButtonID, kClosetLinkID, kVisitingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID, kHelpButtonID, kBoldCheckBoxID, + kSearchTextfieldID, kHelpLinkID }; + + const int kTraversalIDs[] = { kTopCheckBoxID, kAppleTextfieldID, + kOrangeTextfieldID, kKiwiTextfieldID, kFruitButtonID, kBroccoliButtonID, + kRosettaLinkID, kStupeurEtTremblementLinkID, kDinerGameLinkID, + kRidiculeLinkID, kAmelieLinkID, kJoyeuxNoelLinkID, kCampingLinkID, + kOKButtonID, kCancelButtonID, kStyleContainerID, kItalicCheckBoxID, + kUnderlinedCheckBoxID, kStyleHelpLinkID, kStyleTextEditID, + kSearchButtonID, kThumbnailContainerID, kThumbnailStarID, + kThumbnailSuperStarID }; + + // Let's disable some views. + for (size_t i = 0; i < arraysize(kDisabledIDs); i++) { + View* v = FindViewByID(kDisabledIDs[i]); + ASSERT_TRUE(v != NULL); + v->SetEnabled(false); + } + + // Uncomment the following line if you want to test manually the UI of this + // test. + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + View* focused_view; + // Let's do one traversal (several times, to make sure it loops ok). + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } + + // Same thing in reverse. + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } +} + +TEST_F(FocusTraversalTest, TraversalWithInvisibleViews) { + const int kInvisibleIDs[] = { kTopCheckBoxID, kOKButtonID, + kThumbnailContainerID }; + + const int kTraversalIDs[] = { kAppleTextfieldID, kOrangeTextfieldID, + kBananaTextfieldID, kKiwiTextfieldID, kFruitButtonID, kFruitCheckBoxID, + kComboboxID, kBroccoliButtonID, kRosettaLinkID, + kStupeurEtTremblementLinkID, kDinerGameLinkID, kRidiculeLinkID, + kClosetLinkID, kVisitingLinkID, kAmelieLinkID, kJoyeuxNoelLinkID, + kCampingLinkID, kBriceDeNiceLinkID, kTaxiLinkID, kAsterixLinkID, + kCancelButtonID, kHelpButtonID, kStyleContainerID, kBoldCheckBoxID, + kItalicCheckBoxID, kUnderlinedCheckBoxID, kStyleHelpLinkID, + kStyleTextEditID, kSearchTextfieldID, kSearchButtonID, kHelpLinkID }; + + + // Let's make some views invisible. + for (size_t i = 0; i < arraysize(kInvisibleIDs); i++) { + View* v = FindViewByID(kInvisibleIDs[i]); + ASSERT_TRUE(v != NULL); + v->SetVisible(false); + } + + // Uncomment the following line if you want to test manually the UI of this + // test. + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + View* focused_view; + // Let's do one traversal (several times, to make sure it loops ok). + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } + + // Same thing in reverse. + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } +} + +TEST_F(FocusTraversalTest, PaneTraversal) { + // Tests trapping the traversal within a pane - useful for full + // keyboard accessibility for toolbars. + + // First test the left container. + const int kLeftTraversalIDs[] = { + kAppleTextfieldID, + kOrangeTextfieldID, kBananaTextfieldID, kKiwiTextfieldID, + kFruitButtonID, kFruitCheckBoxID, kComboboxID }; + + FocusSearch focus_search_left(left_container_, true, false); + left_container_->EnablePaneFocus(&focus_search_left); + FindViewByID(kComboboxID)->RequestFocus(); + + // Traverse the focus hierarchy within the pane several times. + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kLeftTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kLeftTraversalIDs[j], focused_view->id()); + } + } + + // Traverse in reverse order. + FindViewByID(kAppleTextfieldID)->RequestFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kLeftTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kLeftTraversalIDs[j], focused_view->id()); + } + } + + // Now test the right container, but this time with accessibility mode. + // Make some links not focusable, but mark one of them as + // "accessibility focusable", so it should show up in the traversal. + const int kRightTraversalIDs[] = { + kBroccoliButtonID, kDinerGameLinkID, kRidiculeLinkID, + kClosetLinkID, kVisitingLinkID, kAmelieLinkID, kJoyeuxNoelLinkID, + kCampingLinkID, kBriceDeNiceLinkID, kTaxiLinkID, kAsterixLinkID }; + + FocusSearch focus_search_right(right_container_, true, true); + right_container_->EnablePaneFocus(&focus_search_right); + FindViewByID(kRosettaLinkID)->set_focusable(false); + FindViewByID(kStupeurEtTremblementLinkID)->set_focusable(false); + FindViewByID(kDinerGameLinkID)->set_accessibility_focusable(true); + FindViewByID(kDinerGameLinkID)->set_focusable(false); + FindViewByID(kAsterixLinkID)->RequestFocus(); + + // Traverse the focus hierarchy within the pane several times. + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kRightTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kRightTraversalIDs[j], focused_view->id()); + } + } + + // Traverse in reverse order. + FindViewByID(kBroccoliButtonID)->RequestFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kRightTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kRightTraversalIDs[j], focused_view->id()); + } + } +} + +// Counts accelerator calls. +class TestAcceleratorTarget : public ui::AcceleratorTarget { + public: + explicit TestAcceleratorTarget(bool process_accelerator) + : accelerator_count_(0), process_accelerator_(process_accelerator) {} + + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + ++accelerator_count_; + return process_accelerator_; + } + + int accelerator_count() const { return accelerator_count_; } + + private: + int accelerator_count_; // number of times that the accelerator is activated + bool process_accelerator_; // return value of AcceleratorPressed + + DISALLOW_COPY_AND_ASSIGN(TestAcceleratorTarget); +}; + +TEST_F(FocusManagerTest, CallsNormalAcceleratorTarget) { + FocusManager* focus_manager = GetFocusManager(); + ui::Accelerator return_accelerator(ui::VKEY_RETURN, false, false, false); + ui::Accelerator escape_accelerator(ui::VKEY_ESCAPE, false, false, false); + + TestAcceleratorTarget return_target(true); + TestAcceleratorTarget escape_target(true); + EXPECT_EQ(return_target.accelerator_count(), 0); + EXPECT_EQ(escape_target.accelerator_count(), 0); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(escape_accelerator)); + + // Register targets. + focus_manager->RegisterAccelerator(return_accelerator, &return_target); + focus_manager->RegisterAccelerator(escape_accelerator, &escape_target); + + // Checks if the correct target is registered. + EXPECT_EQ(&return_target, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + EXPECT_EQ(&escape_target, + focus_manager->GetCurrentTargetForAccelerator(escape_accelerator)); + + // Hitting the return key. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(escape_target.accelerator_count(), 0); + + // Hitting the escape key. + EXPECT_TRUE(focus_manager->ProcessAccelerator(escape_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(escape_target.accelerator_count(), 1); + + // Register another target for the return key. + TestAcceleratorTarget return_target2(true); + EXPECT_EQ(return_target2.accelerator_count(), 0); + focus_manager->RegisterAccelerator(return_accelerator, &return_target2); + EXPECT_EQ(&return_target2, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key; return_target2 has the priority. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(return_target2.accelerator_count(), 1); + + // Register a target that does not process the accelerator event. + TestAcceleratorTarget return_target3(false); + EXPECT_EQ(return_target3.accelerator_count(), 0); + focus_manager->RegisterAccelerator(return_accelerator, &return_target3); + EXPECT_EQ(&return_target3, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key. + // Since the event handler of return_target3 returns false, return_target2 + // should be called too. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(return_target2.accelerator_count(), 2); + EXPECT_EQ(return_target3.accelerator_count(), 1); + + // Unregister return_target2. + focus_manager->UnregisterAccelerator(return_accelerator, &return_target2); + EXPECT_EQ(&return_target3, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key. return_target3 and return_target should be called. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 2); + EXPECT_EQ(return_target2.accelerator_count(), 2); + EXPECT_EQ(return_target3.accelerator_count(), 2); + + // Unregister targets. + focus_manager->UnregisterAccelerator(return_accelerator, &return_target); + focus_manager->UnregisterAccelerator(return_accelerator, &return_target3); + focus_manager->UnregisterAccelerator(escape_accelerator, &escape_target); + + // Now there is no target registered. + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(escape_accelerator)); + + // Hitting the return key and the escape key. Nothing should happen. + EXPECT_FALSE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 2); + EXPECT_EQ(return_target2.accelerator_count(), 2); + EXPECT_EQ(return_target3.accelerator_count(), 2); + EXPECT_FALSE(focus_manager->ProcessAccelerator(escape_accelerator)); + EXPECT_EQ(escape_target.accelerator_count(), 1); +} + +// Unregisters itself when its accelerator is invoked. +class SelfUnregisteringAcceleratorTarget : public ui::AcceleratorTarget { + public: + SelfUnregisteringAcceleratorTarget(ui::Accelerator accelerator, + FocusManager* focus_manager) + : accelerator_(accelerator), + focus_manager_(focus_manager), + accelerator_count_(0) { + } + + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + ++accelerator_count_; + focus_manager_->UnregisterAccelerator(accelerator, this); + return true; + } + + int accelerator_count() const { return accelerator_count_; } + + private: + ui::Accelerator accelerator_; + FocusManager* focus_manager_; + int accelerator_count_; + + DISALLOW_COPY_AND_ASSIGN(SelfUnregisteringAcceleratorTarget); +}; + +TEST_F(FocusManagerTest, CallsSelfDeletingAcceleratorTarget) { + FocusManager* focus_manager = GetFocusManager(); + ui::Accelerator return_accelerator(ui::VKEY_RETURN, false, false, false); + SelfUnregisteringAcceleratorTarget target(return_accelerator, focus_manager); + EXPECT_EQ(target.accelerator_count(), 0); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Register the target. + focus_manager->RegisterAccelerator(return_accelerator, &target); + EXPECT_EQ(&target, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key. The target will be unregistered. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(target.accelerator_count(), 1); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key again; nothing should happen. + EXPECT_FALSE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(target.accelerator_count(), 1); +} + +class MessageTrackingView : public View { + public: + MessageTrackingView() : accelerator_pressed_(false) { + } + + virtual bool OnKeyPressed(const KeyEvent& e) { + keys_pressed_.push_back(e.key_code()); + return true; + } + + virtual bool OnKeyReleased(const KeyEvent& e) { + keys_released_.push_back(e.key_code()); + return true; + } + + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + accelerator_pressed_ = true; + return true; + } + + void Reset() { + accelerator_pressed_ = false; + keys_pressed_.clear(); + keys_released_.clear(); + } + + const std::vector<ui::KeyboardCode>& keys_pressed() const { + return keys_pressed_; + } + + const std::vector<ui::KeyboardCode>& keys_released() const { + return keys_released_; + } + + bool accelerator_pressed() const { + return accelerator_pressed_; + } + + private: + bool accelerator_pressed_; + std::vector<ui::KeyboardCode> keys_pressed_; + std::vector<ui::KeyboardCode> keys_released_; + + DISALLOW_COPY_AND_ASSIGN(MessageTrackingView); +}; + +#if defined(OS_WIN) +// This test is now Windows only. Linux Views port does not handle accelerator +// keys in AcceleratorHandler anymore. The logic has been moved into +// NativeWidgetGtk::OnKeyEvent(). +// Tests that the keyup messages are eaten for accelerators. +TEST_F(FocusManagerTest, IgnoreKeyupForAccelerators) { + FocusManager* focus_manager = GetFocusManager(); + MessageTrackingView* mtv = new MessageTrackingView(); + mtv->AddAccelerator(ui::Accelerator(ui::VKEY_0, false, false, false)); + mtv->AddAccelerator(ui::Accelerator(ui::VKEY_1, false, false, false)); + content_view_->AddChildView(mtv); + focus_manager->SetFocusedView(mtv); + + // First send a non-accelerator key sequence. + PostKeyDown(ui::VKEY_9); + PostKeyUp(ui::VKEY_9); + AcceleratorHandler accelerator_handler; + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + // Make sure we get a key-up and key-down. + ASSERT_EQ(1U, mtv->keys_pressed().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[0]); + ASSERT_EQ(1U, mtv->keys_released().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_released()[0]); + EXPECT_FALSE(mtv->accelerator_pressed()); + mtv->Reset(); + + // Same thing with repeat and more than one key at once. + PostKeyDown(ui::VKEY_9); + PostKeyDown(ui::VKEY_9); + PostKeyDown(ui::VKEY_8); + PostKeyDown(ui::VKEY_9); + PostKeyDown(ui::VKEY_7); + PostKeyUp(ui::VKEY_9); + PostKeyUp(ui::VKEY_7); + PostKeyUp(ui::VKEY_8); + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + // Make sure we get a key-up and key-down. + ASSERT_EQ(5U, mtv->keys_pressed().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[0]); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[1]); + EXPECT_EQ(ui::VKEY_8, mtv->keys_pressed()[2]); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[3]); + EXPECT_EQ(ui::VKEY_7, mtv->keys_pressed()[4]); + ASSERT_EQ(3U, mtv->keys_released().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_released()[0]); + EXPECT_EQ(ui::VKEY_7, mtv->keys_released()[1]); + EXPECT_EQ(ui::VKEY_8, mtv->keys_released()[2]); + EXPECT_FALSE(mtv->accelerator_pressed()); + mtv->Reset(); + + // Now send an accelerator key sequence. + PostKeyDown(ui::VKEY_0); + PostKeyUp(ui::VKEY_0); + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + EXPECT_TRUE(mtv->keys_pressed().empty()); + EXPECT_TRUE(mtv->keys_released().empty()); + EXPECT_TRUE(mtv->accelerator_pressed()); + mtv->Reset(); + + // Same thing with repeat and more than one key at once. + PostKeyDown(ui::VKEY_0); + PostKeyDown(ui::VKEY_1); + PostKeyDown(ui::VKEY_1); + PostKeyDown(ui::VKEY_0); + PostKeyDown(ui::VKEY_0); + PostKeyUp(ui::VKEY_1); + PostKeyUp(ui::VKEY_0); + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + EXPECT_TRUE(mtv->keys_pressed().empty()); + EXPECT_TRUE(mtv->keys_released().empty()); + EXPECT_TRUE(mtv->accelerator_pressed()); + mtv->Reset(); +} +#endif + +#if defined(OS_WIN) && !defined(USE_AURA) +// Test that the focus manager is created successfully for the first view +// window parented to a native dialog. +TEST_F(FocusManagerTest, CreationForNativeRoot) { + // Create a window class. + WNDCLASSEX class_ex; + memset(&class_ex, 0, sizeof(class_ex)); + class_ex.cbSize = sizeof(WNDCLASSEX); + class_ex.lpfnWndProc = &DefWindowProc; + class_ex.lpszClassName = L"TestWindow"; + ATOM atom = RegisterClassEx(&class_ex); + ASSERT_TRUE(atom); + + // Create a native dialog window. + HWND hwnd = CreateWindowEx(0, class_ex.lpszClassName, NULL, + WS_OVERLAPPEDWINDOW, 0, 0, 200, 200, + NULL, NULL, NULL, NULL); + ASSERT_TRUE(hwnd); + + // Create a view window parented to native dialog. + scoped_ptr<Widget> widget1(new Widget); + Widget::InitParams params(Widget::InitParams::TYPE_CONTROL); + params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.parent = hwnd; + params.bounds = gfx::Rect(0, 0, 100, 100); + params.top_level = true; // This is top level in views hierarchy. + widget1->Init(params); + + // Get the focus manager directly from the first window. Should exist + // because the first window is the root widget. + views::FocusManager* focus_manager1 = widget1->GetFocusManager(); + EXPECT_TRUE(focus_manager1); + + // Create another view window parented to the first view window. + scoped_ptr<Widget> widget2(new Widget); + params.parent = widget1->GetNativeView(); + params.top_level = false; // This is child widget. + widget2->Init(params); + + // Access the shared focus manager directly from the second window. + views::FocusManager* focus_manager2 = widget2->GetFocusManager(); + EXPECT_EQ(focus_manager2, focus_manager1); + + // Access the shared focus manager indirectly from the first window handle. + gfx::NativeWindow native_window = widget1->GetNativeWindow(); + views::Widget* widget = + views::Widget::GetWidgetForNativeWindow(native_window); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + // Access the shared focus manager indirectly from the second window handle. + native_window = widget2->GetNativeWindow(); + widget = views::Widget::GetWidgetForNativeWindow(native_window); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + // Access the shared focus manager indirectly from the first view handle. + gfx::NativeView native_view = widget1->GetNativeView(); + widget = views::Widget::GetTopLevelWidgetForNativeView(native_view); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + // Access the shared focus manager indirectly from the second view handle. + native_view = widget2->GetNativeView(); + widget = views::Widget::GetTopLevelWidgetForNativeView(native_view); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + DestroyWindow(hwnd); +} +#endif + +class FocusManagerDtorTest : public FocusManagerTest { + protected: + typedef std::vector<std::string> DtorTrackVector; + + class FocusManagerDtorTracked : public FocusManager { + public: + FocusManagerDtorTracked(Widget* widget, DtorTrackVector* dtor_tracker) + : FocusManager(widget), + dtor_tracker_(dtor_tracker) { + } + + virtual ~FocusManagerDtorTracked() { + dtor_tracker_->push_back("FocusManagerDtorTracked"); + } + + DtorTrackVector* dtor_tracker_; + + private: + DISALLOW_COPY_AND_ASSIGN(FocusManagerDtorTracked); + }; + + class TestFocusManagerFactory : public FocusManagerFactory { + public: + explicit TestFocusManagerFactory(DtorTrackVector* dtor_tracker) + : dtor_tracker_(dtor_tracker) { + } + + FocusManager* CreateFocusManager(Widget* widget) OVERRIDE { + return new FocusManagerDtorTracked(widget, dtor_tracker_); + } + + private: + DtorTrackVector* dtor_tracker_; + DISALLOW_COPY_AND_ASSIGN(TestFocusManagerFactory); + }; + + class NativeButtonDtorTracked : public NativeTextButton { + public: + NativeButtonDtorTracked(const string16& text, + DtorTrackVector* dtor_tracker) + : NativeTextButton(NULL, text), + dtor_tracker_(dtor_tracker) { + }; + virtual ~NativeButtonDtorTracked() { + dtor_tracker_->push_back("NativeButtonDtorTracked"); + } + + DtorTrackVector* dtor_tracker_; + }; + + class WindowDtorTracked : public Widget { + public: + explicit WindowDtorTracked(DtorTrackVector* dtor_tracker) + : dtor_tracker_(dtor_tracker) { + } + + virtual ~WindowDtorTracked() { + dtor_tracker_->push_back("WindowDtorTracked"); + } + + DtorTrackVector* dtor_tracker_; + }; + + virtual void SetUp() { + ViewsTestBase::SetUp(); + FocusManagerFactory::Install(new TestFocusManagerFactory(&dtor_tracker_)); + // Create WindowDtorTracked that uses FocusManagerDtorTracked. + window_ = new WindowDtorTracked(&dtor_tracker_); + Widget::InitParams params; + params.delegate = this; + params.bounds = gfx::Rect(0, 0, 100, 100); + window_->Init(params); + + tracked_focus_manager_ = + static_cast<FocusManagerDtorTracked*>(GetFocusManager()); + window_->Show(); + } + + virtual void TearDown() { + if (window_) { + window_->Close(); + RunPendingMessages(); + } + FocusManagerFactory::Install(NULL); + ViewsTestBase::TearDown(); + } + + FocusManager* tracked_focus_manager_; + DtorTrackVector dtor_tracker_; +}; + +TEST_F(FocusManagerDtorTest, FocusManagerDestructedLast) { + // Setup views hierarchy. + TabbedPane* tabbed_pane = new TabbedPane(); + content_view_->AddChildView(tabbed_pane); + + NativeButtonDtorTracked* button = new NativeButtonDtorTracked( + ASCIIToUTF16("button"), &dtor_tracker_); + tabbed_pane->AddTab(ASCIIToUTF16("Awesome tab"), button); + + // Close the window. + window_->Close(); + RunPendingMessages(); + + // Test window, button and focus manager should all be destructed. + ASSERT_EQ(3, static_cast<int>(dtor_tracker_.size())); + + // Focus manager should be the last one to destruct. + ASSERT_STREQ("FocusManagerDtorTracked", dtor_tracker_[2].c_str()); + + // Clear window_ so that we don't try to close it again. + window_ = NULL; +} + +} // namespace views diff --git a/ui/views/focus/focus_search.cc b/ui/views/focus/focus_search.cc new file mode 100644 index 0000000..eac3f21 --- /dev/null +++ b/ui/views/focus/focus_search.cc @@ -0,0 +1,272 @@ +// Copyright (c) 2011 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 "base/logging.h" +#include "views/focus/focus_manager.h" +#include "views/focus/focus_search.h" +#include "views/view.h" + +namespace views { + +FocusSearch::FocusSearch(View* root, bool cycle, bool accessibility_mode) + : root_(root), + cycle_(cycle), + accessibility_mode_(accessibility_mode) { +} + +View* FocusSearch::FindNextFocusableView(View* starting_view, + bool reverse, + Direction direction, + bool check_starting_view, + FocusTraversable** focus_traversable, + View** focus_traversable_view) { + *focus_traversable = NULL; + *focus_traversable_view = NULL; + + if (!root_->has_children()) { + NOTREACHED(); + // Nothing to focus on here. + return NULL; + } + + View* initial_starting_view = starting_view; + int starting_view_group = -1; + if (starting_view) + starting_view_group = starting_view->GetGroup(); + + if (!starting_view) { + // Default to the first/last child + starting_view = reverse ? root_->child_at(root_->child_count() - 1) : + root_->child_at(0); + // If there was no starting view, then the one we select is a potential + // focus candidate. + check_starting_view = true; + } else { + // The starting view should be a direct or indirect child of the root. + DCHECK(root_->Contains(starting_view)); + } + + View* v = NULL; + if (!reverse) { + v = FindNextFocusableViewImpl(starting_view, check_starting_view, + true, + (direction == DOWN) ? true : false, + starting_view_group, + focus_traversable, + focus_traversable_view); + } else { + // If the starting view is focusable, we don't want to go down, as we are + // traversing the view hierarchy tree bottom-up. + bool can_go_down = (direction == DOWN) && !IsFocusable(starting_view); + v = FindPreviousFocusableViewImpl(starting_view, check_starting_view, + true, + can_go_down, + starting_view_group, + focus_traversable, + focus_traversable_view); + } + + // Don't set the focus to something outside of this view hierarchy. + if (v && v != root_ && !root_->Contains(v)) + v = NULL; + + // If |cycle_| is true, prefer to keep cycling rather than returning NULL. + if (cycle_ && !v && initial_starting_view) { + v = FindNextFocusableView(NULL, reverse, direction, check_starting_view, + focus_traversable, focus_traversable_view); + DCHECK(IsFocusable(v)); + return v; + } + + // Doing some sanity checks. + if (v) { + DCHECK(IsFocusable(v)); + return v; + } + if (*focus_traversable) { + DCHECK(*focus_traversable_view); + return NULL; + } + // Nothing found. + return NULL; +} + +bool FocusSearch::IsViewFocusableCandidate(View* v, int skip_group_id) { + return IsFocusable(v) && + (v->IsGroupFocusTraversable() || skip_group_id == -1 || + v->GetGroup() != skip_group_id); +} + +bool FocusSearch::IsFocusable(View* v) { + if (accessibility_mode_) + return v && v->IsAccessibilityFocusableInRootView(); + + return v && v->IsFocusableInRootView(); +} + +View* FocusSearch::FindSelectedViewForGroup(View* view) { + if (view->IsGroupFocusTraversable() || + view->GetGroup() == -1) // No group for that view. + return view; + + View* selected_view = view->GetSelectedViewForGroup(view->GetGroup()); + if (selected_view) + return selected_view; + + // No view selected for that group, default to the specified view. + return view; +} + +View* FocusSearch::GetParent(View* v) { + return root_->Contains(v) ? v->parent() : NULL; +} + +// Strategy for finding the next focusable view: +// - keep going down the first child, stop when you find a focusable view or +// a focus traversable view (in that case return it) or when you reach a view +// with no children. +// - go to the right sibling and start the search from there (by invoking +// FindNextFocusableViewImpl on that view). +// - if the view has no right sibling, go up the parents until you find a parent +// with a right sibling and start the search from there. +View* FocusSearch::FindNextFocusableViewImpl( + View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view) { + if (check_starting_view) { + if (IsViewFocusableCandidate(starting_view, skip_group_id)) { + View* v = FindSelectedViewForGroup(starting_view); + // The selected view might not be focusable (if it is disabled for + // example). + if (IsFocusable(v)) + return v; + } + + *focus_traversable = starting_view->GetFocusTraversable(); + if (*focus_traversable) { + *focus_traversable_view = starting_view; + return NULL; + } + } + + // First let's try the left child. + if (can_go_down) { + if (starting_view->has_children()) { + View* v = FindNextFocusableViewImpl(starting_view->child_at(0), + true, false, true, skip_group_id, + focus_traversable, + focus_traversable_view); + if (v || *focus_traversable) + return v; + } + } + + // Then try the right sibling. + View* sibling = starting_view->GetNextFocusableView(); + if (sibling) { + View* v = FindNextFocusableViewImpl(sibling, + true, false, true, skip_group_id, + focus_traversable, + focus_traversable_view); + if (v || *focus_traversable) + return v; + } + + // Then go up to the parent sibling. + if (can_go_up) { + View* parent = GetParent(starting_view); + while (parent) { + sibling = parent->GetNextFocusableView(); + if (sibling) { + return FindNextFocusableViewImpl(sibling, + true, true, true, + skip_group_id, + focus_traversable, + focus_traversable_view); + } + parent = GetParent(parent); + } + } + + // We found nothing. + return NULL; +} + +// Strategy for finding the previous focusable view: +// - keep going down on the right until you reach a view with no children, if it +// it is a good candidate return it. +// - start the search on the left sibling. +// - if there are no left sibling, start the search on the parent (without going +// down). +View* FocusSearch::FindPreviousFocusableViewImpl( + View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view) { + // Let's go down and right as much as we can. + if (can_go_down) { + // Before we go into the direct children, we have to check if this view has + // a FocusTraversable. + *focus_traversable = starting_view->GetFocusTraversable(); + if (*focus_traversable) { + *focus_traversable_view = starting_view; + return NULL; + } + + if (starting_view->has_children()) { + View* view = + starting_view->child_at(starting_view->child_count() - 1); + View* v = FindPreviousFocusableViewImpl(view, true, false, true, + skip_group_id, + focus_traversable, + focus_traversable_view); + if (v || *focus_traversable) + return v; + } + } + + // Then look at this view. Here, we do not need to see if the view has + // a FocusTraversable, since we do not want to go down any more. + if (check_starting_view && + IsViewFocusableCandidate(starting_view, skip_group_id)) { + View* v = FindSelectedViewForGroup(starting_view); + // The selected view might not be focusable (if it is disabled for + // example). + if (IsFocusable(v)) + return v; + } + + // Then try the left sibling. + View* sibling = starting_view->GetPreviousFocusableView(); + if (sibling) { + return FindPreviousFocusableViewImpl(sibling, + true, true, true, + skip_group_id, + focus_traversable, + focus_traversable_view); + } + + // Then go up the parent. + if (can_go_up) { + View* parent = GetParent(starting_view); + if (parent) + return FindPreviousFocusableViewImpl(parent, + true, true, false, + skip_group_id, + focus_traversable, + focus_traversable_view); + } + + // We found nothing. + return NULL; +} + +} // namespace views diff --git a/ui/views/focus/focus_search.h b/ui/views/focus/focus_search.h new file mode 100644 index 0000000..9fe37a0 --- /dev/null +++ b/ui/views/focus/focus_search.h @@ -0,0 +1,122 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_FOCUS_FOCUS_SEARCH_H_ +#define UI_VIEWS_FOCUS_FOCUS_SEARCH_H_ +#pragma once + +#include "views/view.h" + +namespace views { + +class FocusTraversable; + +// FocusSearch is an object that implements the algorithm to find the +// next view to focus. +class VIEWS_EXPORT FocusSearch { + public: + // The direction in which the focus traversal is going. + // TODO (jcampan): add support for lateral (left, right) focus traversal. The + // goal is to switch to focusable views on the same level when using the arrow + // keys (ala Windows: in a dialog box, arrow keys typically move between the + // dialog OK, Cancel buttons). + enum Direction { + UP = 0, + DOWN + }; + + // Constructor. + // - |root| is the root of the view hierarchy to traverse. Focus will be + // trapped inside. + // - |cycle| should be true if you want FindNextFocusableView to cycle back + // to the first view within this root when the traversal reaches + // the end. If this is true, then if you pass a valid starting + // view to FindNextFocusableView you will always get a valid view + // out, even if it's the same view. + // - |accessibility_mode| should be true if full keyboard accessibility is + // needed and you want to check IsAccessibilityFocusableInRootView(), + // rather than IsFocusableInRootView(). + FocusSearch(View* root, bool cycle, bool accessibility_mode); + virtual ~FocusSearch() {} + + // Finds the next view that should be focused and returns it. If a + // FocusTraversable is found while searching for the focusable view, + // returns NULL and sets |focus_traversable| to the FocusTraversable + // and |focus_traversable_view| to the view associated with the + // FocusTraversable. + // + // Return NULL if the end of the focus loop is reached, unless this object + // was initialized with |cycle|=true, in which case it goes back to the + // beginning when it reaches the end of the traversal. + // - |starting_view| is the view that should be used as the starting point + // when looking for the previous/next view. It may be NULL (in which case + // the first/last view should be used depending if normal/reverse). + // - |reverse| whether we should find the next (reverse is false) or the + // previous (reverse is true) view. + // - |direction| specifies whether we are traversing down (meaning we should + // look into child views) or traversing up (don't look at child views). + // - |check_starting_view| is true if starting_view may obtain the next focus. + // - |focus_traversable| is set to the focus traversable that should be + // traversed if one is found (in which case the call returns NULL). + // - |focus_traversable_view| is set to the view associated with the + // FocusTraversable set in the previous parameter (it is used as the + // starting view when looking for the next focusable view). + virtual View* FindNextFocusableView(View* starting_view, + bool reverse, + Direction direction, + bool check_starting_view, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + + private: + // Convenience method that returns true if a view is focusable and does not + // belong to the specified group. + bool IsViewFocusableCandidate(View* v, int skip_group_id); + + // Convenience method; returns true if a view is not NULL and is focusable + // (checking IsAccessibilityFocusableInRootView() if accessibility_mode_ is + // true). + bool IsFocusable(View* v); + + // Returns the view selected for the group of the selected view. If the view + // does not belong to a group or if no view is selected in the group, the + // specified view is returned. + View* FindSelectedViewForGroup(View* view); + + // Get the parent, but stay within the root. Returns NULL if asked for + // the parent of root_. + View* GetParent(View* view); + + // Returns the next focusable view or view containing a FocusTraversable + // (NULL if none was found), starting at the starting_view. + // |check_starting_view|, |can_go_up| and |can_go_down| controls the + // traversal of the views hierarchy. |skip_group_id| specifies a group_id, + // -1 means no group. All views from a group are traversed in one pass. + View* FindNextFocusableViewImpl(View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + + // Same as FindNextFocusableViewImpl but returns the previous focusable view. + View* FindPreviousFocusableViewImpl(View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + + View* root_; + bool cycle_; + bool accessibility_mode_; + + DISALLOW_COPY_AND_ASSIGN(FocusSearch); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_FOCUS_SEARCH_H_ diff --git a/ui/views/focus/view_storage.cc b/ui/views/focus/view_storage.cc new file mode 100644 index 0000000..3b9ec10 --- /dev/null +++ b/ui/views/focus/view_storage.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2011 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 "views/focus/view_storage.h" + +#include <algorithm> + +#include "base/logging.h" +#include "base/stl_util.h" + +namespace views { + +// static +ViewStorage* ViewStorage::GetInstance() { + return Singleton<ViewStorage>::get(); +} + +ViewStorage::ViewStorage() : view_storage_next_id_(0) { +} + +ViewStorage::~ViewStorage() { + STLDeleteContainerPairSecondPointers(view_to_ids_.begin(), + view_to_ids_.end()); +} + +int ViewStorage::CreateStorageID() { + return view_storage_next_id_++; +} + +void ViewStorage::StoreView(int storage_id, View* view) { + DCHECK(view); + std::map<int, View*>::iterator iter = id_to_view_.find(storage_id); + + if (iter != id_to_view_.end()) { + NOTREACHED(); + RemoveView(storage_id); + } + + id_to_view_[storage_id] = view; + + std::vector<int>* ids = NULL; + std::map<View*, std::vector<int>*>::iterator id_iter = + view_to_ids_.find(view); + if (id_iter == view_to_ids_.end()) { + ids = new std::vector<int>(); + view_to_ids_[view] = ids; + } else { + ids = id_iter->second; + } + ids->push_back(storage_id); +} + +View* ViewStorage::RetrieveView(int storage_id) { + std::map<int, View*>::iterator iter = id_to_view_.find(storage_id); + if (iter == id_to_view_.end()) + return NULL; + return iter->second; +} + +void ViewStorage::RemoveView(int storage_id) { + EraseView(storage_id, false); +} + +void ViewStorage::ViewRemoved(View* removed) { + // Let's first retrieve the ids for that view. + std::map<View*, std::vector<int>*>::iterator ids_iter = + view_to_ids_.find(removed); + + if (ids_iter == view_to_ids_.end()) { + // That view is not in the view storage. + return; + } + + std::vector<int>* ids = ids_iter->second; + DCHECK(!ids->empty()); + EraseView((*ids)[0], true); +} + +void ViewStorage::EraseView(int storage_id, bool remove_all_ids) { + // Remove the view from id_to_view_location_. + std::map<int, View*>::iterator view_iter = id_to_view_.find(storage_id); + if (view_iter == id_to_view_.end()) + return; + + View* view = view_iter->second; + id_to_view_.erase(view_iter); + + // Also update view_to_ids_. + std::map<View*, std::vector<int>*>::iterator ids_iter = + view_to_ids_.find(view); + DCHECK(ids_iter != view_to_ids_.end()); + std::vector<int>* ids = ids_iter->second; + + if (remove_all_ids) { + for (size_t i = 0; i < ids->size(); ++i) { + view_iter = id_to_view_.find((*ids)[i]); + if (view_iter != id_to_view_.end()) + id_to_view_.erase(view_iter); + } + ids->clear(); + } else { + std::vector<int>::iterator id_iter = + std::find(ids->begin(), ids->end(), storage_id); + DCHECK(id_iter != ids->end()); + ids->erase(id_iter); + } + + if (ids->empty()) { + delete ids; + view_to_ids_.erase(ids_iter); + } +} + +} // namespace views diff --git a/ui/views/focus/view_storage.h b/ui/views/focus/view_storage.h new file mode 100644 index 0000000..7d748e2 --- /dev/null +++ b/ui/views/focus/view_storage.h @@ -0,0 +1,70 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_FOCUS_VIEW_STORAGE_H_ +#define UI_VIEWS_FOCUS_VIEW_STORAGE_H_ +#pragma once + +#include "base/memory/singleton.h" +#include "views/view.h" + +// This class is a simple storage place for storing/retrieving views. It is +// used for example in the FocusManager to store/restore focused views when the +// main window becomes active/inactive. +// It automatically removes a view from the storage if the view is removed from +// the tree hierarchy. +// +// To use it, you first need to create a view storage id that can then be used +// to store/retrieve views. + +namespace views { + +class VIEWS_EXPORT ViewStorage { + public: + // Returns the global ViewStorage instance. + // It is guaranted to be non NULL. + static ViewStorage* GetInstance(); + + // Returns a unique storage id that can be used to store/retrieve views. + int CreateStorageID(); + + // Associates |view| with the specified |storage_id|. + void StoreView(int storage_id, View* view); + + // Returns the view associated with |storage_id| if any, NULL otherwise. + View* RetrieveView(int storage_id); + + // Removes the view associated with |storage_id| if any. + void RemoveView(int storage_id); + + // Notifies the ViewStorage that a view was removed from its parent somewhere. + void ViewRemoved(View* removed); + + size_t view_count() const { return view_to_ids_.size(); } + + private: + friend struct DefaultSingletonTraits<ViewStorage>; + + ViewStorage(); + ~ViewStorage(); + + // Removes the view associated with |storage_id|. If |remove_all_ids| is true, + // all other mapping pointing to the same view are removed as well. + void EraseView(int storage_id, bool remove_all_ids); + + // Next id for the view storage. + int view_storage_next_id_; + + // The association id to View used for the view storage. + std::map<int, View*> id_to_view_; + + // Association View to id, used to speed up view notification removal. + std::map<View*, std::vector<int>*> view_to_ids_; + + DISALLOW_COPY_AND_ASSIGN(ViewStorage); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_VIEW_STORAGE_H_ diff --git a/ui/views/focus/widget_focus_manager.cc b/ui/views/focus/widget_focus_manager.cc new file mode 100644 index 0000000..57cd2cd --- /dev/null +++ b/ui/views/focus/widget_focus_manager.cc @@ -0,0 +1,50 @@ +// Copyright (c) 2011 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 "views/focus/widget_focus_manager.h" + +#include "base/memory/singleton.h" + +namespace views { + +// WidgetFocusManager ---------------------------------------------------------- + +// static +WidgetFocusManager* WidgetFocusManager::GetInstance() { + return Singleton<WidgetFocusManager>::get(); +} + +void WidgetFocusManager::AddFocusChangeListener( + WidgetFocusChangeListener* listener) { + focus_change_listeners_.AddObserver(listener); +} + +void WidgetFocusManager::RemoveFocusChangeListener( + WidgetFocusChangeListener* listener) { + focus_change_listeners_.RemoveObserver(listener); +} + +void WidgetFocusManager::OnWidgetFocusEvent(gfx::NativeView focused_before, + gfx::NativeView focused_now) { + if (enabled_) { + FOR_EACH_OBSERVER(WidgetFocusChangeListener, focus_change_listeners_, + OnNativeFocusChange(focused_before, focused_now)); + } +} + +WidgetFocusManager::WidgetFocusManager() : enabled_(true) {} + +WidgetFocusManager::~WidgetFocusManager() {} + +// AutoNativeNotificationDisabler ---------------------------------------------- + +AutoNativeNotificationDisabler::AutoNativeNotificationDisabler() { + WidgetFocusManager::GetInstance()->DisableNotifications(); +} + +AutoNativeNotificationDisabler::~AutoNativeNotificationDisabler() { + WidgetFocusManager::GetInstance()->EnableNotifications(); +} + +} // namespace views diff --git a/ui/views/focus/widget_focus_manager.h b/ui/views/focus/widget_focus_manager.h new file mode 100644 index 0000000..7a0143b --- /dev/null +++ b/ui/views/focus/widget_focus_manager.h @@ -0,0 +1,82 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_FOCUS_WIDGET_FOCUS_MANAGER_H_ +#define UI_VIEWS_FOCUS_WIDGET_FOCUS_MANAGER_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/observer_list.h" +#include "ui/gfx/native_widget_types.h" +#include "views/views_export.h" + +template <typename T> struct DefaultSingletonTraits; + +namespace views { + +// This interface should be implemented by classes that want to be notified when +// the native focus is about to change. Listeners implementing this interface +// will be invoked for all native focus changes across the entire Chrome +// application. FocusChangeListeners are only called for changes within the +// children of a single top-level native-view. +class WidgetFocusChangeListener { + public: + virtual void OnNativeFocusChange(gfx::NativeView focused_before, + gfx::NativeView focused_now) = 0; + + protected: + virtual ~WidgetFocusChangeListener() {} +}; + +class VIEWS_EXPORT WidgetFocusManager { + public: + // Returns the singleton instance. + static WidgetFocusManager* GetInstance(); + + // Adds/removes a WidgetFocusChangeListener |listener| to the set of + // active listeners. + void AddFocusChangeListener(WidgetFocusChangeListener* listener); + void RemoveFocusChangeListener(WidgetFocusChangeListener* listener); + + // To be called when native-focus shifts from |focused_before| to + // |focused_now|. + // TODO(port) : Invocations to this routine are only implemented for + // the Win32 platform. Calls need to be placed appropriately for + // non-Windows environments. + void OnWidgetFocusEvent(gfx::NativeView focused_before, + gfx::NativeView focused_now); + + // Enable/Disable notification of registered listeners during calls + // to OnWidgetFocusEvent. Used to prevent unwanted focus changes from + // propagating notifications. + void EnableNotifications() { enabled_ = true; } + void DisableNotifications() { enabled_ = false; } + + private: + friend struct DefaultSingletonTraits<WidgetFocusManager>; + + WidgetFocusManager(); + ~WidgetFocusManager(); + + ObserverList<WidgetFocusChangeListener> focus_change_listeners_; + + bool enabled_; + + DISALLOW_COPY_AND_ASSIGN(WidgetFocusManager); +}; + +// A basic helper class that is used to disable native focus change +// notifications within a scope. +class VIEWS_EXPORT AutoNativeNotificationDisabler { + public: + AutoNativeNotificationDisabler(); + ~AutoNativeNotificationDisabler(); + + private: + DISALLOW_COPY_AND_ASSIGN(AutoNativeNotificationDisabler); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_WIDGET_FOCUS_MANAGER_H_ diff --git a/ui/views/layout/box_layout.cc b/ui/views/layout/box_layout.cc new file mode 100644 index 0000000..5441e87 --- /dev/null +++ b/ui/views/layout/box_layout.cc @@ -0,0 +1,76 @@ +// Copyright (c) 2011 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 "views/layout/box_layout.h" + +#include "ui/gfx/insets.h" +#include "ui/gfx/rect.h" +#include "views/view.h" + +namespace views { + +BoxLayout::BoxLayout(BoxLayout::Orientation orientation, + int inside_border_horizontal_spacing, + int inside_border_vertical_spacing, + int between_child_spacing) + : orientation_(orientation), + inside_border_horizontal_spacing_(inside_border_horizontal_spacing), + inside_border_vertical_spacing_(inside_border_vertical_spacing), + between_child_spacing_(between_child_spacing) { +} + +BoxLayout::~BoxLayout() { +} + +void BoxLayout::Layout(View* host) { + gfx::Rect child_area(host->GetLocalBounds()); + child_area.Inset(host->GetInsets()); + child_area.Inset(inside_border_horizontal_spacing_, + inside_border_vertical_spacing_); + int x = child_area.x(); + int y = child_area.y(); + for (int i = 0; i < host->child_count(); ++i) { + View* child = host->child_at(i); + if (child->IsVisible()) { + gfx::Rect bounds(x, y, child_area.width(), child_area.height()); + gfx::Size size(child->GetPreferredSize()); + if (orientation_ == kHorizontal) { + bounds.set_width(size.width()); + x += size.width() + between_child_spacing_; + } else { + bounds.set_height(size.height()); + y += size.height() + between_child_spacing_; + } + // Clamp child view bounds to |child_area|. + child->SetBoundsRect(bounds.Intersect(child_area)); + } + } +} + +gfx::Size BoxLayout::GetPreferredSize(View* host) { + gfx::Rect bounds; + int position = 0; + for (int i = 0; i < host->child_count(); ++i) { + View* child = host->child_at(i); + if (child->IsVisible()) { + gfx::Size size(child->GetPreferredSize()); + if (orientation_ == kHorizontal) { + gfx::Rect child_bounds(position, 0, size.width(), size.height()); + bounds = bounds.Union(child_bounds); + position += size.width(); + } else { + gfx::Rect child_bounds(0, position, size.width(), size.height()); + bounds = bounds.Union(child_bounds); + position += size.height(); + } + position += between_child_spacing_; + } + } + gfx::Insets insets(host->GetInsets()); + return gfx::Size( + bounds.width() + insets.width() + 2 * inside_border_horizontal_spacing_, + bounds.height() + insets.height() + 2 * inside_border_vertical_spacing_); +} + +} // namespace views diff --git a/ui/views/layout/box_layout.h b/ui/views/layout/box_layout.h new file mode 100644 index 0000000..81e5a94 --- /dev/null +++ b/ui/views/layout/box_layout.h @@ -0,0 +1,62 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_LAYOUT_BOX_LAYOUT_H_ +#define UI_VIEWS_LAYOUT_BOX_LAYOUT_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "views/layout/layout_manager.h" + +namespace gfx { +class Size; +} + +namespace views { + +class View; + +// A Layout manager that arranges child views vertically or horizontally in a +// side-by-side fashion with spacing around and between the child views. The +// child views are always sized according to their preferred size. If the +// host's bounds provide insufficient space, child views will be clamped. +// Excess space will not be distributed. +class VIEWS_EXPORT BoxLayout : public LayoutManager { + public: + enum Orientation { + kHorizontal, + kVertical, + }; + + // Use |inside_border_horizontal_spacing| and + // |inside_border_vertical_spacing| to add additional space between the child + // view area and the host view border. |between_child_spacing| controls the + // space in between child views. + BoxLayout(Orientation orientation, + int inside_border_horizontal_spacing, + int inside_border_vertical_spacing, + int between_child_spacing); + virtual ~BoxLayout(); + + // Overridden from views::LayoutManager: + virtual void Layout(View* host) OVERRIDE; + virtual gfx::Size GetPreferredSize(View* host) OVERRIDE; + + private: + const Orientation orientation_; + + // Spacing between child views and host view border. + const int inside_border_horizontal_spacing_; + const int inside_border_vertical_spacing_; + + // Spacing to put in between child views. + const int between_child_spacing_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(BoxLayout); +}; + +} // namespace views + +#endif // UI_VIEWS_LAYOUT_BOX_LAYOUT_H_ diff --git a/ui/views/layout/box_layout_unittest.cc b/ui/views/layout/box_layout_unittest.cc new file mode 100644 index 0000000..12ea2b5 --- /dev/null +++ b/ui/views/layout/box_layout_unittest.cc @@ -0,0 +1,111 @@ +// Copyright (c) 2011 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 "testing/gtest/include/gtest/gtest.h" +#include "views/layout/box_layout.h" +#include "views/view.h" + +class StaticSizedView : public views::View { + public: + explicit StaticSizedView(const gfx::Size& size) + : size_(size) { } + + virtual gfx::Size GetPreferredSize() { + return size_; + } + + private: + gfx::Size size_; +}; + +class BoxLayoutTest : public testing::Test { + public: + virtual void SetUp() { + host_.reset(new views::View); + } + + scoped_ptr<views::View> host_; + scoped_ptr<views::BoxLayout> layout_; +}; + +TEST_F(BoxLayoutTest, Empty) { + layout_.reset( + new views::BoxLayout(views::BoxLayout::kHorizontal, 10, 10, 20)); + EXPECT_EQ(gfx::Size(20, 20), layout_->GetPreferredSize(host_.get())); +} + +TEST_F(BoxLayoutTest, AlignmentHorizontal) { + layout_.reset(new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 0)); + views::View* v1 = new StaticSizedView(gfx::Size(10, 20)); + host_->AddChildView(v1); + views::View* v2 = new StaticSizedView(gfx::Size(10, 10)); + host_->AddChildView(v2); + EXPECT_EQ(gfx::Size(20, 20), layout_->GetPreferredSize(host_.get())); + host_->SetBounds(0, 0, 20, 20); + layout_->Layout(host_.get()); + EXPECT_EQ(gfx::Rect(0, 0, 10, 20), v1->bounds()); + EXPECT_EQ(gfx::Rect(10, 0, 10, 20), v2->bounds()); +} + +TEST_F(BoxLayoutTest, AlignmentVertical) { + layout_.reset(new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0)); + views::View* v1 = new StaticSizedView(gfx::Size(20, 10)); + host_->AddChildView(v1); + views::View* v2 = new StaticSizedView(gfx::Size(10, 10)); + host_->AddChildView(v2); + EXPECT_EQ(gfx::Size(20, 20), layout_->GetPreferredSize(host_.get())); + host_->SetBounds(0, 0, 20, 20); + layout_->Layout(host_.get()); + EXPECT_EQ(gfx::Rect(0, 0, 20, 10), v1->bounds()); + EXPECT_EQ(gfx::Rect(0, 10, 20, 10), v2->bounds()); +} + +TEST_F(BoxLayoutTest, Spacing) { + layout_.reset(new views::BoxLayout(views::BoxLayout::kHorizontal, 7, 7, 8)); + views::View* v1 = new StaticSizedView(gfx::Size(10, 20)); + host_->AddChildView(v1); + views::View* v2 = new StaticSizedView(gfx::Size(10, 20)); + host_->AddChildView(v2); + EXPECT_EQ(gfx::Size(42, 34), layout_->GetPreferredSize(host_.get())); + host_->SetBounds(0, 0, 100, 100); + layout_->Layout(host_.get()); + EXPECT_EQ(gfx::Rect(7, 7, 10, 86), v1->bounds()); + EXPECT_EQ(gfx::Rect(25, 7, 10, 86), v2->bounds()); +} + +TEST_F(BoxLayoutTest, Overflow) { + layout_.reset(new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 0)); + views::View* v1 = new StaticSizedView(gfx::Size(20, 20)); + host_->AddChildView(v1); + views::View* v2 = new StaticSizedView(gfx::Size(10, 20)); + host_->AddChildView(v2); + host_->SetBounds(0, 0, 10, 10); + layout_->Layout(host_.get()); + EXPECT_EQ(gfx::Rect(0, 0, 10, 10), v1->bounds()); + EXPECT_EQ(gfx::Rect(0, 0, 0, 0), v2->bounds()); +} + +TEST_F(BoxLayoutTest, NoSpace) { + layout_.reset( + new views::BoxLayout(views::BoxLayout::kHorizontal, 10, 10, 10)); + views::View* childView = new StaticSizedView(gfx::Size(20, 20)); + host_->AddChildView(childView); + host_->SetBounds(0, 0, 10, 10); + layout_->Layout(host_.get()); + EXPECT_EQ(gfx::Rect(0, 0, 0, 0), childView->bounds()); +} + +TEST_F(BoxLayoutTest, InvisibleChild) { + layout_.reset( + new views::BoxLayout(views::BoxLayout::kHorizontal, 10, 10, 10)); + views::View* v1 = new StaticSizedView(gfx::Size(20, 20)); + v1->SetVisible(false); + host_->AddChildView(v1); + views::View* v2 = new StaticSizedView(gfx::Size(10, 10)); + host_->AddChildView(v2); + EXPECT_EQ(gfx::Size(30, 30), layout_->GetPreferredSize(host_.get())); + host_->SetBounds(0, 0, 30, 30); + layout_->Layout(host_.get()); + EXPECT_EQ(gfx::Rect(10, 10, 10, 10), v2->bounds()); +} diff --git a/ui/views/layout/fill_layout.cc b/ui/views/layout/fill_layout.cc new file mode 100644 index 0000000..13fbcce --- /dev/null +++ b/ui/views/layout/fill_layout.cc @@ -0,0 +1,30 @@ +// Copyright (c) 2011 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 "views/layout/fill_layout.h" + +#include "base/logging.h" + +namespace views { + +FillLayout::FillLayout() { +} + +FillLayout::~FillLayout() { +} + +void FillLayout::Layout(View* host) { + if (!host->has_children()) + return; + + View* frame_view = host->child_at(0); + frame_view->SetBounds(0, 0, host->width(), host->height()); +} + +gfx::Size FillLayout::GetPreferredSize(View* host) { + DCHECK_EQ(1, host->child_count()); + return host->child_at(0)->GetPreferredSize(); +} + +} // namespace views diff --git a/ui/views/layout/fill_layout.h b/ui/views/layout/fill_layout.h new file mode 100644 index 0000000..604d9b5 --- /dev/null +++ b/ui/views/layout/fill_layout.h @@ -0,0 +1,36 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_LAYOUT_FILL_LAYOUT_H_ +#define UI_VIEWS_LAYOUT_FILL_LAYOUT_H_ +#pragma once + +#include "views/layout/layout_manager.h" +#include "views/view.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// +// FillLayout +// A simple LayoutManager that causes the associated view's one child to be +// sized to match the bounds of its parent. +// +/////////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT FillLayout : public LayoutManager { + public: + FillLayout(); + virtual ~FillLayout(); + + // Overridden from LayoutManager: + virtual void Layout(View* host); + virtual gfx::Size GetPreferredSize(View* host); + + private: + DISALLOW_COPY_AND_ASSIGN(FillLayout); +}; + +} // namespace views + +#endif // UI_VIEWS_LAYOUT_FILL_LAYOUT_H_ diff --git a/ui/views/layout/grid_layout.cc b/ui/views/layout/grid_layout.cc new file mode 100644 index 0000000..55a50ceb --- /dev/null +++ b/ui/views/layout/grid_layout.cc @@ -0,0 +1,1070 @@ +// Copyright (c) 2011 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 "views/layout/grid_layout.h" + +#include <algorithm> + +#include "base/logging.h" +#include "base/stl_util.h" +#include "ui/gfx/insets.h" +#include "views/layout/layout_constants.h" +#include "views/view.h" + +namespace views { + +// LayoutElement ------------------------------------------------------ + +// A LayoutElement has a size and location along one axis. It contains +// methods that are used along both axis. +class LayoutElement { + public: + // Invokes ResetSize on all the layout elements. + template <class T> + static void ResetSizes(std::vector<T*>* elements) { + // Reset the layout width of each column. + for (typename std::vector<T*>::iterator i = elements->begin(); + i != elements->end(); ++i) { + (*i)->ResetSize(); + } + } + + // Sets the location of each element to be the sum of the sizes of the + // preceding elements. + template <class T> + static void CalculateLocationsFromSize(std::vector<T*>* elements) { + // Reset the layout width of each column. + int location = 0; + for (typename std::vector<T*>::iterator i = elements->begin(); + i != elements->end(); ++i) { + (*i)->SetLocation(location); + location += (*i)->Size(); + } + } + + // Distributes delta among the resizable elements. + // Each resizable element is given ResizePercent / total_percent * delta + // pixels extra of space. + template <class T> + static void DistributeDelta(int delta, std::vector<T*>* elements) { + if (delta == 0) + return; + + float total_percent = 0; + int resize_count = 0; + for (typename std::vector<T*>::iterator i = elements->begin(); + i != elements->end(); ++i) { + total_percent += (*i)->ResizePercent(); + resize_count++; + } + if (total_percent == 0) { + // None of the elements are resizable, return. + return; + } + int remaining = delta; + int resized = resize_count; + for (typename std::vector<T*>::iterator i = elements->begin(); + i != elements->end(); ++i) { + T* element = *i; + if (element->ResizePercent() > 0) { + int to_give; + if (--resized == 0) { + to_give = remaining; + } else { + to_give = static_cast<int>(delta * + (element->resize_percent_ / total_percent)); + remaining -= to_give; + } + element->SetSize(element->Size() + to_give); + } + } + } + + // Returns the sum of the size of the elements from start to start + length. + template <class T> + static int TotalSize(int start, int length, std::vector<T*>* elements) { + DCHECK(start >= 0 && length > 0 && + start + length <= static_cast<int>(elements->size())); + int size = 0; + for (int i = start, max = start + length; i < max; ++i) { + size += (*elements)[i]->Size(); + } + return size; + } + + explicit LayoutElement(float resize_percent) + : resize_percent_(resize_percent) { + DCHECK(resize_percent >= 0); + } + + virtual ~LayoutElement() {} + + void SetLocation(int location) { + location_ = location; + } + + int Location() { + return location_; + } + + // Adjusts the size of this LayoutElement to be the max of the current size + // and the specified size. + virtual void AdjustSize(int size) { + size_ = std::max(size_, size); + } + + // Resets the size to the initial size. This sets the size to 0, but + // subclasses that have a different initial size should override. + virtual void ResetSize() { + SetSize(0); + } + + void SetSize(int size) { + size_ = size; + } + + int Size() { + return size_; + } + + void SetResizePercent(float percent) { + resize_percent_ = percent; + } + + float ResizePercent() { + return resize_percent_; + } + + bool IsResizable() { + return resize_percent_ > 0; + } + + private: + float resize_percent_; + int location_; + int size_; + + DISALLOW_COPY_AND_ASSIGN(LayoutElement); +}; + +// Column ------------------------------------------------------------- + +// As the name implies, this represents a Column. Column contains default +// values for views originating in this column. +class Column : public LayoutElement { + public: + Column(GridLayout::Alignment h_align, + GridLayout::Alignment v_align, + float resize_percent, + GridLayout::SizeType size_type, + int fixed_width, + int min_width, + size_t index, + bool is_padding) + : LayoutElement(resize_percent), + h_align_(h_align), + v_align_(v_align), + size_type_(size_type), + same_size_column_(-1), + fixed_width_(fixed_width), + min_width_(min_width), + index_(index), + is_padding_(is_padding), + master_column_(NULL) {} + + virtual ~Column() {} + + GridLayout::Alignment h_align() { return h_align_; } + GridLayout::Alignment v_align() { return v_align_; } + + virtual void ResetSize(); + + private: + friend class ColumnSet; + friend class GridLayout; + + Column* GetLastMasterColumn(); + + // Determines the max size of all linked columns, and sets each column + // to that size. This should only be used for the master column. + void UnifySameSizedColumnSizes(); + + virtual void AdjustSize(int size); + + const GridLayout::Alignment h_align_; + const GridLayout::Alignment v_align_; + const GridLayout::SizeType size_type_; + int same_size_column_; + const int fixed_width_; + const int min_width_; + + // Index of this column in the ColumnSet. + const size_t index_; + + const bool is_padding_; + + // If multiple columns have their sizes linked, one is the + // master column. The master column is identified by the + // master_column field being equal to itself. The master columns + // same_size_columns field contains the set of Columns with the + // the same size. Columns who are linked to other columns, but + // are not the master column have their master_column pointing to + // one of the other linked columns. Use the method GetLastMasterColumn + // to resolve the true master column. + std::vector<Column*> same_size_columns_; + Column* master_column_; + + DISALLOW_COPY_AND_ASSIGN(Column); +}; + +void Column::ResetSize() { + if (size_type_ == GridLayout::FIXED) { + SetSize(fixed_width_); + } else { + SetSize(min_width_); + } +} + +Column* Column::GetLastMasterColumn() { + if (master_column_ == NULL) { + return NULL; + } + if (master_column_ == this) { + return this; + } + return master_column_->GetLastMasterColumn(); +} + +void Column::UnifySameSizedColumnSizes() { + DCHECK(master_column_ == this); + + // Accumulate the size first. + int size = 0; + for (std::vector<Column*>::iterator i = same_size_columns_.begin(); + i != same_size_columns_.end(); ++i) { + size = std::max(size, (*i)->Size()); + } + + // Then apply it. + for (std::vector<Column*>::iterator i = same_size_columns_.begin(); + i != same_size_columns_.end(); ++i) { + (*i)->SetSize(size); + } +} + +void Column::AdjustSize(int size) { + if (size_type_ == GridLayout::USE_PREF) + LayoutElement::AdjustSize(size); +} + +// Row ------------------------------------------------------------- + +class Row : public LayoutElement { + public: + Row(bool fixed_height, int height, float resize_percent, + ColumnSet* column_set) + : LayoutElement(resize_percent), + fixed_height_(fixed_height), + height_(height), + column_set_(column_set), + max_ascent_(0), + max_descent_(0) { + } + + virtual ~Row() {} + + virtual void ResetSize() { + max_ascent_ = max_descent_ = 0; + SetSize(height_); + } + + ColumnSet* column_set() { + return column_set_; + } + + // Adjusts the size to accomodate the specified ascent/descent. + void AdjustSizeForBaseline(int ascent, int descent) { + max_ascent_ = std::max(ascent, max_ascent_); + max_descent_ = std::max(descent, max_descent_); + AdjustSize(max_ascent_ + max_descent_); + } + + int max_ascent() const { + return max_ascent_; + } + + int max_descent() const { + return max_descent_; + } + + private: + const bool fixed_height_; + const int height_; + // The column set used for this row; null for padding rows. + ColumnSet* column_set_; + + int max_ascent_; + int max_descent_; + + DISALLOW_COPY_AND_ASSIGN(Row); +}; + +// ViewState ------------------------------------------------------------- + +// Identifies the location in the grid of a particular view, along with +// placement information and size information. +struct ViewState { + ViewState(ColumnSet* column_set, View* view, int start_col, int start_row, + int col_span, int row_span, GridLayout::Alignment h_align, + GridLayout::Alignment v_align, int pref_width, int pref_height) + : column_set(column_set), + view(view), + start_col(start_col), + start_row(start_row), + col_span(col_span), + row_span(row_span), + h_align(h_align), + v_align(v_align), + pref_width_fixed(pref_width > 0), + pref_height_fixed(pref_height > 0), + pref_width(pref_width), + pref_height(pref_height), + remaining_width(0), + remaining_height(0), + baseline(-1) { + DCHECK(view && start_col >= 0 && start_row >= 0 && col_span > 0 && + row_span > 0 && start_col < column_set->num_columns() && + (start_col + col_span) <= column_set->num_columns()); + } + + ColumnSet* const column_set; + View* const view; + const int start_col; + const int start_row; + const int col_span; + const int row_span; + const GridLayout::Alignment h_align; + const GridLayout::Alignment v_align; + + // If true, the pref_width/pref_height were explicitly set and the view's + // preferred size is ignored. + const bool pref_width_fixed; + const bool pref_height_fixed; + + // The preferred width/height. These are reset during the layout process. + int pref_width; + int pref_height; + + // Used during layout. Gives how much width/height has not yet been + // distributed to the columns/rows the view is in. + int remaining_width; + int remaining_height; + + // The baseline. Only used if the view is vertically aligned along the + // baseline. + int baseline; +}; + +static bool CompareByColumnSpan(const ViewState* v1, const ViewState* v2) { + return v1->col_span < v2->col_span; +} + +static bool CompareByRowSpan(const ViewState* v1, const ViewState* v2) { + return v1->row_span < v2->row_span; +} + +// ColumnSet ------------------------------------------------------------- + +ColumnSet::ColumnSet(int id) : id_(id) { +} + +ColumnSet::~ColumnSet() { + STLDeleteElements(&columns_); +} + +void ColumnSet::AddPaddingColumn(float resize_percent, int width) { + AddColumn(GridLayout::FILL, GridLayout::FILL, resize_percent, + GridLayout::FIXED, width, width, true); +} + +void ColumnSet::AddColumn(GridLayout::Alignment h_align, + GridLayout::Alignment v_align, + float resize_percent, + GridLayout::SizeType size_type, + int fixed_width, + int min_width) { + AddColumn(h_align, v_align, resize_percent, size_type, fixed_width, + min_width, false); +} + + +void ColumnSet::LinkColumnSizes(int first, ...) { + va_list marker; + va_start(marker, first); + DCHECK(first >= 0 && first < num_columns()); + for (int last = first, next = va_arg(marker, int); next != -1; + next = va_arg(marker, int)) { + DCHECK(next >= 0 && next < num_columns()); + columns_[last]->same_size_column_ = next; + last = next; + } + va_end(marker); +} + +void ColumnSet::AddColumn(GridLayout::Alignment h_align, + GridLayout::Alignment v_align, + float resize_percent, + GridLayout::SizeType size_type, + int fixed_width, + int min_width, + bool is_padding) { + Column* column = new Column(h_align, v_align, resize_percent, size_type, + fixed_width, min_width, columns_.size(), + is_padding); + columns_.push_back(column); +} + +void ColumnSet::AddViewState(ViewState* view_state) { + // view_states are ordered by column_span (in ascending order). + std::vector<ViewState*>::iterator i = lower_bound(view_states_.begin(), + view_states_.end(), + view_state, + CompareByColumnSpan); + view_states_.insert(i, view_state); +} + +void ColumnSet::CalculateMasterColumns() { + for (std::vector<Column*>::iterator i = columns_.begin(); + i != columns_.end(); ++i) { + Column* column = *i; + int same_size_column_index = column->same_size_column_; + if (same_size_column_index != -1) { + DCHECK(same_size_column_index >= 0 && + same_size_column_index < static_cast<int>(columns_.size())); + Column* master_column = column->master_column_; + Column* same_size_column = columns_[same_size_column_index]; + Column* same_size_column_master = same_size_column->master_column_; + if (master_column == NULL) { + // Current column is not linked to any other column. + if (same_size_column_master == NULL) { + // Both columns are not linked. + column->master_column_ = column; + same_size_column->master_column_ = column; + column->same_size_columns_.push_back(same_size_column); + column->same_size_columns_.push_back(column); + } else { + // Column to link to is linked with other columns. + // Add current column to list of linked columns in other columns + // master column. + same_size_column->GetLastMasterColumn()-> + same_size_columns_.push_back(column); + // And update the master column for the current column to that + // of the same sized column. + column->master_column_ = same_size_column; + } + } else { + // Current column is already linked with another column. + if (same_size_column_master == NULL) { + // Column to link with is not linked to any other columns. + // Update it's master_column. + same_size_column->master_column_ = column; + // Add linked column to list of linked column. + column->GetLastMasterColumn()->same_size_columns_. + push_back(same_size_column); + } else if (column->GetLastMasterColumn() != + same_size_column->GetLastMasterColumn()) { + // The two columns are already linked with other columns. + std::vector<Column*>* same_size_columns = + &(column->GetLastMasterColumn()->same_size_columns_); + std::vector<Column*>* other_same_size_columns = + &(same_size_column->GetLastMasterColumn()->same_size_columns_); + // Add all the columns from the others master to current columns + // master. + same_size_columns->insert(same_size_columns->end(), + other_same_size_columns->begin(), + other_same_size_columns->end()); + // The other master is no longer a master, clear its vector of + // linked columns, and reset its master_column. + other_same_size_columns->clear(); + same_size_column->GetLastMasterColumn()->master_column_ = column; + } + } + } + } + AccumulateMasterColumns(); +} + +void ColumnSet::AccumulateMasterColumns() { + DCHECK(master_columns_.empty()); + for (std::vector<Column*>::iterator i = columns_.begin(); + i != columns_.end(); ++i) { + Column* column = *i; + Column* master_column = column->GetLastMasterColumn(); + if (master_column && + find(master_columns_.begin(), master_columns_.end(), + master_column) == master_columns_.end()) { + master_columns_.push_back(master_column); + } + // At this point, GetLastMasterColumn may not == master_column + // (may have to go through a few Columns)_. Reset master_column to + // avoid hops. + column->master_column_ = master_column; + } +} + +void ColumnSet::UnifySameSizedColumnSizes() { + for (std::vector<Column*>::iterator i = master_columns_.begin(); + i != master_columns_.end(); ++i) { + (*i)->UnifySameSizedColumnSizes(); + } +} + +void ColumnSet::UpdateRemainingWidth(ViewState* view_state) { + for (int i = view_state->start_col; i < view_state->col_span; ++i) { + view_state->remaining_width -= columns_[i]->Size(); + } +} + +void ColumnSet::DistributeRemainingWidth(ViewState* view_state) { + // This is nearly the same as that for rows, but differs in so far as how + // Rows and Columns are treated. Rows have two states, resizable or not. + // Columns have three, resizable, USE_PREF or not resizable. This results + // in slightly different handling for distributing unaccounted size. + int width = view_state->remaining_width; + if (width <= 0) { + // The columns this view is in are big enough to accommodate it. + return; + } + + // Determine which columns are resizable, and which have a size type + // of USE_PREF. + int resizable_columns = 0; + int pref_size_columns = 0; + int start_col = view_state->start_col; + int max_col = view_state->start_col + view_state->col_span; + float total_resize = 0; + for (int i = start_col; i < max_col; ++i) { + if (columns_[i]->IsResizable()) { + total_resize += columns_[i]->ResizePercent(); + resizable_columns++; + } else if (columns_[i]->size_type_ == GridLayout::USE_PREF) { + pref_size_columns++; + } + } + + if (resizable_columns > 0) { + // There are resizable columns, give them the remaining width. The extra + // width is distributed using the resize values of each column. + int remaining_width = width; + for (int i = start_col, resize_i = 0; i < max_col; ++i) { + if (columns_[i]->IsResizable()) { + resize_i++; + int delta = (resize_i == resizable_columns) ? remaining_width : + static_cast<int>(width * columns_[i]->ResizePercent() / + total_resize); + remaining_width -= delta; + columns_[i]->SetSize(columns_[i]->Size() + delta); + } + } + } else if (pref_size_columns > 0) { + // None of the columns are resizable, distribute the width among those + // that use the preferred size. + int to_distribute = width / pref_size_columns; + for (int i = start_col; i < max_col; ++i) { + if (columns_[i]->size_type_ == GridLayout::USE_PREF) { + width -= to_distribute; + if (width < to_distribute) + to_distribute += width; + columns_[i]->SetSize(columns_[i]->Size() + to_distribute); + } + } + } +} + +int ColumnSet::LayoutWidth() { + int width = 0; + for (std::vector<Column*>::iterator i = columns_.begin(); + i != columns_.end(); ++i) { + width += (*i)->Size(); + } + return width; +} + +int ColumnSet::GetColumnWidth(int start_col, int col_span) { + return LayoutElement::TotalSize(start_col, col_span, &columns_); +} + +void ColumnSet::ResetColumnXCoordinates() { + LayoutElement::CalculateLocationsFromSize(&columns_); +} + +void ColumnSet::CalculateSize() { + gfx::Size pref; + // Reset the preferred and remaining sizes. + for (std::vector<ViewState*>::iterator i = view_states_.begin(); + i != view_states_.end(); ++i) { + ViewState* view_state = *i; + if (!view_state->pref_width_fixed || !view_state->pref_height_fixed) { + pref = view_state->view->GetPreferredSize(); + if (!view_state->pref_width_fixed) + view_state->pref_width = pref.width(); + if (!view_state->pref_height_fixed) + view_state->pref_height = pref.height(); + } + view_state->remaining_width = pref.width(); + view_state->remaining_height = pref.height(); + } + + // Let layout element reset the sizes for us. + LayoutElement::ResetSizes(&columns_); + + // Distribute the size of each view with a col span == 1. + std::vector<ViewState*>::iterator view_state_iterator = + view_states_.begin(); + for (; view_state_iterator != view_states_.end() && + (*view_state_iterator)->col_span == 1; ++view_state_iterator) { + ViewState* view_state = *view_state_iterator; + Column* column = columns_[view_state->start_col]; + column->AdjustSize(view_state->pref_width); + view_state->remaining_width -= column->Size(); + } + + // Make sure all linked columns have the same size. + UnifySameSizedColumnSizes(); + + // Distribute the size of each view with a column span > 1. + for (; view_state_iterator != view_states_.end(); ++view_state_iterator) { + ViewState* view_state = *view_state_iterator; + + // Update the remaining_width from columns this view_state touches. + UpdateRemainingWidth(view_state); + + // Distribute the remaining width. + DistributeRemainingWidth(view_state); + + // Update the size of linked columns. + // This may need to be combined with previous step. + UnifySameSizedColumnSizes(); + } +} + +void ColumnSet::Resize(int delta) { + LayoutElement::DistributeDelta(delta, &columns_); +} + +// GridLayout ------------------------------------------------------------- + +GridLayout::GridLayout(View* host) + : host_(host), + calculated_master_columns_(false), + remaining_row_span_(0), + current_row_(-1), + next_column_(0), + current_row_col_set_(NULL), + top_inset_(0), + bottom_inset_(0), + left_inset_(0), + right_inset_(0), + adding_view_(false) { + DCHECK(host); +} + +GridLayout::~GridLayout() { + STLDeleteElements(&column_sets_); + STLDeleteElements(&view_states_); + STLDeleteElements(&rows_); +} + +// static +GridLayout* GridLayout::CreatePanel(View* host) { + GridLayout* layout = new GridLayout(host); + layout->SetInsets(kPanelVertMargin, kPanelHorizMargin, + kPanelVertMargin, kPanelHorizMargin); + return layout; +} + +void GridLayout::SetInsets(int top, int left, int bottom, int right) { + top_inset_ = top; + bottom_inset_ = bottom; + left_inset_ = left; + right_inset_ = right; +} + +void GridLayout::SetInsets(const gfx::Insets& insets) { + SetInsets(insets.top(), insets.left(), insets.bottom(), insets.right()); +} + +ColumnSet* GridLayout::AddColumnSet(int id) { + DCHECK(GetColumnSet(id) == NULL); + ColumnSet* column_set = new ColumnSet(id); + column_sets_.push_back(column_set); + return column_set; +} + +void GridLayout::StartRowWithPadding(float vertical_resize, int column_set_id, + float padding_resize, int padding) { + AddPaddingRow(padding_resize, padding); + StartRow(vertical_resize, column_set_id); +} + +void GridLayout::StartRow(float vertical_resize, int column_set_id) { + ColumnSet* column_set = GetColumnSet(column_set_id); + DCHECK(column_set); + AddRow(new Row(false, 0, vertical_resize, column_set)); +} + +void GridLayout::AddPaddingRow(float vertical_resize, int pixel_count) { + AddRow(new Row(true, pixel_count, vertical_resize, NULL)); +} + +void GridLayout::SkipColumns(int col_count) { + DCHECK(col_count > 0); + next_column_ += col_count; + DCHECK(current_row_col_set_ && + next_column_ <= current_row_col_set_->num_columns()); + SkipPaddingColumns(); +} + +void GridLayout::AddView(View* view) { + AddView(view, 1, 1); +} + +void GridLayout::AddView(View* view, int col_span, int row_span) { + DCHECK(current_row_col_set_ && + next_column_ < current_row_col_set_->num_columns()); + Column* column = current_row_col_set_->columns_[next_column_]; + AddView(view, col_span, row_span, column->h_align(), column->v_align()); +} + +void GridLayout::AddView(View* view, int col_span, int row_span, + Alignment h_align, Alignment v_align) { + AddView(view, col_span, row_span, h_align, v_align, 0, 0); +} + +void GridLayout::AddView(View* view, int col_span, int row_span, + Alignment h_align, Alignment v_align, + int pref_width, int pref_height) { + DCHECK(current_row_col_set_ && col_span > 0 && row_span > 0 && + (next_column_ + col_span) <= current_row_col_set_->num_columns()); + // We don't support baseline alignment of views spanning rows. Please add if + // you need it. + DCHECK(v_align != BASELINE || row_span == 1); + ViewState* state = + new ViewState(current_row_col_set_, view, next_column_, current_row_, + col_span, row_span, h_align, v_align, pref_width, + pref_height); + AddViewState(state); +} + +static void CalculateSize(int pref_size, GridLayout::Alignment alignment, + int* location, int* size) { + if (alignment != GridLayout::FILL) { + int available_size = *size; + *size = std::min(*size, pref_size); + switch (alignment) { + case GridLayout::LEADING: + // Nothing to do, location already points to start. + break; + case GridLayout::BASELINE: // If we were asked to align on baseline, but + // the view doesn't have a baseline, fall back + // to center. + case GridLayout::CENTER: + *location += (available_size - *size) / 2; + break; + case GridLayout::TRAILING: + *location = *location + available_size - *size; + break; + default: + NOTREACHED(); + } + } +} + +void GridLayout::Installed(View* host) { + DCHECK(host_ == host); +} + +void GridLayout::Uninstalled(View* host) { + DCHECK(host_ == host); +} + +void GridLayout::ViewAdded(View* host, View* view) { + DCHECK(host_ == host && adding_view_); +} + +void GridLayout::ViewRemoved(View* host, View* view) { + DCHECK(host_ == host); +} + +void GridLayout::Layout(View* host) { + DCHECK(host_ == host); + // SizeRowsAndColumns sets the size and location of each row/column, but + // not of the views. + gfx::Size pref; + SizeRowsAndColumns(true, host_->width(), host_->height(), &pref); + + // Size each view. + for (std::vector<ViewState*>::iterator i = view_states_.begin(); + i != view_states_.end(); ++i) { + ViewState* view_state = *i; + ColumnSet* column_set = view_state->column_set; + View* view = (*i)->view; + DCHECK(view); + int x = column_set->columns_[view_state->start_col]->Location() + + left_inset_; + int width = column_set->GetColumnWidth(view_state->start_col, + view_state->col_span); + CalculateSize(view_state->pref_width, view_state->h_align, + &x, &width); + int y = rows_[view_state->start_row]->Location() + top_inset_; + int height = LayoutElement::TotalSize(view_state->start_row, + view_state->row_span, &rows_); + if (view_state->v_align == BASELINE && view_state->baseline != -1) { + y += rows_[view_state->start_row]->max_ascent() - view_state->baseline; + height = view_state->pref_height; + } else { + CalculateSize(view_state->pref_height, view_state->v_align, &y, &height); + } + view->SetBounds(x, y, width, height); + } +} + +gfx::Size GridLayout::GetPreferredSize(View* host) { + DCHECK(host_ == host); + gfx::Size out; + SizeRowsAndColumns(false, 0, 0, &out); + return out; +} + +int GridLayout::GetPreferredHeightForWidth(View* host, int width) { + DCHECK(host_ == host); + gfx::Size pref; + SizeRowsAndColumns(false, width, 0, &pref); + return pref.height(); +} + +void GridLayout::SizeRowsAndColumns(bool layout, int width, int height, + gfx::Size* pref) { + // Make sure the master columns have been calculated. + CalculateMasterColumnsIfNecessary(); + pref->SetSize(0, 0); + if (rows_.empty()) + return; + + // Calculate the preferred width of each of the columns. Some views' + // preferred heights are derived from their width, as such we need to + // calculate the size of the columns first. + for (std::vector<ColumnSet*>::iterator i = column_sets_.begin(); + i != column_sets_.end(); ++i) { + (*i)->CalculateSize(); + pref->set_width(std::max(pref->width(), (*i)->LayoutWidth())); + } + pref->set_width(pref->width() + left_inset_ + right_inset_); + + // Go over the columns again and set them all to the size we settled for. + width = width ? width : pref->width(); + for (std::vector<ColumnSet*>::iterator i = column_sets_.begin(); + i != column_sets_.end(); ++i) { + // We're doing a layout, divy up any extra space. + (*i)->Resize(width - (*i)->LayoutWidth() - left_inset_ - right_inset_); + // And reset the x coordinates. + (*i)->ResetColumnXCoordinates(); + } + + // Reset the height of each row. + LayoutElement::ResetSizes(&rows_); + + // Do the following: + // . If the view is aligned along it's baseline, obtain the baseline from the + // view and update the rows ascent/descent. + // . Reset the remaining_height of each view state. + // . If the width the view will be given is different than it's pref, ask + // for the height given a particularly width. + for (std::vector<ViewState*>::iterator i= view_states_.begin(); + i != view_states_.end() ; ++i) { + ViewState* view_state = *i; + view_state->remaining_height = view_state->pref_height; + + if (view_state->v_align == BASELINE) + view_state->baseline = view_state->view->GetBaseline(); + + if (view_state->h_align == FILL) { + // The view is resizable. As the pref height may vary with the width, + // ask for the pref again. + int actual_width = + view_state->column_set->GetColumnWidth(view_state->start_col, + view_state->col_span); + if (actual_width != view_state->pref_width && + !view_state->pref_height_fixed) { + // The width this view will get differs from it's preferred. Some Views + // pref height varies with it's width; ask for the preferred again. + view_state->pref_height = + view_state->view->GetHeightForWidth(actual_width); + view_state->remaining_height = view_state->pref_height; + } + } + } + + // Update the height/ascent/descent of each row from the views. + std::vector<ViewState*>::iterator view_states_iterator = view_states_.begin(); + for (; view_states_iterator != view_states_.end() && + (*view_states_iterator)->row_span == 1; ++view_states_iterator) { + ViewState* view_state = *view_states_iterator; + Row* row = rows_[view_state->start_row]; + row->AdjustSize(view_state->remaining_height); + if (view_state->baseline != -1 && + view_state->baseline <= view_state->pref_height) { + row->AdjustSizeForBaseline(view_state->baseline, + view_state->pref_height - view_state->baseline); + } + view_state->remaining_height = 0; + } + + // Distribute the height of each view with a row span > 1. + for (; view_states_iterator != view_states_.end(); ++view_states_iterator) { + ViewState* view_state = *view_states_iterator; + + // Update the remaining_width from columns this view_state touches. + UpdateRemainingHeightFromRows(view_state); + + // Distribute the remaining height. + DistributeRemainingHeight(view_state); + } + + // Update the location of each of the rows. + LayoutElement::CalculateLocationsFromSize(&rows_); + + // We now know the preferred height, set it here. + pref->set_height(rows_[rows_.size() - 1]->Location() + + rows_[rows_.size() - 1]->Size() + top_inset_ + bottom_inset_); + + if (layout && height != pref->height()) { + // We're doing a layout, and the height differs from the preferred height, + // divy up the extra space. + LayoutElement::DistributeDelta(height - pref->height(), &rows_); + + // Reset y locations. + LayoutElement::CalculateLocationsFromSize(&rows_); + } +} + +void GridLayout::CalculateMasterColumnsIfNecessary() { + if (!calculated_master_columns_) { + calculated_master_columns_ = true; + for (std::vector<ColumnSet*>::iterator i = column_sets_.begin(); + i != column_sets_.end(); ++i) { + (*i)->CalculateMasterColumns(); + } + } +} + +void GridLayout::AddViewState(ViewState* view_state) { + DCHECK(view_state->view && (view_state->view->parent() == NULL || + view_state->view->parent() == host_)); + if (!view_state->view->parent()) { + adding_view_ = true; + host_->AddChildView(view_state->view); + adding_view_ = false; + } + remaining_row_span_ = std::max(remaining_row_span_, view_state->row_span); + next_column_ += view_state->col_span; + current_row_col_set_->AddViewState(view_state); + // view_states are ordered by row_span (in ascending order). + std::vector<ViewState*>::iterator i = lower_bound(view_states_.begin(), + view_states_.end(), + view_state, + CompareByRowSpan); + view_states_.insert(i, view_state); + SkipPaddingColumns(); +} + +ColumnSet* GridLayout::GetColumnSet(int id) { + for (std::vector<ColumnSet*>::iterator i = column_sets_.begin(); + i != column_sets_.end(); ++i) { + if ((*i)->id_ == id) { + return *i; + } + } + return NULL; +} + +void GridLayout::AddRow(Row* row) { + current_row_++; + remaining_row_span_--; + DCHECK(remaining_row_span_ <= 0 || + row->column_set() == NULL || + row->column_set() == GetLastValidColumnSet()); + next_column_ = 0; + rows_.push_back(row); + current_row_col_set_ = row->column_set(); + SkipPaddingColumns(); +} + +void GridLayout::UpdateRemainingHeightFromRows(ViewState* view_state) { + for (int i = 0, start_row = view_state->start_row; + i < view_state->row_span; ++i) { + view_state->remaining_height -= rows_[i + start_row]->Size(); + } +} + +void GridLayout::DistributeRemainingHeight(ViewState* view_state) { + int height = view_state->remaining_height; + if (height <= 0) + return; + + // Determine the number of resizable rows the view touches. + int resizable_rows = 0; + int start_row = view_state->start_row; + int max_row = view_state->start_row + view_state->row_span; + for (int i = start_row; i < max_row; ++i) { + if (rows_[i]->IsResizable()) { + resizable_rows++; + } + } + + if (resizable_rows > 0) { + // There are resizable rows, give the remaining height to them. + int to_distribute = height / resizable_rows; + for (int i = start_row; i < max_row; ++i) { + if (rows_[i]->IsResizable()) { + height -= to_distribute; + if (height < to_distribute) { + // Give all slop to the last column. + to_distribute += height; + } + rows_[i]->SetSize(rows_[i]->Size() + to_distribute); + } + } + } else { + // None of the rows are resizable, divy the remaining height up equally + // among all rows the view touches. + int each_row_height = height / view_state->row_span; + for (int i = start_row; i < max_row; ++i) { + height -= each_row_height; + if (height < each_row_height) + each_row_height += height; + rows_[i]->SetSize(rows_[i]->Size() + each_row_height); + } + view_state->remaining_height = 0; + } +} + +void GridLayout::SkipPaddingColumns() { + if (!current_row_col_set_) + return; + while (next_column_ < current_row_col_set_->num_columns() && + current_row_col_set_->columns_[next_column_]->is_padding_) { + next_column_++; + } +} + +ColumnSet* GridLayout::GetLastValidColumnSet() { + for (int i = current_row_ - 1; i >= 0; --i) { + if (rows_[i]->column_set()) + return rows_[i]->column_set(); + } + return NULL; +} + +} // namespace views diff --git a/ui/views/layout/grid_layout.h b/ui/views/layout/grid_layout.h new file mode 100644 index 0000000..6c38585 --- /dev/null +++ b/ui/views/layout/grid_layout.h @@ -0,0 +1,371 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_LAYOUT_GRID_LAYOUT_H_ +#define UI_VIEWS_LAYOUT_GRID_LAYOUT_H_ +#pragma once + +#include <string> +#include <vector> + +#include "views/layout/layout_manager.h" +#include "views/view.h" + +namespace gfx { +class Insets; +} + +// GridLayout is a LayoutManager that positions child Views in a grid. You +// define the structure of the Grid first, then add the Views. +// The following creates a trivial grid with two columns separated by +// a column with padding: +// ColumnSet* columns = layout->AddColumnSet(0); // Give this column an +// // identifier of 0. +// columns->AddColumn(FILL, // Views are horizontally resized to fill column. +// FILL, // Views starting in this column are vertically +// // resized. +// 1, // This column has a resize weight of 1. +// USE_PREF, // Use the preferred size of the view. +// 0, // Ignored for USE_PREF. +// 0); // A minimum width of 0. +// columns->AddPaddingColumn(0, // The padding column is not resizable. +// 10); // And has a width of 10 pixels. +// columns->AddColumn(FILL, FILL, 0, USE_PREF, 0, 0); +// Now add the views: +// // First start a row. +// layout->StartRow(0, // This row isn't vertically resizable. +// 0); // The column set to use for this row. +// layout->AddView(v1); +// Notice you need not skip over padding columns, that's done for you. +// layout->AddView(v2); +// +// When adding a Column you give it the default alignment for all views +// originating in that column. You can override this for specific views +// when adding them. For example, the following forces a View to have +// a horizontal and vertical alignment of leading regardless of that defined +// for the column: +// layout->AddView(v1, 1, 1, LEADING, LEADING); +// +// If the View using GridLayout is given a size bigger than the preferred, +// columns and rows with a resize percent > 0 are resized. Each column/row +// is given resize_percent / total_resize_percent * extra_pixels extra +// pixels. Only Views with an Alignment of FILL are given extra space, others +// are aligned in the provided space. +// +// GridLayout allows you to define multiple column sets. When you start a +// new row you specify the id of the column set the row is to use. +// +// GridLayout allows you to force columns to have the same width. This is +// done using the LinkColumnSizes method. +// +// AddView takes care of adding the View to the View the GridLayout was +// created with. +namespace views { + +class Column; +class ColumnSet; +class Row; +class View; + +struct ViewState; + +class VIEWS_EXPORT GridLayout : public LayoutManager { + public: + // An enumeration of the possible alignments supported by GridLayout. + enum Alignment { + // Leading equates to left along the horizontal axis, and top along the + // vertical axis. + LEADING, + + // Centers the view along the axis. + CENTER, + + // Trailing equals to right along the horizontal axis, and bottom along + // the vertical axis. + TRAILING, + + // The view is resized to fill the space. + FILL, + + // The view is aligned along the baseline. This is only valid for the + // vertical axis. + BASELINE + }; + + // An enumeration of the possible ways the size of a column may be obtained. + enum SizeType { + // The column size is fixed. + FIXED, + + // The preferred size of the view is used to determine the column size. + USE_PREF + }; + + explicit GridLayout(View* host); + virtual ~GridLayout(); + + // Creates a GridLayout with kPanel*Margin insets. + static GridLayout* CreatePanel(View* host); + + // Sets the insets. All views are placed relative to these offsets. + void SetInsets(int top, int left, int bottom, int right); + void SetInsets(const gfx::Insets& insets); + + // Creates a new column set with the specified id and returns it. + // The id is later used when starting a new row. + // GridLayout takes ownership of the ColumnSet and will delete it when + // the GridLayout is deleted. + ColumnSet* AddColumnSet(int id); + + // Adds a padding row. Padding rows typically don't have any views, and + // but are used to provide vertical white space between views. + // Size specifies the height of the row. + void AddPaddingRow(float vertical_resize, int size); + + // A convenience for AddPaddingRow followed by StartRow. + void StartRowWithPadding(float vertical_resize, int column_set_id, + float padding_resize, int padding); + + // Starts a new row with the specified column set. + void StartRow(float vertical_resize, int column_set_id); + + // Advances past columns. Use this when the current column should not + // contain any views. + void SkipColumns(int col_count); + + // Adds a view using the default alignment from the column. The added + // view has a column and row span of 1. + // As a convenience this adds the view to the host. The view becomes owned + // by the host, and NOT this GridLayout. + void AddView(View* view); + + // Adds a view using the default alignment from the column. + // As a convenience this adds the view to the host. The view becomes owned + // by the host, and NOT this GridLayout. + void AddView(View* view, int col_span, int row_span); + + // Adds a view with the specified alignment and spans. + // As a convenience this adds the view to the host. The view becomes owned + // by the host, and NOT this GridLayout. + void AddView(View* view, int col_span, int row_span, Alignment h_align, + Alignment v_align); + + // Adds a view with the specified alignment and spans. If + // pref_width/pref_height is > 0 then the preferred width/height of the view + // is fixed to the specified value. + // As a convenience this adds the view to the host. The view becomes owned + // by the host, and NOT this GridLayout. + void AddView(View* view, int col_span, int row_span, + Alignment h_align, Alignment v_align, + int pref_width, int pref_height); + + // Notification we've been installed on a particular host. Checks that host + // is the same as the View supplied in the constructor. + virtual void Installed(View* host); + + // Notification we've been uninstalled on a particular host. Checks that host + // is the same as the View supplied in the constructor. + virtual void Uninstalled(View* host); + + // Notification that a view has been added. + virtual void ViewAdded(View* host, View* view); + + // Notification that a view has been removed. + virtual void ViewRemoved(View* host, View* view); + + // Layouts out the components. + virtual void Layout(View* host); + + // Returns the preferred size for the GridLayout. + virtual gfx::Size GetPreferredSize(View* host); + + virtual int GetPreferredHeightForWidth(View* host, int width); + + private: + // As both Layout and GetPreferredSize need to do nearly the same thing, + // they both call into this method. This sizes the Columns/Rows as + // appropriate. If layout is true, width/height give the width/height the + // of the host, otherwise they are ignored. + void SizeRowsAndColumns(bool layout, int width, int height, gfx::Size* pref); + + // Calculates the master columns of all the column sets. See Column for + // a description of what a master column is. + void CalculateMasterColumnsIfNecessary(); + + // This is called internally from AddView. It adds the ViewState to the + // appropriate structures, and updates internal fields such as next_column_. + void AddViewState(ViewState* view_state); + + // Returns the column set for the specified id, or NULL if one doesn't exist. + ColumnSet* GetColumnSet(int id); + + // Adds the Row to rows_, as well as updating next_column_, + // current_row_col_set ... + void AddRow(Row* row); + + // As the name says, updates the remaining_height of the ViewState for + // all Rows the supplied ViewState touches. + void UpdateRemainingHeightFromRows(ViewState* state); + + // If the view state's remaining height is > 0, it is distributed among + // the rows the view state touches. This is used during layout to make + // sure the Rows can accommodate a view. + void DistributeRemainingHeight(ViewState* state); + + // Advances next_column_ past any padding columns. + void SkipPaddingColumns(); + + // Returns the column set of the last non-padding row. + ColumnSet* GetLastValidColumnSet(); + + // The view we were created with. We don't own this. + View* const host_; + + // Whether or not we've calculated the master/linked columns. + bool calculated_master_columns_; + + // Used to verify a view isn't added with a row span that expands into + // another column structure. + int remaining_row_span_; + + // Current row. + int current_row_; + + // Current column. + int next_column_; + + // Column set for the current row. This is null for padding rows. + ColumnSet* current_row_col_set_; + + // Insets. + int top_inset_; + int bottom_inset_; + int left_inset_; + int right_inset_; + + // Set to true when adding a View. + bool adding_view_; + + // ViewStates. This is ordered by row_span in ascending order. + std::vector<ViewState*> view_states_; + + // ColumnSets. + std::vector<ColumnSet*> column_sets_; + + // Rows. + std::vector<Row*> rows_; + + DISALLOW_COPY_AND_ASSIGN(GridLayout); +}; + +// ColumnSet is used to define a set of columns. GridLayout may have any +// number of ColumnSets. You don't create a ColumnSet directly, instead +// use the AddColumnSet method of GridLayout. +class VIEWS_EXPORT ColumnSet { + public: + ~ColumnSet(); + + // Adds a column for padding. When adding views, padding columns are + // automatically skipped. For example, if you create a column set with + // two columns separated by a padding column, the first AddView automatically + // skips past the padding column. That is, to add two views, do: + // layout->AddView(v1); layout->AddView(v2);, not: + // layout->AddView(v1); layout->SkipColumns(1); layout->AddView(v2); + void AddPaddingColumn(float resize_percent, int width); + + // Adds a column. The alignment gives the default alignment for views added + // with no explicit alignment. fixed_width gives a specific width for the + // column, and is only used if size_type == FIXED. min_width gives the + // minimum width for the column. + // + // If none of the columns in a columnset are resizable, the views are only + // made as wide as the widest views in each column, even if extra space is + // provided. In other words, GridLayout does not automatically resize views + // unless the column is marked as resizable. + void AddColumn(GridLayout::Alignment h_align, + GridLayout::Alignment v_align, + float resize_percent, + GridLayout::SizeType size_type, + int fixed_width, + int min_width); + + // Forces the specified columns to have the same size. The size of + // linked columns is that of the max of the specified columns. This + // must end with -1. For example, the following forces the first and + // second column to have the same size: + // LinkColumnSizes(0, 1, -1); + void LinkColumnSizes(int first, ...); + + // ID of this ColumnSet. + int id() const { return id_; } + + int num_columns() { return static_cast<int>(columns_.size()); } + + private: + friend class GridLayout; + + explicit ColumnSet(int id); + + void AddColumn(GridLayout::Alignment h_align, + GridLayout::Alignment v_align, + float resize_percent, + GridLayout::SizeType size_type, + int fixed_width, + int min_width, + bool is_padding); + + void AddViewState(ViewState* view_state); + + // Set description of these. + void CalculateMasterColumns(); + void AccumulateMasterColumns(); + + // Sets the size of each linked column to be the same. + void UnifySameSizedColumnSizes(); + + // Updates the remaining width field of the ViewState from that of the + // columns the view spans. + void UpdateRemainingWidth(ViewState* view_state); + + // Makes sure the columns touched by view state are big enough for the + // view. + void DistributeRemainingWidth(ViewState* view_state); + + // Returns the total size needed for this ColumnSet. + int LayoutWidth(); + + // Returns the width of the specified columns. + int GetColumnWidth(int start_col, int col_span); + + // Updates the x coordinate of each column from the previous ones. + // NOTE: this doesn't include the insets. + void ResetColumnXCoordinates(); + + // Calculate the preferred width of each view in this column set, as well + // as updating the remaining_width. + void CalculateSize(); + + // Distributes delta amoung the resizable columns. + void Resize(int delta); + + // ID for this columnset. + const int id_; + + // The columns. + std::vector<Column*> columns_; + + // The ViewStates. This is sorted based on column_span in ascending + // order. + std::vector<ViewState*> view_states_; + + // The master column of those columns that are linked. See Column + // for a description of what the master column is. + std::vector<Column*> master_columns_; + + DISALLOW_COPY_AND_ASSIGN(ColumnSet); +}; + +} // namespace views + +#endif // UI_VIEWS_LAYOUT_GRID_LAYOUT_H_ diff --git a/ui/views/layout/grid_layout_unittest.cc b/ui/views/layout/grid_layout_unittest.cc new file mode 100644 index 0000000..542d7c5 --- /dev/null +++ b/ui/views/layout/grid_layout_unittest.cc @@ -0,0 +1,606 @@ +// Copyright (c) 2011 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 "testing/gtest/include/gtest/gtest.h" +#include "views/layout/grid_layout.h" +#include "views/view.h" + +using views::ColumnSet; +using views::GridLayout; +using views::View; + +static void ExpectViewBoundsEquals(int x, int y, int w, int h, + const View* view) { + EXPECT_EQ(x, view->x()); + EXPECT_EQ(y, view->y()); + EXPECT_EQ(w, view->width()); + EXPECT_EQ(h, view->height()); +} + +class SettableSizeView : public View { + public: + explicit SettableSizeView(const gfx::Size& pref) { + pref_ = pref; + } + + virtual gfx::Size GetPreferredSize() { + return pref_; + } + + private: + gfx::Size pref_; +}; + +// A view with fixed circumference that trades height for width. +class FlexibleView : public View { + public: + explicit FlexibleView(int circumference) { + circumference_ = circumference; + } + + virtual gfx::Size GetPreferredSize() { + return gfx::Size(0, circumference_ / 2); + } + + virtual int GetHeightForWidth(int width) { + return std::max(0, circumference_ / 2 - width); + } + + private: + int circumference_; +}; + +class GridLayoutTest : public testing::Test { + public: + virtual void SetUp() { + layout = new GridLayout(&host); + } + + virtual void TearDown() { + delete layout; + } + + virtual void RemoveAll() { + for (int i = host.child_count() - 1; i >= 0; i--) + host.RemoveChildView(host.child_at(i)); + } + + void GetPreferredSize() { + pref = layout->GetPreferredSize(&host); + } + + gfx::Size pref; + gfx::Rect bounds; + View host; + GridLayout* layout; +}; + +class GridLayoutAlignmentTest : public testing::Test { + public: + GridLayoutAlignmentTest() : + host(), + v1(gfx::Size(10, 20)), + layout(new GridLayout(&host)) {} + + virtual void SetUp() { + } + + virtual void TearDown() { + delete layout; + } + + virtual void RemoveAll() { + for (int i = host.child_count() - 1; i >= 0; i--) + host.RemoveChildView(host.child_at(i)); + } + + void TestAlignment(GridLayout::Alignment alignment, gfx::Rect* bounds) { + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(alignment, alignment, 1, GridLayout::USE_PREF, 0, 0); + layout->StartRow(1, 0); + layout->AddView(&v1); + gfx::Size pref = layout->GetPreferredSize(&host); + EXPECT_EQ(gfx::Size(10, 20), pref); + host.SetBounds(0, 0, 100, 100); + layout->Layout(&host); + *bounds = v1.bounds(); + RemoveAll(); + } + + View host; + SettableSizeView v1; + GridLayout* layout; +}; + +TEST_F(GridLayoutAlignmentTest, Fill) { + gfx::Rect bounds; + TestAlignment(GridLayout::FILL, &bounds); + EXPECT_EQ(gfx::Rect(0, 0, 100, 100), bounds); +} + +TEST_F(GridLayoutAlignmentTest, Leading) { + gfx::Rect bounds; + TestAlignment(GridLayout::LEADING, &bounds); + EXPECT_EQ(gfx::Rect(0, 0, 10, 20), bounds); +} + +TEST_F(GridLayoutAlignmentTest, Center) { + gfx::Rect bounds; + TestAlignment(GridLayout::CENTER, &bounds); + EXPECT_EQ(gfx::Rect(45, 40, 10, 20), bounds); +} + +TEST_F(GridLayoutAlignmentTest, Trailing) { + gfx::Rect bounds; + TestAlignment(GridLayout::TRAILING, &bounds); + EXPECT_EQ(gfx::Rect(90, 80, 10, 20), bounds); +} + +TEST_F(GridLayoutTest, TwoColumns) { + SettableSizeView v1(gfx::Size(10, 20)); + SettableSizeView v2(gfx::Size(20, 20)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + layout->StartRow(0, 0); + layout->AddView(&v1); + layout->AddView(&v2); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(30, 20), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 10, 20, &v1); + ExpectViewBoundsEquals(10, 0, 20, 20, &v2); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, ColSpan1) { + SettableSizeView v1(gfx::Size(100, 20)); + SettableSizeView v2(gfx::Size(10, 40)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 1, GridLayout::USE_PREF, 0, 0); + layout->StartRow(0, 0); + layout->AddView(&v1, 2, 1); + layout->StartRow(0, 0); + layout->AddView(&v2); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(100, 60), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 100, 20, &v1); + ExpectViewBoundsEquals(0, 20, 10, 40, &v2); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, ColSpan2) { + SettableSizeView v1(gfx::Size(100, 20)); + SettableSizeView v2(gfx::Size(10, 20)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 1, GridLayout::USE_PREF, 0, 0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + layout->StartRow(0, 0); + layout->AddView(&v1, 2, 1); + layout->StartRow(0, 0); + layout->SkipColumns(1); + layout->AddView(&v2); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(100, 40), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 100, 20, &v1); + ExpectViewBoundsEquals(90, 20, 10, 20, &v2); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, ColSpan3) { + SettableSizeView v1(gfx::Size(100, 20)); + SettableSizeView v2(gfx::Size(10, 20)); + SettableSizeView v3(gfx::Size(10, 20)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + layout->StartRow(0, 0); + layout->AddView(&v1, 2, 1); + layout->StartRow(0, 0); + layout->AddView(&v2); + layout->AddView(&v3); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(100, 40), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 100, 20, &v1); + ExpectViewBoundsEquals(0, 20, 10, 20, &v2); + ExpectViewBoundsEquals(50, 20, 10, 20, &v3); + + RemoveAll(); +} + + +TEST_F(GridLayoutTest, ColSpan4) { + views::ColumnSet* set = layout->AddColumnSet(0); + + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, 0, + GridLayout::USE_PREF, 0, 0); + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, 0, + GridLayout::USE_PREF, 0, 0); + + SettableSizeView v1(gfx::Size(10, 10)); + SettableSizeView v2(gfx::Size(10, 10)); + SettableSizeView v3(gfx::Size(25, 20)); + layout->StartRow(0, 0); + layout->AddView(&v1); + layout->AddView(&v2); + layout->StartRow(0, 0); + layout->AddView(&v3, 2, 1); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(25, 30), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 10, 10, &v1); + ExpectViewBoundsEquals(12, 0, 10, 10, &v2); + ExpectViewBoundsEquals(0, 10, 25, 20, &v3); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, SameSizeColumns) { + SettableSizeView v1(gfx::Size(50, 20)); + SettableSizeView v2(gfx::Size(10, 10)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + c1->LinkColumnSizes(0, 1, -1); + layout->StartRow(0, 0); + layout->AddView(&v1); + layout->AddView(&v2); + + gfx::Size pref = layout->GetPreferredSize(&host); + EXPECT_EQ(gfx::Size(100, 20), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 50, 20, &v1); + ExpectViewBoundsEquals(50, 0, 10, 10, &v2); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, HorizontalResizeTest1) { + SettableSizeView v1(gfx::Size(50, 20)); + SettableSizeView v2(gfx::Size(10, 10)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::FILL, GridLayout::LEADING, + 1, GridLayout::USE_PREF, 0, 0); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + layout->StartRow(0, 0); + layout->AddView(&v1); + layout->AddView(&v2); + + host.SetBounds(0, 0, 110, 20); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 100, 20, &v1); + ExpectViewBoundsEquals(100, 0, 10, 10, &v2); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, HorizontalResizeTest2) { + SettableSizeView v1(gfx::Size(50, 20)); + SettableSizeView v2(gfx::Size(10, 10)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::FILL, GridLayout::LEADING, + 1, GridLayout::USE_PREF, 0, 0); + c1->AddColumn(GridLayout::TRAILING, GridLayout::LEADING, + 1, GridLayout::USE_PREF, 0, 0); + layout->StartRow(0, 0); + layout->AddView(&v1); + layout->AddView(&v2); + + host.SetBounds(0, 0, 120, 20); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 80, 20, &v1); + ExpectViewBoundsEquals(110, 0, 10, 10, &v2); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, TestVerticalResize1) { + SettableSizeView v1(gfx::Size(50, 20)); + SettableSizeView v2(gfx::Size(10, 10)); + ColumnSet* c1 = layout->AddColumnSet(0); + c1->AddColumn(GridLayout::FILL, GridLayout::FILL, + 1, GridLayout::USE_PREF, 0, 0); + layout->StartRow(1, 0); + layout->AddView(&v1); + layout->StartRow(0, 0); + layout->AddView(&v2); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(50, 30), pref); + + host.SetBounds(0, 0, 50, 100); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 50, 90, &v1); + ExpectViewBoundsEquals(0, 90, 50, 10, &v2); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, Insets) { + SettableSizeView v1(gfx::Size(10, 20)); + ColumnSet* c1 = layout->AddColumnSet(0); + layout->SetInsets(1, 2, 3, 4); + c1->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + layout->StartRow(0, 0); + layout->AddView(&v1); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(16, 24), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(2, 1, 10, 20, &v1); + + RemoveAll(); +} + +TEST_F(GridLayoutTest, FixedSize) { + layout->SetInsets(2, 2, 2, 2); + + views::ColumnSet* set = layout->AddColumnSet(0); + + int column_count = 4; + int title_width = 100; + int row_count = 2; + int pref_width = 10; + int pref_height = 20; + + for (int i = 0; i < column_count; ++i) { + set->AddColumn(views::GridLayout::CENTER, + views::GridLayout::CENTER, + 0, + views::GridLayout::FIXED, + title_width, + title_width); + } + + for (int row = 0; row < row_count; ++row) { + layout->StartRow(0, 0); + for (int col = 0; col < column_count; ++col) { + layout->AddView(new SettableSizeView(gfx::Size(pref_width, pref_height))); + } + } + + layout->Layout(&host); + + for (int i = 0; i < column_count; ++i) { + for (int row = 0; row < row_count; ++row) { + View* view = host.child_at(row * column_count + i); + ExpectViewBoundsEquals( + 2 + title_width * i + (title_width - pref_width) / 2, + 2 + pref_height * row, + pref_width, + pref_height, view); + } + } + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(column_count * title_width + 4, + row_count * pref_height + 4), pref); +} + +TEST_F(GridLayoutTest, RowSpanWithPaddingRow) { + views::ColumnSet* set = layout->AddColumnSet(0); + + set->AddColumn(views::GridLayout::CENTER, + views::GridLayout::CENTER, + 0, + views::GridLayout::FIXED, + 10, + 10); + + layout->StartRow(0, 0); + layout->AddView(new SettableSizeView(gfx::Size(10, 10)), 1, 2); + layout->AddPaddingRow(0, 10); +} + +TEST_F(GridLayoutTest, RowSpan) { + views::ColumnSet* set = layout->AddColumnSet(0); + + set->AddColumn(views::GridLayout::LEADING, + views::GridLayout::LEADING, + 0, + views::GridLayout::USE_PREF, + 0, + 0); + set->AddColumn(views::GridLayout::LEADING, + views::GridLayout::LEADING, + 0, + views::GridLayout::USE_PREF, + 0, + 0); + + layout->StartRow(0, 0); + layout->AddView(new SettableSizeView(gfx::Size(20, 10))); + layout->AddView(new SettableSizeView(gfx::Size(20, 40)), 1, 2); + layout->StartRow(1, 0); + views::View* s3 = new SettableSizeView(gfx::Size(20, 10)); + layout->AddView(s3); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(40, 40), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 10, 20, 10, s3); +} + +TEST_F(GridLayoutTest, RowSpan2) { + views::ColumnSet* set = layout->AddColumnSet(0); + + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0,GridLayout::USE_PREF, 0, 0); + + layout->StartRow(0, 0); + layout->AddView(new SettableSizeView(gfx::Size(20, 20))); + views::View* s3 = new SettableSizeView(gfx::Size(64, 64)); + layout->AddView(s3, 1, 3); + + layout->AddPaddingRow(0, 10); + + layout->StartRow(0, 0); + layout->AddView(new SettableSizeView(gfx::Size(10, 20))); + + GetPreferredSize(); + EXPECT_EQ(gfx::Size(84, 64), pref); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(20, 0, 64, 64, s3); +} + +TEST_F(GridLayoutTest, FixedViewWidth) { + views::ColumnSet* set = layout->AddColumnSet(0); + + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0,GridLayout::USE_PREF, 0, 0); + + layout->StartRow(0, 0); + View* view = new SettableSizeView(gfx::Size(30, 40)); + layout->AddView(view, 1, 1, GridLayout::LEADING, GridLayout::LEADING, 10, 0); + + GetPreferredSize(); + EXPECT_EQ(10, pref.width()); + EXPECT_EQ(40, pref.height()); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 10, 40, view); +} + +TEST_F(GridLayoutTest, FixedViewHeight) { + views::ColumnSet* set = layout->AddColumnSet(0); + + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0, GridLayout::USE_PREF, 0, 0); + set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, + 0,GridLayout::USE_PREF, 0, 0); + + layout->StartRow(0, 0); + View* view = new SettableSizeView(gfx::Size(30, 40)); + layout->AddView(view, 1, 1, GridLayout::LEADING, GridLayout::LEADING, 0, 10); + + GetPreferredSize(); + EXPECT_EQ(30, pref.width()); + EXPECT_EQ(10, pref.height()); + + host.SetBounds(0, 0, pref.width(), pref.height()); + layout->Layout(&host); + ExpectViewBoundsEquals(0, 0, 30, 10, view); +} + +// Make sure that for views that span columns the underlying columns are resized +// based on the resize percent of the column. +TEST_F(GridLayoutTest, ColumnSpanResizing) { + views::ColumnSet* set = layout->AddColumnSet(0); + + set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER, + 2, views::GridLayout::USE_PREF, 0, 0); + set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER, + 4, views::GridLayout::USE_PREF, 0, 0); + + layout->StartRow(0, 0); + // span_view spans two columns and is twice as big the views added below. + View* span_view = new SettableSizeView(gfx::Size(12, 40)); + layout->AddView(span_view, 2, 1, GridLayout::LEADING, GridLayout::LEADING); + + layout->StartRow(0, 0); + View* view1 = new SettableSizeView(gfx::Size(2, 40)); + View* view2 = new SettableSizeView(gfx::Size(4, 40)); + layout->AddView(view1); + layout->AddView(view2); + + host.SetBounds(0, 0, 12, 80); + layout->Layout(&host); + + ExpectViewBoundsEquals(0, 0, 12, 40, span_view); + + // view1 should be 4 pixels wide + // column_pref + (remaining_width * column_resize / total_column_resize) = + // 2 + (6 * 2 / 6). + ExpectViewBoundsEquals(0, 40, 4, 40, view1); + + // And view2 should be 8 pixels wide: + // 4 + (6 * 4 / 6). + ExpectViewBoundsEquals(4, 40, 8, 40, view2); +} + +// Check that GetPreferredSize() takes resizing of columns into account when +// there is additional space in the case we have column sets of different +// preferred sizes. +TEST_F(GridLayoutTest, ColumnResizingOnGetPreferredSize) { + views::ColumnSet* set = layout->AddColumnSet(0); + set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER, + 1, views::GridLayout::USE_PREF, 0, 0); + + set = layout->AddColumnSet(1); + set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER, + 1, views::GridLayout::USE_PREF, 0, 0); + + set = layout->AddColumnSet(2); + set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER, + 1, views::GridLayout::USE_PREF, 0, 0); + + // Make a row containing a flexible view that trades width for height. + layout->StartRow(0, 0); + View* view1 = new FlexibleView(100); + layout->AddView(view1, 1, 1, GridLayout::FILL, GridLayout::LEADING); + + // The second row contains a view of fixed size that will enforce a column + // width of 20 pixels. + layout->StartRow(0, 1); + View* view2 = new SettableSizeView(gfx::Size(20, 20)); + layout->AddView(view2, 1, 1, GridLayout::FILL, GridLayout::LEADING); + + // Add another flexible view in row three in order to ensure column set + // ordering doesn't influence sizing behaviour. + layout->StartRow(0, 2); + View* view3 = new FlexibleView(40); + layout->AddView(view3, 1, 1, GridLayout::FILL, GridLayout::LEADING); + + // We expect a height of 50: 30 from the variable width view in the first row + // plus 20 from the statically sized view in the second row. The flexible + // view in the third row should contribute no height. + EXPECT_EQ(gfx::Size(20, 50), layout->GetPreferredSize(&host)); +} diff --git a/ui/views/layout/layout_constants.h b/ui/views/layout/layout_constants.h new file mode 100644 index 0000000..3d9b69a --- /dev/null +++ b/ui/views/layout/layout_constants.h @@ -0,0 +1,71 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_LAYOUT_LAYOUT_CONSTANTS_H_ +#define UI_VIEWS_LAYOUT_LAYOUT_CONSTANTS_H_ +#pragma once + +// This file contains some constants we use to implement our standard panel +// layout. +// see: spec 21/4 + +namespace views { + +// Left or right margin. +const int kPanelHorizMargin = 13; + +// Top or bottom margin. +const int kPanelVertMargin = 13; + +// If some UI has some sub UI. Indent horizontally by the following value. +const int kPanelHorizIndentation = 24; + +// When several controls are aligned vertically, the baseline should be spaced +// by the following number of pixels. +const int kPanelVerticalSpacing = 32; + +// Vertical spacing between sub UI. +const int kPanelSubVerticalSpacing = 24; + +// Vertical spacing between a label and some control. +const int kLabelToControlVerticalSpacing = 8; + +// Small horizontal spacing between controls that are logically related. +const int kRelatedControlSmallHorizontalSpacing = 8; + +// Horizontal spacing between controls that are logically related. +const int kRelatedControlHorizontalSpacing = 8; + +// Vertical spacing between controls that are logically related. +const int kRelatedControlVerticalSpacing = 8; + +// Small vertical spacing between controls that are logically related. +const int kRelatedControlSmallVerticalSpacing = 4; + +// Horizontal spacing between controls that are logically unrelated. +const int kUnrelatedControlHorizontalSpacing = 12; + +// Larger horizontal spacing between unrelated controls. +const int kUnrelatedControlLargeHorizontalSpacing = 20; + +// Vertical spacing between controls that are logically unrelated. +const int kUnrelatedControlVerticalSpacing = 20; + +// Larger vertical spacing between unrelated controls. +const int kUnrelatedControlLargeVerticalSpacing = 30; + +// Vertical spacing between the edge of the window and the +// top or bottom of a button. +const int kButtonVEdgeMargin = 6; + +// Vertical spacing between the edge of the window and the +// left or right of a button. +const int kButtonHEdgeMargin = 7; + +// Horizontal spacing between buttons that are logically related. +const int kRelatedButtonHSpacing = 6; + +} // namespace views + +#endif // UI_VIEWS_LAYOUT_LAYOUT_CONSTANTS_H_ diff --git a/ui/views/layout/layout_manager.cc b/ui/views/layout/layout_manager.cc new file mode 100644 index 0000000..c62c6bb --- /dev/null +++ b/ui/views/layout/layout_manager.cc @@ -0,0 +1,30 @@ +// Copyright (c) 2011 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 "views/layout/layout_manager.h" + +#include "views/view.h" + +namespace views { + +LayoutManager::~LayoutManager() { +} + +void LayoutManager::Installed(View* host) { +} + +void LayoutManager::Uninstalled(View* host) { +} + +int LayoutManager::GetPreferredHeightForWidth(View* host, int width) { + return GetPreferredSize(host).height(); +} + +void LayoutManager::ViewAdded(View* host, View* view) { +} + +void LayoutManager::ViewRemoved(View* host, View* view) { +} + +} // namespace views diff --git a/ui/views/layout/layout_manager.h b/ui/views/layout/layout_manager.h new file mode 100644 index 0000000..1241c80 --- /dev/null +++ b/ui/views/layout/layout_manager.h @@ -0,0 +1,61 @@ +// Copyright (c) 2011 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. + +#ifndef UI_VIEWS_LAYOUT_LAYOUT_MANAGER_H_ +#define UI_VIEWS_LAYOUT_LAYOUT_MANAGER_H_ +#pragma once + +#include "views/views_export.h" + +namespace gfx { +class Size; +} + +namespace views { + +class View; + +///////////////////////////////////////////////////////////////////////////// +// +// LayoutManager interface +// +// The LayoutManager interface provides methods to handle the sizing of +// the children of a View according to implementation-specific heuristics. +// +///////////////////////////////////////////////////////////////////////////// +class VIEWS_EXPORT LayoutManager { + public: + virtual ~LayoutManager(); + + // Notification that this LayoutManager has been installed on a particular + // host. + virtual void Installed(View* host); + + // Notification that this LayoutManager has been uninstalled on a particular + // host. + virtual void Uninstalled(View* host); + + // Lay out the children of |host| according to implementation-specific + // heuristics. The graphics used during painting is provided to allow for + // string sizing. + virtual void Layout(View* host) = 0; + + // Return the preferred size which is the size required to give each + // children their respective preferred size. + virtual gfx::Size GetPreferredSize(View* host) = 0; + + // Returns the preferred height for the specified width. The default + // implementation returns the value from GetPreferredSize. + virtual int GetPreferredHeightForWidth(View* host, int width); + + // Notification that a view has been added. + virtual void ViewAdded(View* host, View* view); + + // Notification that a view has been removed. + virtual void ViewRemoved(View* host, View* view); +}; + +} // namespace views + +#endif // UI_VIEWS_LAYOUT_LAYOUT_MANAGER_H_ |