diff options
Diffstat (limited to 'views')
154 files changed, 41087 insertions, 0 deletions
diff --git a/views/DEPS b/views/DEPS new file mode 100644 index 0000000..8ff5f48 --- /dev/null +++ b/views/DEPS @@ -0,0 +1,12 @@ +include_rules = [
+ "+app",
+ "+chrome/app",
+ "+chrome/app/theme",
+ "+chrome/browser",
+ "+chrome/browser/views",
+ "+chrome/common",
+ "+grit", # For generated headers
+ "+skia/ext",
+ "+skia/include",
+ "+webkit/glue",
+]
diff --git a/views/accelerator.cc b/views/accelerator.cc new file mode 100644 index 0000000..b9a474a --- /dev/null +++ b/views/accelerator.cc @@ -0,0 +1,130 @@ +// Copyright (c) 2006-2008 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/accelerator.h" + +#include <windows.h> + +#include "app/l10n_util.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "grit/generated_resources.h" + +namespace views { + +std::wstring Accelerator::GetShortcutText() const { + int string_id = 0; + switch(key_code_) { + case VK_TAB: + string_id = IDS_TAB_KEY; + break; + case VK_RETURN: + string_id = IDS_ENTER_KEY; + break; + case VK_ESCAPE: + string_id = IDS_ESC_KEY; + break; + case VK_PRIOR: + string_id = IDS_PAGEUP_KEY; + break; + case VK_NEXT: + string_id = IDS_PAGEDOWN_KEY; + break; + case VK_END: + string_id = IDS_END_KEY; + break; + case VK_HOME: + string_id = IDS_HOME_KEY; + break; + case VK_INSERT: + string_id = IDS_INSERT_KEY; + break; + case VK_DELETE: + string_id = IDS_DELETE_KEY; + break; + case VK_F1: + string_id = IDS_F1_KEY; + break; + case VK_F11: + string_id = IDS_F11_KEY; + break; + } + + std::wstring shortcut; + if (!string_id) { + // Our fallback is to try translate the key code to a regular character + // unless it is one of digits (VK_0 to VK_9). Some keyboard + // layouts have characters other than digits assigned in + // an unshifted mode (e.g. French AZERY layout has 'a with grave + // accent' for '0'). For display in the menu (e.g. Ctrl-0 for the + // default zoom level), we leave VK_[0-9] alone without translation. + wchar_t key; + if (key_code_ >= '0' && key_code_ <= '9') + key = key_code_; + else + key = LOWORD(::MapVirtualKeyW(key_code_, MAPVK_VK_TO_CHAR)); + shortcut += key; + } else { + shortcut = l10n_util::GetString(string_id); + } + + // Checking whether the character used for the accelerator is alphanumeric. + // If it is not, then we need to adjust the string later on if the locale is + // right-to-left. See below for more information of why such adjustment is + // required. + std::wstring shortcut_rtl; + bool adjust_shortcut_for_rtl = false; + if (l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT && + shortcut.length() == 1 && + !IsAsciiAlpha(shortcut.at(0)) && + !IsAsciiDigit(shortcut.at(0))) { + adjust_shortcut_for_rtl = true; + shortcut_rtl.assign(shortcut); + } + + if (IsShiftDown()) + shortcut = l10n_util::GetStringF(IDS_SHIFT_MODIFIER, shortcut); + + // Note that we use 'else-if' in order to avoid using Ctrl+Alt as a shortcut. + // See http://blogs.msdn.com/oldnewthing/archive/2004/03/29/101121.aspx for + // more information. + if (IsCtrlDown()) + shortcut = l10n_util::GetStringF(IDS_CONTROL_MODIFIER, shortcut); + else if (IsAltDown()) + shortcut = l10n_util::GetStringF(IDS_ALT_MODIFIER, shortcut); + + // For some reason, menus in Windows ignore standard Unicode directionality + // marks (such as LRE, PDF, etc.). On RTL locales, we use RTL menus and + // therefore any text we draw for the menu items is drawn in an RTL context. + // Thus, the text "Ctrl++" (which we currently use for the Zoom In option) + // appears as "++Ctrl" in RTL because the Unicode BiDi algorithm puts + // punctuations on the left when the context is right-to-left. Shortcuts that + // do not end with a punctuation mark (such as "Ctrl+H" do not have this + // problem). + // + // The only way to solve this problem is to adjust the string if the locale + // is RTL so that it is drawn correnctly in an RTL context. Instead of + // returning "Ctrl++" in the above example, we return "++Ctrl". This will + // cause the text to appear as "Ctrl++" when Windows draws the string in an + // RTL context because the punctunation no longer appears at the end of the + // string. + // + // TODO(idana) bug# 1232732: this hack can be avoided if instead of using + // views::Menu we use views::MenuItemView because the latter is a View + // subclass and therefore it supports marking text as RTL or LTR using + // standard Unicode directionality marks. + if (adjust_shortcut_for_rtl) { + int key_length = static_cast<int>(shortcut_rtl.length()); + DCHECK_GT(key_length, 0); + shortcut_rtl.append(L"+"); + + // Subtracting the size of the shortcut key and 1 for the '+' sign. + shortcut_rtl.append(shortcut, 0, shortcut.length() - key_length - 1); + shortcut.swap(shortcut_rtl); + } + + return shortcut; +} + +} // namespace views diff --git a/views/accelerator.h b/views/accelerator.h new file mode 100644 index 0000000..bb8f91f --- /dev/null +++ b/views/accelerator.h @@ -0,0 +1,101 @@ +// Copyright (c) 2006-2008 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. + +// This class describe a keyboard accelerator (or keyboard shortcut). +// Keyboard accelerators are registered with the FocusManager. +// It has a copy constructor and assignment operator so that it can be copied. +// It also defines the < operator so that it can be used as a key in a std::map. +// + +#ifndef VIEWS_ACCELERATOR_H_ +#define VIEWS_ACCELERATOR_H_ + +#include <string> + +#include "views/event.h" + +namespace views { + +class Accelerator { + public: + Accelerator(int keycode, + bool shift_pressed, bool ctrl_pressed, bool alt_pressed) + : key_code_(keycode) { + modifiers_ = 0; + if (shift_pressed) + modifiers_ |= Event::EF_SHIFT_DOWN; + if (ctrl_pressed) + modifiers_ |= Event::EF_CONTROL_DOWN; + if (alt_pressed) + modifiers_ |= Event::EF_ALT_DOWN; + } + + Accelerator(const Accelerator& accelerator) { + key_code_ = accelerator.key_code_; + modifiers_ = accelerator.modifiers_; + } + + ~Accelerator() { }; + + Accelerator& operator=(const Accelerator& accelerator) { + if (this != &accelerator) { + key_code_ = accelerator.key_code_; + modifiers_ = accelerator.modifiers_; + } + return *this; + } + + // We define the < operator so that the KeyboardShortcut can be used as a key + // in a std::map. + bool operator <(const Accelerator& rhs) const { + if (key_code_ != rhs.key_code_) + return key_code_ < rhs.key_code_; + return modifiers_ < rhs.modifiers_; + } + + bool operator ==(const Accelerator& rhs) const { + return (key_code_ == rhs.key_code_) && (modifiers_ == rhs.modifiers_); + } + + bool operator !=(const Accelerator& rhs) const { + return !(*this == rhs); + } + + bool IsShiftDown() const { + return (modifiers_ & Event::EF_SHIFT_DOWN) == Event::EF_SHIFT_DOWN; + } + + bool IsCtrlDown() const { + return (modifiers_ & Event::EF_CONTROL_DOWN) == Event::EF_CONTROL_DOWN; + } + + bool IsAltDown() const { + return (modifiers_ & Event::EF_ALT_DOWN) == Event::EF_ALT_DOWN; + } + + int GetKeyCode() const { + return key_code_; + } + + // Returns a string with the localized shortcut if any. + std::wstring GetShortcutText() const; + + private: + // The window keycode (VK_...). + int key_code_; + + // The state of the Shift/Ctrl/Alt keys (see event.h). + int modifiers_; +}; + +// An interface that classes that want to register for keyboard accelerators +// should implement. +class AcceleratorTarget { + public: + // This method should return true if the accelerator was processed. + virtual bool AcceleratorPressed(const Accelerator& accelerator) = 0; +}; +} + +#endif // VIEWS_ACCELERATOR_H_ diff --git a/views/accessibility/view_accessibility.cc b/views/accessibility/view_accessibility.cc new file mode 100644 index 0000000..0e2f874 --- /dev/null +++ b/views/accessibility/view_accessibility.cc @@ -0,0 +1,698 @@ +// Copyright (c) 2006-2008 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/accessibility/view_accessibility.h" + +#include "views/accessibility/view_accessibility_wrapper.h" +#include "views/widget/widget.h" + +HRESULT ViewAccessibility::Initialize(views::View* view) { + if (!view) { + return E_INVALIDARG; + } + + view_ = view; + return S_OK; +} + +// TODO(klink): Handle case where child View is not contained by parent. +STDMETHODIMP ViewAccessibility::accHitTest(LONG x_left, LONG y_top, + VARIANT* child) { + if (!child) { + return E_INVALIDARG; + } + + gfx::Point pt(x_left, y_top); + views::View::ConvertPointToView(NULL, view_, &pt); + + if (!view_->HitTest(pt)) { + // If containing parent is not hit, return with failure. + child->vt = VT_EMPTY; + return S_FALSE; + } + + int child_count = view_->GetChildViewCount(); + bool child_hit = false; + views::View* child_view = NULL; + for (int child_id = 0; child_id < child_count; ++child_id) { + // Search for hit within any of the children. + child_view = view_->GetChildViewAt(child_id); + views::View::ConvertPointToView(view_, child_view, &pt); + if (child_view->HitTest(pt)) { + // Store child_id (adjusted with +1 to convert to MSAA indexing). + child->lVal = child_id + 1; + child_hit = true; + break; + } + // Convert point back to parent view to test next child. + views::View::ConvertPointToView(child_view, view_, &pt); + } + + child->vt = VT_I4; + + if (!child_hit) { + // No child hit, return parent id. + child->lVal = CHILDID_SELF; + } else { + if (child_view == NULL) { + return E_FAIL; + } else if (child_view->GetChildViewCount() != 0) { + // Retrieve IDispatch for child, if it is not a leaf. + child->vt = VT_DISPATCH; + if ((GetViewAccessibilityWrapper(child_view))-> + GetInstance(IID_IAccessible, + reinterpret_cast<void**>(&child->pdispVal)) == S_OK) { + // Increment the reference count for the retrieved interface. + child->pdispVal->AddRef(); + return S_OK; + } else { + return E_NOINTERFACE; + } + } + } + + return S_OK; +} + +STDMETHODIMP ViewAccessibility::accLocation(LONG* x_left, LONG* y_top, + LONG* width, LONG* height, + VARIANT var_id) { + if (var_id.vt != VT_I4 || !x_left || !y_top || !width || !height) { + return E_INVALIDARG; + } + + gfx::Rect view_bounds; + // Retrieving the parent View to be used for converting from view-to-screen + // coordinates. + views::View* parent = view_->GetParent(); + + if (parent == NULL) { + // If no parent, remain within the same View. + parent = view_; + } + + if (var_id.lVal == CHILDID_SELF) { + // Retrieve active View's bounds. + view_bounds = view_->bounds(); + } else { + // Check to see if child is out-of-bounds. + if (!IsValidChild((var_id.lVal - 1), view_)) { + return E_INVALIDARG; + } + // Retrieve child bounds. + view_bounds = view_->GetChildViewAt(var_id.lVal - 1)->bounds(); + // Parent View is current View. + parent = view_; + } + + if (!view_bounds.IsEmpty()) { + *width = view_bounds.width(); + *height = view_bounds.height(); + + gfx::Point topleft(view_bounds.origin()); + views::View::ConvertPointToScreen(parent, &topleft); + *x_left = topleft.x(); + *y_top = topleft.y(); + } else { + return E_FAIL; + } + return S_OK; +} + +STDMETHODIMP ViewAccessibility::accNavigate(LONG nav_dir, VARIANT start, + VARIANT* end) { + if (start.vt != VT_I4 || !end) { + return E_INVALIDARG; + } + + switch (nav_dir) { + case NAVDIR_FIRSTCHILD: + case NAVDIR_LASTCHILD: { + if (start.lVal != CHILDID_SELF) { + // Start of navigation must be on the View itself. + return E_INVALIDARG; + } else if (view_->GetChildViewCount() == 0) { + // No children found. + return S_FALSE; + } + + // Set child_id based on first or last child. + int child_id = 0; + if (nav_dir == NAVDIR_LASTCHILD) { + child_id = view_->GetChildViewCount() - 1; + } + + views::View* child = view_->GetChildViewAt(child_id); + + if (child->GetChildViewCount() != 0) { + end->vt = VT_DISPATCH; + if ((GetViewAccessibilityWrapper(child))-> + GetInstance(IID_IAccessible, + reinterpret_cast<void**>(&end->pdispVal)) == S_OK) { + // Increment the reference count for the retrieved interface. + end->pdispVal->AddRef(); + return S_OK; + } else { + return E_NOINTERFACE; + } + } else { + end->vt = VT_I4; + // Set return child lVal, adjusted for MSAA indexing. (MSAA + // child indexing starts with 1, whereas View indexing starts with 0). + end->lVal = child_id + 1; + } + break; + } + case NAVDIR_LEFT: + case NAVDIR_UP: + case NAVDIR_PREVIOUS: + case NAVDIR_RIGHT: + case NAVDIR_DOWN: + case NAVDIR_NEXT: { + // Retrieve parent to access view index and perform bounds checking. + views::View* parent = view_->GetParent(); + if (!parent) { + return E_FAIL; + } + + if (start.lVal == CHILDID_SELF) { + int view_index = parent->GetChildIndex(view_); + // Check navigation bounds, adjusting for View child indexing (MSAA + // child indexing starts with 1, whereas View indexing starts with 0). + if (!IsValidNav(nav_dir, view_index, -1, + parent->GetChildViewCount())) { + // Navigation attempted to go out-of-bounds. + end->vt = VT_EMPTY; + return S_FALSE; + } else { + if (IsNavDirNext(nav_dir)) { + view_index += 1; + } else { + view_index -=1; + } + } + + views::View* child = parent->GetChildViewAt(view_index); + if (child->GetChildViewCount() != 0) { + end->vt = VT_DISPATCH; + // Retrieve IDispatch for non-leaf child. + if ((GetViewAccessibilityWrapper(child))-> + GetInstance(IID_IAccessible, + reinterpret_cast<void**>(&end->pdispVal)) == S_OK) { + // Increment the reference count for the retrieved interface. + end->pdispVal->AddRef(); + return S_OK; + } else { + return E_NOINTERFACE; + } + } else { + end->vt = VT_I4; + // Modifying view_index to give lVal correct MSAA-based value. (MSAA + // child indexing starts with 1, whereas View indexing starts with 0). + end->lVal = view_index + 1; + } + } else { + // Check navigation bounds, adjusting for MSAA child indexing (MSAA + // child indexing starts with 1, whereas View indexing starts with 0). + if (!IsValidNav(nav_dir, start.lVal, 0, + parent->GetChildViewCount() + 1)) { + // Navigation attempted to go out-of-bounds. + end->vt = VT_EMPTY; + return S_FALSE; + } else { + if (IsNavDirNext(nav_dir)) { + start.lVal += 1; + } else { + start.lVal -= 1; + } + } + + HRESULT result = this->get_accChild(start, &end->pdispVal); + if (result == S_FALSE) { + // Child is a leaf. + end->vt = VT_I4; + end->lVal = start.lVal; + } else if (result == E_INVALIDARG) { + return E_INVALIDARG; + } else { + // Child is not a leaf. + end->vt = VT_DISPATCH; + } + } + break; + } + default: + return E_INVALIDARG; + } + // Navigation performed correctly. Global return for this function, if no + // error triggered an escape earlier. + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accChild(VARIANT var_child, + IDispatch** disp_child) { + if (var_child.vt != VT_I4 || !disp_child) { + return E_INVALIDARG; + } + + // If var_child is the parent, remain with the same IDispatch. + if (var_child.lVal == CHILDID_SELF) { + return S_OK; + } + + views::View* child_view = NULL; + bool get_iaccessible = false; + + // Check to see if child is out-of-bounds. + if (IsValidChild((var_child.lVal - 1), view_)) { + child_view = view_->GetChildViewAt(var_child.lVal - 1); + } else { + // Child is located elsewhere in the hierarchy, get ID and adjust for MSAA. + child_view = view_->GetViewByID(static_cast<int>(var_child.lVal)); + get_iaccessible = true; + } + + if (!child_view) { + // No child found. + *disp_child = NULL; + return E_FAIL; + } + + if (get_iaccessible || child_view->GetChildViewCount() != 0) { + // Retrieve the IUnknown interface for the requested child view, and + // assign the IDispatch returned. + if ((GetViewAccessibilityWrapper(child_view))-> + GetInstance(IID_IAccessible, + reinterpret_cast<void**>(disp_child)) == S_OK) { + // Increment the reference count for the retrieved interface. + (*disp_child)->AddRef(); + return S_OK; + } else { + // No interface, return failure. + return E_NOINTERFACE; + } + } else { + // When at a leaf, children are handled by the parent object. + *disp_child = NULL; + return S_FALSE; + } +} + +STDMETHODIMP ViewAccessibility::get_accChildCount(LONG* child_count) { + if (!child_count || !view_) { + return E_INVALIDARG; + } + + *child_count = view_->GetChildViewCount(); + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accDefaultAction(VARIANT var_id, + BSTR* def_action) { + if (var_id.vt != VT_I4 || !def_action) { + return E_INVALIDARG; + } + + std::wstring temp_action; + + if (var_id.lVal == CHILDID_SELF) { + view_->GetAccessibleDefaultAction(&temp_action); + } else { + if (!IsValidChild((var_id.lVal - 1), view_)) { + return E_INVALIDARG; + } + view_->GetChildViewAt(var_id.lVal - 1)-> + GetAccessibleDefaultAction(&temp_action); + } + if (!temp_action.empty()) { + *def_action = CComBSTR(temp_action.c_str()).Detach(); + } else { + return S_FALSE; + } + + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accDescription(VARIANT var_id, BSTR* desc) { + if (var_id.vt != VT_I4 || !desc) { + return E_INVALIDARG; + } + + std::wstring temp_desc; + + if (var_id.lVal == CHILDID_SELF) { + view_->GetTooltipText(0, 0, &temp_desc); + } else { + if (!IsValidChild((var_id.lVal - 1), view_)) { + return E_INVALIDARG; + } + view_->GetChildViewAt(var_id.lVal - 1)->GetTooltipText(0, 0, &temp_desc); + } + if (!temp_desc.empty()) { + *desc = CComBSTR(temp_desc.c_str()).Detach(); + } else { + return S_FALSE; + } + + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accFocus(VARIANT* focus_child) { + if (!focus_child) { + return E_INVALIDARG; + } + + if (view_->GetChildViewCount() == 0 && view_->HasFocus()) { + // Parent view has focus. + focus_child->vt = VT_I4; + focus_child->lVal = CHILDID_SELF; + } else { + bool has_focus = false; + int child_count = view_->GetChildViewCount(); + // Search for child view with focus. + for (int child_id = 0; child_id < child_count; ++child_id) { + if (view_->GetChildViewAt(child_id)->HasFocus()) { + focus_child->vt = VT_I4; + focus_child->lVal = child_id + 1; + + // If child view is no leaf, retrieve IDispatch. + if (view_->GetChildViewAt(child_id)->GetChildViewCount() != 0) { + focus_child->vt = VT_DISPATCH; + this->get_accChild(*focus_child, &focus_child->pdispVal); + } + has_focus = true; + break; + } + } + // No current focus on any of the children. + if (!has_focus) { + focus_child->vt = VT_EMPTY; + return S_FALSE; + } + } + + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accKeyboardShortcut(VARIANT var_id, + BSTR* acc_key) { + if (var_id.vt != VT_I4 || !acc_key) { + return E_INVALIDARG; + } + + std::wstring temp_key; + + if (var_id.lVal == CHILDID_SELF) { + view_->GetAccessibleKeyboardShortcut(&temp_key); + } else { + if (!IsValidChild((var_id.lVal - 1), view_)) { + return E_INVALIDARG; + } + view_->GetChildViewAt(var_id.lVal - 1)-> + GetAccessibleKeyboardShortcut(&temp_key); + } + if (!temp_key.empty()) { + *acc_key = CComBSTR(temp_key.c_str()).Detach(); + } else { + return S_FALSE; + } + + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accName(VARIANT var_id, BSTR* name) { + if (var_id.vt != VT_I4 || !name) { + return E_INVALIDARG; + } + + std::wstring temp_name; + + if (var_id.lVal == CHILDID_SELF) { + // Retrieve the parent view's name. + view_->GetAccessibleName(&temp_name); + } else { + if (!IsValidChild((var_id.lVal - 1), view_)) { + return E_INVALIDARG; + } + // Retrieve the child view's name. + view_->GetChildViewAt(var_id.lVal - 1)->GetAccessibleName(&temp_name); + } + if (!temp_name.empty()) { + // Return name retrieved. + *name = CComBSTR(temp_name.c_str()).Detach(); + } else { + // If view has no name, return S_FALSE. + return S_FALSE; + } + + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accParent(IDispatch** disp_parent) { + if (!disp_parent) { + return E_INVALIDARG; + } + + views::View* parent_view = view_->GetParent(); + + if (!parent_view) { + // This function can get called during teardown of WidetWin so we + // should bail out if we fail to get the HWND. + if (!view_->GetWidget() || !view_->GetWidget()->GetNativeView()) { + *disp_parent = NULL; + return S_FALSE; + } + + // For a View that has no parent (e.g. root), point the accessible parent to + // the default implementation, to interface with Windows' hierarchy and to + // support calls from e.g. WindowFromAccessibleObject. + HRESULT hr = + ::AccessibleObjectFromWindow(view_->GetWidget()->GetNativeView(), + OBJID_WINDOW, IID_IAccessible, + reinterpret_cast<void**>(disp_parent)); + + if (!SUCCEEDED(hr)) { + *disp_parent = NULL; + return S_FALSE; + } + + return S_OK; + } + + // Retrieve the IUnknown interface for the parent view, and assign the + // IDispatch returned. + if ((GetViewAccessibilityWrapper(parent_view))-> + GetInstance(IID_IAccessible, + reinterpret_cast<void**>(disp_parent)) == S_OK) { + // Increment the reference count for the retrieved interface. + (*disp_parent)->AddRef(); + return S_OK; + } else { + return E_NOINTERFACE; + } +} + +STDMETHODIMP ViewAccessibility::get_accRole(VARIANT var_id, VARIANT* role) { + if (var_id.vt != VT_I4 || !role) { + return E_INVALIDARG; + } + + AccessibilityTypes::Role acc_role; + + if (var_id.lVal == CHILDID_SELF) { + // Retrieve parent role. + if (!view_->GetAccessibleRole(&acc_role)) { + return E_FAIL; + } + } else { + if (!IsValidChild((var_id.lVal - 1), view_)) { + return E_INVALIDARG; + } + // Retrieve child role. + if (!view_->GetChildViewAt(var_id.lVal - 1)->GetAccessibleRole(&acc_role)) { + role->vt = VT_EMPTY; + return E_FAIL; + } + } + + role->vt = VT_I4; + role->lVal = MSAARole(acc_role); + return S_OK; +} + +STDMETHODIMP ViewAccessibility::get_accState(VARIANT var_id, VARIANT* state) { + if (var_id.vt != VT_I4 || !state) { + return E_INVALIDARG; + } + + state->vt = VT_I4; + + if (var_id.lVal == CHILDID_SELF) { + // Retrieve all currently applicable states of the parent. + this->SetState(state, view_); + } else { + if (!IsValidChild((var_id.lVal - 1), view_)) { + return E_INVALIDARG; + } + // Retrieve all currently applicable states of the child. + this->SetState(state, view_->GetChildViewAt(var_id.lVal - 1)); + } + + // Make sure that state is not empty, and has the proper type. + if (state->vt == VT_EMPTY) + return E_FAIL; + + return S_OK; +} + +// Helper functions. + +bool ViewAccessibility::IsValidChild(int child_id, views::View* view) const { + if (((child_id) < 0) || + ((child_id) >= view->GetChildViewCount())) { + return false; + } + return true; +} + +bool ViewAccessibility::IsNavDirNext(int nav_dir) const { + if (nav_dir == NAVDIR_RIGHT || nav_dir == NAVDIR_DOWN || + nav_dir == NAVDIR_NEXT) { + return true; + } + return false; +} + +bool ViewAccessibility::IsValidNav(int nav_dir, int start_id, int lower_bound, + int upper_bound) const { + if (IsNavDirNext(nav_dir)) { + if ((start_id + 1) > upper_bound) { + return false; + } + } else { + if ((start_id - 1) <= lower_bound) { + return false; + } + } + return true; +} + +void ViewAccessibility::SetState(VARIANT* msaa_state, views::View* view) { + // Default state; all views can have accessibility focus. + msaa_state->lVal |= STATE_SYSTEM_FOCUSABLE; + + if (!view) + return; + + if (!view->IsEnabled()) { + msaa_state->lVal |= STATE_SYSTEM_UNAVAILABLE; + } + if (!view->IsVisible()) { + msaa_state->lVal |= STATE_SYSTEM_INVISIBLE; + } + if (view->IsHotTracked()) { + msaa_state->lVal |= STATE_SYSTEM_HOTTRACKED; + } + if (view->IsPushed()) { + msaa_state->lVal |= STATE_SYSTEM_PRESSED; + } + // Check both for actual View focus, as well as accessibility focus. + views::View* parent = view->GetParent(); + + if (view->HasFocus() || + (parent && parent->GetAccFocusedChildView() == view)) { + msaa_state->lVal |= STATE_SYSTEM_FOCUSED; + } + + // Add on any view-specific states. + AccessibilityTypes::State state; + view->GetAccessibleState(&state); + + msaa_state->lVal |= MSAAState(state); +} + +long ViewAccessibility::MSAARole(AccessibilityTypes::Role role) { + switch (role) { + case AccessibilityTypes::ROLE_APPLICATION : + return ROLE_SYSTEM_APPLICATION; + case AccessibilityTypes::ROLE_BUTTONDROPDOWN : + return ROLE_SYSTEM_BUTTONDROPDOWN; + case AccessibilityTypes::ROLE_GROUPING : + return ROLE_SYSTEM_GROUPING; + case AccessibilityTypes::ROLE_PAGETAB : + return ROLE_SYSTEM_PAGETAB; + case AccessibilityTypes::ROLE_PUSHBUTTON : + return ROLE_SYSTEM_PUSHBUTTON; + case AccessibilityTypes::ROLE_TEXT : + return ROLE_SYSTEM_TEXT; + case AccessibilityTypes::ROLE_TOOLBAR : + return ROLE_SYSTEM_TOOLBAR; + case AccessibilityTypes::ROLE_CLIENT : + default: + // This is the default role for MSAA. + return ROLE_SYSTEM_CLIENT; + } +} + +long ViewAccessibility::MSAAState(AccessibilityTypes::State state) { + switch (state) { + case AccessibilityTypes::STATE_HASPOPUP : + return STATE_SYSTEM_HASPOPUP; + case AccessibilityTypes::STATE_READONLY : + return STATE_SYSTEM_READONLY; + default : + // No default state in MSAA. + return 0; + } +} + +// IAccessible functions not supported. + +HRESULT ViewAccessibility::accDoDefaultAction(VARIANT var_id) { + return E_NOTIMPL; +} + +STDMETHODIMP ViewAccessibility::get_accValue(VARIANT var_id, BSTR* value) { + if (value) + *value = NULL; + return E_NOTIMPL; +} + +STDMETHODIMP ViewAccessibility::get_accSelection(VARIANT* selected) { + if (selected) + selected->vt = VT_EMPTY; + return E_NOTIMPL; +} + +STDMETHODIMP ViewAccessibility::accSelect(LONG flagsSelect, VARIANT var_id) { + return E_NOTIMPL; +} + +STDMETHODIMP ViewAccessibility::get_accHelp(VARIANT var_id, BSTR* help) { + if (help) + *help = NULL; + return E_NOTIMPL; +} + +STDMETHODIMP ViewAccessibility::get_accHelpTopic(BSTR* help_file, + VARIANT var_id, + LONG* topic_id) { + if (help_file) { + *help_file = NULL; + } + if (topic_id) { + *topic_id = static_cast<LONG>(-1); + } + return E_NOTIMPL; +} + +STDMETHODIMP ViewAccessibility::put_accName(VARIANT var_id, BSTR put_name) { + // Deprecated. + return E_NOTIMPL; +} + +STDMETHODIMP ViewAccessibility::put_accValue(VARIANT var_id, BSTR put_val) { + // Deprecated. + return E_NOTIMPL; +} diff --git a/views/accessibility/view_accessibility.h b/views/accessibility/view_accessibility.h new file mode 100644 index 0000000..840a685 --- /dev/null +++ b/views/accessibility/view_accessibility.h @@ -0,0 +1,144 @@ +// Copyright (c) 2006-2008 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 VIEWS_ACCESSIBILITY_VIEW_ACCESSIBILITY_H_ +#define VIEWS_ACCESSIBILITY_VIEW_ACCESSIBILITY_H_ + +#include <atlbase.h> +#include <atlcom.h> + +#include <oleacc.h> + +#include "views/view.h" + +//////////////////////////////////////////////////////////////////////////////// +// +// ViewAccessibility +// +// Class implementing the MSAA IAccessible COM interface for a generic View, +// providing accessibility to be used by screen readers and other assistive +// technology (AT). +// +//////////////////////////////////////////////////////////////////////////////// +class ATL_NO_VTABLE ViewAccessibility + : public CComObjectRootEx<CComMultiThreadModel>, + public IDispatchImpl<IAccessible, &IID_IAccessible, &LIBID_Accessibility> { + public: + BEGIN_COM_MAP(ViewAccessibility) + COM_INTERFACE_ENTRY2(IDispatch, IAccessible) + COM_INTERFACE_ENTRY(IAccessible) + END_COM_MAP() + + ViewAccessibility() {} + ~ViewAccessibility() {} + + HRESULT Initialize(views::View* view); + + // Supported IAccessible methods. + + // Retrieves the child element or child object at a given point on the screen. + STDMETHODIMP accHitTest(LONG x_left, LONG y_top, VARIANT* child); + + // Retrieves the specified object's current screen location. + STDMETHODIMP accLocation(LONG* x_left, + LONG* y_top, + LONG* width, + LONG* height, + VARIANT var_id); + + // Traverses to another UI element and retrieves the object. + STDMETHODIMP accNavigate(LONG nav_dir, VARIANT start, VARIANT* end); + + // Retrieves an IDispatch interface pointer for the specified child. + STDMETHODIMP get_accChild(VARIANT var_child, IDispatch** disp_child); + + // Retrieves the number of accessible children. + STDMETHODIMP get_accChildCount(LONG* child_count); + + // Retrieves a string that describes the object's default action. + STDMETHODIMP get_accDefaultAction(VARIANT var_id, BSTR* default_action); + + // Retrieves the tooltip description. + STDMETHODIMP get_accDescription(VARIANT var_id, BSTR* desc); + + // Retrieves the object that has the keyboard focus. + STDMETHODIMP get_accFocus(VARIANT* focus_child); + + // Retrieves the specified object's shortcut. + STDMETHODIMP get_accKeyboardShortcut(VARIANT var_id, BSTR* access_key); + + // Retrieves the name of the specified object. + STDMETHODIMP get_accName(VARIANT var_id, BSTR* name); + + // Retrieves the IDispatch interface of the object's parent. + STDMETHODIMP get_accParent(IDispatch** disp_parent); + + // Retrieves information describing the role of the specified object. + STDMETHODIMP get_accRole(VARIANT var_id, VARIANT* role); + + // Retrieves the current state of the specified object. + STDMETHODIMP get_accState(VARIANT var_id, VARIANT* state); + + // Non-supported IAccessible methods. + + // Out-dated and can be safely said to be very rarely used. + STDMETHODIMP accDoDefaultAction(VARIANT var_id); + + // No value associated with views. + STDMETHODIMP get_accValue(VARIANT var_id, BSTR* value); + + // Selections not applicable to views. + STDMETHODIMP get_accSelection(VARIANT* selected); + STDMETHODIMP accSelect(LONG flags_sel, VARIANT var_id); + + // Help functions not supported. + STDMETHODIMP get_accHelp(VARIANT var_id, BSTR* help); + STDMETHODIMP get_accHelpTopic(BSTR* help_file, + VARIANT var_id, + LONG* topic_id); + + // Deprecated functions, not implemented here. + STDMETHODIMP put_accName(VARIANT var_id, BSTR put_name); + STDMETHODIMP put_accValue(VARIANT var_id, BSTR put_val); + + private: + // Checks to see if child_id is within the child bounds of view. Returns true + // if the child is within the bounds, false otherwise. + bool IsValidChild(int child_id, views::View* view) const; + + // Determines navigation direction for accNavigate, based on left, up and + // previous being mapped all to previous and right, down, next being mapped + // to next. Returns true if navigation direction is next, false otherwise. + bool IsNavDirNext(int nav_dir) const; + + // Determines if the navigation target is within the allowed bounds. Returns + // true if it is, false otherwise. + bool IsValidNav(int nav_dir, + int start_id, + int lower_bound, + int upper_bound) const; + + // Wrapper to retrieve the view's instance of IAccessible. + ViewAccessibilityWrapper* GetViewAccessibilityWrapper(views::View* v) const { + return v->GetViewAccessibilityWrapper(); + } + + // Helper function which sets applicable states of view. + void SetState(VARIANT* msaa_state, views::View* view); + + // Returns a conversion from the Role (as defined in + // chrome/common/accessibility_types.h) to an MSAA role. + long MSAARole(AccessibilityTypes::Role role); + + // Returns a conversion from the State (as defined in + // chrome/common/accessibility_types.h) to MSAA states set. + long MSAAState(AccessibilityTypes::State state); + + // Member View needed for view-specific calls. + views::View* view_; + + DISALLOW_EVIL_CONSTRUCTORS(ViewAccessibility); +}; + +#endif // VIEWS_ACCESSIBILITY_VIEW_ACCESSIBILITY_H_ diff --git a/views/accessibility/view_accessibility_wrapper.cc b/views/accessibility/view_accessibility_wrapper.cc new file mode 100644 index 0000000..ff336ae --- /dev/null +++ b/views/accessibility/view_accessibility_wrapper.cc @@ -0,0 +1,79 @@ +// Copyright (c) 2006-2008 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/accessibility/view_accessibility_wrapper.h" + +#include "views/accessibility/view_accessibility.h" + +//////////////////////////////////////////////////////////////////////////////// +// +// ViewAccessibilityWrapper - constructors, destructors +// +//////////////////////////////////////////////////////////////////////////////// + +ViewAccessibilityWrapper::ViewAccessibilityWrapper(views::View* view) + : accessibility_info_(NULL), + view_(view) { +} + +STDMETHODIMP ViewAccessibilityWrapper::CreateDefaultInstance(REFIID iid) { + if (IID_IUnknown == iid || IID_IDispatch == iid || IID_IAccessible == iid) { + // If there is no instance of ViewAccessibility created, create it + // now. Otherwise reuse previous instance. + if (!accessibility_info_) { + CComObject<ViewAccessibility>* instance = NULL; + + HRESULT hr = CComObject<ViewAccessibility>::CreateInstance(&instance); + + if (!SUCCEEDED(hr) || !instance) + return E_FAIL; + + CComPtr<IAccessible> accessibility_instance(instance); + + if (!SUCCEEDED(instance->Initialize(view_))) + return E_FAIL; + + // All is well, assign the temp instance to the class smart pointer. + accessibility_info_.Attach(accessibility_instance.Detach()); + } + return S_OK; + } + // Interface not supported. + return E_NOINTERFACE; +} + +STDMETHODIMP ViewAccessibilityWrapper::GetInstance(REFIID iid, + void** interface_ptr) { + if (IID_IUnknown == iid || IID_IDispatch == iid || IID_IAccessible == iid) { + // If there is no accessibility instance created, create a default now. + // Otherwise reuse previous instance. + if (!accessibility_info_) { + HRESULT hr = CreateDefaultInstance(iid); + + if (hr != S_OK) { + // Interface creation failed. + *interface_ptr = NULL; + return E_NOINTERFACE; + } + } + *interface_ptr = static_cast<IAccessible*>(accessibility_info_); + return S_OK; + } + // No supported interface found, return error. + *interface_ptr = NULL; + return E_NOINTERFACE; +} + +STDMETHODIMP ViewAccessibilityWrapper::SetInstance(IAccessible* interface_ptr) { + if (!interface_ptr) + return E_NOINTERFACE; + + accessibility_info_.Attach(interface_ptr); + + // Paranoia check, to make sure we do have a valid IAccessible pointer stored. + if (!accessibility_info_) + return E_FAIL; + + return S_OK; +} diff --git a/views/accessibility/view_accessibility_wrapper.h b/views/accessibility/view_accessibility_wrapper.h new file mode 100644 index 0000000..4390662 --- /dev/null +++ b/views/accessibility/view_accessibility_wrapper.h @@ -0,0 +1,55 @@ +// Copyright (c) 2006-2008 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 VIEWS_ACCESSIBILITY_VIEW_ACCESSIBILITY_WRAPPER_H_ +#define VIEWS_ACCESSIBILITY_VIEW_ACCESSIBILITY_WRAPPER_H_ + +#include <atlcomcli.h> +#include <oleacc.h> + +#include "base/basictypes.h" + +namespace views { +class View; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// ViewAccessibilityWrapper +// +// Wrapper class for returning a pointer to the appropriate (platform-specific) +// accessibility interface for a given View. Needed to keep platform-specific +// code out of the View class, when answering calls for child/parent IAccessible +// implementations, for instance. +// +//////////////////////////////////////////////////////////////////////////////// +class ViewAccessibilityWrapper { + public: + explicit ViewAccessibilityWrapper(views::View* view); + ~ViewAccessibilityWrapper() {} + + STDMETHODIMP CreateDefaultInstance(REFIID iid); + + // Returns a pointer to a specified interface on an object to which a client + // currently holds an interface pointer. If pointer exists, it is reused, + // otherwise a new pointer is created. Used by accessibility implementation to + // retrieve MSAA implementation for child or parent, when navigating MSAA + // hierarchy. + STDMETHODIMP GetInstance(REFIID iid, void** interface_ptr); + + // Sets the accessibility interface implementation of this wrapper to be + // anything the user specifies. + STDMETHODIMP SetInstance(IAccessible* interface_ptr); + + private: + // Instance of accessibility information and handling for a View. + CComPtr<IAccessible> accessibility_info_; + + // View needed to initialize IAccessible. + views::View* view_; + + DISALLOW_COPY_AND_ASSIGN(ViewAccessibilityWrapper); +}; + +#endif // VIEWS_ACCESSIBILITY_VIEW_ACCESSIBILITY_WRAPPER_H_ diff --git a/views/background.cc b/views/background.cc new file mode 100644 index 0000000..f2bd176 --- /dev/null +++ b/views/background.cc @@ -0,0 +1,113 @@ +// Copyright (c) 2006-2008 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/background.h" + +#include "app/gfx/chrome_canvas.h" +#include "base/logging.h" +#include "skia/ext/skia_utils_win.h" +#include "skia/include/SkPaint.h" +#include "views/painter.h" +#include "views/view.h" + +namespace views { + +// SolidBackground is a trivial Background implementation that fills the +// background in a solid color. +class SolidBackground : public Background { + public: + explicit SolidBackground(const SkColor& color) : + color_(color) { + SetNativeControlColor(color_); + } + + void Paint(ChromeCanvas* canvas, View* view) const { + // Fill the background. Note that we don't constrain to the bounds as + // canvas is already clipped for us. + canvas->drawColor(color_); + } + + private: + const SkColor color_; + + DISALLOW_EVIL_CONSTRUCTORS(SolidBackground); +}; + +class BackgroundPainter : public Background { + public: + BackgroundPainter(bool owns_painter, Painter* painter) + : owns_painter_(owns_painter), painter_(painter) { + DCHECK(painter); + } + + virtual ~BackgroundPainter() { + if (owns_painter_) + delete painter_; + } + + + void Paint(ChromeCanvas* canvas, View* view) const { + Painter::PaintPainterAt(0, 0, view->width(), view->height(), canvas, + painter_); + } + + private: + bool owns_painter_; + Painter* painter_; + + DISALLOW_EVIL_CONSTRUCTORS(BackgroundPainter); +}; + +Background::Background() +#if defined(OS_WIN) + : native_control_brush_(NULL) +#endif +{ +} + +Background::~Background() { +#if defined(OS_WIN) + DeleteObject(native_control_brush_); +#endif +} + +void Background::SetNativeControlColor(SkColor color) { +#if defined(OS_WIN) + DeleteObject(native_control_brush_); + native_control_brush_ = CreateSolidBrush(skia::SkColorToCOLORREF(color)); +#endif +} + +//static +Background* Background::CreateSolidBackground(const SkColor& color) { + return new SolidBackground(color); +} + +//static +Background* Background::CreateStandardPanelBackground() { + return CreateVerticalGradientBackground(SkColorSetRGB(246, 250, 255), + SkColorSetRGB(219, 235, 255)); +} + +//static +Background* Background::CreateVerticalGradientBackground( + const SkColor& color1, const SkColor& color2) { + Background *background = + CreateBackgroundPainter(true, Painter::CreateVerticalGradient(color1, + color2)); + background->SetNativeControlColor( // 50% blend of colors 1 & 2 + SkColorSetRGB((SkColorGetR(color1) + SkColorGetR(color2)) / 2, + (SkColorGetG(color1) + SkColorGetG(color2)) / 2, + (SkColorGetB(color1) + SkColorGetB(color2)) / 2)); + + return background; +} + +//static +Background* Background::CreateBackgroundPainter(bool owns_painter, + Painter* painter) { + return new BackgroundPainter(owns_painter, painter); +} + +} // namespace views diff --git a/views/background.h b/views/background.h new file mode 100644 index 0000000..acc3948 --- /dev/null +++ b/views/background.h @@ -0,0 +1,91 @@ +// Copyright (c) 2006-2008 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 VIEWS_BACKGROUND_H_ +#define VIEWS_BACKGROUND_H_ + +#include "build/build_config.h" + +#if defined(OS_WIN) +#include <windows.h> +#endif // defined(OS_WIN) + +#include "base/basictypes.h" +#include "skia/include/SkColor.h" + +class ChromeCanvas; + +namespace views { + +class Painter; +class View; + +///////////////////////////////////////////////////////////////////////////// +// +// Background class +// +// A background implements a way for views to paint a background. The +// background can be either solid or based on a gradient. Of course, +// Background can be subclassed to implement various effects. +// +// Any View can have a background. See View::SetBackground() and +// View::PaintBackground() +// +///////////////////////////////////////////////////////////////////////////// +class Background { + public: + Background(); + virtual ~Background(); + + // Creates a background that fills the canvas in the specified color. + static Background* CreateSolidBackground(const SkColor& color); + + // Creates a background that fills the canvas in the specified color. + static Background* CreateSolidBackground(int r, int g, int b) { + return CreateSolidBackground(SkColorSetRGB(r, g, b)); + } + + // Creates a background that fills the canvas in the specified color. + static Background* CreateSolidBackground(int r, int g, int b, int a) { + return CreateSolidBackground(SkColorSetARGB(a, r, g, b)); + } + + // Creates a background that contains a vertical gradient that varies + // from |color1| to |color2| + static Background* CreateVerticalGradientBackground(const SkColor& color1, + const SkColor& color2); + + // Creates Chrome's standard panel background + static Background* CreateStandardPanelBackground(); + + // Creates a Background from the specified Painter. If owns_painter is + // true, the Painter is deleted when the Border is deleted. + static Background* CreateBackgroundPainter(bool owns_painter, + Painter* painter); + + // Render the background for the provided view + virtual void Paint(ChromeCanvas* canvas, View* view) const = 0; + + // Set a solid, opaque color to be used when drawing backgrounds of native + // controls. Unfortunately alpha=0 is not an option. + void SetNativeControlColor(SkColor color); + +#if defined(OS_WIN) + // TODO(port): Make GetNativeControlBrush portable (currently uses HBRUSH). + + // Get the brush that was specified by SetNativeControlColor + HBRUSH GetNativeControlBrush() const { return native_control_brush_; }; +#endif // defined(OS_WIN) + + private: +#if defined(OS_WIN) + // TODO(port): Create portable replacement for HBRUSH. + HBRUSH native_control_brush_; +#endif // defined(OS_WIN) + DISALLOW_COPY_AND_ASSIGN(Background); +}; + +} // namespace views + +#endif // VIEWS_BACKGROUND_H_ diff --git a/views/border.cc b/views/border.cc new file mode 100644 index 0000000..9127bce --- /dev/null +++ b/views/border.cc @@ -0,0 +1,106 @@ +// Copyright (c) 2006-2008 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/border.h" + +#include "app/gfx/chrome_canvas.h" +#include "base/logging.h" + +namespace views { + +namespace { + +// A simple border with a fixed thickness and single color. +class SolidBorder : public Border { + public: + SolidBorder(int thickness, SkColor color); + + virtual void Paint(const View& view, ChromeCanvas* canvas) const; + virtual void GetInsets(gfx::Insets* insets) const; + + private: + int thickness_; + SkColor color_; + gfx::Insets insets_; + + DISALLOW_EVIL_CONSTRUCTORS(SolidBorder); +}; + +SolidBorder::SolidBorder(int thickness, SkColor color) + : thickness_(thickness), + color_(color), + insets_(thickness, thickness, thickness, thickness) { +} + +void SolidBorder::Paint(const View& view, ChromeCanvas* canvas) const { + gfx::Rect clip_rect; + if (!canvas->GetClipRect(&clip_rect)) + return; // Empty clip rectangle, nothing to paint. + + // Top border. + gfx::Rect border_bounds(0, 0, view.width(), insets_.top()); + if (clip_rect.Intersects(border_bounds)) + canvas->FillRectInt(color_, 0, 0, view.width(), insets_.top()); + // Left border. + border_bounds.SetRect(0, 0, insets_.left(), view.height()); + if (clip_rect.Intersects(border_bounds)) + canvas->FillRectInt(color_, 0, 0, insets_.left(), view.height()); + // Bottom border. + border_bounds.SetRect(0, view.height() - insets_.bottom(), + view.width(), insets_.bottom()); + if (clip_rect.Intersects(border_bounds)) + canvas->FillRectInt(color_, 0, view.height() - insets_.bottom(), + view.width(), insets_.bottom()); + // Right border. + border_bounds.SetRect(view.width() - insets_.right(), 0, + insets_.right(), view.height()); + if (clip_rect.Intersects(border_bounds)) + canvas->FillRectInt(color_, view.width() - insets_.right(), 0, + insets_.right(), view.height()); +} + +void SolidBorder::GetInsets(gfx::Insets* insets) const { + DCHECK(insets); + insets->Set(insets_.top(), insets_.left(), insets_.bottom(), insets_.right()); +} + +class EmptyBorder : public Border { + public: + EmptyBorder(int top, int left, int bottom, int right) + : top_(top), left_(left), bottom_(bottom), right_(right) {} + + virtual void Paint(const View& view, ChromeCanvas* canvas) const {} + + virtual void GetInsets(gfx::Insets* insets) const { + DCHECK(insets); + insets->Set(top_, left_, bottom_, right_); + } + + private: + int top_; + int left_; + int bottom_; + int right_; + + DISALLOW_EVIL_CONSTRUCTORS(EmptyBorder); +}; +} + +Border::Border() { +} + +Border::~Border() { +} + +// static +Border* Border::CreateSolidBorder(int thickness, SkColor color) { + return new SolidBorder(thickness, color); +} + +// static +Border* Border::CreateEmptyBorder(int top, int left, int bottom, int right) { + return new EmptyBorder(top, left, bottom, right); +} + +} // namespace views diff --git a/views/border.h b/views/border.h new file mode 100644 index 0000000..7e7e2bd --- /dev/null +++ b/views/border.h @@ -0,0 +1,59 @@ +// Copyright (c) 2006-2008 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 VIEWS_BORDER_H_ +#define VIEWS_BORDER_H_ + +#include "app/gfx/insets.h" +#include "skia/include/SkColor.h" +#include "views/view.h" + +class ChromeCanvas; + +namespace views { + +class View; + +//////////////////////////////////////////////////////////////////////////////// +// +// Border class. +// +// The border class is used to display a border around a view. +// To set a border on a view, just call SetBorder on the view, for example: +// view->SetBorder(Border::CreateSolidBorder(1, SkColorSetRGB(25, 25, 112)); +// Once set on a view, the border is owned by the view. +// +// IMPORTANT NOTE: not all views support borders at this point. In order to +// support the border, views should make sure to use bounds excluding the +// border (by calling View::GetLocalBoundsExcludingBorder) when doing layout and +// painting. +// +//////////////////////////////////////////////////////////////////////////////// + +class Border { + public: + Border(); + virtual ~Border(); + + // Creates a border that is a simple line of the specified thickness and + // color. + static Border* CreateSolidBorder(int thickness, SkColor color); + + // Creates a border for reserving space. The returned border does not + // paint anything. + static Border* CreateEmptyBorder(int top, int left, int bottom, int right); + + // Renders the border for the specified view. + virtual void Paint(const View& view, ChromeCanvas* canvas) const = 0; + + // Sets the specified insets to the the border insets. + virtual void GetInsets(gfx::Insets* insets) const = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(Border); +}; + +} // namespace views + +#endif // VIEWS_BORDER_H_ diff --git a/views/controls/button/button.cc b/views/controls/button/button.cc new file mode 100644 index 0000000..cbb42bf --- /dev/null +++ b/views/controls/button/button.cc @@ -0,0 +1,79 @@ +// Copyright (c) 2006-2008 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/controls/button/button.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// Button, public: + +Button::~Button() { +} + +void Button::SetTooltipText(const std::wstring& tooltip_text) { + tooltip_text_ = tooltip_text; + TooltipTextChanged(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Button, View overrides: + +bool Button::GetTooltipText(int x, int y, std::wstring* tooltip) { + if (!tooltip_text_.empty()) { + *tooltip = tooltip_text_; + return true; + } + return false; +} + +bool Button::GetAccessibleKeyboardShortcut(std::wstring* shortcut) { + if (!accessible_shortcut_.empty()) { + *shortcut = accessible_shortcut_; + return true; + } + return false; +} + +bool Button::GetAccessibleName(std::wstring* name) { + if (!accessible_name_.empty()) { + *name = accessible_name_; + return true; + } + return false; +} + +bool Button::GetAccessibleRole(AccessibilityTypes::Role* role) { + *role = AccessibilityTypes::ROLE_PUSHBUTTON; + return true; +} + +void Button::SetAccessibleKeyboardShortcut(const std::wstring& shortcut) { + accessible_shortcut_.assign(shortcut); +} + +void Button::SetAccessibleName(const std::wstring& name) { + accessible_name_.assign(name); +} + +//////////////////////////////////////////////////////////////////////////////// +// Button, protected: + +Button::Button(ButtonListener* listener) + : listener_(listener), + tag_(-1), + mouse_event_flags_(0) { +} + +void Button::NotifyClick(int mouse_event_flags) { + mouse_event_flags_ = mouse_event_flags; + // We can be called when there is no listener, in cases like double clicks on + // menu buttons etc. + if (listener_) + listener_->ButtonPressed(this); + // NOTE: don't attempt to reset mouse_event_flags_ as the listener may have + // deleted us. +} + +} // namespace views diff --git a/views/controls/button/button.h b/views/controls/button/button.h new file mode 100644 index 0000000..514758f --- /dev/null +++ b/views/controls/button/button.h @@ -0,0 +1,74 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_BUTTON_BUTTON_H_ +#define VIEWS_CONTROLS_BUTTON_BUTTON_H_ + +#include "views/view.h" + +namespace views { + +class Button; + +// An interface implemented by an object to let it know that a button was +// pressed. +class ButtonListener { + public: + virtual void ButtonPressed(Button* sender) = 0; +}; + +// A View representing a button. Depending on the specific type, the button +// could be implemented by a native control or custom rendered. +class Button : public View { + public: + virtual ~Button(); + + void SetTooltipText(const std::wstring& tooltip_text); + + int tag() const { return tag_; } + void set_tag(int tag) { tag_ = tag; } + + int mouse_event_flags() const { return mouse_event_flags_; } + + // Overridden from View: + virtual bool GetTooltipText(int x, int y, std::wstring* tooltip); + virtual bool GetAccessibleKeyboardShortcut(std::wstring* shortcut); + virtual bool GetAccessibleName(std::wstring* name); + virtual bool GetAccessibleRole(AccessibilityTypes::Role* role); + virtual void SetAccessibleKeyboardShortcut(const std::wstring& shortcut); + virtual void SetAccessibleName(const std::wstring& name); + + protected: + // Construct the Button with a Listener. The listener can be NULL. This can be + // true of buttons that don't have a listener - e.g. menubuttons where there's + // no default action and checkboxes. + explicit Button(ButtonListener* listener); + + // Cause the button to notify the listener that a click occurred. + virtual void NotifyClick(int mouse_event_flags); + + // The button's listener. Notified when clicked. + ButtonListener* listener_; + + private: + // The text shown in a tooltip. + std::wstring tooltip_text_; + + // Accessibility data. + std::wstring accessible_shortcut_; + std::wstring accessible_name_; + + // The id tag associated with this button. Used to disambiguate buttons in + // the ButtonListener implementation. + int tag_; + + // Event flags present when the button was clicked. + int mouse_event_flags_; + + DISALLOW_COPY_AND_ASSIGN(Button); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_BUTTON_BUTTON_H_ diff --git a/views/controls/button/button_dropdown.cc b/views/controls/button/button_dropdown.cc new file mode 100644 index 0000000..270894b --- /dev/null +++ b/views/controls/button/button_dropdown.cc @@ -0,0 +1,192 @@ +// Copyright (c) 2006-2008 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/controls/button/button_dropdown.h" + +#include "app/l10n_util.h" +#include "base/message_loop.h" +#include "grit/generated_resources.h" +#include "views/controls/menu/view_menu_delegate.h" +#include "views/widget/widget.h" + +namespace views { + +// How long to wait before showing the menu +static const int kMenuTimerDelay = 500; + +//////////////////////////////////////////////////////////////////////////////// +// +// ButtonDropDown - constructors, destructors, initialization, cleanup +// +//////////////////////////////////////////////////////////////////////////////// + +ButtonDropDown::ButtonDropDown(ButtonListener* listener, + Menu::Delegate* menu_delegate) + : ImageButton(listener), + menu_delegate_(menu_delegate), + y_position_on_lbuttondown_(0), + show_menu_factory_(this) { +} + +ButtonDropDown::~ButtonDropDown() { +} + +//////////////////////////////////////////////////////////////////////////////// +// +// ButtonDropDown - Events +// +//////////////////////////////////////////////////////////////////////////////// + +bool ButtonDropDown::OnMousePressed(const MouseEvent& e) { + if (IsEnabled() && e.IsLeftMouseButton() && HitTest(e.location())) { + // Store the y pos of the mouse coordinates so we can use them later to + // determine if the user dragged the mouse down (which should pop up the + // drag down menu immediately, instead of waiting for the timer) + y_position_on_lbuttondown_ = e.y(); + + // Schedule a task that will show the menu. + MessageLoop::current()->PostDelayedTask(FROM_HERE, + show_menu_factory_.NewRunnableMethod(&ButtonDropDown::ShowDropDownMenu, + GetWidget()->GetNativeView()), + kMenuTimerDelay); + } + + return ImageButton::OnMousePressed(e); +} + +void ButtonDropDown::OnMouseReleased(const MouseEvent& e, bool canceled) { + ImageButton::OnMouseReleased(e, canceled); + + if (canceled) + return; + + if (e.IsLeftMouseButton()) + show_menu_factory_.RevokeAll(); + + if (IsEnabled() && e.IsRightMouseButton() && HitTest(e.location())) { + show_menu_factory_.RevokeAll(); + // Make the button look depressed while the menu is open. + // NOTE: SetState() schedules a paint, but it won't occur until after the + // context menu message loop has terminated, so we PaintNow() to + // update the appearance synchronously. + SetState(BS_PUSHED); + PaintNow(); + ShowDropDownMenu(GetWidget()->GetNativeView()); + } +} + +bool ButtonDropDown::OnMouseDragged(const MouseEvent& e) { + bool result = ImageButton::OnMouseDragged(e); + + if (!show_menu_factory_.empty()) { + // SM_CYDRAG is a pixel value for minimum dragging distance before operation + // counts as a drag, and not just as a click and accidental move of a mouse. + // See http://msdn2.microsoft.com/en-us/library/ms724385.aspx for details. + int dragging_threshold = GetSystemMetrics(SM_CYDRAG); + + // If the mouse is dragged to a y position lower than where it was when + // clicked then we should not wait for the menu to appear but show + // it immediately. + if (e.y() > y_position_on_lbuttondown_ + dragging_threshold) { + show_menu_factory_.RevokeAll(); + ShowDropDownMenu(GetWidget()->GetNativeView()); + } + } + + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// ButtonDropDown - Menu functions +// +//////////////////////////////////////////////////////////////////////////////// + +void ButtonDropDown::ShowContextMenu(int x, int y, bool is_mouse_gesture) { + show_menu_factory_.RevokeAll(); + // Make the button look depressed while the menu is open. + // NOTE: SetState() schedules a paint, but it won't occur until after the + // context menu message loop has terminated, so we PaintNow() to + // update the appearance synchronously. + SetState(BS_PUSHED); + PaintNow(); + ShowDropDownMenu(GetWidget()->GetNativeView()); + SetState(BS_HOT); +} + +void ButtonDropDown::ShowDropDownMenu(HWND window) { + if (menu_delegate_) { + gfx::Rect lb = GetLocalBounds(true); + + // Both the menu position and the menu anchor type change if the UI layout + // is right-to-left. + gfx::Point menu_position(lb.origin()); + menu_position.Offset(0, lb.height() - 1); + if (UILayoutIsRightToLeft()) + menu_position.Offset(lb.width() - 1, 0); + + Menu::AnchorPoint anchor = Menu::TOPLEFT; + if (UILayoutIsRightToLeft()) + anchor = Menu::TOPRIGHT; + + View::ConvertPointToScreen(this, &menu_position); + + int left_bound = GetSystemMetrics(SM_XVIRTUALSCREEN); + if (menu_position.x() < left_bound) + menu_position.set_x(left_bound); + + Menu menu(menu_delegate_, anchor, window); + + // ID's for AppendMenu is 1-based because RunMenu will ignore the user + // selection if id=0 is selected (0 = NO-OP) so we add 1 here and subtract 1 + // in the handlers above to get the actual index + int item_count = menu_delegate_->GetItemCount(); + for (int i = 0; i < item_count; i++) { + if (menu_delegate_->IsItemSeparator(i + 1)) { + menu.AppendSeparator(); + } else { + if (menu_delegate_->HasIcon(i + 1)) + menu.AppendMenuItemWithIcon(i + 1, L"", SkBitmap()); + else + menu.AppendMenuItem(i+1, L"", Menu::NORMAL); + } + } + + menu.RunMenuAt(menu_position.x(), menu_position.y()); + + // Need to explicitly clear mouse handler so that events get sent + // properly after the menu finishes running. If we don't do this, then + // the first click to other parts of the UI is eaten. + SetMouseHandler(NULL); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// +// ButtonDropDown - Accessibility +// +//////////////////////////////////////////////////////////////////////////////// + +bool ButtonDropDown::GetAccessibleDefaultAction(std::wstring* action) { + DCHECK(action); + + action->assign(l10n_util::GetString(IDS_ACCACTION_PRESS)); + return true; +} + +bool ButtonDropDown::GetAccessibleRole(AccessibilityTypes::Role* role) { + DCHECK(role); + + *role = AccessibilityTypes::ROLE_BUTTONDROPDOWN; + return true; +} + +bool ButtonDropDown::GetAccessibleState(AccessibilityTypes::State* state) { + DCHECK(state); + + *state = AccessibilityTypes::STATE_HASPOPUP; + return true; +} + +} // namespace views diff --git a/views/controls/button/button_dropdown.h b/views/controls/button/button_dropdown.h new file mode 100644 index 0000000..feb5b5c --- /dev/null +++ b/views/controls/button/button_dropdown.h @@ -0,0 +1,62 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_BUTTON_BUTTON_DROPDOWN_H_ +#define VIEWS_CONTROLS_BUTTON_BUTTON_DROPDOWN_H_ + +#include "base/task.h" +#include "views/controls/button/image_button.h" +#include "views/controls/menu/menu.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// +// ButtonDropDown +// +// A button class that when pressed (and held) or pressed (and drag down) will +// display a menu +// +//////////////////////////////////////////////////////////////////////////////// +class ButtonDropDown : public ImageButton { + public: + ButtonDropDown(ButtonListener* listener, Menu::Delegate* menu_delegate); + virtual ~ButtonDropDown(); + + // Accessibility accessors, overridden from View. + virtual bool GetAccessibleDefaultAction(std::wstring* action); + virtual bool GetAccessibleRole(AccessibilityTypes::Role* role); + virtual bool GetAccessibleState(AccessibilityTypes::State* state); + + private: + // Overridden from Button + virtual bool OnMousePressed(const MouseEvent& e); + virtual void OnMouseReleased(const MouseEvent& e, bool canceled); + virtual bool OnMouseDragged(const MouseEvent& e); + + // Overridden from View. Used to display the right-click menu, as triggered + // by the keyboard, for instance. Using the member function ShowDropDownMenu + // for the actual display. + virtual void ShowContextMenu(int x, + int y, + bool is_mouse_gesture); + + // Internal function to show the dropdown menu + void ShowDropDownMenu(HWND window); + + // Specifies who to delegate populating the menu + Menu::Delegate* menu_delegate_; + + // Y position of mouse when left mouse button is pressed + int y_position_on_lbuttondown_; + + // A factory for tasks that show the dropdown context menu for the button. + ScopedRunnableMethodFactory<ButtonDropDown> show_menu_factory_; + + DISALLOW_COPY_AND_ASSIGN(ButtonDropDown); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_BUTTON_BUTTON_DROPDOWN_H_ diff --git a/views/controls/button/checkbox.cc b/views/controls/button/checkbox.cc new file mode 100644 index 0000000..8783bcf --- /dev/null +++ b/views/controls/button/checkbox.cc @@ -0,0 +1,172 @@ +// Copyright (c) 2009 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/controls/button/checkbox.h" + +#include "app/gfx/chrome_canvas.h" +#include "views/controls/label.h" + +namespace views { + +// static +const char Checkbox::kViewClassName[] = "views/Checkbox"; + +static const int kCheckboxLabelSpacing = 4; +static const int kLabelFocusPaddingHorizontal = 2; +static const int kLabelFocusPaddingVertical = 1; + +//////////////////////////////////////////////////////////////////////////////// +// Checkbox, public: + +Checkbox::Checkbox() : NativeButton(NULL), checked_(false) { + Init(std::wstring()); +} + +Checkbox::Checkbox(const std::wstring& label) + : NativeButton(NULL, label), + checked_(false) { + Init(label); +} + +Checkbox::~Checkbox() { +} + +void Checkbox::SetMultiLine(bool multiline) { + label_->SetMultiLine(multiline); +} + +void Checkbox::SetChecked(bool checked) { + if (checked_ == checked) + return; + checked_ = checked; + if (native_wrapper_) + native_wrapper_->UpdateChecked(); +} + +// static +int Checkbox::GetTextIndent() { + return NativeButtonWrapper::GetFixedWidth() + kCheckboxLabelSpacing; +} + +//////////////////////////////////////////////////////////////////////////////// +// Checkbox, View overrides: + +gfx::Size Checkbox::GetPreferredSize() { + gfx::Size prefsize = native_wrapper_->GetView()->GetPreferredSize(); + prefsize.set_width( + prefsize.width() + kCheckboxLabelSpacing + + kLabelFocusPaddingHorizontal * 2); + gfx::Size label_prefsize = label_->GetPreferredSize(); + prefsize.set_width(prefsize.width() + label_prefsize.width()); + prefsize.set_height( + std::max(prefsize.height(), + label_prefsize.height() + kLabelFocusPaddingVertical * 2)); + return prefsize; +} + +void Checkbox::Layout() { + if (!native_wrapper_) + return; + + gfx::Size checkmark_prefsize = native_wrapper_->GetView()->GetPreferredSize(); + int label_x = checkmark_prefsize.width() + kCheckboxLabelSpacing + + kLabelFocusPaddingHorizontal; + label_->SetBounds( + label_x, 0, std::max(0, width() - label_x - kLabelFocusPaddingHorizontal), + height()); + int first_line_height = label_->GetFont().height(); + native_wrapper_->GetView()->SetBounds( + 0, ((first_line_height - checkmark_prefsize.height()) / 2), + checkmark_prefsize.width(), checkmark_prefsize.height()); + native_wrapper_->GetView()->Layout(); +} + +void Checkbox::PaintFocusBorder(ChromeCanvas* canvas) { + // Our focus border is rendered by the label, so we don't do anything here. +} + +View* Checkbox::GetViewForPoint(const gfx::Point& point) { + return GetViewForPoint(point, false); +} + +View* Checkbox::GetViewForPoint(const gfx::Point& point, + bool can_create_floating) { + return GetLocalBounds(true).Contains(point) ? this : NULL; +} + +void Checkbox::OnMouseEntered(const MouseEvent& e) { + native_wrapper_->SetPushed(HitTestLabel(e)); +} + +void Checkbox::OnMouseMoved(const MouseEvent& e) { + native_wrapper_->SetPushed(HitTestLabel(e)); +} + +void Checkbox::OnMouseExited(const MouseEvent& e) { + native_wrapper_->SetPushed(false); +} + +bool Checkbox::OnMousePressed(const MouseEvent& e) { + native_wrapper_->SetPushed(HitTestLabel(e)); + return true; +} + +void Checkbox::OnMouseReleased(const MouseEvent& e, bool canceled) { + native_wrapper_->SetPushed(false); + if (!canceled && HitTestLabel(e)) { + SetChecked(!checked()); + ButtonPressed(); + } +} + +bool Checkbox::OnMouseDragged(const MouseEvent& e) { + return false; +} + +void Checkbox::WillGainFocus() { + label_->set_paint_as_focused(true); +} + +void Checkbox::WillLoseFocus() { + label_->set_paint_as_focused(false); +} + +std::string Checkbox::GetClassName() const { + return kViewClassName; +} + +//////////////////////////////////////////////////////////////////////////////// +// Checkbox, NativeButton overrides: + +void Checkbox::CreateWrapper() { + native_wrapper_ = NativeButtonWrapper::CreateCheckboxWrapper(this); + native_wrapper_->UpdateLabel(); + native_wrapper_->UpdateChecked(); +} + +void Checkbox::InitBorder() { + // No border, so we do nothing. +} + +//////////////////////////////////////////////////////////////////////////////// +// Checkbox, protected: + +bool Checkbox::HitTestLabel(const MouseEvent& e) { + gfx::Point tmp(e.location()); + ConvertPointToView(this, label_, &tmp); + return label_->HitTest(tmp); +} + +//////////////////////////////////////////////////////////////////////////////// +// Checkbox, private: + +void Checkbox::Init(const std::wstring& label_text) { + set_minimum_size(gfx::Size(0, 0)); + label_ = new Label(label_text); + label_->set_has_focus_border(true); + label_->SetHorizontalAlignment(Label::ALIGN_LEFT); + AddChildView(label_); +} + +} // namespace views diff --git a/views/controls/button/checkbox.h b/views/controls/button/checkbox.h new file mode 100644 index 0000000..db6706b --- /dev/null +++ b/views/controls/button/checkbox.h @@ -0,0 +1,84 @@ +// Copyright (c) 2009 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 VIEWS_CONTROLS_BUTTON_CHECKBOX_H_ +#define VIEWS_CONTROLS_BUTTON_CHECKBOX_H_ + +#include "views/controls/button/native_button.h" + +namespace views { + +class Label; + +// A NativeButton subclass representing a checkbox. +class Checkbox : public NativeButton { + public: + // The button's class name. + static const char kViewClassName[]; + + Checkbox(); + Checkbox(const std::wstring& label); + virtual ~Checkbox(); + + // Sets a listener for this checkbox. Checkboxes aren't required to have them + // since their state can be read independently of them being toggled. + void set_listener(ButtonListener* listener) { listener_ = listener; } + + // Sets whether or not the checkbox label should wrap multiple lines of text. + // If true, long lines are wrapped, and this is reflected in the preferred + // size returned by GetPreferredSize. If false, text that will not fit within + // the available bounds for the label will be cropped. + void SetMultiLine(bool multiline); + + // Sets/Gets whether or not the checkbox is checked. + virtual void SetChecked(bool checked); + bool checked() const { return checked_; } + + // Returns the indentation of the text from the left edge of the view. + static int GetTextIndent(); + + // Overridden from View: + virtual gfx::Size GetPreferredSize(); + virtual void Layout(); + virtual void PaintFocusBorder(ChromeCanvas* canvas); + virtual View* GetViewForPoint(const gfx::Point& point); + virtual View* GetViewForPoint(const gfx::Point& point, + bool can_create_floating); + virtual void OnMouseEntered(const MouseEvent& e); + virtual void OnMouseMoved(const MouseEvent& e); + virtual void OnMouseExited(const MouseEvent& e); + virtual bool OnMousePressed(const MouseEvent& e); + virtual void OnMouseReleased(const MouseEvent& e, bool canceled); + virtual bool OnMouseDragged(const MouseEvent& e); + virtual void WillGainFocus(); + virtual void WillLoseFocus(); + + protected: + virtual std::string GetClassName() const; + + // Overridden from NativeButton2: + virtual void CreateWrapper(); + virtual void InitBorder(); + + // Returns true if the event (in Checkbox coordinates) is within the bounds of + // the label. + bool HitTestLabel(const MouseEvent& e); + + private: + // Called from the constructor to create and configure the checkbox label. + void Init(const std::wstring& label_text); + + // The checkbox's label. We don't use the OS version because of transparency + // and sizing issues. + Label* label_; + + // True if the checkbox is checked. + bool checked_; + + DISALLOW_COPY_AND_ASSIGN(Checkbox); +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_BUTTON_CHECKBOX_H_ diff --git a/views/controls/button/custom_button.cc b/views/controls/button/custom_button.cc new file mode 100644 index 0000000..2b5f43d --- /dev/null +++ b/views/controls/button/custom_button.cc @@ -0,0 +1,236 @@ +// Copyright (c) 2006-2008 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/controls/button/custom_button.h" + +#include "app/throb_animation.h" +#include "base/keyboard_codes.h" + +namespace views { + +// How long the hover animation takes if uninterrupted. +static const int kHoverFadeDurationMs = 150; + +//////////////////////////////////////////////////////////////////////////////// +// CustomButton, public: + +CustomButton::~CustomButton() { +} + +void CustomButton::SetState(ButtonState state) { + if (state != state_) { + if (animate_on_state_change_ || !hover_animation_->IsAnimating()) { + animate_on_state_change_ = true; + if (state_ == BS_NORMAL && state == BS_HOT) { + // Button is hovered from a normal state, start hover animation. + hover_animation_->Show(); + } else if (state_ == BS_HOT && state == BS_NORMAL) { + // Button is returning to a normal state from hover, start hover + // fade animation. + hover_animation_->Hide(); + } else { + hover_animation_->Stop(); + } + } + + state_ = state; + SchedulePaint(); + } +} + +void CustomButton::StartThrobbing(int cycles_til_stop) { + animate_on_state_change_ = false; + hover_animation_->StartThrobbing(cycles_til_stop); +} + +void CustomButton::SetAnimationDuration(int duration) { + hover_animation_->SetSlideDuration(duration); +} + +//////////////////////////////////////////////////////////////////////////////// +// CustomButton, View overrides: + +void CustomButton::SetEnabled(bool enabled) { + if (enabled && state_ == BS_DISABLED) { + SetState(BS_NORMAL); + } else if (!enabled && state_ != BS_DISABLED) { + SetState(BS_DISABLED); + } +} + +bool CustomButton::IsEnabled() const { + return state_ != BS_DISABLED; +} + +bool CustomButton::IsFocusable() const { + return (state_ != BS_DISABLED) && View::IsFocusable(); +} + +//////////////////////////////////////////////////////////////////////////////// +// CustomButton, protected: + +CustomButton::CustomButton(ButtonListener* listener) + : Button(listener), + state_(BS_NORMAL), + animate_on_state_change_(true), + triggerable_event_flags_(MouseEvent::EF_LEFT_BUTTON_DOWN) { + hover_animation_.reset(new ThrobAnimation(this)); + hover_animation_->SetSlideDuration(kHoverFadeDurationMs); +} + +bool CustomButton::IsTriggerableEvent(const MouseEvent& e) { + return (triggerable_event_flags_ & e.GetFlags()) != 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// CustomButton, View overrides (protected): + +bool CustomButton::AcceleratorPressed(const Accelerator& accelerator) { + if (enabled_) { + SetState(BS_NORMAL); + NotifyClick(0); + return true; + } + return false; +} + +bool CustomButton::OnMousePressed(const MouseEvent& e) { + if (state_ != BS_DISABLED) { + if (IsTriggerableEvent(e) && HitTest(e.location())) + SetState(BS_PUSHED); + RequestFocus(); + } + return true; +} + +bool CustomButton::OnMouseDragged(const MouseEvent& e) { + if (state_ != BS_DISABLED) { + if (!HitTest(e.location())) + SetState(BS_NORMAL); + else if (IsTriggerableEvent(e)) + SetState(BS_PUSHED); + else + SetState(BS_HOT); + } + return true; +} + +void CustomButton::OnMouseReleased(const MouseEvent& e, bool canceled) { + if (InDrag()) { + // Starting a drag results in a MouseReleased, we need to ignore it. + return; + } + + if (state_ != BS_DISABLED) { + if (canceled || !HitTest(e.location())) { + SetState(BS_NORMAL); + } else { + SetState(BS_HOT); + if (IsTriggerableEvent(e)) { + NotifyClick(e.GetFlags()); + // We may be deleted at this point (by the listener's notification + // handler) so no more doing anything, just return. + return; + } + } + } +} + +void CustomButton::OnMouseEntered(const MouseEvent& e) { + if (state_ != BS_DISABLED) + SetState(BS_HOT); +} + +void CustomButton::OnMouseMoved(const MouseEvent& e) { + if (state_ != BS_DISABLED) { + if (HitTest(e.location())) { + SetState(BS_HOT); + } else { + SetState(BS_NORMAL); + } + } +} + +void CustomButton::OnMouseExited(const MouseEvent& e) { + // Starting a drag results in a MouseExited, we need to ignore it. + if (state_ != BS_DISABLED && !InDrag()) + SetState(BS_NORMAL); +} + +bool CustomButton::OnKeyPressed(const KeyEvent& e) { + if (state_ != BS_DISABLED) { + // Space sets button state to pushed. Enter clicks the button. This matches + // the Windows native behavior of buttons, where Space clicks the button + // on KeyRelease and Enter clicks the button on KeyPressed. + if (e.GetCharacter() == base::VKEY_SPACE) { + SetState(BS_PUSHED); + return true; + } else if (e.GetCharacter() == base::VKEY_RETURN) { + SetState(BS_NORMAL); + NotifyClick(0); + return true; + } + } + return false; +} + +bool CustomButton::OnKeyReleased(const KeyEvent& e) { + if (state_ != BS_DISABLED) { + if (e.GetCharacter() == base::VKEY_SPACE) { + SetState(BS_NORMAL); + NotifyClick(0); + return true; + } + } + return false; +} + +void CustomButton::OnDragDone() { + SetState(BS_NORMAL); +} + +void CustomButton::ShowContextMenu(int x, int y, bool is_mouse_gesture) { + if (GetContextMenuController()) { + // We're about to show the context menu. Showing the context menu likely + // means we won't get a mouse exited and reset state. Reset it now to be + // sure. + if (state_ != BS_DISABLED) + SetState(BS_NORMAL); + View::ShowContextMenu(x, y, is_mouse_gesture); + } +} + +void CustomButton::ViewHierarchyChanged(bool is_add, View *parent, + View *child) { + if (!is_add && state_ != BS_DISABLED) + SetState(BS_NORMAL); +} + +//////////////////////////////////////////////////////////////////////////////// +// CustomButton, AnimationDelegate implementation: + +void CustomButton::AnimationProgressed(const Animation* animation) { + SchedulePaint(); +} + +//////////////////////////////////////////////////////////////////////////////// +// CustomButton, private: + +void CustomButton::SetHighlighted(bool highlighted) { + if (highlighted && state_ != BS_DISABLED) { + SetState(BS_HOT); + } else if (!highlighted && state_ != BS_DISABLED) { + SetState(BS_NORMAL); + } +} + +bool CustomButton::IsHighlighted() const { + return state_ == BS_HOT; +} + +bool CustomButton::IsPushed() const { + return state_ == BS_PUSHED; +} + +} // namespace views diff --git a/views/controls/button/custom_button.h b/views/controls/button/custom_button.h new file mode 100644 index 0000000..32ff76b --- /dev/null +++ b/views/controls/button/custom_button.h @@ -0,0 +1,105 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_BUTTON_CUSTOM_BUTTON_H_ +#define VIEWS_CONTROLS_BUTTON_CUSTOM_BUTTON_H_ + +#include "app/animation.h" +#include "views/controls/button/button.h" + +class ThrobAnimation; + +namespace views { + +// A button with custom rendering. The common base class of IconButton and +// TextButton. +class CustomButton : public Button, + public AnimationDelegate { + public: + virtual ~CustomButton(); + + // Possible states + enum ButtonState { + BS_NORMAL = 0, + BS_HOT, + BS_PUSHED, + BS_DISABLED, + BS_COUNT + }; + + // Get/sets the current display state of the button. + ButtonState state() const { return state_; } + void SetState(ButtonState state); + + // Starts throbbing. See HoverAnimation for a description of cycles_til_stop. + void StartThrobbing(int cycles_til_stop); + + // Set how long the hover animation will last for. + void SetAnimationDuration(int duration); + + // Overridden from View: + virtual void SetEnabled(bool enabled); + virtual bool IsEnabled() const; + virtual bool IsFocusable() const; + + void set_triggerable_event_flags(int triggerable_event_flags) { + triggerable_event_flags_ = triggerable_event_flags; + } + + int triggerable_event_flags() const { + return triggerable_event_flags_; + } + + protected: + // Construct the Button with a Listener. See comment for Button's ctor. + explicit CustomButton(ButtonListener* listener); + + // Returns true if the event is one that can trigger notifying the listener. + // This implementation returns true if the left mouse button is down. + virtual bool IsTriggerableEvent(const MouseEvent& e); + + // Overridden from View: + virtual bool AcceleratorPressed(const Accelerator& accelerator); + virtual bool OnMousePressed(const MouseEvent& e); + virtual bool OnMouseDragged(const MouseEvent& e); + virtual void OnMouseReleased(const MouseEvent& e, bool canceled); + virtual void OnMouseEntered(const MouseEvent& e); + virtual void OnMouseMoved(const MouseEvent& e); + virtual void OnMouseExited(const MouseEvent& e); + virtual bool OnKeyPressed(const KeyEvent& e); + virtual bool OnKeyReleased(const KeyEvent& e); + virtual void OnDragDone(); + virtual void ShowContextMenu(int x, int y, bool is_mouse_gesture); + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + + // Overridden from AnimationDelegate: + virtual void AnimationProgressed(const Animation* animation); + + // The button state (defined in implementation) + ButtonState state_; + + // Hover animation. + scoped_ptr<ThrobAnimation> hover_animation_; + + private: + // Set / test whether the button is highlighted (in the hover state). + void SetHighlighted(bool highlighted); + bool IsHighlighted() const; + + // Returns whether the button is pushed. + bool IsPushed() const; + + // Should we animate when the state changes? Defaults to true, but false while + // throbbing. + bool animate_on_state_change_; + + // Mouse event flags which can trigger button actions. + int triggerable_event_flags_; + + DISALLOW_COPY_AND_ASSIGN(CustomButton); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_BUTTON_CUSTOM_BUTTON_H_ diff --git a/views/controls/button/image_button.cc b/views/controls/button/image_button.cc new file mode 100644 index 0000000..3c4c7fe --- /dev/null +++ b/views/controls/button/image_button.cc @@ -0,0 +1,154 @@ +// Copyright (c) 2006-2008 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/controls/button/image_button.h" + +#include "app/gfx/chrome_canvas.h" +#include "app/throb_animation.h" +#include "skia/ext/image_operations.h" + +namespace views { + +static const int kDefaultWidth = 16; // Default button width if no theme. +static const int kDefaultHeight = 14; // Default button height if no theme. + +//////////////////////////////////////////////////////////////////////////////// +// ImageButton, public: + +ImageButton::ImageButton(ButtonListener* listener) + : CustomButton(listener), + h_alignment_(ALIGN_LEFT), + v_alignment_(ALIGN_TOP) { + // By default, we request that the ChromeCanvas passed to our View::Paint() + // implementation is flipped horizontally so that the button's bitmaps are + // mirrored when the UI directionality is right-to-left. + EnableCanvasFlippingForRTLUI(true); +} + +ImageButton::~ImageButton() { +} + +void ImageButton::SetImage(ButtonState aState, SkBitmap* anImage) { + images_[aState] = anImage ? *anImage : SkBitmap(); +} + +void ImageButton::SetImageAlignment(HorizontalAlignment h_align, + VerticalAlignment v_align) { + h_alignment_ = h_align; + v_alignment_ = v_align; + SchedulePaint(); +} + +//////////////////////////////////////////////////////////////////////////////// +// ImageButton, View overrides: + +gfx::Size ImageButton::GetPreferredSize() { + if (!images_[BS_NORMAL].isNull()) + return gfx::Size(images_[BS_NORMAL].width(), images_[BS_NORMAL].height()); + return gfx::Size(kDefaultWidth, kDefaultHeight); +} + +void ImageButton::Paint(ChromeCanvas* canvas) { + // Call the base class first to paint any background/borders. + View::Paint(canvas); + + SkBitmap img = GetImageToPaint(); + + if (!img.isNull()) { + int x = 0, y = 0; + + if (h_alignment_ == ALIGN_CENTER) + x = (width() - img.width()) / 2; + else if (h_alignment_ == ALIGN_RIGHT) + x = width() - img.width(); + + if (v_alignment_ == ALIGN_MIDDLE) + y = (height() - img.height()) / 2; + else if (v_alignment_ == ALIGN_BOTTOM) + y = height() - img.height(); + + canvas->DrawBitmapInt(img, x, y); + } + PaintFocusBorder(canvas); +} + +//////////////////////////////////////////////////////////////////////////////// +// ImageButton, protected: + +SkBitmap ImageButton::GetImageToPaint() { + SkBitmap img; + + if (!images_[BS_HOT].isNull() && hover_animation_->IsAnimating()) { + img = skia::ImageOperations::CreateBlendedBitmap(images_[BS_NORMAL], + images_[BS_HOT], hover_animation_->GetCurrentValue()); + } else { + img = images_[state_]; + } + + return !img.isNull() ? img : images_[BS_NORMAL]; +} + +//////////////////////////////////////////////////////////////////////////////// +// ToggleImageButton, public: + +ToggleImageButton::ToggleImageButton(ButtonListener* listener) + : ImageButton(listener), + toggled_(false) { +} + +ToggleImageButton::~ToggleImageButton() { +} + +void ToggleImageButton::SetToggled(bool toggled) { + if (toggled == toggled_) + return; + + for (int i = 0; i < BS_COUNT; ++i) { + SkBitmap temp = images_[i]; + images_[i] = alternate_images_[i]; + alternate_images_[i] = temp; + } + toggled_ = toggled; + SchedulePaint(); +} + +void ToggleImageButton::SetToggledImage(ButtonState state, SkBitmap* image) { + if (toggled_) { + images_[state] = image ? *image : SkBitmap(); + if (state_ == state) + SchedulePaint(); + } else { + alternate_images_[state] = image ? *image : SkBitmap(); + } +} + +void ToggleImageButton::SetToggledTooltipText(const std::wstring& tooltip) { + toggled_tooltip_text_.assign(tooltip); +} + +//////////////////////////////////////////////////////////////////////////////// +// ToggleImageButton, ImageButton overrides: + +void ToggleImageButton::SetImage(ButtonState state, SkBitmap* image) { + if (toggled_) { + alternate_images_[state] = image ? *image : SkBitmap(); + } else { + images_[state] = image ? *image : SkBitmap(); + if (state_ == state) + SchedulePaint(); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// ToggleImageButton, View overrides: + +bool ToggleImageButton::GetTooltipText(int x, int y, std::wstring* tooltip) { + if (!toggled_ || toggled_tooltip_text_.empty()) + return Button::GetTooltipText(x, y, tooltip); + + *tooltip = toggled_tooltip_text_; + return true; +} + +} // namespace views diff --git a/views/controls/button/image_button.h b/views/controls/button/image_button.h new file mode 100644 index 0000000..c687561 --- /dev/null +++ b/views/controls/button/image_button.h @@ -0,0 +1,101 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_BUTTON_IMAGE_BUTTON_H_ +#define VIEWS_CONTROLS_BUTTON_IMAGE_BUTTON_H_ + +#include "skia/include/SkBitmap.h" +#include "views/controls/button/custom_button.h" + +namespace views { + +// An image button. +class ImageButton : public CustomButton { + public: + explicit ImageButton(ButtonListener* listener); + virtual ~ImageButton(); + + // Set the image the button should use for the provided state. + virtual void SetImage(ButtonState aState, SkBitmap* anImage); + + enum HorizontalAlignment { ALIGN_LEFT = 0, + ALIGN_CENTER, + ALIGN_RIGHT, }; + + enum VerticalAlignment { ALIGN_TOP = 0, + ALIGN_MIDDLE, + ALIGN_BOTTOM }; + + // Sets how the image is laid out within the button's bounds. + void SetImageAlignment(HorizontalAlignment h_align, + VerticalAlignment v_align); + + // Overridden from View: + virtual gfx::Size GetPreferredSize(); + virtual void Paint(ChromeCanvas* canvas); + + protected: + // Returns the image to paint. This is invoked from paint and returns a value + // from images. + virtual SkBitmap GetImageToPaint(); + + // The images used to render the different states of this button. + SkBitmap images_[BS_COUNT]; + + private: + // Image alignment. + HorizontalAlignment h_alignment_; + VerticalAlignment v_alignment_; + + DISALLOW_COPY_AND_ASSIGN(ImageButton); +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// ToggleImageButton +// +// A toggle-able ImageButton. It swaps out its graphics when toggled. +// +//////////////////////////////////////////////////////////////////////////////// +class ToggleImageButton : public ImageButton { + public: + ToggleImageButton(ButtonListener* listener); + virtual ~ToggleImageButton(); + + // Change the toggled state. + void SetToggled(bool toggled); + + // Like Button::SetImage(), but to set the graphics used for the + // "has been toggled" state. Must be called for each button state + // before the button is toggled. + void SetToggledImage(ButtonState state, SkBitmap* image); + + // Set the tooltip text displayed when the button is toggled. + void SetToggledTooltipText(const std::wstring& tooltip); + + // Overridden from ImageButton: + virtual void SetImage(ButtonState aState, SkBitmap* anImage); + + // Overridden from View: + virtual bool GetTooltipText(int x, int y, std::wstring* tooltip); + + private: + // The parent class's images_ member is used for the current images, + // and this array is used to hold the alternative images. + // We swap between the two when toggling. + SkBitmap alternate_images_[BS_COUNT]; + + // True if the button is currently toggled. + bool toggled_; + + // The parent class's tooltip_text_ is displayed when not toggled, and + // this one is shown when toggled. + std::wstring toggled_tooltip_text_; + + DISALLOW_EVIL_CONSTRUCTORS(ToggleImageButton); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_BUTTON_IMAGE_BUTTON_H_ diff --git a/views/controls/button/menu_button.cc b/views/controls/button/menu_button.cc new file mode 100644 index 0000000..888137b --- /dev/null +++ b/views/controls/button/menu_button.cc @@ -0,0 +1,253 @@ +// Copyright (c) 2006-2008 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/controls/button/menu_button.h" + +#include "app/drag_drop_types.h" +#include "app/gfx/chrome_canvas.h" +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "chrome/common/win_util.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "views/controls/button/button.h" +#include "views/controls/menu/view_menu_delegate.h" +#include "views/event.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" + +using base::Time; +using base::TimeDelta; + +namespace views { + +// The amount of time, in milliseconds, we wait before allowing another mouse +// pressed event to show the menu. +static const int64 kMinimumTimeBetweenButtonClicks = 100; + +// The down arrow used to differentiate the menu button from normal +// text buttons. +static const SkBitmap* kMenuMarker = NULL; + +// How much padding to put on the left and right of the menu marker. +static const int kMenuMarkerPaddingLeft = 3; +static const int kMenuMarkerPaddingRight = -1; + +//////////////////////////////////////////////////////////////////////////////// +// +// MenuButton - constructors, destructors, initialization +// +//////////////////////////////////////////////////////////////////////////////// + +MenuButton::MenuButton(ButtonListener* listener, + const std::wstring& text, + ViewMenuDelegate* menu_delegate, + bool show_menu_marker) + : TextButton(listener, text), + menu_visible_(false), + menu_closed_time_(), + menu_delegate_(menu_delegate), + show_menu_marker_(show_menu_marker) { + if (kMenuMarker == NULL) { + kMenuMarker = ResourceBundle::GetSharedInstance() + .GetBitmapNamed(IDR_MENU_DROPARROW); + } + set_alignment(TextButton::ALIGN_LEFT); +} + +MenuButton::~MenuButton() { +} + +//////////////////////////////////////////////////////////////////////////////// +// +// MenuButton - Public APIs +// +//////////////////////////////////////////////////////////////////////////////// + +gfx::Size MenuButton::GetPreferredSize() { + gfx::Size prefsize = TextButton::GetPreferredSize(); + if (show_menu_marker_) { + prefsize.Enlarge(kMenuMarker->width() + kMenuMarkerPaddingLeft + + kMenuMarkerPaddingRight, + 0); + } + return prefsize; +} + +void MenuButton::Paint(ChromeCanvas* canvas, bool for_drag) { + TextButton::Paint(canvas, for_drag); + + if (show_menu_marker_) { + gfx::Insets insets = GetInsets(); + + // We can not use the views' mirroring infrastructure for mirroring a + // MenuButton control (see TextButton::Paint() for a detailed explanation + // regarding why we can not flip the canvas). Therefore, we need to + // manually mirror the position of the down arrow. + gfx::Rect arrow_bounds(width() - insets.right() - + kMenuMarker->width() - kMenuMarkerPaddingRight, + height() / 2 - kMenuMarker->height() / 2, + kMenuMarker->width(), + kMenuMarker->height()); + arrow_bounds.set_x(MirroredLeftPointForRect(arrow_bounds)); + canvas->DrawBitmapInt(*kMenuMarker, arrow_bounds.x(), arrow_bounds.y()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// +// MenuButton - Events +// +//////////////////////////////////////////////////////////////////////////////// + +int MenuButton::GetMaximumScreenXCoordinate() { + Widget* widget = GetWidget(); + + if (!widget) { + NOTREACHED(); + return 0; + } + + HWND hwnd = widget->GetNativeView(); + CRect t; + ::GetWindowRect(hwnd, &t); + + gfx::Rect r(t); + gfx::Rect monitor_rect = win_util::GetMonitorBoundsForRect(r); + return monitor_rect.x() + monitor_rect.width() - 1; +} + +bool MenuButton::Activate() { + SetState(BS_PUSHED); + // We need to synchronously paint here because subsequently we enter a + // menu modal loop which will stop this window from updating and + // receiving the paint message that should be spawned by SetState until + // after the menu closes. + PaintNow(); + if (menu_delegate_) { + gfx::Rect lb = GetLocalBounds(true); + + // The position of the menu depends on whether or not the locale is + // right-to-left. + gfx::Point menu_position(lb.right(), lb.bottom()); + if (UILayoutIsRightToLeft()) + menu_position.set_x(lb.x()); + + View::ConvertPointToScreen(this, &menu_position); + if (UILayoutIsRightToLeft()) + menu_position.Offset(2, -4); + else + menu_position.Offset(-2, -4); + + int max_x_coordinate = GetMaximumScreenXCoordinate(); + if (max_x_coordinate && max_x_coordinate <= menu_position.x()) + menu_position.set_x(max_x_coordinate - 1); + + // We're about to show the menu from a mouse press. By showing from the + // mouse press event we block RootView in mouse dispatching. This also + // appears to cause RootView to get a mouse pressed BEFORE the mouse + // release is seen, which means RootView sends us another mouse press no + // matter where the user pressed. To force RootView to recalculate the + // mouse target during the mouse press we explicitly set the mouse handler + // to NULL. + GetRootView()->SetMouseHandler(NULL); + + menu_visible_ = true; + menu_delegate_->RunMenu(this, menu_position.ToPOINT(), + GetWidget()->GetNativeView()); + menu_visible_ = false; + menu_closed_time_ = Time::Now(); + + // Now that the menu has closed, we need to manually reset state to + // "normal" since the menu modal loop will have prevented normal + // mouse move messages from getting to this View. We set "normal" + // and not "hot" because the likelihood is that the mouse is now + // somewhere else (user clicked elsewhere on screen to close the menu + // or selected an item) and we will inevitably refresh the hot state + // in the event the mouse _is_ over the view. + SetState(BS_NORMAL); + + // We must return false here so that the RootView does not get stuck + // sending all mouse pressed events to us instead of the appropriate + // target. + return false; + } + return true; +} + +bool MenuButton::OnMousePressed(const MouseEvent& e) { + RequestFocus(); + if (state() != BS_DISABLED) { + // If we're draggable (GetDragOperations returns a non-zero value), then + // don't pop on press, instead wait for release. + if (e.IsOnlyLeftMouseButton() && HitTest(e.location()) && + GetDragOperations(e.x(), e.y()) == DragDropTypes::DRAG_NONE) { + TimeDelta delta = Time::Now() - menu_closed_time_; + int64 delta_in_milliseconds = delta.InMilliseconds(); + if (delta_in_milliseconds > kMinimumTimeBetweenButtonClicks) { + return Activate(); + } + } + } + return true; +} + +void MenuButton::OnMouseReleased(const MouseEvent& e, + bool canceled) { + if (GetDragOperations(e.x(), e.y()) != DragDropTypes::DRAG_NONE && + state() != BS_DISABLED && !canceled && !InDrag() && + e.IsOnlyLeftMouseButton() && HitTest(e.location())) { + Activate(); + } else { + TextButton::OnMouseReleased(e, canceled); + } +} + +// When the space bar or the enter key is pressed we need to show the menu. +bool MenuButton::OnKeyReleased(const KeyEvent& e) { + if ((e.GetCharacter() == VK_SPACE) || (e.GetCharacter() == VK_RETURN)) { + return Activate(); + } + return true; +} + +// The reason we override View::OnMouseExited is because we get this event when +// we display the menu. If we don't override this method then +// BaseButton::OnMouseExited will get the event and will set the button's state +// to BS_NORMAL instead of keeping the state BM_PUSHED. This, in turn, will +// cause the button to appear depressed while the menu is displayed. +void MenuButton::OnMouseExited(const MouseEvent& event) { + if ((state_ != BS_DISABLED) && (!menu_visible_) && (!InDrag())) { + SetState(BS_NORMAL); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// +// MenuButton - accessibility +// +//////////////////////////////////////////////////////////////////////////////// + +bool MenuButton::GetAccessibleDefaultAction(std::wstring* action) { + DCHECK(action); + + action->assign(l10n_util::GetString(IDS_ACCACTION_PRESS)); + return true; +} + +bool MenuButton::GetAccessibleRole(AccessibilityTypes::Role* role) { + DCHECK(role); + + *role = AccessibilityTypes::ROLE_BUTTONDROPDOWN; + return true; +} + +bool MenuButton::GetAccessibleState(AccessibilityTypes::State* state) { + DCHECK(state); + + *state = AccessibilityTypes::STATE_HASPOPUP; + return true; +} + +} // namespace views diff --git a/views/controls/button/menu_button.h b/views/controls/button/menu_button.h new file mode 100644 index 0000000..cbe9ab8 --- /dev/null +++ b/views/controls/button/menu_button.h @@ -0,0 +1,92 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_BUTTON_MENU_BUTTON_H_ +#define VIEWS_CONTROLS_BUTTON_MENU_BUTTON_H_ + +#include <windows.h> + +#include "app/gfx/chrome_font.h" +#include "base/time.h" +#include "views/background.h" +#include "views/controls/button/text_button.h" + +namespace views { + +class MouseEvent; +class ViewMenuDelegate; + + +//////////////////////////////////////////////////////////////////////////////// +// +// MenuButton +// +// A button that shows a menu when the left mouse button is pushed +// +//////////////////////////////////////////////////////////////////////////////// +class MenuButton : public TextButton { + public: + // + // Create a Button + MenuButton(ButtonListener* listener, + const std::wstring& text, + ViewMenuDelegate* menu_delegate, + bool show_menu_marker); + virtual ~MenuButton(); + + void set_menu_delegate(ViewMenuDelegate* delegate) { + menu_delegate_ = delegate; + } + + // Activate the button (called when the button is pressed). + virtual bool Activate(); + + // Overridden to take into account the potential use of a drop marker. + virtual gfx::Size GetPreferredSize(); + virtual void Paint(ChromeCanvas* canvas, bool for_drag); + + // These methods are overriden to implement a simple push button + // behavior + virtual bool OnMousePressed(const MouseEvent& e); + void OnMouseReleased(const MouseEvent& e, bool canceled); + virtual bool OnKeyReleased(const KeyEvent& e); + virtual void OnMouseExited(const MouseEvent& event); + + // Accessibility accessors, overridden from View. + virtual bool GetAccessibleDefaultAction(std::wstring* action); + virtual bool GetAccessibleRole(AccessibilityTypes::Role* role); + virtual bool GetAccessibleState(AccessibilityTypes::State* state); + + protected: + // true if the menu is currently visible. + bool menu_visible_; + + private: + + // Compute the maximum X coordinate for the current screen. MenuButtons + // use this to make sure a menu is never shown off screen. + int GetMaximumScreenXCoordinate(); + + DISALLOW_EVIL_CONSTRUCTORS(MenuButton); + + // We use a time object in order to keep track of when the menu was closed. + // The time is used for simulating menu behavior for the menu button; that + // is, if the menu is shown and the button is pressed, we need to close the + // menu. There is no clean way to get the second click event because the + // menu is displayed using a modal loop and, unlike regular menus in Windows, + // the button is not part of the displayed menu. + base::Time menu_closed_time_; + + // The associated menu's resource identifier. + ViewMenuDelegate* menu_delegate_; + + // Whether or not we're showing a drop marker. + bool show_menu_marker_; + + friend class TextButtonBackground; +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_BUTTON_MENU_BUTTON_H_ diff --git a/views/controls/button/native_button.cc b/views/controls/button/native_button.cc new file mode 100644 index 0000000..0af6f88 --- /dev/null +++ b/views/controls/button/native_button.cc @@ -0,0 +1,178 @@ +// Copyright (c) 2009 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/controls/button/native_button.h" + +#include "app/l10n_util.h" +#include "base/logging.h" + +namespace views { + +static int kButtonBorderHWidth = 8; + +// static +const char NativeButton::kViewClassName[] = "views/NativeButton"; + +//////////////////////////////////////////////////////////////////////////////// +// NativeButton, public: + +NativeButton::NativeButton(ButtonListener* listener) + : Button(listener), + native_wrapper_(NULL), + is_default_(false), + ignore_minimum_size_(false), + minimum_size_(50, 14) { + // The min size in DLUs comes from + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwue/html/ch14e.asp + InitBorder(); + SetFocusable(true); +} + +NativeButton::NativeButton(ButtonListener* listener, const std::wstring& label) + : Button(listener), + native_wrapper_(NULL), + is_default_(false), + ignore_minimum_size_(false), + minimum_size_(50, 14) { + SetLabel(label); // SetLabel takes care of label layout in RTL UI. + // The min size in DLUs comes from + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwue/html/ch14e.asp + InitBorder(); + SetFocusable(true); +} + +NativeButton::~NativeButton() { +} + +void NativeButton::SetLabel(const std::wstring& label) { + label_ = label; + + // Even though we create a flipped HWND for a native button when the locale + // is right-to-left, Windows does not render text for the button using a + // right-to-left context (perhaps because the parent HWND is not flipped). + // The result is that RTL strings containing punctuation marks are not + // displayed properly. For example, the string "...ABC" (where A, B and C are + // Hebrew characters) is displayed as "ABC..." which is incorrect. + // + // In order to overcome this problem, we mark the localized Hebrew strings as + // RTL strings explicitly (using the appropriate Unicode formatting) so that + // Windows displays the text correctly regardless of the HWND hierarchy. + std::wstring localized_label; + if (l10n_util::AdjustStringForLocaleDirection(label_, &localized_label)) + label_ = localized_label; + + if (native_wrapper_) + native_wrapper_->UpdateLabel(); +} + +void NativeButton::SetIsDefault(bool is_default) { + if (is_default == is_default_) + return; + if (is_default) + AddAccelerator(Accelerator(VK_RETURN, false, false, false)); + else + RemoveAccelerator(Accelerator(VK_RETURN, false, false, false)); + SetAppearsAsDefault(is_default); +} + +void NativeButton::SetAppearsAsDefault(bool appears_as_default) { + is_default_ = appears_as_default; + if (native_wrapper_) + native_wrapper_->UpdateDefault(); +} + +void NativeButton::ButtonPressed() { + RequestFocus(); + + // TODO(beng): obtain mouse event flags for native buttons someday. + NotifyClick(mouse_event_flags()); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeButton, View overrides: + +gfx::Size NativeButton::GetPreferredSize() { + if (!native_wrapper_) + return gfx::Size(); + + gfx::Size sz = native_wrapper_->GetView()->GetPreferredSize(); + + // Add in the border size. (Do this before clamping the minimum size in case + // that clamping causes an increase in size that would include the borders. + gfx::Insets border = GetInsets(); + sz.set_width(sz.width() + border.left() + border.right()); + sz.set_height(sz.height() + border.top() + border.bottom()); + + // Clamp the size returned to at least the minimum size. + if (!ignore_minimum_size_) { + if (minimum_size_.width()) { + int min_width = font_.horizontal_dlus_to_pixels(minimum_size_.width()); + sz.set_width(std::max(static_cast<int>(sz.width()), min_width)); + } + if (minimum_size_.height()) { + int min_height = font_.vertical_dlus_to_pixels(minimum_size_.height()); + sz.set_height(std::max(static_cast<int>(sz.height()), min_height)); + } + } + + return sz; +} + +void NativeButton::Layout() { + if (native_wrapper_) { + native_wrapper_->GetView()->SetBounds(0, 0, width(), height()); + native_wrapper_->GetView()->Layout(); + } +} + +void NativeButton::SetEnabled(bool flag) { + Button::SetEnabled(flag); + if (native_wrapper_) + native_wrapper_->UpdateEnabled(); +} + +void NativeButton::ViewHierarchyChanged(bool is_add, View* parent, + View* child) { + if (is_add && !native_wrapper_ && GetWidget()) { + CreateWrapper(); + AddChildView(native_wrapper_->GetView()); + } +} + +std::string NativeButton::GetClassName() const { + return kViewClassName; +} + +bool NativeButton::AcceleratorPressed(const Accelerator& accelerator) { + if (IsEnabled()) { + NotifyClick(mouse_event_flags()); + return true; + } + return false; +} + +void NativeButton::Focus() { + // Forward the focus to the wrapper. + if (native_wrapper_) + native_wrapper_->SetFocus(); + else + Button::Focus(); // Will focus the RootView window (so we still get + // keyboard messages). +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeButton, protected: + +void NativeButton::CreateWrapper() { + native_wrapper_ = NativeButtonWrapper::CreateNativeButtonWrapper(this); + native_wrapper_->UpdateLabel(); + native_wrapper_->UpdateEnabled(); +} + +void NativeButton::InitBorder() { + set_border(Border::CreateEmptyBorder(0, kButtonBorderHWidth, 0, + kButtonBorderHWidth)); +} + +} // namespace views diff --git a/views/controls/button/native_button.h b/views/controls/button/native_button.h new file mode 100644 index 0000000..52946d7 --- /dev/null +++ b/views/controls/button/native_button.h @@ -0,0 +1,100 @@ +// Copyright (c) 2009 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 VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_H_ +#define VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_H_ + +#include "app/gfx/chrome_font.h" +#include "views/controls/button/button.h" +#include "views/controls/button/native_button_wrapper.h" + +class ChromeFont; + +namespace views { + +class NativeButton : public Button { + public: + // The button's class name. + static const char kViewClassName[]; + + explicit NativeButton(ButtonListener* listener); + NativeButton(ButtonListener* listener, const std::wstring& label); + virtual ~NativeButton(); + + // Sets/Gets the text to be used as the button's label. + void SetLabel(const std::wstring& label); + std::wstring label() const { return label_; } + + // Sets the font to be used when displaying the button's label. + void set_font(const ChromeFont& font) { font_ = font; } + const ChromeFont& font() const { return font_; } + + // Sets/Gets whether or not the button appears and behaves as the default + // button in its current context. + void SetIsDefault(bool default_button); + bool is_default() const { return is_default_; } + + // Sets whether or not the button appears as the default button. This does + // not make it behave as the default (i.e. no enter key accelerator is + // registered, use SetIsDefault for that). + void SetAppearsAsDefault(bool default_button); + + void set_minimum_size(const gfx::Size& minimum_size) { + minimum_size_ = minimum_size; + } + void set_ignore_minimum_size(bool ignore_minimum_size) { + ignore_minimum_size_ = ignore_minimum_size; + } + + // Called by the wrapper when the actual wrapped native button was pressed. + void ButtonPressed(); + + // Overridden from View: + virtual gfx::Size GetPreferredSize(); + virtual void Layout(); + virtual void SetEnabled(bool flag); + virtual void Focus(); + + protected: + virtual void ViewHierarchyChanged(bool is_add, View* parent, View* child); + virtual std::string GetClassName() const; + virtual bool AcceleratorPressed(const Accelerator& accelerator); + + // Create the button wrapper. Can be overridden by subclass to create a + // wrapper of a particular type. See NativeButtonWrapper interface for types. + virtual void CreateWrapper(); + + // Sets a border to the button. Override to set a different border or to not + // set one (the default is 0,8,0,8 for push buttons). + virtual void InitBorder(); + + // The object that actually implements the native button. + NativeButtonWrapper* native_wrapper_; + + private: + // The button label. + std::wstring label_; + + // True if the button is the default button in its context. + bool is_default_; + + // The font used to render the button label. + ChromeFont font_; + + // True if the button should ignore the minimum size for the platform. Default + // is false. Set to true to create narrower buttons. + bool ignore_minimum_size_; + + // The minimum size of the button from the specified size in native dialog + // units. The definition of this unit may vary from platform to platform. If + // the width/height is non-zero, the preferred size of the button will not be + // less than this value when the dialog units are converted to pixels. + gfx::Size minimum_size_; + + DISALLOW_COPY_AND_ASSIGN(NativeButton); +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_H_ diff --git a/views/controls/button/native_button_win.cc b/views/controls/button/native_button_win.cc new file mode 100644 index 0000000..6748241 --- /dev/null +++ b/views/controls/button/native_button_win.cc @@ -0,0 +1,234 @@ +// Copyright (c) 2009 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/controls/button/native_button_win.h" + +#include "base/logging.h" +#include "views/controls/button/checkbox.h" +#include "views/controls/button/native_button.h" +#include "views/controls/button/radio_button.h" +#include "views/widget/widget.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// NativeButtonWin, public: + +NativeButtonWin::NativeButtonWin(NativeButton* native_button) + : NativeControlWin(), + native_button_(native_button) { + // Associates the actual HWND with the native_button so the native_button is + // the one considered as having the focus (not the wrapper) when the HWND is + // focused directly (with a click for example). + SetAssociatedFocusView(native_button); +} + +NativeButtonWin::~NativeButtonWin() { +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeButtonWin, NativeButtonWrapper implementation: + +void NativeButtonWin::UpdateLabel() { + SetWindowText(GetHWND(), native_button_->label().c_str()); +} + +void NativeButtonWin::UpdateFont() { + SendMessage(GetHWND(), WM_SETFONT, + reinterpret_cast<WPARAM>(native_button_->font().hfont()), + FALSE); +} + +void NativeButtonWin::UpdateEnabled() { + SetEnabled(native_button_->IsEnabled()); +} + +void NativeButtonWin::UpdateDefault() { + if (!IsCheckbox()) { + SendMessage(GetHWND(), BM_SETSTYLE, + native_button_->is_default() ? BS_DEFPUSHBUTTON : BS_PUSHBUTTON, + true); + } +} + +View* NativeButtonWin::GetView() { + return this; +} + +void NativeButtonWin::SetFocus() { + // Focus the associated HWND. + Focus(); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeButtonWin, View overrides: + +gfx::Size NativeButtonWin::GetPreferredSize() { + SIZE sz = {0}; + SendMessage(GetHWND(), BCM_GETIDEALSIZE, 0, reinterpret_cast<LPARAM>(&sz)); + + return gfx::Size(sz.cx, sz.cy); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeButtonWin, NativeControlWin overrides: + +bool NativeButtonWin::ProcessMessage(UINT message, WPARAM w_param, + LPARAM l_param, LRESULT* result) { + if (message == WM_COMMAND && HIWORD(w_param) == BN_CLICKED) { + native_button_->ButtonPressed(); + *result = 0; + return true; + } + return NativeControlWin::ProcessMessage(message, w_param, l_param, result); +} + +bool NativeButtonWin::OnKeyDown(int vkey) { + bool enter_pressed = vkey == VK_RETURN; + if (enter_pressed) + native_button_->ButtonPressed(); + return enter_pressed; +} + +bool NativeButtonWin::NotifyOnKeyDown() const { + return true; +} + +void NativeButtonWin::CreateNativeControl() { + DWORD flags = WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | BS_PUSHBUTTON; + if (native_button_->is_default()) + flags |= BS_DEFPUSHBUTTON; + HWND control_hwnd = CreateWindowEx(GetAdditionalExStyle(), L"BUTTON", L"", + flags, 0, 0, width(), height(), + GetWidget()->GetNativeView(), NULL, NULL, + NULL); + NativeControlCreated(control_hwnd); +} + +void NativeButtonWin::NativeControlCreated(HWND control_hwnd) { + NativeControlWin::NativeControlCreated(control_hwnd); + + UpdateFont(); + UpdateLabel(); + UpdateDefault(); +} + +// We could obtain this from the theme, but that only works if themes are +// active. +static const int kCheckboxSize = 13; // pixels + +//////////////////////////////////////////////////////////////////////////////// +// NativeCheckboxWin, public: + +NativeCheckboxWin::NativeCheckboxWin(Checkbox* checkbox) + : NativeButtonWin(checkbox), + checkbox_(checkbox) { +} + +NativeCheckboxWin::~NativeCheckboxWin() { +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeCheckboxWin, View overrides: + +gfx::Size NativeCheckboxWin::GetPreferredSize() { + return gfx::Size(kCheckboxSize, kCheckboxSize); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeCheckboxWin, NativeButtonWrapper implementation: + +void NativeCheckboxWin::UpdateChecked() { + SendMessage(GetHWND(), BM_SETCHECK, + checkbox_->checked() ? BST_CHECKED : BST_UNCHECKED, 0); +} + +void NativeCheckboxWin::SetPushed(bool pushed) { + SendMessage(GetHWND(), BM_SETSTATE, pushed, 0); +} + +bool NativeCheckboxWin::OnKeyDown(int vkey) { + // Override the NativeButtonWin behavior which triggers the button on enter + // key presses when focused. + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeCheckboxWin, NativeButtonWin overrides: + +bool NativeCheckboxWin::ProcessMessage(UINT message, WPARAM w_param, + LPARAM l_param, LRESULT* result) { + if (message == WM_COMMAND && HIWORD(w_param) == BN_CLICKED) { + if (!IsRadioButton() || !checkbox_->checked()) + checkbox_->SetChecked(!checkbox_->checked()); + // Fall through to the NativeButtonWin's handler, which will send the + // clicked notification to the listener... + } + return NativeButtonWin::ProcessMessage(message, w_param, l_param, result); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeCheckboxWin, protected: + +void NativeCheckboxWin::CreateNativeControl() { + HWND control_hwnd = CreateWindowEx( + WS_EX_TRANSPARENT | GetAdditionalExStyle(), L"BUTTON", L"", + WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | BS_CHECKBOX, + 0, 0, width(), height(), GetWidget()->GetNativeView(), NULL, NULL, NULL); + NativeControlCreated(control_hwnd); +} + +void NativeCheckboxWin::NativeControlCreated(HWND control_hwnd) { + NativeButtonWin::NativeControlCreated(control_hwnd); + UpdateChecked(); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeRadioButtonWin, public: + +NativeRadioButtonWin::NativeRadioButtonWin(RadioButton* radio_button) + : NativeCheckboxWin(radio_button) { +} + +NativeRadioButtonWin::~NativeRadioButtonWin() { +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeRadioButtonWin, NativeCheckboxWin overrides: + +void NativeRadioButtonWin::CreateNativeControl() { + HWND control_hwnd = CreateWindowEx( + GetAdditionalExStyle(), L"BUTTON", + L"", WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | BS_RADIOBUTTON, + 0, 0, width(), height(), GetWidget()->GetNativeView(), NULL, NULL, NULL); + NativeControlCreated(control_hwnd); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeButtonWrapper, public: + +// static +int NativeButtonWrapper::GetFixedWidth() { + return kCheckboxSize; +} + +// static +NativeButtonWrapper* NativeButtonWrapper::CreateNativeButtonWrapper( + NativeButton* native_button) { + return new NativeButtonWin(native_button); +} + +// static +NativeButtonWrapper* NativeButtonWrapper::CreateCheckboxWrapper( + Checkbox* checkbox) { + return new NativeCheckboxWin(checkbox); +} + +// static +NativeButtonWrapper* NativeButtonWrapper::CreateRadioButtonWrapper( + RadioButton* radio_button) { + return new NativeRadioButtonWin(radio_button); +} + +} // namespace views diff --git a/views/controls/button/native_button_win.h b/views/controls/button/native_button_win.h new file mode 100644 index 0000000..3f5141a --- /dev/null +++ b/views/controls/button/native_button_win.h @@ -0,0 +1,105 @@ +// Copyright (c) 2009 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 VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_WIN_H_ +#define VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_WIN_H_ + +#include "views/controls/native_control_win.h" +#include "views/controls/button/native_button_wrapper.h" + +namespace views { + +// A View that hosts a native Windows button. +class NativeButtonWin : public NativeControlWin, + public NativeButtonWrapper { + public: + explicit NativeButtonWin(NativeButton* native_button); + virtual ~NativeButtonWin(); + + // Overridden from NativeButtonWrapper: + virtual void UpdateLabel(); + virtual void UpdateFont(); + virtual void UpdateEnabled(); + virtual void UpdateDefault(); + virtual View* GetView(); + virtual void SetFocus(); + + // Overridden from View: + virtual gfx::Size GetPreferredSize(); + + // Overridden from NativeControlWin: + virtual bool ProcessMessage(UINT message, + WPARAM w_param, + LPARAM l_param, + LRESULT* result); + virtual bool OnKeyDown(int vkey); + + protected: + virtual bool NotifyOnKeyDown() const; + virtual void CreateNativeControl(); + virtual void NativeControlCreated(HWND control_hwnd); + + // Returns true if this button is actually a checkbox or radio button. + virtual bool IsCheckbox() const { return false; } + + private: + // The NativeButton we are bound to. + NativeButton* native_button_; + + DISALLOW_COPY_AND_ASSIGN(NativeButtonWin); +}; + +// A View that hosts a native Windows checkbox. +class NativeCheckboxWin : public NativeButtonWin { + public: + explicit NativeCheckboxWin(Checkbox* native_button); + virtual ~NativeCheckboxWin(); + + // Overridden from View: + virtual gfx::Size GetPreferredSize(); + + // Overridden from NativeButtonWrapper: + virtual void UpdateChecked(); + virtual void SetPushed(bool pushed); + virtual bool OnKeyDown(int vkey); + + // Overridden from NativeControlWin: + virtual bool ProcessMessage(UINT message, + WPARAM w_param, + LPARAM l_param, + LRESULT* result); + + protected: + virtual void CreateNativeControl(); + virtual void NativeControlCreated(HWND control_hwnd); + virtual bool IsCheckbox() const { return true; } + + // Returns true if this button is actually a radio button. + virtual bool IsRadioButton() const { return false; } + + private: + // The Checkbox we are bound to. + Checkbox* checkbox_; + + DISALLOW_COPY_AND_ASSIGN(NativeCheckboxWin); +}; + +// A View that hosts a native Windows radio button. +class NativeRadioButtonWin : public NativeCheckboxWin { + public: + explicit NativeRadioButtonWin(RadioButton* radio_button); + virtual ~NativeRadioButtonWin(); + + protected: + // Overridden from NativeCheckboxWin: + virtual void CreateNativeControl(); + virtual bool IsRadioButton() const { return true; } + + private: + DISALLOW_COPY_AND_ASSIGN(NativeRadioButtonWin); +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_WIN_H_ diff --git a/views/controls/button/native_button_wrapper.h b/views/controls/button/native_button_wrapper.h new file mode 100644 index 0000000..5535ed4 --- /dev/null +++ b/views/controls/button/native_button_wrapper.h @@ -0,0 +1,62 @@ +// Copyright (c) 2009 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 VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_WRAPPER_H_ +#define VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_WRAPPER_H_ + +class ChromeFont; + +namespace views { + +class Checkbox; +class NativeButton; +class RadioButton; + +// A specialization of NativeControlWrapper that hosts a platform-native button. +class NativeButtonWrapper { + public: + // Updates the native button's label from the state stored in its associated + // NativeButton. + virtual void UpdateLabel() = 0; + + // Updates the native button's label font from the state stored in its + // associated NativeButton. + virtual void UpdateFont() = 0; + + // Updates the native button's enabled state from the state stored in its + // associated NativeButton. + virtual void UpdateEnabled() = 0; + + // Updates the native button's default state from the state stored in its + // associated NativeButton. + virtual void UpdateDefault() = 0; + + // Updates the native button's checked state from the state stored in its + // associated NativeCheckbox. Valid only for checkboxes and radio buttons. + virtual void UpdateChecked() {} + + // Shows the pushed state for the button if |pushed| is true. + virtual void SetPushed(bool pushed) {}; + + // Retrieves the views::View that hosts the native control. + virtual View* GetView() = 0; + + // Sets the focus to the button. + virtual void SetFocus() = 0; + + // Return the width of the button. Used for fixed size buttons (checkboxes and + // radio buttons) only. + static int GetFixedWidth(); + + // Creates an appropriate NativeButtonWrapper for the platform. + static NativeButtonWrapper* CreateNativeButtonWrapper(NativeButton* button); + static NativeButtonWrapper* CreateCheckboxWrapper(Checkbox* checkbox); + static NativeButtonWrapper* CreateRadioButtonWrapper( + RadioButton* radio_button); + +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_BUTTON_NATIVE_BUTTON_WRAPPER_H_ diff --git a/views/controls/button/radio_button.cc b/views/controls/button/radio_button.cc new file mode 100644 index 0000000..3f4820d --- /dev/null +++ b/views/controls/button/radio_button.cc @@ -0,0 +1,107 @@ +// Copyright (c) 2009 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/controls/button/radio_button.h" + +#include "views/widget/root_view.h" + +namespace views { + +// static +const char RadioButton::kViewClassName[] = "views/RadioButton"; + +//////////////////////////////////////////////////////////////////////////////// +// RadioButton, public: + +RadioButton::RadioButton() : Checkbox() { +} + +RadioButton::RadioButton(const std::wstring& label) : Checkbox(label) { +} + +RadioButton::RadioButton(const std::wstring& label, int group_id) + : Checkbox(label) { + SetGroup(group_id); +} + +RadioButton::~RadioButton() { +} + +//////////////////////////////////////////////////////////////////////////////// +// RadioButton, Checkbox overrides: + +void RadioButton::SetChecked(bool checked) { + if (checked == RadioButton::checked()) + return; + if (checked) { + // We can't just get the root view here because sometimes the radio + // button isn't attached to a root view (e.g., if it's part of a tab page + // that is currently not active). + View* container = GetParent(); + while (container && container->GetParent()) + container = container->GetParent(); + if (container) { + std::vector<View*> other; + container->GetViewsWithGroup(GetGroup(), &other); + std::vector<View*>::iterator i; + for (i = other.begin(); i != other.end(); ++i) { + if (*i != this) { + RadioButton* peer = static_cast<RadioButton*>(*i); + peer->SetChecked(false); + } + } + } + } + Checkbox::SetChecked(checked); + +} + +//////////////////////////////////////////////////////////////////////////////// +// RadioButton, View overrides: + +View* RadioButton::GetSelectedViewForGroup(int group_id) { + std::vector<View*> views; + GetRootView()->GetViewsWithGroup(group_id, &views); + if (views.empty()) + return NULL; + + for (std::vector<View*>::const_iterator iter = views.begin(); + iter != views.end(); ++iter) { + RadioButton* radio_button = static_cast<RadioButton*>(*iter); + if (radio_button->checked()) + return radio_button; + } + return NULL; +} + +bool RadioButton::IsGroupFocusTraversable() const { + // When focusing a radio button with tab/shift+tab, only the selected button + // from the group should be focused. + return false; +} + +void RadioButton::OnMouseReleased(const MouseEvent& event, bool canceled) { + native_wrapper_->SetPushed(false); + // Set the checked state to true only if we are unchecked, since we can't + // be toggled on and off like a checkbox. + if (!checked() && !canceled && HitTestLabel(event)) + SetChecked(true); + + ButtonPressed(); +} + +std::string RadioButton::GetClassName() const { + return kViewClassName; +} + +//////////////////////////////////////////////////////////////////////////////// +// RadioButton, NativeButton overrides: + +void RadioButton::CreateWrapper() { + native_wrapper_ = NativeButtonWrapper::CreateRadioButtonWrapper(this); + native_wrapper_->UpdateLabel(); + native_wrapper_->UpdateChecked(); +} + +} // namespace views diff --git a/views/controls/button/radio_button.h b/views/controls/button/radio_button.h new file mode 100644 index 0000000..ab7fbe2 --- /dev/null +++ b/views/controls/button/radio_button.h @@ -0,0 +1,43 @@ +// Copyright (c) 2009 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 VIEWS_CONTROLS_BUTTON_RADIO_BUTTON_H_ +#define VIEWS_CONTROLS_BUTTON_RADIO_BUTTON_H_ + +#include "views/controls/button/checkbox.h" + +namespace views { + +// A Checkbox subclass representing a radio button. +class RadioButton : public Checkbox { + public: + // The button's class name. + static const char kViewClassName[]; + + RadioButton(); + RadioButton(const std::wstring& label); + RadioButton(const std::wstring& label, int group_id); + virtual ~RadioButton(); + + // Overridden from Checkbox: + virtual void SetChecked(bool checked); + + // Overridden from View: + virtual View* GetSelectedViewForGroup(int group_id); + virtual bool IsGroupFocusTraversable() const; + virtual void OnMouseReleased(const MouseEvent& event, bool canceled); + + protected: + virtual std::string GetClassName() const; + + // Overridden from NativeButton: + virtual void CreateWrapper(); + + private: + DISALLOW_COPY_AND_ASSIGN(RadioButton); +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_BUTTON_RADIO_BUTTON_H_ diff --git a/views/controls/button/text_button.cc b/views/controls/button/text_button.cc new file mode 100644 index 0000000..4f68b90 --- /dev/null +++ b/views/controls/button/text_button.cc @@ -0,0 +1,325 @@ +// Copyright (c) 2006-2008 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/controls/button/text_button.h" + +#include "app/gfx/chrome_canvas.h" +#include "app/l10n_util.h" +#include "app/throb_animation.h" +#include "app/resource_bundle.h" +#include "views/controls/button/button.h" +#include "views/event.h" +#include "grit/theme_resources.h" + +namespace views { + +// Padding between the icon and text. +static const int kIconTextPadding = 5; + +// Preferred padding between text and edge +static const int kPreferredPaddingHorizontal = 6; +static const int kPreferredPaddingVertical = 5; + +static const SkColor kEnabledColor = SkColorSetRGB(6, 45, 117); +static const SkColor kHighlightColor = SkColorSetARGB(200, 255, 255, 255); +static const SkColor kDisabledColor = SkColorSetRGB(161, 161, 146); + +// How long the hover fade animation should last. +static const int kHoverAnimationDurationMs = 170; + +//////////////////////////////////////////////////////////////////////////////// +// +// TextButtonBorder - constructors, destructors, initialization +// +//////////////////////////////////////////////////////////////////////////////// + +TextButtonBorder::TextButtonBorder() { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + + hot_set_.top_left = rb.GetBitmapNamed(IDR_TEXTBUTTON_TOP_LEFT_H); + hot_set_.top = rb.GetBitmapNamed(IDR_TEXTBUTTON_TOP_H); + hot_set_.top_right = rb.GetBitmapNamed(IDR_TEXTBUTTON_TOP_RIGHT_H); + hot_set_.left = rb.GetBitmapNamed(IDR_TEXTBUTTON_LEFT_H); + hot_set_.center = rb.GetBitmapNamed(IDR_TEXTBUTTON_CENTER_H); + hot_set_.right = rb.GetBitmapNamed(IDR_TEXTBUTTON_RIGHT_H); + hot_set_.bottom_left = rb.GetBitmapNamed(IDR_TEXTBUTTON_BOTTOM_LEFT_H); + hot_set_.bottom = rb.GetBitmapNamed(IDR_TEXTBUTTON_BOTTOM_H); + hot_set_.bottom_right = rb.GetBitmapNamed(IDR_TEXTBUTTON_BOTTOM_RIGHT_H); + + pushed_set_.top_left = rb.GetBitmapNamed(IDR_TEXTBUTTON_TOP_LEFT_P); + pushed_set_.top = rb.GetBitmapNamed(IDR_TEXTBUTTON_TOP_P); + pushed_set_.top_right = rb.GetBitmapNamed(IDR_TEXTBUTTON_TOP_RIGHT_P); + pushed_set_.left = rb.GetBitmapNamed(IDR_TEXTBUTTON_LEFT_P); + pushed_set_.center = rb.GetBitmapNamed(IDR_TEXTBUTTON_CENTER_P); + pushed_set_.right = rb.GetBitmapNamed(IDR_TEXTBUTTON_RIGHT_P); + pushed_set_.bottom_left = rb.GetBitmapNamed(IDR_TEXTBUTTON_BOTTOM_LEFT_P); + pushed_set_.bottom = rb.GetBitmapNamed(IDR_TEXTBUTTON_BOTTOM_P); + pushed_set_.bottom_right = rb.GetBitmapNamed(IDR_TEXTBUTTON_BOTTOM_RIGHT_P); +} + +TextButtonBorder::~TextButtonBorder() { +} + +//////////////////////////////////////////////////////////////////////////////// +// +// TextButtonBackground - painting +// +//////////////////////////////////////////////////////////////////////////////// + +void TextButtonBorder::Paint(const View& view, ChromeCanvas* canvas) const { + const TextButton* mb = static_cast<const TextButton*>(&view); + int state = mb->state(); + + // TextButton takes care of deciding when to call Paint. + const MBBImageSet* set = &hot_set_; + if (state == TextButton::BS_PUSHED) + set = &pushed_set_; + + if (set) { + gfx::Rect bounds = view.bounds(); + + // Draw the top left image + canvas->DrawBitmapInt(*set->top_left, 0, 0); + + // Tile the top image + canvas->TileImageInt( + *set->top, + set->top_left->width(), + 0, + bounds.width() - set->top_right->width() - set->top_left->width(), + set->top->height()); + + // Draw the top right image + canvas->DrawBitmapInt(*set->top_right, + bounds.width() - set->top_right->width(), 0); + + // Tile the left image + canvas->TileImageInt( + *set->left, + 0, + set->top_left->height(), + set->top_left->width(), + bounds.height() - set->top->height() - set->bottom_left->height()); + + // Tile the center image + canvas->TileImageInt( + *set->center, + set->left->width(), + set->top->height(), + bounds.width() - set->right->width() - set->left->width(), + bounds.height() - set->bottom->height() - set->top->height()); + + // Tile the right image + canvas->TileImageInt( + *set->right, + bounds.width() - set->right->width(), + set->top_right->height(), + bounds.width(), + bounds.height() - set->bottom_right->height() - + set->top_right->height()); + + // Draw the bottom left image + canvas->DrawBitmapInt(*set->bottom_left, + 0, + bounds.height() - set->bottom_left->height()); + + // Tile the bottom image + canvas->TileImageInt( + *set->bottom, + set->bottom_left->width(), + bounds.height() - set->bottom->height(), + bounds.width() - set->bottom_right->width() - + set->bottom_left->width(), + set->bottom->height()); + + // Draw the bottom right image + canvas->DrawBitmapInt(*set->bottom_right, + bounds.width() - set->bottom_right->width(), + bounds.height() - set->bottom_right->height()); + } else { + // Do nothing + } +} + +void TextButtonBorder::GetInsets(gfx::Insets* insets) const { + insets->Set(kPreferredPaddingVertical, kPreferredPaddingHorizontal, + kPreferredPaddingVertical, kPreferredPaddingHorizontal); +} + +//////////////////////////////////////////////////////////////////////////////// +// TextButton, public: + +TextButton::TextButton(ButtonListener* listener, const std::wstring& text) + : CustomButton(listener), + alignment_(ALIGN_LEFT), + font_(ResourceBundle::GetSharedInstance().GetFont( + ResourceBundle::BaseFont)), + color_(kEnabledColor), + max_width_(0) { + SetText(text); + set_border(new TextButtonBorder); + SetAnimationDuration(kHoverAnimationDurationMs); +} + +TextButton::~TextButton() { +} + +void TextButton::SetText(const std::wstring& text) { + text_ = text; + // Update our new current and max text size + text_size_.SetSize(font_.GetStringWidth(text_), font_.height()); + max_text_size_.SetSize(std::max(max_text_size_.width(), text_size_.width()), + std::max(max_text_size_.height(), + text_size_.height())); +} + +void TextButton::SetIcon(const SkBitmap& icon) { + icon_ = icon; +} + +void TextButton::ClearMaxTextSize() { + max_text_size_ = text_size_; +} + +void TextButton::Paint(ChromeCanvas* canvas, bool for_drag) { + if (!for_drag) { + PaintBackground(canvas); + + if (hover_animation_->IsAnimating()) { + // Draw the hover bitmap into an offscreen buffer, then blend it + // back into the current canvas. + canvas->saveLayerAlpha(NULL, + static_cast<int>(hover_animation_->GetCurrentValue() * 255), + SkCanvas::kARGB_NoClipLayer_SaveFlag); + canvas->drawARGB(0, 255, 255, 255, SkPorterDuff::kClear_Mode); + PaintBorder(canvas); + canvas->restore(); + } else if (state_ == BS_HOT || state_ == BS_PUSHED) { + PaintBorder(canvas); + } + + PaintFocusBorder(canvas); + } + + gfx::Insets insets = GetInsets(); + int available_width = width() - insets.width(); + int available_height = height() - insets.height(); + // Use the actual text (not max) size to properly center the text. + int content_width = text_size_.width(); + if (icon_.width() > 0) { + content_width += icon_.width(); + if (!text_.empty()) + content_width += kIconTextPadding; + } + // Place the icon along the left edge. + int icon_x; + if (alignment_ == ALIGN_LEFT) { + icon_x = insets.left(); + } else if (alignment_ == ALIGN_RIGHT) { + icon_x = available_width - content_width; + } else { + icon_x = + std::max(0, (available_width - content_width) / 2) + insets.left(); + } + int text_x = icon_x; + if (icon_.width() > 0) + text_x += icon_.width() + kIconTextPadding; + const int text_width = std::min(text_size_.width(), + width() - insets.right() - text_x); + int text_y = (available_height - text_size_.height()) / 2 + insets.top(); + + if (text_width > 0) { + // Because the text button can (at times) draw multiple elements on the + // canvas, we can not mirror the button by simply flipping the canvas as + // doing this will mirror the text itself. Flipping the canvas will also + // make the icons look wrong because icons are almost always represented as + // direction insentisive bitmaps and such bitmaps should never be flipped + // horizontally. + // + // Due to the above, we must perform the flipping manually for RTL UIs. + gfx::Rect text_bounds(text_x, text_y, text_width, text_size_.height()); + text_bounds.set_x(MirroredLeftPointForRect(text_bounds)); + + if (for_drag) { +#if defined(OS_WIN) + // TODO(erg): Either port DrawStringWithHalo to linux or find an + // alternative here. + canvas->DrawStringWithHalo(text_, font_, color_, kHighlightColor, + text_bounds.x(), + text_bounds.y(), + text_bounds.width(), + text_bounds.height(), + l10n_util::DefaultCanvasTextAlignment()); +#endif + } else { + // Draw bevel highlight + canvas->DrawStringInt(text_, + font_, + kHighlightColor, + text_bounds.x() + 1, + text_bounds.y() + 1, + text_bounds.width(), + text_bounds.height()); + + canvas->DrawStringInt(text_, + font_, + color_, + text_bounds.x(), + text_bounds.y(), + text_bounds.width(), + text_bounds.height()); + } + } + + if (icon_.width() > 0) { + int icon_y = (available_height - icon_.height()) / 2 + insets.top(); + + // Mirroring the icon position if necessary. + gfx::Rect icon_bounds(icon_x, icon_y, icon_.width(), icon_.height()); + icon_bounds.set_x(MirroredLeftPointForRect(icon_bounds)); + canvas->DrawBitmapInt(icon_, icon_bounds.x(), icon_bounds.y()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// TextButton, View overrides: + +gfx::Size TextButton::GetPreferredSize() { + gfx::Insets insets = GetInsets(); + + // Use the max size to set the button boundaries. + gfx::Size prefsize(max_text_size_.width() + icon_.width() + insets.width(), + std::max(max_text_size_.height(), icon_.height()) + + insets.height()); + + if (icon_.width() > 0 && !text_.empty()) + prefsize.Enlarge(kIconTextPadding, 0); + + if (max_width_ > 0) + prefsize.set_width(std::min(max_width_, prefsize.width())); + + return prefsize; +} + +gfx::Size TextButton::GetMinimumSize() { + return max_text_size_; +} + +void TextButton::SetEnabled(bool enabled) { + if (enabled == IsEnabled()) + return; + CustomButton::SetEnabled(enabled); + color_ = enabled ? kEnabledColor : kDisabledColor; + SchedulePaint(); +} + +bool TextButton::OnMousePressed(const MouseEvent& e) { + return true; +} + +void TextButton::Paint(ChromeCanvas* canvas) { + Paint(canvas, false); +} + +} // namespace views diff --git a/views/controls/button/text_button.h b/views/controls/button/text_button.h new file mode 100644 index 0000000..16bac57 --- /dev/null +++ b/views/controls/button/text_button.h @@ -0,0 +1,138 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_BUTTON_TEXT_BUTTON_H_ +#define VIEWS_CONTROLS_BUTTON_TEXT_BUTTON_H_ + +#include "app/gfx/chrome_font.h" +#include "skia/include/SkBitmap.h" +#include "views/border.h" +#include "views/controls/button/custom_button.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// +// TextButtonBorder +// +// A Border subclass that paints a TextButton's background layer - +// basically the button frame in the hot/pushed states. +// +//////////////////////////////////////////////////////////////////////////////// +class TextButtonBorder : public Border { + public: + TextButtonBorder(); + virtual ~TextButtonBorder(); + + // Render the background for the provided view + virtual void Paint(const View& view, ChromeCanvas* canvas) const; + + // Returns the insets for the border. + virtual void GetInsets(gfx::Insets* insets) const; + + private: + DISALLOW_EVIL_CONSTRUCTORS(TextButtonBorder); + + // Images + struct MBBImageSet { + SkBitmap* top_left; + SkBitmap* top; + SkBitmap* top_right; + SkBitmap* left; + SkBitmap* center; + SkBitmap* right; + SkBitmap* bottom_left; + SkBitmap* bottom; + SkBitmap* bottom_right; + }; + MBBImageSet hot_set_; + MBBImageSet pushed_set_; +}; + + +//////////////////////////////////////////////////////////////////////////////// +// +// TextButton +// +// A button which displays text and/or and icon that can be changed in +// response to actions. TextButton reserves space for the largest string +// passed to SetText. To reset the cached max size invoke ClearMaxTextSize. +// +//////////////////////////////////////////////////////////////////////////////// +class TextButton : public CustomButton { + public: + TextButton(ButtonListener* listener, const std::wstring& text); + virtual ~TextButton(); + + // Call SetText once per string in your set of possible values at button + // creation time, so that it can contain the largest of them and avoid + // resizing the button when the text changes. + virtual void SetText(const std::wstring& text); + std::wstring text() const { return text_; } + + enum TextAlignment { + ALIGN_LEFT, + ALIGN_CENTER, + ALIGN_RIGHT + }; + + void set_alignment(TextAlignment alignment) { alignment_ = alignment; } + + // Sets the icon. + void SetIcon(const SkBitmap& icon); + SkBitmap icon() const { return icon_; } + + // TextButton remembers the maximum display size of the text passed to + // SetText. This method resets the cached maximum display size to the + // current size. + void ClearMaxTextSize(); + + void set_max_width(int max_width) { max_width_ = max_width; } + + // Paint the button into the specified canvas. If |for_drag| is true, the + // function paints a drag image representation into the canvas. + virtual void Paint(ChromeCanvas* canvas, bool for_drag); + + // Overridden from View: + virtual gfx::Size GetPreferredSize(); + virtual gfx::Size GetMinimumSize(); + virtual void SetEnabled(bool enabled); + + protected: + virtual bool OnMousePressed(const MouseEvent& e); + virtual void Paint(ChromeCanvas* canvas); + + private: + // The text string that is displayed in the button. + std::wstring text_; + + // The size of the text string. + gfx::Size text_size_; + + // Track the size of the largest text string seen so far, so that + // changing text_ will not resize the button boundary. + gfx::Size max_text_size_; + + // The alignment of the text string within the button. + TextAlignment alignment_; + + // The font used to paint the text. + ChromeFont font_; + + // Text color. + SkColor color_; + + // An icon displayed with the text. + SkBitmap icon_; + + // The width of the button will never be larger than this value. A value <= 0 + // indicates the width is not constrained. + int max_width_; + + DISALLOW_EVIL_CONSTRUCTORS(TextButton); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_BUTTON_TEXT_BUTTON_H_ diff --git a/views/controls/combo_box.cc b/views/controls/combo_box.cc new file mode 100644 index 0000000..eb079f7 --- /dev/null +++ b/views/controls/combo_box.cc @@ -0,0 +1,179 @@ +// Copyright (c) 2006-2008 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/controls/combo_box.h" + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "base/gfx/native_theme.h" +#include "base/gfx/rect.h" + +// Limit how small a combobox can be. +static const int kMinComboboxWidth = 148; + +// Add a couple extra pixels to the widths of comboboxes and combobox +// dropdowns so that text isn't too crowded. +static const int kComboboxExtraPaddingX = 6; + +namespace views { + +ComboBox::ComboBox(Model* model) + : model_(model), selected_item_(0), listener_(NULL), content_width_(0) { +} + +ComboBox::~ComboBox() { +} + +void ComboBox::SetListener(Listener* listener) { + listener_ = listener; +} + +gfx::Size ComboBox::GetPreferredSize() { + HWND hwnd = GetNativeControlHWND(); + if (!hwnd) + return gfx::Size(); + + COMBOBOXINFO cbi; + memset(reinterpret_cast<unsigned char*>(&cbi), 0, sizeof(cbi)); + cbi.cbSize = sizeof(cbi); + // Note: Don't use CB_GETCOMBOBOXINFO since that crashes on WOW64 systems + // when you have a global message hook installed. + GetComboBoxInfo(hwnd, &cbi); + gfx::Rect rect_item(cbi.rcItem); + gfx::Rect rect_button(cbi.rcButton); + gfx::Size border = gfx::NativeTheme::instance()->GetThemeBorderSize( + gfx::NativeTheme::MENULIST); + + // The padding value of '3' is the xy offset from the corner of the control + // to the corner of rcItem. It does not seem to be queryable from the theme. + // It is consistent on all versions of Windows from 2K to Vista, and is + // invariant with respect to the combobox border size. We could conceivably + // get this number from rect_item.x, but it seems fragile to depend on + // position here, inside of the layout code. + const int kItemOffset = 3; + int item_to_button_distance = std::max(kItemOffset - border.width(), 0); + + // The cx computation can be read as measuring from left to right. + int pref_width = std::max(kItemOffset + content_width_ + + kComboboxExtraPaddingX + + item_to_button_distance + rect_button.width() + + border.width(), kMinComboboxWidth); + // The two arguments to ::max below should be typically be equal. + int pref_height = std::max(rect_item.height() + 2 * kItemOffset, + rect_button.height() + 2 * border.height()); + return gfx::Size(pref_width, pref_height); +} + +// VK_ESCAPE should be handled by this view when the drop down list is active. +// In other words, the list should be closed instead of the dialog. +bool ComboBox::OverrideAccelerator(const Accelerator& accelerator) { + if (accelerator != Accelerator(VK_ESCAPE, false, false, false)) + return false; + + HWND hwnd = GetNativeControlHWND(); + if (!hwnd) + return false; + + return ::SendMessage(hwnd, CB_GETDROPPEDSTATE, 0, 0) != 0; +} + +HWND ComboBox::CreateNativeControl(HWND parent_container) { + HWND r = ::CreateWindowEx(GetAdditionalExStyle(), L"COMBOBOX", L"", + WS_CHILD | WS_VSCROLL | CBS_DROPDOWNLIST, + 0, 0, width(), height(), + parent_container, NULL, NULL, NULL); + HFONT font = ResourceBundle::GetSharedInstance(). + GetFont(ResourceBundle::BaseFont).hfont(); + SendMessage(r, WM_SETFONT, reinterpret_cast<WPARAM>(font), FALSE); + UpdateComboBoxFromModel(r); + return r; +} + +LRESULT ComboBox::OnCommand(UINT code, int id, HWND source) { + HWND hwnd = GetNativeControlHWND(); + if (!hwnd) + return 0; + + if (code == CBN_SELCHANGE && source == hwnd) { + LRESULT r = ::SendMessage(hwnd, CB_GETCURSEL, 0, 0); + if (r != CB_ERR) { + int prev_selected_item = selected_item_; + selected_item_ = static_cast<int>(r); + if (listener_) + listener_->ItemChanged(this, prev_selected_item, selected_item_); + } + } + return 0; +} + +LRESULT ComboBox::OnNotify(int w_param, LPNMHDR l_param) { + return 0; +} + +void ComboBox::UpdateComboBoxFromModel(HWND hwnd) { + ::SendMessage(hwnd, CB_RESETCONTENT, 0, 0); + ChromeFont font = ResourceBundle::GetSharedInstance().GetFont( + ResourceBundle::BaseFont); + int max_width = 0; + int num_items = model_->GetItemCount(this); + for (int i = 0; i < num_items; ++i) { + const std::wstring& text = model_->GetItemAt(this, i); + + // Inserting the Unicode formatting characters if necessary so that the + // text is displayed correctly in right-to-left UIs. + std::wstring localized_text; + const wchar_t* text_ptr = text.c_str(); + if (l10n_util::AdjustStringForLocaleDirection(text, &localized_text)) + text_ptr = localized_text.c_str(); + + ::SendMessage(hwnd, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(text_ptr)); + max_width = std::max(max_width, font.GetStringWidth(text)); + + } + content_width_ = max_width; + + if (num_items > 0) { + ::SendMessage(hwnd, CB_SETCURSEL, selected_item_, 0); + + // Set the width for the drop down while accounting for the scrollbar and + // borders. + if (num_items > ComboBox_GetMinVisible(hwnd)) + max_width += ::GetSystemMetrics(SM_CXVSCROLL); + // SM_CXEDGE would not be correct here, since the dropdown is flat, not 3D. + int kComboboxDropdownBorderSize = 1; + max_width += 2 * kComboboxDropdownBorderSize + kComboboxExtraPaddingX; + ::SendMessage(hwnd, CB_SETDROPPEDWIDTH, max_width, 0); + } +} + +void ComboBox::ModelChanged() { + HWND hwnd = GetNativeControlHWND(); + if (!hwnd) + return; + selected_item_ = std::min(0, model_->GetItemCount(this)); + UpdateComboBoxFromModel(hwnd); +} + +void ComboBox::SetSelectedItem(int index) { + selected_item_ = index; + HWND hwnd = GetNativeControlHWND(); + if (!hwnd) + return; + + // Note that we use CB_SETCURSEL and not CB_SELECTSTRING because on RTL + // locales the strings we get from our ComboBox::Model might be augmented + // with Unicode directionality marks before we insert them into the combo box + // and therefore we can not assume that the string we get from + // ComboBox::Model can be safely searched for and selected (which is what + // CB_SELECTSTRING does). + ::SendMessage(hwnd, CB_SETCURSEL, selected_item_, 0); +} + +int ComboBox::GetSelectedItem() { + return selected_item_; +} + +} // namespace views diff --git a/views/controls/combo_box.h b/views/controls/combo_box.h new file mode 100644 index 0000000..3e8eeae --- /dev/null +++ b/views/controls/combo_box.h @@ -0,0 +1,80 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_COMBO_BOX_H_ +#define VIEWS_CONTROLS_COMBO_BOX_H_ + +#include "views/controls/native_control.h" + +namespace views { +//////////////////////////////////////////////////////////////////////////////// +// +// ComboBox is a basic non editable combo box. It is initialized from a simple +// model. +// +//////////////////////////////////////////////////////////////////////////////// +class ComboBox : public NativeControl { + public: + class Model { + public: + // Return the number of items in the combo box. + virtual int GetItemCount(ComboBox* source) = 0; + + // Return the string that should be used to represent a given item. + virtual std::wstring GetItemAt(ComboBox* source, int index) = 0; + }; + + class Listener { + public: + // This is invoked once the selected item changed. + virtual void ItemChanged(ComboBox* combo_box, + int prev_index, + int new_index) = 0; + }; + + // |model is not owned by the combo box. + explicit ComboBox(Model* model); + virtual ~ComboBox(); + + // Register |listener| for item change events. + void SetListener(Listener* listener); + + // Overridden from View. + virtual gfx::Size GetPreferredSize(); + virtual bool OverrideAccelerator(const Accelerator& accelerator); + + // Overridden from NativeControl + virtual HWND CreateNativeControl(HWND parent_container); + virtual LRESULT OnCommand(UINT code, int id, HWND source); + virtual LRESULT OnNotify(int w_param, LPNMHDR l_param); + + // Inform the combo box that its model changed. + void ModelChanged(); + + // Set / Get the selected item. + void SetSelectedItem(int index); + int GetSelectedItem(); + + private: + // Update a combo box from our model. + void UpdateComboBoxFromModel(HWND hwnd); + + // Our model. + Model* model_; + + // The current selection. + int selected_item_; + + // Item change listener. + Listener* listener_; + + // The min width, in pixels, for the text content. + int content_width_; + + DISALLOW_EVIL_CONSTRUCTORS(ComboBox); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_COMBO_BOX_H_ diff --git a/views/controls/hwnd_view.cc b/views/controls/hwnd_view.cc new file mode 100644 index 0000000..677e60a --- /dev/null +++ b/views/controls/hwnd_view.cc @@ -0,0 +1,137 @@ +// Copyright (c) 2006-2008 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/controls/hwnd_view.h" + +#include "app/gfx/chrome_canvas.h" +#include "base/logging.h" +#include "views/focus/focus_manager.h" +#include "views/widget/widget.h" + +namespace views { + +static const char kViewClassName[] = "views/HWNDView"; + +HWNDView::HWNDView() { +} + +HWNDView::~HWNDView() { +} + +void HWNDView::Attach(HWND hwnd) { + DCHECK(native_view() == NULL); + DCHECK(hwnd) << "Impossible detatched tab case; See crbug.com/6316"; + + set_native_view(hwnd); + + // First hide the new window. We don't want anything to draw (like sub-hwnd + // borders), when we change the parent below. + ShowWindow(hwnd, SW_HIDE); + + // Need to set the HWND's parent before changing its size to avoid flashing. + ::SetParent(hwnd, GetWidget()->GetNativeView()); + Layout(); + + // Register with the focus manager so the associated view is focused when the + // native control gets the focus. + FocusManager::InstallFocusSubclass( + hwnd, associated_focus_view() ? associated_focus_view() : this); +} + +void HWNDView::Detach() { + DCHECK(native_view()); + FocusManager::UninstallFocusSubclass(native_view()); + set_native_view(NULL); + set_installed_clip(false); +} + +void HWNDView::Paint(ChromeCanvas* canvas) { + // The area behind our window is black, so during a fast resize (where our + // content doesn't draw over the full size of our HWND, and the HWND + // background color doesn't show up), we need to cover that blackness with + // something so that fast resizes don't result in black flash. + // + // It would be nice if this used some approximation of the page's + // current background color. + if (installed_clip()) + canvas->FillRectInt(SkColorSetRGB(255, 255, 255), 0, 0, width(), height()); +} + +std::string HWNDView::GetClassName() const { + return kViewClassName; +} + +void HWNDView::ViewHierarchyChanged(bool is_add, View *parent, View *child) { + if (!native_view()) + return; + + Widget* widget = GetWidget(); + if (is_add && widget) { + HWND parent_hwnd = ::GetParent(native_view()); + HWND widget_hwnd = widget->GetNativeView(); + if (parent_hwnd != widget_hwnd) + ::SetParent(native_view(), widget_hwnd); + if (IsVisibleInRootView()) + ::ShowWindow(native_view(), SW_SHOW); + else + ::ShowWindow(native_view(), SW_HIDE); + Layout(); + } else if (!is_add) { + ::ShowWindow(native_view(), SW_HIDE); + ::SetParent(native_view(), NULL); + } +} + +void HWNDView::Focus() { + ::SetFocus(native_view()); +} + +void HWNDView::InstallClip(int x, int y, int w, int h) { + HRGN clip_region = CreateRectRgn(x, y, x + w, y + h); + // NOTE: SetWindowRgn owns the region (as well as the deleting the + // current region), as such we don't delete the old region. + SetWindowRgn(native_view(), clip_region, FALSE); +} + +void HWNDView::UninstallClip() { + SetWindowRgn(native_view(), 0, FALSE); +} + +void HWNDView::ShowWidget(int x, int y, int w, int h) { + UINT swp_flags = SWP_DEFERERASE | + SWP_NOACTIVATE | + SWP_NOCOPYBITS | + SWP_NOOWNERZORDER | + SWP_NOZORDER; + // Only send the SHOWWINDOW flag if we're invisible, to avoid flashing. + if (!::IsWindowVisible(native_view())) + swp_flags = (swp_flags | SWP_SHOWWINDOW) & ~SWP_NOREDRAW; + + if (fast_resize()) { + // In a fast resize, we move the window and clip it with SetWindowRgn. + CRect rect; + GetWindowRect(native_view(), &rect); + ::SetWindowPos(native_view(), 0, x, y, rect.Width(), rect.Height(), + swp_flags); + + HRGN clip_region = CreateRectRgn(0, 0, w, h); + SetWindowRgn(native_view(), clip_region, FALSE); + set_installed_clip(true); + } else { + ::SetWindowPos(native_view(), 0, x, y, w, h, swp_flags); + } +} + +void HWNDView::HideWidget() { + if (!::IsWindowVisible(native_view())) + return; // Currently not visible, nothing to do. + + // The window is currently visible, but its clipped by another view. Hide + // it. + ::SetWindowPos(native_view(), 0, 0, 0, 0, 0, + SWP_HIDEWINDOW | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | + SWP_NOREDRAW | SWP_NOOWNERZORDER); +} + +} // namespace views diff --git a/views/controls/hwnd_view.h b/views/controls/hwnd_view.h new file mode 100644 index 0000000..a8d52b3 --- /dev/null +++ b/views/controls/hwnd_view.h @@ -0,0 +1,66 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_HWND_VIEW_H_ +#define VIEWS_CONTROLS_HWND_VIEW_H_ + +#include <string> + +#include "views/controls/native_view_host.h" + +namespace views { + +///////////////////////////////////////////////////////////////////////////// +// +// HWNDView class +// +// The HWNDView class hosts a native window handle (HWND) sizing it +// according to the bounds of the view. This is useful whenever you need to +// show a UI control that has a HWND (e.g. a native windows Edit control) +// within thew View hierarchy and benefit from the sizing/layout. +// +///////////////////////////////////////////////////////////////////////////// +// TODO: Rename this to NativeViewHostWin. +class HWNDView : public NativeViewHost { + public: + HWNDView(); + virtual ~HWNDView(); + + // Attach a window handle to this View, making the window it represents + // subject to sizing according to this View's parent container's Layout + // Manager's sizing heuristics. + // + // This object should be added to the view hierarchy before calling this + // function, which will expect the parent to be valid. + void Attach(HWND hwnd); + + // Detach the attached window handle. It will no longer be updated + void Detach(); + + // TODO(sky): convert this to native_view(). + HWND GetHWND() const { return native_view(); } + + virtual void Paint(ChromeCanvas* canvas); + + // Overridden from View. + virtual std::string GetClassName() const; + + protected: + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + + virtual void Focus(); + + // NativeHostView overrides. + virtual void InstallClip(int x, int y, int w, int h); + virtual void UninstallClip(); + virtual void ShowWidget(int x, int y, int w, int h); + virtual void HideWidget(); + + private: + DISALLOW_COPY_AND_ASSIGN(HWNDView); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_HWND_VIEW_H_ diff --git a/views/controls/image_view.cc b/views/controls/image_view.cc new file mode 100644 index 0000000..8f58744 --- /dev/null +++ b/views/controls/image_view.cc @@ -0,0 +1,170 @@ +// Copyright (c) 2006-2008 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/controls/image_view.h" + +#include "app/gfx/chrome_canvas.h" +#include "base/logging.h" + +namespace views { + +ImageView::ImageView() + : image_size_set_(false), + horiz_alignment_(CENTER), + vert_alignment_(CENTER) { +} + +ImageView::~ImageView() { +} + +void ImageView::SetImage(const SkBitmap& bm) { + image_ = bm; + SchedulePaint(); +} + +void ImageView::SetImage(SkBitmap* bm) { + if (bm) { + SetImage(*bm); + } else { + SkBitmap t; + SetImage(t); + } +} + +const SkBitmap& ImageView::GetImage() { + return image_; +} + +void ImageView::SetImageSize(const gfx::Size& image_size) { + image_size_set_ = true; + image_size_ = image_size; +} + +bool ImageView::GetImageSize(gfx::Size* image_size) { + DCHECK(image_size); + if (image_size_set_) + *image_size = image_size_; + return image_size_set_; +} + +void ImageView::ResetImageSize() { + image_size_set_ = false; +} + +gfx::Size ImageView::GetPreferredSize() { + if (image_size_set_) { + gfx::Size image_size; + GetImageSize(&image_size); + return image_size; + } + return gfx::Size(image_.width(), image_.height()); +} + +void ImageView::ComputeImageOrigin(int image_width, int image_height, + int *x, int *y) { + // In order to properly handle alignment of images in RTL locales, we need + // to flip the meaning of trailing and leading. For example, if the + // horizontal alignment is set to trailing, then we'll use left alignment for + // the image instead of right alignment if the UI layout is RTL. + Alignment actual_horiz_alignment = horiz_alignment_; + if (UILayoutIsRightToLeft()) { + if (horiz_alignment_ == TRAILING) + actual_horiz_alignment = LEADING; + if (horiz_alignment_ == LEADING) + actual_horiz_alignment = TRAILING; + } + + switch(actual_horiz_alignment) { + case LEADING: + *x = 0; + break; + case TRAILING: + *x = width() - image_width; + break; + case CENTER: + *x = (width() - image_width) / 2; + break; + default: + NOTREACHED(); + } + + switch (vert_alignment_) { + case LEADING: + *y = 0; + break; + case TRAILING: + *y = height() - image_height; + break; + case CENTER: + *y = (height() - image_height) / 2; + break; + default: + NOTREACHED(); + } +} + +void ImageView::Paint(ChromeCanvas* canvas) { + View::Paint(canvas); + int image_width = image_.width(); + int image_height = image_.height(); + + if (image_width == 0 || image_height == 0) + return; + + int x, y; + if (image_size_set_ && + (image_size_.width() != image_width || + image_size_.width() != image_height)) { + // Resize case + image_.buildMipMap(false); + ComputeImageOrigin(image_size_.width(), image_size_.height(), &x, &y); + canvas->DrawBitmapInt(image_, 0, 0, image_width, image_height, + x, y, image_size_.width(), image_size_.height(), + true); + } else { + ComputeImageOrigin(image_width, image_height, &x, &y); + canvas->DrawBitmapInt(image_, x, y); + } +} + +void ImageView::SetHorizontalAlignment(Alignment ha) { + if (ha != horiz_alignment_) { + horiz_alignment_ = ha; + SchedulePaint(); + } +} + +ImageView::Alignment ImageView::GetHorizontalAlignment() { + return horiz_alignment_; +} + +void ImageView::SetVerticalAlignment(Alignment va) { + if (va != vert_alignment_) { + vert_alignment_ = va; + SchedulePaint(); + } +} + +ImageView::Alignment ImageView::GetVerticalAlignment() { + return vert_alignment_; +} + +void ImageView::SetTooltipText(const std::wstring& tooltip) { + tooltip_text_ = tooltip; +} + +std::wstring ImageView::GetTooltipText() { + return tooltip_text_; +} + +bool ImageView::GetTooltipText(int x, int y, std::wstring* tooltip) { + if (tooltip_text_.empty()) { + return false; + } else { + * tooltip = GetTooltipText(); + return true; + } +} + +} // namespace views diff --git a/views/controls/image_view.h b/views/controls/image_view.h new file mode 100644 index 0000000..b7ac792 --- /dev/null +++ b/views/controls/image_view.h @@ -0,0 +1,107 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_IMAGE_VIEW_H_ +#define VIEWS_CONTROLS_IMAGE_VIEW_H_ + +#include "skia/include/SkBitmap.h" +#include "views/view.h" + +class ChromeCanvas; + +namespace views { + +///////////////////////////////////////////////////////////////////////////// +// +// ImageView class. +// +// An ImageView can display an image from an SkBitmap. If a size is provided, +// the ImageView will resize the provided image to fit if it is too big or will +// center the image if smaller. Otherwise, the preferred size matches the +// provided image size. +// +///////////////////////////////////////////////////////////////////////////// +class ImageView : public View { + public: + enum Alignment { + LEADING = 0, + CENTER, + TRAILING + }; + + ImageView(); + virtual ~ImageView(); + + // Set the bitmap that should be displayed. + void SetImage(const SkBitmap& bm); + + // Set the bitmap that should be displayed from a pointer. Reset the image + // if the pointer is NULL. The pointer contents is copied in the receiver's + // bitmap. + void SetImage(SkBitmap* bm); + + // Returns the bitmap currently displayed or NULL of none is currently set. + // The returned bitmap is still owned by the ImageView. + const SkBitmap& GetImage(); + + // Set the desired image size for the receiving ImageView. + void SetImageSize(const gfx::Size& image_size); + + // Return the preferred size for the receiving view. Returns false if the + // preferred size is not defined, which means that the view uses the image + // size. + bool GetImageSize(gfx::Size* image_size); + + // Reset the image size to the current image dimensions. + void ResetImageSize(); + + // Set / Get the horizontal alignment. + void SetHorizontalAlignment(Alignment ha); + Alignment GetHorizontalAlignment(); + + // Set / Get the vertical alignment. + void SetVerticalAlignment(Alignment va); + Alignment GetVerticalAlignment(); + + // Set / Get the tooltip text. + void SetTooltipText(const std::wstring& tooltip); + std::wstring GetTooltipText(); + + // Return whether the image should be centered inside the view. + // Overriden from View + virtual gfx::Size GetPreferredSize(); + virtual void Paint(ChromeCanvas* canvas); + + // Overriden from View. + virtual bool GetTooltipText(int x, int y, std::wstring* tooltip); + + private: + // Compute the image origin given the desired size and the receiver alignment + // properties. + void ComputeImageOrigin(int image_width, int image_height, int *x, int *y); + + // Whether the image size is set. + bool image_size_set_; + + // The actual image size. + gfx::Size image_size_; + + // The underlying bitmap. + SkBitmap image_; + + // Horizontal alignment. + Alignment horiz_alignment_; + + // Vertical alignment. + Alignment vert_alignment_; + + // The current tooltip text. + std::wstring tooltip_text_; + + DISALLOW_EVIL_CONSTRUCTORS(ImageView); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_IMAGE_VIEW_H_ diff --git a/views/controls/label.cc b/views/controls/label.cc new file mode 100644 index 0000000..1dc0b54 --- /dev/null +++ b/views/controls/label.cc @@ -0,0 +1,444 @@ +// Copyright (c) 2006-2008 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/controls/label.h" + +#include <math.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/gfx/insets.h" +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "chrome/common/gfx/text_elider.h" +#include "views/background.h" + +namespace views { + +const char Label::kViewClassName[] = "views/Label"; + +static const SkColor kEnabledColor = SK_ColorBLACK; +static const SkColor kDisabledColor = SkColorSetRGB(161, 161, 146); +static const int kFocusBorderPadding = 1; + +Label::Label() { + Init(L"", GetDefaultFont()); +} + +Label::Label(const std::wstring& text) { + Init(text, GetDefaultFont()); +} + +Label::Label(const std::wstring& text, const ChromeFont& font) { + Init(text, font); +} + +void Label::Init(const std::wstring& text, const ChromeFont& font) { + contains_mouse_ = false; + font_ = font; + text_size_valid_ = false; + SetText(text); + url_set_ = false; + color_ = kEnabledColor; + horiz_alignment_ = ALIGN_CENTER; + is_multi_line_ = false; + allow_character_break_ = false; + collapse_when_hidden_ = false; + rtl_alignment_mode_ = USE_UI_ALIGNMENT; + paint_as_focused_ = false; + has_focus_border_ = false; +} + +Label::~Label() { +} + +gfx::Size Label::GetPreferredSize() { + gfx::Size prefsize; + + // Return a size of (0, 0) if the label is not visible and if the + // collapse_when_hidden_ flag is set. + // TODO(munjal): This logic probably belongs to the View class. But for now, + // put it here since putting it in View class means all inheriting classes + // need ot respect the collapse_when_hidden_ flag. + if (!IsVisible() && collapse_when_hidden_) + return prefsize; + + if (is_multi_line_) { + int w = width(), h = 0; + ChromeCanvas::SizeStringInt(text_, font_, &w, &h, ComputeMultiLineFlags()); + prefsize.SetSize(w, h); + } else { + prefsize = GetTextSize(); + } + + gfx::Insets insets = GetInsets(); + prefsize.Enlarge(insets.width(), insets.height()); + return prefsize; +} + +int Label::ComputeMultiLineFlags() { + int flags = ChromeCanvas::MULTI_LINE; + if (allow_character_break_) + flags |= ChromeCanvas::CHARACTER_BREAK; + switch (horiz_alignment_) { + case ALIGN_LEFT: + flags |= ChromeCanvas::TEXT_ALIGN_LEFT; + break; + case ALIGN_CENTER: + flags |= ChromeCanvas::TEXT_ALIGN_CENTER; + break; + case ALIGN_RIGHT: + flags |= ChromeCanvas::TEXT_ALIGN_RIGHT; + break; + } + return flags; +} + +void Label::CalculateDrawStringParams( + std::wstring* paint_text, gfx::Rect* text_bounds, int* flags) { + DCHECK(paint_text && text_bounds && flags); + + if (url_set_) { + // TODO(jungshik) : Figure out how to get 'intl.accept_languages' + // preference and use it when calling ElideUrl. + *paint_text = gfx::ElideUrl(url_, font_, width(), std::wstring()); + + // An URLs is always treated as an LTR text and therefore we should + // explicitly mark it as such if the locale is RTL so that URLs containing + // Hebrew or Arabic characters are displayed correctly. + // + // Note that we don't check the View's UI layout setting in order to + // determine whether or not to insert the special Unicode formatting + // characters. We use the locale settings because an URL is always treated + // as an LTR string, even if its containing view does not use an RTL UI + // layout. + if (l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT) + l10n_util::WrapStringWithLTRFormatting(paint_text); + } else { + *paint_text = text_; + } + + if (is_multi_line_) { + gfx::Insets insets = GetInsets(); + text_bounds->SetRect(insets.left(), + insets.top(), + width() - insets.width(), + height() - insets.height()); + *flags = ComputeMultiLineFlags(); + } else { + *text_bounds = GetTextBounds(); + *flags = 0; + } +} + +void Label::Paint(ChromeCanvas* canvas) { + PaintBackground(canvas); + std::wstring paint_text; + gfx::Rect text_bounds; + int flags = 0; + CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + canvas->DrawStringInt(paint_text, + font_, + color_, + text_bounds.x(), + text_bounds.y(), + text_bounds.width(), + text_bounds.height(), + flags); + + // The focus border always hugs the text, regardless of the label's bounds. + if (HasFocus() || paint_as_focused_) { + int w = text_bounds.width(); + int h = 0; + // We explicitly OR in MULTI_LINE here since SizeStringInt seems to return + // an incorrect height for single line text when the MULTI_LINE flag isn't + // specified. o_O... + ChromeCanvas::SizeStringInt(paint_text, font_, &w, &h, + flags | ChromeCanvas::MULTI_LINE); + gfx::Rect focus_rect = text_bounds; + focus_rect.set_width(w); + focus_rect.set_height(h); + focus_rect.Inset(-kFocusBorderPadding, -kFocusBorderPadding); + canvas->DrawFocusRect(MirroredLeftPointForRect(focus_rect), focus_rect.y(), + focus_rect.width(), focus_rect.height()); + } +} + +void Label::PaintBackground(ChromeCanvas* canvas) { + const Background* bg = contains_mouse_ ? GetMouseOverBackground() : NULL; + if (!bg) + bg = background(); + if (bg) + bg->Paint(canvas, this); +} + +void Label::SetFont(const ChromeFont& font) { + font_ = font; + text_size_valid_ = false; + SchedulePaint(); +} + +ChromeFont Label::GetFont() const { + return font_; +} + +void Label::SetText(const std::wstring& text) { + text_ = text; + url_set_ = false; + text_size_valid_ = false; + SchedulePaint(); +} + +void Label::SetURL(const GURL& url) { + url_ = url; + text_ = UTF8ToWide(url_.spec()); + url_set_ = true; + text_size_valid_ = false; + SchedulePaint(); +} + +const std::wstring Label::GetText() const { + if (url_set_) + return UTF8ToWide(url_.spec()); + else + return text_; +} + +const GURL Label::GetURL() const { + if (url_set_) + return url_; + else + return GURL(WideToUTF8(text_)); +} + +gfx::Size Label::GetTextSize() { + if (!text_size_valid_) { + text_size_.SetSize(font_.GetStringWidth(text_), font_.height()); + text_size_valid_ = true; + } + + if (text_size_valid_) + return text_size_; + return gfx::Size(); +} + +int Label::GetHeightForWidth(int w) { + if (is_multi_line_) { + gfx::Insets insets = GetInsets(); + w = std::max<int>(0, w - insets.width()); + int h = 0; + ChromeCanvas cc(0, 0, true); + cc.SizeStringInt(text_, font_, &w, &h, ComputeMultiLineFlags()); + return h + insets.height(); + } + + return View::GetHeightForWidth(w); +} + +std::string Label::GetClassName() const { + return kViewClassName; +} + +void Label::SetColor(const SkColor& color) { + color_ = color; +} + +const SkColor Label::GetColor() const { + return color_; +} + +void Label::SetHorizontalAlignment(Alignment a) { + // If the View's UI layout is right-to-left and rtl_alignment_mode_ is + // USE_UI_ALIGNMENT, we need to flip the alignment so that the alignment + // settings take into account the text directionality. + if (UILayoutIsRightToLeft() && rtl_alignment_mode_ == USE_UI_ALIGNMENT) { + if (a == ALIGN_LEFT) + a = ALIGN_RIGHT; + else if (a == ALIGN_RIGHT) + a = ALIGN_LEFT; + } + if (horiz_alignment_ != a) { + horiz_alignment_ = a; + SchedulePaint(); + } +} + +Label::Alignment Label::GetHorizontalAlignment() const { + return horiz_alignment_; +} + +void Label::SetRTLAlignmentMode(RTLAlignmentMode mode) { + rtl_alignment_mode_ = mode; +} + +Label::RTLAlignmentMode Label::GetRTLAlignmentMode() const { + return rtl_alignment_mode_; +} + +void Label::SetMultiLine(bool f) { + if (f != is_multi_line_) { + is_multi_line_ = f; + SchedulePaint(); + } +} + +void Label::SetAllowCharacterBreak(bool f) { + if (f != allow_character_break_) { + allow_character_break_ = f; + SchedulePaint(); + } +} + +bool Label::IsMultiLine() { + return is_multi_line_; +} + +void Label::SetTooltipText(const std::wstring& tooltip_text) { + tooltip_text_ = tooltip_text; +} + +bool Label::GetTooltipText(int x, int y, std::wstring* tooltip) { + DCHECK(tooltip); + + // If a tooltip has been explicitly set, use it. + if (!tooltip_text_.empty()) { + tooltip->assign(tooltip_text_); + return true; + } + + // Show the full text if the text does not fit. + if (!is_multi_line_ && font_.GetStringWidth(text_) > width()) { + *tooltip = text_; + return true; + } + return false; +} + +void Label::OnMouseMoved(const MouseEvent& e) { + UpdateContainsMouse(e); +} + +void Label::OnMouseEntered(const MouseEvent& event) { + UpdateContainsMouse(event); +} + +void Label::OnMouseExited(const MouseEvent& event) { + SetContainsMouse(false); +} + +void Label::SetMouseOverBackground(Background* background) { + mouse_over_background_.reset(background); +} + +const Background* Label::GetMouseOverBackground() const { + return mouse_over_background_.get(); +} + +void Label::SetEnabled(bool enabled) { + if (enabled == enabled_) + return; + View::SetEnabled(enabled); + SetColor(enabled ? kEnabledColor : kDisabledColor); +} + +gfx::Insets Label::GetInsets() const { + gfx::Insets insets = View::GetInsets(); + if (IsFocusable() || has_focus_border_) { + insets += gfx::Insets(kFocusBorderPadding, kFocusBorderPadding, + kFocusBorderPadding, kFocusBorderPadding); + } + return insets; +} + +// static +ChromeFont Label::GetDefaultFont() { + return ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont); +} + +void Label::UpdateContainsMouse(const MouseEvent& event) { + SetContainsMouse(GetTextBounds().Contains(event.x(), event.y())); +} + +void Label::SetContainsMouse(bool contains_mouse) { + if (contains_mouse_ == contains_mouse) + return; + contains_mouse_ = contains_mouse; + if (GetMouseOverBackground()) + SchedulePaint(); +} + +gfx::Rect Label::GetTextBounds() { + gfx::Size text_size = GetTextSize(); + gfx::Insets insets = GetInsets(); + int avail_width = width() - insets.width(); + // Respect the size set by the owner view + text_size.set_width(std::min(avail_width, text_size.width())); + + int text_y = insets.top() + + (height() - text_size.height() - insets.height()) / 2; + int text_x; + switch (horiz_alignment_) { + case ALIGN_LEFT: + text_x = insets.left(); + break; + case ALIGN_CENTER: + // We put any extra margin pixel on the left rather than the right, since + // GetTextExtentPoint32() can report a value one too large on the right. + text_x = insets.left() + (avail_width + 1 - text_size.width()) / 2; + break; + case ALIGN_RIGHT: + text_x = width() - insets.right() - text_size.width(); + break; + default: + NOTREACHED(); + text_x = 0; + break; + } + return gfx::Rect(text_x, text_y, text_size.width(), text_size.height()); +} + +void Label::SizeToFit(int max_width) { + DCHECK(is_multi_line_); + + std::vector<std::wstring> lines; + SplitString(text_, L'\n', &lines); + + int label_width = 0; + for (std::vector<std::wstring>::const_iterator iter = lines.begin(); + iter != lines.end(); ++iter) { + label_width = std::max(label_width, font_.GetStringWidth(*iter)); + } + + gfx::Insets insets = GetInsets(); + label_width += insets.width(); + + if (max_width > 0) + label_width = std::min(label_width, max_width); + + SetBounds(x(), y(), label_width, 0); + SizeToPreferredSize(); +} + +bool Label::GetAccessibleRole(AccessibilityTypes::Role* role) { + DCHECK(role); + + *role = AccessibilityTypes::ROLE_TEXT; + return true; +} + +bool Label::GetAccessibleName(std::wstring* name) { + *name = GetText(); + return true; +} + +bool Label::GetAccessibleState(AccessibilityTypes::State* state) { + DCHECK(state); + + *state = AccessibilityTypes::STATE_READONLY; + return true; +} + +} // namespace views diff --git a/views/controls/label.h b/views/controls/label.h new file mode 100644 index 0000000..fa620e4 --- /dev/null +++ b/views/controls/label.h @@ -0,0 +1,250 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_LABEL_H_ +#define VIEWS_CONTROLS_LABEL_H_ + +#include "app/gfx/chrome_font.h" +#include "googleurl/src/gurl.h" +#include "skia/include/SkColor.h" +#include "testing/gtest/include/gtest/gtest_prod.h" +#include "views/view.h" + +namespace views { + +///////////////////////////////////////////////////////////////////////////// +// +// Label class +// +// A label is a view subclass that can display a string. +// +///////////////////////////////////////////////////////////////////////////// +class Label : public View { + public: + enum Alignment { ALIGN_LEFT = 0, + ALIGN_CENTER, + ALIGN_RIGHT }; + + // The following enum is used to indicate whether using the Chrome UI's + // alignment as the label's alignment, or autodetecting the label's + // alignment. + // + // If the label text originates from the Chrome UI, we should use the Chrome + // UI's alignment as the label's alignment. + // + // If the text originates from a web page, the text's alignment is determined + // based on the first character with strong directionality, disregarding what + // directionality the Chrome UI is. And its alignment will not be flipped + // around in RTL locales. + enum RTLAlignmentMode { + USE_UI_ALIGNMENT = 0, + AUTO_DETECT_ALIGNMENT + }; + + // The view class name. + static const char kViewClassName[]; + + // Create a new label with a default font and empty value + Label(); + + // Create a new label with a default font + explicit Label(const std::wstring& text); + + Label(const std::wstring& text, const ChromeFont& font); + + virtual ~Label(); + + // Overridden to compute the size required to display this label + virtual gfx::Size GetPreferredSize(); + + // Return the height necessary to display this label with the provided width. + // This method is used to layout multi-line labels. It is equivalent to + // GetPreferredSize().height() if the receiver is not multi-line + virtual int GetHeightForWidth(int w); + + // Returns views/Label. + virtual std::string GetClassName() const; + + // Overridden to paint + virtual void Paint(ChromeCanvas* canvas); + + // If the mouse is over the label, and a mouse over background has been + // specified, its used. Otherwise super's implementation is invoked + virtual void PaintBackground(ChromeCanvas* canvas); + + // Set the font. + void SetFont(const ChromeFont& font); + + // Return the font used by this label + ChromeFont GetFont() const; + + // Set the label text. + void SetText(const std::wstring& text); + + // Return the label text. + const std::wstring GetText() const; + + // Set URL Value - text_ is set to spec(). + void SetURL(const GURL& url); + + // Return the label URL. + const GURL GetURL() const; + + // Set the color + virtual void SetColor(const SkColor& color); + + // Return a reference to the currently used color + virtual const SkColor GetColor() const; + + // Set horizontal alignment. If the locale is RTL, and the RTL alignment + // setting is set as USE_UI_ALIGNMENT, the alignment is flipped around. + // + // Caveat: for labels originating from a web page, the RTL alignment mode + // should be reset to AUTO_DETECT_ALIGNMENT before the horizontal alignment + // is set. Otherwise, the label's alignment specified as a parameter will be + // flipped in RTL locales. Please see the comments in SetRTLAlignmentMode for + // more information. + void SetHorizontalAlignment(Alignment a); + + Alignment GetHorizontalAlignment() const; + + // Set the RTL alignment mode. The RTL alignment mode is initialized to + // USE_UI_ALIGNMENT when the label is constructed. USE_UI_ALIGNMENT applies + // to every label that originates from the Chrome UI. However, if the label + // originates from a web page, its alignment should not be flipped around for + // RTL locales. For such labels, we need to set the RTL alignment mode to + // AUTO_DETECT_ALIGNMENT so that subsequent SetHorizontalAlignment() calls + // will not flip the label's alignment around. + void SetRTLAlignmentMode(RTLAlignmentMode mode); + + RTLAlignmentMode GetRTLAlignmentMode() const; + + // Set whether the label text can wrap on multiple lines. + // Default is false. + void SetMultiLine(bool f); + + // Set whether the label text can be split on words. + // Default is false. This only works when is_multi_line is true. + void SetAllowCharacterBreak(bool f); + + // Return whether the label text can wrap on multiple lines + bool IsMultiLine(); + + // Sets the tooltip text. Default behavior for a label (single-line) is to + // show the full text if it is wider than its bounds. Calling this overrides + // the default behavior and lets you set a custom tooltip. To revert to + // default behavior, call this with an empty string. + void SetTooltipText(const std::wstring& tooltip_text); + + // Gets the tooltip text for labels that are wider than their bounds, except + // when the label is multiline, in which case it just returns false (no + // tooltip). If a custom tooltip has been specified with SetTooltipText() + // it is returned instead. + virtual bool GetTooltipText(int x, int y, std::wstring* tooltip); + + // Mouse enter/exit are overridden to render mouse over background color. + // These invoke SetContainsMouse as necessary. + virtual void OnMouseMoved(const MouseEvent& e); + virtual void OnMouseEntered(const MouseEvent& event); + virtual void OnMouseExited(const MouseEvent& event); + + // The background color to use when the mouse is over the label. Label + // takes ownership of the Background. + void SetMouseOverBackground(Background* background); + const Background* GetMouseOverBackground() const; + + // Sets the enabled state. Setting the enabled state resets the color. + virtual void SetEnabled(bool enabled); + + // Overridden from View: + virtual gfx::Insets GetInsets() const; + + // Resizes the label so its width is set to the width of the longest line and + // its height deduced accordingly. + // This is only intended for multi-line labels and is useful when the label's + // text contains several lines separated with \n. + // |max_width| is the maximum width that will be used (longer lines will be + // wrapped). If 0, no maximum width is enforced. + void SizeToFit(int max_width); + + // Accessibility accessors, overridden from View. + virtual bool GetAccessibleRole(AccessibilityTypes::Role* role); + virtual bool GetAccessibleName(std::wstring* name); + virtual bool GetAccessibleState(AccessibilityTypes::State* state); + + // Gets/sets the flag to determine whether the label should be collapsed when + // it's hidden (not visible). If this flag is true, the label will return a + // preferred size of (0, 0) when it's not visible. + void set_collapse_when_hidden(bool value) { collapse_when_hidden_ = value; } + bool collapse_when_hidden() const { return collapse_when_hidden_; } + + void set_paint_as_focused(bool paint_as_focused) { + paint_as_focused_ = paint_as_focused; + } + void set_has_focus_border(bool has_focus_border) { + has_focus_border_ = has_focus_border; + } + + private: + // These tests call CalculateDrawStringParams in order to verify the + // calculations done for drawing text. + FRIEND_TEST(LabelTest, DrawSingleLineString); + FRIEND_TEST(LabelTest, DrawMultiLineString); + + static ChromeFont GetDefaultFont(); + + // Returns parameters to be used for the DrawString call. + void CalculateDrawStringParams(std::wstring* paint_text, + gfx::Rect* text_bounds, + int* flags); + + // If the mouse is over the text, SetContainsMouse(true) is invoked, otherwise + // SetContainsMouse(false) is invoked. + void UpdateContainsMouse(const MouseEvent& event); + + // Updates whether the mouse is contained in the Label. If the new value + // differs from the current value, and a mouse over background is specified, + // SchedulePaint is invoked. + void SetContainsMouse(bool contains_mouse); + + // Returns where the text is drawn, in the receivers coordinate system. + gfx::Rect GetTextBounds(); + + int ComputeMultiLineFlags(); + gfx::Size GetTextSize(); + void Init(const std::wstring& text, const ChromeFont& font); + std::wstring text_; + GURL url_; + ChromeFont font_; + SkColor color_; + gfx::Size text_size_; + bool text_size_valid_; + bool is_multi_line_; + bool allow_character_break_; + bool url_set_; + Alignment horiz_alignment_; + std::wstring tooltip_text_; + // Whether the mouse is over this label. + bool contains_mouse_; + scoped_ptr<Background> mouse_over_background_; + // Whether to collapse the label when it's not visible. + bool collapse_when_hidden_; + // The following member variable is used to control whether the alignment + // needs to be flipped around for RTL locales. Please refer to the definition + // of RTLAlignmentMode for more information. + RTLAlignmentMode rtl_alignment_mode_; + // When embedded in a larger control that is focusable, setting this flag + // allows this view to be painted as focused even when it is itself not. + bool paint_as_focused_; + // When embedded in a larger control that is focusable, setting this flag + // allows this view to reserve space for a focus border that it otherwise + // might not have because it is not itself focusable. + bool has_focus_border_; + + DISALLOW_COPY_AND_ASSIGN(Label); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_LABEL_H_ diff --git a/views/controls/label_unittest.cc b/views/controls/label_unittest.cc new file mode 100644 index 0000000..1ac74b1 --- /dev/null +++ b/views/controls/label_unittest.cc @@ -0,0 +1,441 @@ +// Copyright (c) 2006-2008 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 "app/gfx/chrome_canvas.h" +#include "app/l10n_util.h" +#include "base/string_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "views/border.h" +#include "views/controls/label.h" + +namespace views { + +// All text sizing measurements (width and height) should be greater than this. +const int kMinTextDimension = 4; + +TEST(LabelTest, FontProperty) { + Label label; + std::wstring font_name(L"courier"); + ChromeFont font = ChromeFont::CreateFont(font_name, 30); + label.SetFont(font); + ChromeFont font_used = label.GetFont(); + EXPECT_STREQ(font_name.c_str(), font_used.FontName().c_str()); + EXPECT_EQ(30, font_used.FontSize()); +} + +TEST(LabelTest, TextProperty) { + Label label; + std::wstring test_text(L"A random string."); + label.SetText(test_text); + EXPECT_STREQ(test_text.c_str(), label.GetText().c_str()); +} + +TEST(LabelTest, UrlProperty) { + Label label; + std::string my_url("http://www.orkut.com/some/Random/path"); + GURL url(my_url); + label.SetURL(url); + EXPECT_STREQ(my_url.c_str(), label.GetURL().spec().c_str()); + EXPECT_STREQ(UTF8ToWide(my_url).c_str(), label.GetText().c_str()); +} + +TEST(LabelTest, ColorProperty) { + Label label; + SkColor color = SkColorSetARGB(20, 40, 10, 5); + label.SetColor(color); + EXPECT_EQ(color, label.GetColor()); +} + +TEST(LabelTest, AlignmentProperty) { + Label label; + bool reverse_alignment = + l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT; + + label.SetHorizontalAlignment(Label::ALIGN_RIGHT); + EXPECT_EQ( + reverse_alignment ? Label::ALIGN_LEFT : Label::ALIGN_RIGHT, + label.GetHorizontalAlignment()); + label.SetHorizontalAlignment(Label::ALIGN_LEFT); + EXPECT_EQ( + reverse_alignment ? Label::ALIGN_RIGHT : Label::ALIGN_LEFT, + label.GetHorizontalAlignment()); + label.SetHorizontalAlignment(Label::ALIGN_CENTER); + EXPECT_EQ(Label::ALIGN_CENTER, label.GetHorizontalAlignment()); + + // The label's alignment should not be flipped if the RTL alignment mode + // is AUTO_DETECT_ALIGNMENT. + label.SetRTLAlignmentMode(Label::AUTO_DETECT_ALIGNMENT); + label.SetHorizontalAlignment(Label::ALIGN_RIGHT); + EXPECT_EQ(Label::ALIGN_RIGHT, label.GetHorizontalAlignment()); + label.SetHorizontalAlignment(Label::ALIGN_LEFT); + EXPECT_EQ(Label::ALIGN_LEFT, label.GetHorizontalAlignment()); + label.SetHorizontalAlignment(Label::ALIGN_CENTER); + EXPECT_EQ(Label::ALIGN_CENTER, label.GetHorizontalAlignment()); +} + +TEST(LabelTest, RTLAlignmentModeProperty) { + Label label; + EXPECT_EQ(Label::USE_UI_ALIGNMENT, label.GetRTLAlignmentMode()); + + label.SetRTLAlignmentMode(Label::AUTO_DETECT_ALIGNMENT); + EXPECT_EQ(Label::AUTO_DETECT_ALIGNMENT, label.GetRTLAlignmentMode()); + + label.SetRTLAlignmentMode(Label::USE_UI_ALIGNMENT); + EXPECT_EQ(Label::USE_UI_ALIGNMENT, label.GetRTLAlignmentMode()); +} + +TEST(LabelTest, MultiLineProperty) { + Label label; + EXPECT_FALSE(label.IsMultiLine()); + label.SetMultiLine(true); + EXPECT_TRUE(label.IsMultiLine()); + label.SetMultiLine(false); + EXPECT_FALSE(label.IsMultiLine()); +} + +TEST(LabelTest, TooltipProperty) { + Label label; + std::wstring test_text(L"My cool string."); + label.SetText(test_text); + + std::wstring tooltip; + EXPECT_TRUE(label.GetTooltipText(0, 0, &tooltip)); + EXPECT_STREQ(test_text.c_str(), tooltip.c_str()); + + std::wstring tooltip_text(L"The tooltip!"); + label.SetTooltipText(tooltip_text); + EXPECT_TRUE(label.GetTooltipText(0, 0, &tooltip)); + EXPECT_STREQ(tooltip_text.c_str(), tooltip.c_str()); + + std::wstring empty_text; + label.SetTooltipText(empty_text); + EXPECT_TRUE(label.GetTooltipText(0, 0, &tooltip)); + EXPECT_STREQ(test_text.c_str(), tooltip.c_str()); + + // Make the label big enough to hold the text + // and expect there to be no tooltip. + label.SetBounds(0, 0, 1000, 40); + EXPECT_FALSE(label.GetTooltipText(0, 0, &tooltip)); + + // Verify that setting the tooltip still shows it. + label.SetTooltipText(tooltip_text); + EXPECT_TRUE(label.GetTooltipText(0, 0, &tooltip)); + EXPECT_STREQ(tooltip_text.c_str(), tooltip.c_str()); + // Clear out the tooltip. + label.SetTooltipText(empty_text); + + // Shrink the bounds and the tooltip should come back. + label.SetBounds(0, 0, 1, 1); + EXPECT_TRUE(label.GetTooltipText(0, 0, &tooltip)); + + // Make the label multiline and there is no tooltip again. + label.SetMultiLine(true); + EXPECT_FALSE(label.GetTooltipText(0, 0, &tooltip)); + + // Verify that setting the tooltip still shows it. + label.SetTooltipText(tooltip_text); + EXPECT_TRUE(label.GetTooltipText(0, 0, &tooltip)); + EXPECT_STREQ(tooltip_text.c_str(), tooltip.c_str()); + // Clear out the tooltip. + label.SetTooltipText(empty_text); +} + +TEST(LabelTest, Accessibility) { + Label label; + std::wstring test_text(L"My special text."); + label.SetText(test_text); + + AccessibilityTypes::Role role; + EXPECT_TRUE(label.GetAccessibleRole(&role)); + EXPECT_EQ(AccessibilityTypes::ROLE_TEXT, role); + + std::wstring name; + EXPECT_TRUE(label.GetAccessibleName(&name)); + EXPECT_STREQ(test_text.c_str(), name.c_str()); + + AccessibilityTypes::State state; + EXPECT_TRUE(label.GetAccessibleState(&state)); + EXPECT_EQ(AccessibilityTypes::STATE_READONLY, state); +} + +TEST(LabelTest, SingleLineSizing) { + Label label; + std::wstring test_text(L"A not so random string in one line."); + label.SetText(test_text); + + // GetPreferredSize + gfx::Size required_size = label.GetPreferredSize(); + EXPECT_GT(required_size.height(), kMinTextDimension); + EXPECT_GT(required_size.width(), kMinTextDimension); + + // Test everything with borders. + gfx::Insets border(10, 20, 30, 40); + label.set_border(Border::CreateEmptyBorder(border.top(), + border.left(), + border.bottom(), + border.right())); + + // GetPreferredSize and borders. + label.SetBounds(0, 0, 0, 0); + gfx::Size required_size_with_border = label.GetPreferredSize(); + EXPECT_EQ(required_size_with_border.height(), + required_size.height() + border.height()); + EXPECT_EQ(required_size_with_border.width(), + required_size.width() + border.width()); +} + +TEST(LabelTest, MultiLineSizing) { + Label label; + label.SetFocusable(false); + std::wstring test_text(L"A random string\nwith multiple lines\nand returns!"); + label.SetText(test_text); + label.SetMultiLine(true); + + // GetPreferredSize + gfx::Size required_size = label.GetPreferredSize(); + EXPECT_GT(required_size.height(), kMinTextDimension); + EXPECT_GT(required_size.width(), kMinTextDimension); + + // SizeToFit with unlimited width. + label.SizeToFit(0); + int required_width = label.GetLocalBounds(true).width(); + EXPECT_GT(required_width, kMinTextDimension); + + // SizeToFit with limited width. + label.SizeToFit(required_width - 1); + int constrained_width = label.GetLocalBounds(true).width(); + EXPECT_LT(constrained_width, required_width); + EXPECT_GT(constrained_width, kMinTextDimension); + + // Change the width back to the desire width. + label.SizeToFit(required_width); + EXPECT_EQ(required_width, label.GetLocalBounds(true).width()); + + // General tests for GetHeightForWidth. + int required_height = label.GetHeightForWidth(required_width); + EXPECT_GT(required_height, kMinTextDimension); + int height_for_constrained_width = label.GetHeightForWidth(constrained_width); + EXPECT_GT(height_for_constrained_width, required_height); + // Using the constrained width or the required_width - 1 should give the + // same result for the height because the constrainted width is the tight + // width when given "required_width - 1" as the max width. + EXPECT_EQ(height_for_constrained_width, + label.GetHeightForWidth(required_width - 1)); + + // Test everything with borders. + gfx::Insets border(10, 20, 30, 40); + label.set_border(Border::CreateEmptyBorder(border.top(), + border.left(), + border.bottom(), + border.right())); + + // SizeToFit and borders. + label.SizeToFit(0); + int required_width_with_border = label.GetLocalBounds(true).width(); + EXPECT_EQ(required_width_with_border, required_width + border.width()); + + // GetHeightForWidth and borders. + int required_height_with_border = + label.GetHeightForWidth(required_width_with_border); + EXPECT_EQ(required_height_with_border, required_height + border.height()); + + // Test that the border width is subtracted before doing the height + // calculation. If it is, then the height will grow when width + // is shrunk. + int height1 = label.GetHeightForWidth(required_width_with_border - 1); + EXPECT_GT(height1, required_height_with_border); + EXPECT_EQ(height1, height_for_constrained_width + border.height()); + + // GetPreferredSize and borders. + label.SetBounds(0, 0, 0, 0); + gfx::Size required_size_with_border = label.GetPreferredSize(); + EXPECT_EQ(required_size_with_border.height(), + required_size.height() + border.height()); + EXPECT_EQ(required_size_with_border.width(), + required_size.width() + border.width()); +} + +TEST(LabelTest, DrawSingleLineString) { + Label label; + label.SetFocusable(false); + + // Turn off mirroring so that we don't need to figure out if + // align right really means align left. + label.EnableUIMirroringForRTLLanguages(false); + + std::wstring test_text(L"Here's a string with no returns."); + label.SetText(test_text); + gfx::Size required_size(label.GetPreferredSize()); + gfx::Size extra(22, 8); + label.SetBounds(0, + 0, + required_size.width() + extra.width(), + required_size.height() + extra.height()); + + // Do some basic verifications for all three alignments. + std::wstring paint_text; + gfx::Rect text_bounds; + int flags; + + // Centered text. + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + // The text should be centered horizontally and vertically. + EXPECT_EQ(extra.width() / 2, text_bounds.x()); + EXPECT_EQ(extra.height() / 2 , text_bounds.y()); + EXPECT_EQ(required_size.width(), text_bounds.width()); + EXPECT_EQ(required_size.height(), text_bounds.height()); + EXPECT_EQ(0, flags); + + // Left aligned text. + label.SetHorizontalAlignment(Label::ALIGN_LEFT); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + // The text should be left aligned horizontally and centered vertically. + EXPECT_EQ(0, text_bounds.x()); + EXPECT_EQ(extra.height() / 2 , text_bounds.y()); + EXPECT_EQ(required_size.width(), text_bounds.width()); + EXPECT_EQ(required_size.height(), text_bounds.height()); + EXPECT_EQ(0, flags); + + // Right aligned text. + label.SetHorizontalAlignment(Label::ALIGN_RIGHT); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + // The text should be right aligned horizontally and centered vertically. + EXPECT_EQ(extra.width(), text_bounds.x()); + EXPECT_EQ(extra.height() / 2 , text_bounds.y()); + EXPECT_EQ(required_size.width(), text_bounds.width()); + EXPECT_EQ(required_size.height(), text_bounds.height()); + EXPECT_EQ(0, flags); + + // Test single line drawing with a border. + gfx::Insets border(39, 34, 8, 96); + label.set_border(Border::CreateEmptyBorder(border.top(), + border.left(), + border.bottom(), + border.right())); + + gfx::Size required_size_with_border(label.GetPreferredSize()); + EXPECT_EQ(required_size.width() + border.width(), + required_size_with_border.width()); + EXPECT_EQ(required_size.height() + border.height(), + required_size_with_border.height()); + label.SetBounds(0, + 0, + required_size_with_border.width() + extra.width(), + required_size_with_border.height() + extra.height()); + + // Centered text with border. + label.SetHorizontalAlignment(Label::ALIGN_CENTER); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + // The text should be centered horizontally and vertically within the border. + EXPECT_EQ(border.left() + extra.width() / 2, text_bounds.x()); + EXPECT_EQ(border.top() + extra.height() / 2 , text_bounds.y()); + EXPECT_EQ(required_size.width(), text_bounds.width()); + EXPECT_EQ(required_size.height(), text_bounds.height()); + EXPECT_EQ(0, flags); + + // Left aligned text with border. + label.SetHorizontalAlignment(Label::ALIGN_LEFT); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + // The text should be left aligned horizontally and centered vertically. + EXPECT_EQ(border.left(), text_bounds.x()); + EXPECT_EQ(border.top() + extra.height() / 2 , text_bounds.y()); + EXPECT_EQ(required_size.width(), text_bounds.width()); + EXPECT_EQ(required_size.height(), text_bounds.height()); + EXPECT_EQ(0, flags); + + // Right aligned text. + label.SetHorizontalAlignment(Label::ALIGN_RIGHT); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + // The text should be right aligned horizontally and centered vertically. + EXPECT_EQ(border.left() + extra.width(), text_bounds.x()); + EXPECT_EQ(border.top() + extra.height() / 2 , text_bounds.y()); + EXPECT_EQ(required_size.width(), text_bounds.width()); + EXPECT_EQ(required_size.height(), text_bounds.height()); + EXPECT_EQ(0, flags); +} + +TEST(LabelTest, DrawMultiLineString) { + Label label; + label.SetFocusable(false); + + // Turn off mirroring so that we don't need to figure out if + // align right really means align left. + label.EnableUIMirroringForRTLLanguages(false); + + std::wstring test_text(L"Another string\nwith returns\n\n!"); + label.SetText(test_text); + label.SetMultiLine(true); + label.SizeToFit(0); + + // Do some basic verifications for all three alignments. + std::wstring paint_text; + gfx::Rect text_bounds; + int flags; + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + EXPECT_EQ(0, text_bounds.x()); + EXPECT_EQ(0, text_bounds.y()); + EXPECT_GT(text_bounds.width(), kMinTextDimension); + EXPECT_GT(text_bounds.height(), kMinTextDimension); + EXPECT_EQ(ChromeCanvas::MULTI_LINE | ChromeCanvas::TEXT_ALIGN_CENTER, flags); + gfx::Rect center_bounds(text_bounds); + + label.SetHorizontalAlignment(Label::ALIGN_LEFT); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + EXPECT_EQ(0, text_bounds.x()); + EXPECT_EQ(0, text_bounds.y()); + EXPECT_GT(text_bounds.width(), kMinTextDimension); + EXPECT_GT(text_bounds.height(), kMinTextDimension); + EXPECT_EQ(ChromeCanvas::MULTI_LINE | ChromeCanvas::TEXT_ALIGN_LEFT, flags); + + label.SetHorizontalAlignment(Label::ALIGN_RIGHT); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + EXPECT_EQ(0, text_bounds.x()); + EXPECT_EQ(0, text_bounds.y()); + EXPECT_GT(text_bounds.width(), kMinTextDimension); + EXPECT_GT(text_bounds.height(), kMinTextDimension); + EXPECT_EQ(ChromeCanvas::MULTI_LINE | ChromeCanvas::TEXT_ALIGN_RIGHT, flags); + + // Test multiline drawing with a border. + gfx::Insets border(19, 92, 23, 2); + label.set_border(Border::CreateEmptyBorder(border.top(), + border.left(), + border.bottom(), + border.right())); + label.SizeToFit(0); + label.SetHorizontalAlignment(Label::ALIGN_CENTER); + paint_text.clear(); + text_bounds.SetRect(0, 0, 0, 0); + label.CalculateDrawStringParams(&paint_text, &text_bounds, &flags); + EXPECT_STREQ(test_text.c_str(), paint_text.c_str()); + EXPECT_EQ(center_bounds.x() + border.left(), text_bounds.x()); + EXPECT_EQ(center_bounds.y() + border.top(), text_bounds.y()); + EXPECT_EQ(center_bounds.width(), text_bounds.width()); + EXPECT_EQ(center_bounds.height(), text_bounds.height()); + EXPECT_EQ(ChromeCanvas::MULTI_LINE | ChromeCanvas::TEXT_ALIGN_CENTER, flags); +} + +} // namespace views diff --git a/views/controls/link.cc b/views/controls/link.cc new file mode 100644 index 0000000..aae254503 --- /dev/null +++ b/views/controls/link.cc @@ -0,0 +1,183 @@ +// Copyright (c) 2006-2008 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/controls/link.h" + +#include "app/gfx/chrome_font.h" +#include "views/event.h" + +namespace views { + +static HCURSOR g_hand_cursor = NULL; + +// Default colors used for links. +static const SkColor kHighlightedColor = SkColorSetRGB(255, 0x00, 0x00); +static const SkColor kNormalColor = SkColorSetRGB(0, 51, 153); +static const SkColor kDisabledColor = SkColorSetRGB(0, 0, 0); + +const char Link::kViewClassName[] = "views/Link"; + +Link::Link() : Label(L""), + controller_(NULL), + highlighted_(false), + highlighted_color_(kHighlightedColor), + disabled_color_(kDisabledColor), + normal_color_(kNormalColor) { + Init(); + SetFocusable(true); +} + +Link::Link(const std::wstring& title) : Label(title), + controller_(NULL), + highlighted_(false), + highlighted_color_(kHighlightedColor), + disabled_color_(kDisabledColor), + normal_color_(kNormalColor) { + Init(); + SetFocusable(true); +} + +void Link::Init() { + SetColor(normal_color_); + ValidateStyle(); +} + +Link::~Link() { +} + +void Link::SetController(LinkController* controller) { + controller_ = controller; +} + +const LinkController* Link::GetController() { + return controller_; +} + +std::string Link::GetClassName() const { + return kViewClassName; +} + +void Link::SetHighlightedColor(const SkColor& color) { + normal_color_ = color; + ValidateStyle(); +} + +void Link::SetDisabledColor(const SkColor& color) { + disabled_color_ = color; + ValidateStyle(); +} + +void Link::SetNormalColor(const SkColor& color) { + normal_color_ = color; + ValidateStyle(); +} + +bool Link::OnMousePressed(const MouseEvent& e) { + if (!enabled_ || (!e.IsLeftMouseButton() && !e.IsMiddleMouseButton())) + return false; + SetHighlighted(true); + return true; +} + +bool Link::OnMouseDragged(const MouseEvent& e) { + SetHighlighted(enabled_ && + (e.IsLeftMouseButton() || e.IsMiddleMouseButton()) && + HitTest(e.location())); + return true; +} + +void Link::OnMouseReleased(const MouseEvent& e, bool canceled) { + // Change the highlight first just in case this instance is deleted + // while calling the controller + SetHighlighted(false); + if (enabled_ && !canceled && + (e.IsLeftMouseButton() || e.IsMiddleMouseButton()) && + HitTest(e.location())) { + // Focus the link on click. + RequestFocus(); + + if (controller_) + controller_->LinkActivated(this, e.GetFlags()); + } +} + +bool Link::OnKeyPressed(const KeyEvent& e) { + if ((e.GetCharacter() == VK_SPACE) || (e.GetCharacter() == VK_RETURN)) { + SetHighlighted(false); + + // Focus the link on key pressed. + RequestFocus(); + + if (controller_) + controller_->LinkActivated(this, e.GetFlags()); + + return true; + } + return false; +} + +bool Link::OverrideAccelerator(const Accelerator& accelerator) { + return (accelerator.GetKeyCode() == VK_SPACE) || + (accelerator.GetKeyCode() == VK_RETURN); +} + +void Link::SetHighlighted(bool f) { + if (f != highlighted_) { + highlighted_ = f; + ValidateStyle(); + SchedulePaint(); + } +} + +void Link::ValidateStyle() { + ChromeFont font = GetFont(); + + if (enabled_) { + if ((font.style() & ChromeFont::UNDERLINED) == 0) { + Label::SetFont(font.DeriveFont(0, font.style() | + ChromeFont::UNDERLINED)); + } + } else { + if ((font.style() & ChromeFont::UNDERLINED) != 0) { + Label::SetFont(font.DeriveFont(0, font.style() & + ~ChromeFont::UNDERLINED)); + } + } + + if (enabled_) { + if (highlighted_) { + Label::SetColor(highlighted_color_); + } else { + Label::SetColor(normal_color_); + } + } else { + Label::SetColor(disabled_color_); + } +} + +void Link::SetFont(const ChromeFont& font) { + Label::SetFont(font); + ValidateStyle(); +} + +void Link::SetEnabled(bool f) { + if (f != enabled_) { + enabled_ = f; + ValidateStyle(); + SchedulePaint(); + } +} + +HCURSOR Link::GetCursorForPoint(Event::EventType event_type, int x, int y) { + if (enabled_) { + if (!g_hand_cursor) { + g_hand_cursor = LoadCursor(NULL, IDC_HAND); + } + return g_hand_cursor; + } else { + return NULL; + } +} + +} // namespace views diff --git a/views/controls/link.h b/views/controls/link.h new file mode 100644 index 0000000..6da6aa3 --- /dev/null +++ b/views/controls/link.h @@ -0,0 +1,94 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_LINK_H_ +#define VIEWS_CONTROLS_LINK_H_ + +#include "views/controls/label.h" + +namespace views { + +class Link; + +//////////////////////////////////////////////////////////////////////////////// +// +// LinkController defines the method that should be implemented to +// receive a notification when a link is clicked +// +//////////////////////////////////////////////////////////////////////////////// +class LinkController { + public: + virtual void LinkActivated(Link* source, int event_flags) = 0; +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// Link class +// +// A Link is a label subclass that looks like an HTML link. It has a +// controller which is notified when a click occurs. +// +//////////////////////////////////////////////////////////////////////////////// +class Link : public Label { + public: + static const char Link::kViewClassName[]; + + Link(); + Link(const std::wstring& title); + virtual ~Link(); + + void SetController(LinkController* controller); + const LinkController* GetController(); + + // Overridden from View: + virtual bool OnMousePressed(const MouseEvent& event); + virtual bool OnMouseDragged(const MouseEvent& event); + virtual void OnMouseReleased(const MouseEvent& event, + bool canceled); + virtual bool OnKeyPressed(const KeyEvent& e); + virtual bool OverrideAccelerator(const Accelerator& accelerator); + + virtual void SetFont(const ChromeFont& font); + + // Set whether the link is enabled. + virtual void SetEnabled(bool f); + + virtual HCURSOR GetCursorForPoint(Event::EventType event_type, int x, int y); + + virtual std::string GetClassName() const; + + void SetHighlightedColor(const SkColor& color); + void SetDisabledColor(const SkColor& color); + void SetNormalColor(const SkColor& color); + + private: + + // A highlighted link is clicked. + void SetHighlighted(bool f); + + // Make sure the label style matched the current state. + void ValidateStyle(); + + void Init(); + + LinkController* controller_; + + // Whether the link is currently highlighted. + bool highlighted_; + + // The color when the link is highlighted. + SkColor highlighted_color_; + + // The color when the link is disabled. + SkColor disabled_color_; + + // The color when the link is neither highlighted nor disabled. + SkColor normal_color_; + + DISALLOW_COPY_AND_ASSIGN(Link); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_LINK_H_ diff --git a/views/controls/menu/chrome_menu.cc b/views/controls/menu/chrome_menu.cc new file mode 100644 index 0000000..a887fd5 --- /dev/null +++ b/views/controls/menu/chrome_menu.cc @@ -0,0 +1,2816 @@ +// Copyright (c) 2006-2008 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/controls/menu/chrome_menu.h" + +#include <windows.h> +#include <uxtheme.h> +#include <Vssym32.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/l10n_util.h" +#include "app/l10n_util_win.h" +#include "app/os_exchange_data.h" +#include "base/base_drag_source.h" +#include "base/gfx/native_theme.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "base/timer.h" +#include "base/win_util.h" +// TODO(beng): (Cleanup) remove this browser dep. +#include "chrome/browser/drag_utils.h" +#include "chrome/common/gfx/color_utils.h" +#include "grit/generated_resources.h" +#include "skia/ext/skia_utils_win.h" +#include "views/border.h" +#include "views/view_constants.h" +#include "views/widget/root_view.h" +#include "views/widget/widget_win.h" + +#undef min +#undef max + +using base::Time; +using base::TimeDelta; + +// Margins between the top of the item and the label. +static const int kItemTopMargin = 3; + +// Margins between the bottom of the item and the label. +static const int kItemBottomMargin = 4; + +// Margins used if the menu doesn't have icons. +static const int kItemNoIconTopMargin = 1; +static const int kItemNoIconBottomMargin = 3; + +// Margins between the left of the item and the icon. +static const int kItemLeftMargin = 4; + +// Padding between the label and submenu arrow. +static const int kLabelToArrowPadding = 10; + +// Padding between the arrow and the edge. +static const int kArrowToEdgePadding = 5; + +// Padding between the icon and label. +static const int kIconToLabelPadding = 8; + +// Padding between the gutter and label. +static const int kGutterToLabel = 5; + +// Height of the scroll arrow. +// This goes up to 4 with large fonts, but this is close enough for now. +static const int kScrollArrowHeight = 3; + +// Size of the check. This comes from the OS. +static int check_width; +static int check_height; + +// Size of the submenu arrow. This comes from the OS. +static int arrow_width; +static int arrow_height; + +// Width of the gutter. Only used if render_gutter is true. +static int gutter_width; + +// Margins between the right of the item and the label. +static int item_right_margin; + +// X-coordinate of where the label starts. +static int label_start; + +// Height of the separator. +static int separator_height; + +// Padding around the edges of the submenu. +static const int kSubmenuBorderSize = 3; + +// Amount to inset submenus. +static const int kSubmenuHorizontalInset = 3; + +// Delay, in ms, between when menus are selected are moused over and the menu +// appears. +static const int kShowDelay = 400; + +// Amount of time from when the drop exits the menu and the menu is hidden. +static const int kCloseOnExitTime = 1200; + +// Height of the drop indicator. This should be an event number. +static const int kDropIndicatorHeight = 2; + +// Color of the drop indicator. +static const SkColor kDropIndicatorColor = SK_ColorBLACK; + +// Whether or not the gutter should be rendered. The gutter is specific to +// Vista. +static bool render_gutter = false; + +// Max width of a menu. There does not appear to be an OS value for this, yet +// both IE and FF restrict the max width of a menu. +static const int kMaxMenuWidth = 400; + +// Period of the scroll timer (in milliseconds). +static const int kScrollTimerMS = 30; + +// Preferred height of menu items. Reset every time a menu is run. +static int pref_menu_height; + +// Are mnemonics shown? This is updated before the menus are shown. +static bool show_mnemonics; + +using gfx::NativeTheme; + +namespace views { + +namespace { + +// Returns the font menus are to use. +ChromeFont GetMenuFont() { + NONCLIENTMETRICS metrics; + win_util::GetNonClientMetrics(&metrics); + + l10n_util::AdjustUIFont(&(metrics.lfMenuFont)); + HFONT font = CreateFontIndirect(&metrics.lfMenuFont); + DLOG_ASSERT(font); + return ChromeFont::CreateFont(font); +} + +// Calculates all sizes that we can from the OS. +// +// This is invoked prior to Running a menu. +void UpdateMenuPartSizes(bool has_icons) { + HDC dc = GetDC(NULL); + RECT bounds = { 0, 0, 200, 200 }; + SIZE check_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPCHECK, MC_CHECKMARKNORMAL, &bounds, + TS_TRUE, &check_size) == S_OK) { + check_width = check_size.cx; + check_height = check_size.cy; + } else { + check_width = GetSystemMetrics(SM_CXMENUCHECK); + check_height = GetSystemMetrics(SM_CYMENUCHECK); + } + + SIZE arrow_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPSUBMENU, MSM_NORMAL, &bounds, + TS_TRUE, &arrow_size) == S_OK) { + arrow_width = arrow_size.cx; + arrow_height = arrow_size.cy; + } else { + // Sadly I didn't see a specify metrics for this. + arrow_width = GetSystemMetrics(SM_CXMENUCHECK); + arrow_height = GetSystemMetrics(SM_CYMENUCHECK); + } + + SIZE gutter_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPGUTTER, MSM_NORMAL, &bounds, + TS_TRUE, &gutter_size) == S_OK) { + gutter_width = gutter_size.cx; + render_gutter = true; + } else { + gutter_width = 0; + render_gutter = false; + } + + SIZE separator_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPSEPARATOR, MSM_NORMAL, &bounds, + TS_TRUE, &separator_size) == S_OK) { + separator_height = separator_size.cy; + } else { + separator_height = GetSystemMetrics(SM_CYMENU) / 2; + } + + item_right_margin = kLabelToArrowPadding + arrow_width + kArrowToEdgePadding; + + if (has_icons) { + label_start = kItemLeftMargin + check_width + kIconToLabelPadding; + } else { + // If there are no icons don't pad by the icon to label padding. This + // makes us look close to system menus. + label_start = kItemLeftMargin + check_width; + } + if (render_gutter) + label_start += gutter_width + kGutterToLabel; + + ReleaseDC(NULL, dc); + + MenuItemView menu_item(NULL); + menu_item.SetTitle(L"blah"); // Text doesn't matter here. + pref_menu_height = menu_item.GetPreferredSize().height(); +} + +// Convenience for scrolling the view such that the origin is visible. +static void ScrollToVisible(View* view) { + view->ScrollRectToVisible(0, 0, view->width(), view->height()); +} + +// MenuScrollTask -------------------------------------------------------------- + +// MenuScrollTask is used when the SubmenuView does not all fit on screen and +// the mouse is over the scroll up/down buttons. MenuScrollTask schedules +// itself with a RepeatingTimer. When Run is invoked MenuScrollTask scrolls +// appropriately. + +class MenuScrollTask { + public: + MenuScrollTask() : submenu_(NULL) { + pixels_per_second_ = pref_menu_height * 20; + } + + void Update(const MenuController::MenuPart& part) { + if (!part.is_scroll()) { + StopScrolling(); + return; + } + DCHECK(part.submenu); + SubmenuView* new_menu = part.submenu; + bool new_is_up = (part.type == MenuController::MenuPart::SCROLL_UP); + if (new_menu == submenu_ && is_scrolling_up_ == new_is_up) + return; + + start_scroll_time_ = Time::Now(); + start_y_ = part.submenu->GetVisibleBounds().y(); + submenu_ = new_menu; + is_scrolling_up_ = new_is_up; + + if (!scrolling_timer_.IsRunning()) { + scrolling_timer_.Start(TimeDelta::FromMilliseconds(kScrollTimerMS), this, + &MenuScrollTask::Run); + } + } + + void StopScrolling() { + if (scrolling_timer_.IsRunning()) { + scrolling_timer_.Stop(); + submenu_ = NULL; + } + } + + // The menu being scrolled. Returns null if not scrolling. + SubmenuView* submenu() const { return submenu_; } + + private: + void Run() { + DCHECK(submenu_); + gfx::Rect vis_rect = submenu_->GetVisibleBounds(); + const int delta_y = static_cast<int>( + (Time::Now() - start_scroll_time_).InMilliseconds() * + pixels_per_second_ / 1000); + int target_y = start_y_; + if (is_scrolling_up_) + target_y = std::max(0, target_y - delta_y); + else + target_y = std::min(submenu_->height() - vis_rect.height(), + target_y + delta_y); + submenu_->ScrollRectToVisible(vis_rect.x(), target_y, vis_rect.width(), + vis_rect.height()); + } + + // SubmenuView being scrolled. + SubmenuView* submenu_; + + // Direction scrolling. + bool is_scrolling_up_; + + // Timer to periodically scroll. + base::RepeatingTimer<MenuScrollTask> scrolling_timer_; + + // Time we started scrolling at. + Time start_scroll_time_; + + // How many pixels to scroll per second. + int pixels_per_second_; + + // Y-coordinate of submenu_view_ when scrolling started. + int start_y_; + + DISALLOW_COPY_AND_ASSIGN(MenuScrollTask); +}; + +// MenuScrollButton ------------------------------------------------------------ + +// MenuScrollButton is used for the scroll buttons when not all menu items fit +// on screen. MenuScrollButton forwards appropriate events to the +// MenuController. + +class MenuScrollButton : public View { + public: + explicit MenuScrollButton(SubmenuView* host, bool is_up) + : host_(host), + is_up_(is_up), + // Make our height the same as that of other MenuItemViews. + pref_height_(pref_menu_height) { + } + + virtual gfx::Size GetPreferredSize() { + return gfx::Size(kScrollArrowHeight * 2 - 1, pref_height_); + } + + virtual bool CanDrop(const OSExchangeData& data) { + DCHECK(host_->GetMenuItem()->GetMenuController()); + return true; // Always return true so that drop events are targeted to us. + } + + virtual void OnDragEntered(const DropTargetEvent& event) { + DCHECK(host_->GetMenuItem()->GetMenuController()); + host_->GetMenuItem()->GetMenuController()->OnDragEnteredScrollButton( + host_, is_up_); + } + + virtual int OnDragUpdated(const DropTargetEvent& event) { + return DragDropTypes::DRAG_NONE; + } + + virtual void OnDragExited() { + DCHECK(host_->GetMenuItem()->GetMenuController()); + host_->GetMenuItem()->GetMenuController()->OnDragExitedScrollButton(host_); + } + + virtual int OnPerformDrop(const DropTargetEvent& event) { + return DragDropTypes::DRAG_NONE; + } + + virtual void Paint(ChromeCanvas* canvas) { + HDC dc = canvas->beginPlatformPaint(); + + // The background. + RECT item_bounds = { 0, 0, width(), height() }; + NativeTheme::instance()->PaintMenuItemBackground( + NativeTheme::MENU, dc, MENU_POPUPITEM, MPI_NORMAL, false, + &item_bounds); + + // Then the arrow. + int x = width() / 2; + int y = (height() - kScrollArrowHeight) / 2; + int delta_y = 1; + if (!is_up_) { + delta_y = -1; + y += kScrollArrowHeight; + } + SkColor arrow_color = color_utils::GetSysSkColor(COLOR_MENUTEXT); + for (int i = 0; i < kScrollArrowHeight; ++i, --x, y += delta_y) + canvas->FillRectInt(arrow_color, x, y, (i * 2) + 1, 1); + + canvas->endPlatformPaint(); + } + + private: + // SubmenuView we were created for. + SubmenuView* host_; + + // Direction of the button. + bool is_up_; + + // Preferred height. + int pref_height_; + + DISALLOW_COPY_AND_ASSIGN(MenuScrollButton); +}; + +// MenuScrollView -------------------------------------------------------------- + +// MenuScrollView is a viewport for the SubmenuView. It's reason to exist is so +// that ScrollRectToVisible works. +// +// NOTE: It is possible to use ScrollView directly (after making it deal with +// null scrollbars), but clicking on a child of ScrollView forces the window to +// become active, which we don't want. As we really only need a fraction of +// what ScrollView does, we use a one off variant. + +class MenuScrollView : public View { + public: + explicit MenuScrollView(View* child) { + AddChildView(child); + } + + virtual void ScrollRectToVisible(int x, int y, int width, int height) { + // NOTE: this assumes we only want to scroll in the y direction. + + View* child = GetContents(); + // Convert y to view's coordinates. + y -= child->y(); + gfx::Size pref = child->GetPreferredSize(); + // Constrain y to make sure we don't show past the bottom of the view. + y = std::max(0, std::min(pref.height() - this->height(), y)); + child->SetY(-y); + } + + // Returns the contents, which is the SubmenuView. + View* GetContents() { + return GetChildViewAt(0); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MenuScrollView); +}; + +// MenuScrollViewContainer ----------------------------------------------------- + +// MenuScrollViewContainer contains the SubmenuView (through a MenuScrollView) +// and two scroll buttons. The scroll buttons are only visible and enabled if +// the preferred height of the SubmenuView is bigger than our bounds. +class MenuScrollViewContainer : public View { + public: + explicit MenuScrollViewContainer(SubmenuView* content_view) { + scroll_up_button_ = new MenuScrollButton(content_view, true); + scroll_down_button_ = new MenuScrollButton(content_view, false); + AddChildView(scroll_up_button_); + AddChildView(scroll_down_button_); + + scroll_view_ = new MenuScrollView(content_view); + AddChildView(scroll_view_); + + set_border(Border::CreateEmptyBorder( + kSubmenuBorderSize, kSubmenuBorderSize, + kSubmenuBorderSize, kSubmenuBorderSize)); + } + + virtual void Paint(ChromeCanvas* canvas) { + HDC dc = canvas->beginPlatformPaint(); + CRect bounds(0, 0, width(), height()); + NativeTheme::instance()->PaintMenuBackground( + NativeTheme::MENU, dc, MENU_POPUPBACKGROUND, 0, &bounds); + canvas->endPlatformPaint(); + } + + View* scroll_down_button() { return scroll_down_button_; } + + View* scroll_up_button() { return scroll_up_button_; } + + virtual void Layout() { + gfx::Insets insets = GetInsets(); + int x = insets.left(); + int y = insets.top(); + int width = View::width() - insets.width(); + int content_height = height() - insets.height(); + if (!scroll_up_button_->IsVisible()) { + scroll_view_->SetBounds(x, y, width, content_height); + scroll_view_->Layout(); + return; + } + + gfx::Size pref = scroll_up_button_->GetPreferredSize(); + scroll_up_button_->SetBounds(x, y, width, pref.height()); + content_height -= pref.height(); + + const int scroll_view_y = y + pref.height(); + + pref = scroll_down_button_->GetPreferredSize(); + scroll_down_button_->SetBounds(x, height() - pref.height() - insets.top(), + width, pref.height()); + content_height -= pref.height(); + + scroll_view_->SetBounds(x, scroll_view_y, width, content_height); + scroll_view_->Layout(); + } + + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + gfx::Size content_pref = scroll_view_->GetContents()->GetPreferredSize(); + scroll_up_button_->SetVisible(content_pref.height() > height()); + scroll_down_button_->SetVisible(content_pref.height() > height()); + } + + virtual gfx::Size GetPreferredSize() { + gfx::Size prefsize = scroll_view_->GetContents()->GetPreferredSize(); + gfx::Insets insets = GetInsets(); + prefsize.Enlarge(insets.width(), insets.height()); + return prefsize; + } + + private: + // The scroll buttons. + View* scroll_up_button_; + View* scroll_down_button_; + + // The scroll view. + MenuScrollView* scroll_view_; + + DISALLOW_COPY_AND_ASSIGN(MenuScrollViewContainer); +}; + +// MenuSeparator --------------------------------------------------------------- + +// Renders a separator. + +class MenuSeparator : public View { + public: + MenuSeparator() { + } + + void Paint(ChromeCanvas* canvas) { + // The gutter is rendered before the background. + int start_x = 0; + int start_y = height() / 3; + HDC dc = canvas->beginPlatformPaint(); + if (render_gutter) { + // If render_gutter is true, we're on Vista and need to render the + // gutter, then indent the separator from the gutter. + RECT gutter_bounds = { label_start - kGutterToLabel - gutter_width, 0, 0, + height() }; + gutter_bounds.right = gutter_bounds.left + gutter_width; + NativeTheme::instance()->PaintMenuGutter(dc, MENU_POPUPGUTTER, MPI_NORMAL, + &gutter_bounds); + start_x = gutter_bounds.left + gutter_width; + start_y = 0; + } + RECT separator_bounds = { start_x, start_y, width(), height() }; + NativeTheme::instance()->PaintMenuSeparator(dc, MENU_POPUPSEPARATOR, + MPI_NORMAL, &separator_bounds); + canvas->endPlatformPaint(); + } + + gfx::Size GetPreferredSize() { + return gfx::Size(10, // Just in case we're the only item in a menu. + separator_height); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MenuSeparator); +}; + +// MenuHostRootView ---------------------------------------------------------- + +// MenuHostRootView is the RootView of the window showing the menu. +// SubmenuView's scroll view is added as a child of MenuHostRootView. +// MenuHostRootView forwards relevant events to the MenuController. +// +// As all the menu items are owned by the root menu item, care must be taken +// such that when MenuHostRootView is deleted it doesn't delete the menu items. + +class MenuHostRootView : public RootView { + public: + explicit MenuHostRootView(Widget* widget, + SubmenuView* submenu) + : RootView(widget), + submenu_(submenu), + forward_drag_to_menu_controller_(true), + suspend_events_(false) { +#ifdef DEBUG_MENU + DLOG(INFO) << " new MenuHostRootView " << this; +#endif + } + + virtual bool OnMousePressed(const MouseEvent& event) { + if (suspend_events_) + return true; + + forward_drag_to_menu_controller_ = + ((event.x() < 0 || event.y() < 0 || event.x() >= width() || + event.y() >= height()) || + !RootView::OnMousePressed(event)); + if (forward_drag_to_menu_controller_) + GetMenuController()->OnMousePressed(submenu_, event); + return true; + } + + virtual bool OnMouseDragged(const MouseEvent& event) { + if (suspend_events_) + return true; + + if (forward_drag_to_menu_controller_) { +#ifdef DEBUG_MENU + DLOG(INFO) << " MenuHostRootView::OnMouseDragged source=" << submenu_; +#endif + GetMenuController()->OnMouseDragged(submenu_, event); + return true; + } + return RootView::OnMouseDragged(event); + } + + virtual void OnMouseReleased(const MouseEvent& event, bool canceled) { + if (suspend_events_) + return; + + RootView::OnMouseReleased(event, canceled); + if (forward_drag_to_menu_controller_) { + forward_drag_to_menu_controller_ = false; + if (canceled) { + GetMenuController()->Cancel(true); + } else { + GetMenuController()->OnMouseReleased(submenu_, event); + } + } + } + + virtual void OnMouseMoved(const MouseEvent& event) { + if (suspend_events_) + return; + + RootView::OnMouseMoved(event); + GetMenuController()->OnMouseMoved(submenu_, event); + } + + virtual void ProcessOnMouseExited() { + if (suspend_events_) + return; + + RootView::ProcessOnMouseExited(); + } + + virtual bool ProcessMouseWheelEvent(const MouseWheelEvent& e) { + // RootView::ProcessMouseWheelEvent forwards to the focused view. We don't + // have a focused view, so we need to override this then forward to + // the menu. + return submenu_->OnMouseWheel(e); + } + + void SuspendEvents() { + suspend_events_ = true; + } + + private: + MenuController* GetMenuController() { + return submenu_->GetMenuItem()->GetMenuController(); + } + + // The SubmenuView we contain. + SubmenuView* submenu_; + + // Whether mouse dragged/released should be forwarded to the MenuController. + bool forward_drag_to_menu_controller_; + + // Whether events are suspended. If true, no events are forwarded to the + // MenuController. + bool suspend_events_; + + DISALLOW_COPY_AND_ASSIGN(MenuHostRootView); +}; + +// MenuHost ------------------------------------------------------------------ + +// MenuHost is the window responsible for showing a single menu. +// +// Similar to MenuHostRootView, care must be taken such that when MenuHost is +// deleted, it doesn't delete the menu items. MenuHost is closed via a +// DelayedClosed, which avoids timing issues with deleting the window while +// capture or events are directed at it. + +class MenuHost : public WidgetWin { + public: + explicit MenuHost(SubmenuView* submenu) + : closed_(false), + submenu_(submenu), + owns_capture_(false) { + set_window_style(WS_POPUP); + set_initial_class_style( + (win_util::GetWinVersion() < win_util::WINVERSION_XP) ? + 0 : CS_DROPSHADOW); + is_mouse_down_ = + ((GetKeyState(VK_LBUTTON) & 0x80) || + (GetKeyState(VK_RBUTTON) & 0x80) || + (GetKeyState(VK_MBUTTON) & 0x80) || + (GetKeyState(VK_XBUTTON1) & 0x80) || + (GetKeyState(VK_XBUTTON2) & 0x80)); + // Mouse clicks shouldn't give us focus. + set_window_ex_style(WS_EX_TOPMOST | WS_EX_NOACTIVATE); + } + + void Init(HWND parent, + const gfx::Rect& bounds, + View* contents_view, + bool do_capture) { + WidgetWin::Init(parent, bounds, true); + SetContentsView(contents_view); + // We don't want to take focus away from the hosting window. + ShowWindow(SW_SHOWNA); + owns_capture_ = do_capture; + if (do_capture) { + SetCapture(); + has_capture_ = true; +#ifdef DEBUG_MENU + DLOG(INFO) << "Doing capture"; +#endif + } + } + + virtual void Hide() { + if (closed_) { + // We're already closed, nothing to do. + // This is invoked twice if the first time just hid us, and the second + // time deleted Closed (deleted) us. + return; + } + // The menus are freed separately, and possibly before the window is closed, + // remove them so that View doesn't try to access deleted objects. + static_cast<MenuHostRootView*>(GetRootView())->SuspendEvents(); + GetRootView()->RemoveAllChildViews(false); + closed_ = true; + ReleaseCapture(); + WidgetWin::Hide(); + } + + virtual void HideWindow() { + // Make sure we release capture before hiding. + ReleaseCapture(); + WidgetWin::Hide(); + } + + virtual void OnCaptureChanged(HWND hwnd) { + WidgetWin::OnCaptureChanged(hwnd); + owns_capture_ = false; +#ifdef DEBUG_MENU + DLOG(INFO) << "Capture changed"; +#endif + } + + void ReleaseCapture() { + if (owns_capture_) { +#ifdef DEBUG_MENU + DLOG(INFO) << "released capture"; +#endif + owns_capture_ = false; + ::ReleaseCapture(); + } + } + + protected: + // Overriden to create MenuHostRootView. + virtual RootView* CreateRootView() { + return new MenuHostRootView(this, submenu_); + } + + virtual void OnCancelMode() { + if (!closed_) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnCanceMode, closing menu"; +#endif + submenu_->GetMenuItem()->GetMenuController()->Cancel(true); + } + } + + // Overriden to return false, we do NOT want to release capture on mouse + // release. + virtual bool ReleaseCaptureOnMouseReleased() { + return false; + } + + private: + // If true, we've been closed. + bool closed_; + + // If true, we own the capture and need to release it. + bool owns_capture_; + + // The view we contain. + SubmenuView* submenu_; + + DISALLOW_COPY_AND_ASSIGN(MenuHost); +}; + +// EmptyMenuMenuItem --------------------------------------------------------- + +// EmptyMenuMenuItem is used when a menu has no menu items. EmptyMenuMenuItem +// is itself a MenuItemView, but it uses a different ID so that it isn't +// identified as a MenuItemView. + +class EmptyMenuMenuItem : public MenuItemView { + public: + // ID used for EmptyMenuMenuItem. + static const int kEmptyMenuItemViewID; + + explicit EmptyMenuMenuItem(MenuItemView* parent) : + MenuItemView(parent, 0, NORMAL) { + SetTitle(l10n_util::GetString(IDS_MENU_EMPTY_SUBMENU)); + // Set this so that we're not identified as a normal menu item. + SetID(kEmptyMenuItemViewID); + SetEnabled(false); + } + + private: + DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem); +}; + +// static +const int EmptyMenuMenuItem::kEmptyMenuItemViewID = + MenuItemView::kMenuItemViewID + 1; + +} // namespace + +// SubmenuView --------------------------------------------------------------- + +SubmenuView::SubmenuView(MenuItemView* parent) + : parent_menu_item_(parent), + host_(NULL), + drop_item_(NULL), + drop_position_(MenuDelegate::DROP_NONE), + scroll_view_container_(NULL) { + DCHECK(parent); + // We'll delete ourselves, otherwise the ScrollView would delete us on close. + SetParentOwned(false); +} + +SubmenuView::~SubmenuView() { + // The menu may not have been closed yet (it will be hidden, but not + // necessarily closed). + Close(); + + delete scroll_view_container_; +} + +int SubmenuView::GetMenuItemCount() { + int count = 0; + for (int i = 0; i < GetChildViewCount(); ++i) { + if (GetChildViewAt(i)->GetID() == MenuItemView::kMenuItemViewID) + count++; + } + return count; +} + +MenuItemView* SubmenuView::GetMenuItemAt(int index) { + for (int i = 0, count = 0; i < GetChildViewCount(); ++i) { + if (GetChildViewAt(i)->GetID() == MenuItemView::kMenuItemViewID && + count++ == index) { + return static_cast<MenuItemView*>(GetChildViewAt(i)); + } + } + NOTREACHED(); + return NULL; +} + +void SubmenuView::Layout() { + // We're in a ScrollView, and need to set our width/height ourselves. + View* parent = GetParent(); + if (!parent) + return; + SetBounds(x(), y(), parent->width(), GetPreferredSize().height()); + + gfx::Insets insets = GetInsets(); + int x = insets.left(); + int y = insets.top(); + int menu_item_width = width() - insets.width(); + for (int i = 0; i < GetChildViewCount(); ++i) { + View* child = GetChildViewAt(i); + gfx::Size child_pref_size = child->GetPreferredSize(); + child->SetBounds(x, y, menu_item_width, child_pref_size.height()); + y += child_pref_size.height(); + } +} + +gfx::Size SubmenuView::GetPreferredSize() { + if (GetChildViewCount() == 0) + return gfx::Size(); + + int max_width = 0; + int height = 0; + for (int i = 0; i < GetChildViewCount(); ++i) { + View* child = GetChildViewAt(i); + gfx::Size child_pref_size = child->GetPreferredSize(); + max_width = std::max(max_width, child_pref_size.width()); + height += child_pref_size.height(); + } + gfx::Insets insets = GetInsets(); + return gfx::Size(max_width + insets.width(), height + insets.height()); +} + +void SubmenuView::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + SchedulePaint(); +} + +void SubmenuView::PaintChildren(ChromeCanvas* canvas) { + View::PaintChildren(canvas); + + if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON) + PaintDropIndicator(canvas, drop_item_, drop_position_); +} + +bool SubmenuView::CanDrop(const OSExchangeData& data) { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->CanDrop(this, data); +} + +void SubmenuView::OnDragEntered(const DropTargetEvent& event) { + DCHECK(GetMenuItem()->GetMenuController()); + GetMenuItem()->GetMenuController()->OnDragEntered(this, event); +} + +int SubmenuView::OnDragUpdated(const DropTargetEvent& event) { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event); +} + +void SubmenuView::OnDragExited() { + DCHECK(GetMenuItem()->GetMenuController()); + GetMenuItem()->GetMenuController()->OnDragExited(this); +} + +int SubmenuView::OnPerformDrop(const DropTargetEvent& event) { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event); +} + +bool SubmenuView::OnMouseWheel(const MouseWheelEvent& e) { + gfx::Rect vis_bounds = GetVisibleBounds(); + int menu_item_count = GetMenuItemCount(); + if (vis_bounds.height() == height() || !menu_item_count) { + // All menu items are visible, nothing to scroll. + return true; + } + + // Find the index of the first menu item whose y-coordinate is >= visible + // y-coordinate. + int first_vis_index = -1; + for (int i = 0; i < menu_item_count; ++i) { + MenuItemView* menu_item = GetMenuItemAt(i); + if (menu_item->y() == vis_bounds.y()) { + first_vis_index = i; + break; + } else if (menu_item->y() > vis_bounds.y()) { + first_vis_index = std::max(0, i - 1); + break; + } + } + if (first_vis_index == -1) + return true; + + // If the first item isn't entirely visible, make it visible, otherwise make + // the next/previous one entirely visible. + int delta = abs(e.GetOffset() / WHEEL_DELTA); + bool scroll_up = (e.GetOffset() > 0); + while (delta-- > 0) { + int scroll_amount = 0; + if (scroll_up) { + if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) { + if (first_vis_index != 0) { + scroll_amount = GetMenuItemAt(first_vis_index - 1)->y() - + vis_bounds.y(); + first_vis_index--; + } else { + break; + } + } else { + scroll_amount = GetMenuItemAt(first_vis_index)->y() - vis_bounds.y(); + } + } else { + if (first_vis_index + 1 == GetMenuItemCount()) + break; + scroll_amount = GetMenuItemAt(first_vis_index + 1)->y() - + vis_bounds.y(); + if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) + first_vis_index++; + } + ScrollRectToVisible(0, vis_bounds.y() + scroll_amount, vis_bounds.width(), + vis_bounds.height()); + vis_bounds = GetVisibleBounds(); + } + + return true; +} + +bool SubmenuView::IsShowing() { + return host_ && host_->IsVisible(); +} + +void SubmenuView::ShowAt(HWND parent, + const gfx::Rect& bounds, + bool do_capture) { + if (host_) { + host_->ShowWindow(SW_SHOWNA); + return; + } + + host_ = new MenuHost(this); + // Force construction of the scroll view container. + GetScrollViewContainer(); + // Make sure the first row is visible. + ScrollRectToVisible(0, 0, 1, 1); + host_->Init(parent, bounds, scroll_view_container_, do_capture); +} + +void SubmenuView::Close() { + if (host_) { + host_->Close(); + host_ = NULL; + } +} + +void SubmenuView::Hide() { + if (host_) + host_->HideWindow(); +} + +void SubmenuView::ReleaseCapture() { + host_->ReleaseCapture(); +} + +void SubmenuView::SetDropMenuItem(MenuItemView* item, + MenuDelegate::DropPosition position) { + if (drop_item_ == item && drop_position_ == position) + return; + SchedulePaintForDropIndicator(drop_item_, drop_position_); + drop_item_ = item; + drop_position_ = position; + SchedulePaintForDropIndicator(drop_item_, drop_position_); +} + +bool SubmenuView::GetShowSelection(MenuItemView* item) { + if (drop_item_ == NULL) + return true; + // Something is being dropped on one of this menus items. Show the + // selection if the drop is on the passed in item and the drop position is + // ON. + return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON); +} + +MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() { + if (!scroll_view_container_) { + scroll_view_container_ = new MenuScrollViewContainer(this); + // Otherwise MenuHost would delete us. + scroll_view_container_->SetParentOwned(false); + } + return scroll_view_container_; +} + +void SubmenuView::PaintDropIndicator(ChromeCanvas* canvas, + MenuItemView* item, + MenuDelegate::DropPosition position) { + if (position == MenuDelegate::DROP_NONE) + return; + + gfx::Rect bounds = CalculateDropIndicatorBounds(item, position); + canvas->FillRectInt(kDropIndicatorColor, bounds.x(), bounds.y(), + bounds.width(), bounds.height()); +} + +void SubmenuView::SchedulePaintForDropIndicator( + MenuItemView* item, + MenuDelegate::DropPosition position) { + if (item == NULL) + return; + + if (position == MenuDelegate::DROP_ON) { + item->SchedulePaint(); + } else if (position != MenuDelegate::DROP_NONE) { + gfx::Rect bounds = CalculateDropIndicatorBounds(item, position); + SchedulePaint(bounds.x(), bounds.y(), bounds.width(), bounds.height()); + } +} + +gfx::Rect SubmenuView::CalculateDropIndicatorBounds( + MenuItemView* item, + MenuDelegate::DropPosition position) { + DCHECK(position != MenuDelegate::DROP_NONE); + gfx::Rect item_bounds = item->bounds(); + switch (position) { + case MenuDelegate::DROP_BEFORE: + item_bounds.Offset(0, -kDropIndicatorHeight / 2); + item_bounds.set_height(kDropIndicatorHeight); + return item_bounds; + + case MenuDelegate::DROP_AFTER: + item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2); + item_bounds.set_height(kDropIndicatorHeight); + return item_bounds; + + default: + // Don't render anything for on. + return gfx::Rect(); + } +} + +// MenuItemView --------------------------------------------------------------- + +// static +const int MenuItemView::kMenuItemViewID = 1001; + +// static +bool MenuItemView::allow_task_nesting_during_run_ = false; + +MenuItemView::MenuItemView(MenuDelegate* delegate) { + // NOTE: don't check the delegate for NULL, UpdateMenuPartSizes supplies a + // NULL delegate. + Init(NULL, 0, SUBMENU, delegate); +} + +MenuItemView::~MenuItemView() { + if (controller_) { + // We're currently showing. + + // We can't delete ourselves while we're blocking. + DCHECK(!controller_->IsBlockingRun()); + + // Invoking Cancel is going to call us back and notify the delegate. + // Notifying the delegate from the destructor can be problematic. To avoid + // this the delegate is set to NULL. + delegate_ = NULL; + + controller_->Cancel(true); + } + delete submenu_; +} + +void MenuItemView::RunMenuAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor, + bool has_mnemonics) { + PrepareForRun(has_mnemonics); + + int mouse_event_flags; + + MenuController* controller = MenuController::GetActiveInstance(); + if (controller && !controller->IsBlockingRun()) { + // A menu is already showing, but it isn't a blocking menu. Cancel it. + // We can get here during drag and drop if the user right clicks on the + // menu quickly after the drop. + controller->Cancel(true); + controller = NULL; + } + bool owns_controller = false; + if (!controller) { + // No menus are showing, show one. + controller = new MenuController(true); + MenuController::SetActiveInstance(controller); + owns_controller = true; + } else { + // A menu is already showing, use the same controller. + + // Don't support blocking from within non-blocking. + DCHECK(controller->IsBlockingRun()); + } + + controller_ = controller; + + // Run the loop. + MenuItemView* result = + controller->Run(parent, this, bounds, anchor, &mouse_event_flags); + + RemoveEmptyMenus(); + + controller_ = NULL; + + if (owns_controller) { + // We created the controller and need to delete it. + if (MenuController::GetActiveInstance() == controller) + MenuController::SetActiveInstance(NULL); + delete controller; + } + // Make sure all the windows we created to show the menus have been + // destroyed. + DestroyAllMenuHosts(); + if (result && delegate_) + delegate_->ExecuteCommand(result->GetCommand(), mouse_event_flags); +} + +void MenuItemView::RunMenuForDropAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor) { + PrepareForRun(false); + + // If there is a menu, hide it so that only one menu is shown during dnd. + MenuController* current_controller = MenuController::GetActiveInstance(); + if (current_controller) { + current_controller->Cancel(true); + } + + // Always create a new controller for non-blocking. + controller_ = new MenuController(false); + + // Set the instance, that way it can be canceled by another menu. + MenuController::SetActiveInstance(controller_); + + controller_->Run(parent, this, bounds, anchor, NULL); +} + +void MenuItemView::Cancel() { + if (controller_ && !canceled_) { + canceled_ = true; + controller_->Cancel(true); + } +} + +SubmenuView* MenuItemView::CreateSubmenu() { + if (!submenu_) + submenu_ = new SubmenuView(this); + return submenu_; +} + +void MenuItemView::SetSelected(bool selected) { + selected_ = selected; + SchedulePaint(); +} + +void MenuItemView::SetIcon(const SkBitmap& icon, int item_id) { + MenuItemView* item = GetDescendantByID(item_id); + DCHECK(item); + item->SetIcon(icon); +} + +void MenuItemView::SetIcon(const SkBitmap& icon) { + icon_ = icon; + SchedulePaint(); +} + +void MenuItemView::Paint(ChromeCanvas* canvas) { + Paint(canvas, false); +} + +gfx::Size MenuItemView::GetPreferredSize() { + ChromeFont& font = GetRootMenuItem()->font_; + return gfx::Size( + font.GetStringWidth(title_) + label_start + item_right_margin, + font.height() + GetBottomMargin() + GetTopMargin()); +} + +MenuController* MenuItemView::GetMenuController() { + return GetRootMenuItem()->controller_; +} + +MenuDelegate* MenuItemView::GetDelegate() { + return GetRootMenuItem()->delegate_; +} + +MenuItemView* MenuItemView::GetRootMenuItem() { + MenuItemView* item = this; + while (item) { + MenuItemView* parent = item->GetParentMenuItem(); + if (!parent) + return item; + item = parent; + } + NOTREACHED(); + return NULL; +} + +wchar_t MenuItemView::GetMnemonic() { + if (!has_mnemonics_) + return 0; + + const std::wstring& title = GetTitle(); + size_t index = 0; + do { + index = title.find('&', index); + if (index != std::wstring::npos) { + if (index + 1 != title.size() && title[index + 1] != '&') + return title[index + 1]; + index++; + } + } while (index != std::wstring::npos); + return 0; +} + +MenuItemView::MenuItemView(MenuItemView* parent, + int command, + MenuItemView::Type type) { + Init(parent, command, type, NULL); +} + +void MenuItemView::Init(MenuItemView* parent, + int command, + MenuItemView::Type type, + MenuDelegate* delegate) { + delegate_ = delegate; + controller_ = NULL; + canceled_ = false; + parent_menu_item_ = parent; + type_ = type; + selected_ = false; + command_ = command; + submenu_ = NULL; + // Assign our ID, this allows SubmenuItemView to find MenuItemViews. + SetID(kMenuItemViewID); + has_icons_ = false; + + MenuDelegate* root_delegate = GetDelegate(); + if (root_delegate) + SetEnabled(root_delegate->IsCommandEnabled(command)); +} + +MenuItemView* MenuItemView::AppendMenuItemInternal(int item_id, + const std::wstring& label, + const SkBitmap& icon, + Type type) { + if (!submenu_) + CreateSubmenu(); + if (type == SEPARATOR) { + submenu_->AddChildView(new MenuSeparator()); + return NULL; + } + MenuItemView* item = new MenuItemView(this, item_id, type); + if (label.empty() && GetDelegate()) + item->SetTitle(GetDelegate()->GetLabel(item_id)); + else + item->SetTitle(label); + item->SetIcon(icon); + if (type == SUBMENU) + item->CreateSubmenu(); + submenu_->AddChildView(item); + return item; +} + +MenuItemView* MenuItemView::GetDescendantByID(int id) { + if (GetCommand() == id) + return this; + if (!HasSubmenu()) + return NULL; + for (int i = 0; i < GetSubmenu()->GetChildViewCount(); ++i) { + View* child = GetSubmenu()->GetChildViewAt(i); + if (child->GetID() == MenuItemView::kMenuItemViewID) { + MenuItemView* result = static_cast<MenuItemView*>(child)-> + GetDescendantByID(id); + if (result) + return result; + } + } + return NULL; +} + +void MenuItemView::DropMenuClosed(bool notify_delegate) { + DCHECK(controller_); + DCHECK(!controller_->IsBlockingRun()); + if (MenuController::GetActiveInstance() == controller_) + MenuController::SetActiveInstance(NULL); + delete controller_; + controller_ = NULL; + + RemoveEmptyMenus(); + + if (notify_delegate && delegate_) { + // Our delegate is null when invoked from the destructor. + delegate_->DropMenuClosed(this); + } + // WARNING: its possible the delegate deleted us at this point. +} + +void MenuItemView::PrepareForRun(bool has_mnemonics) { + // Currently we only support showing the root. + DCHECK(!parent_menu_item_); + + // Don't invoke run from within run on the same menu. + DCHECK(!controller_); + + // Force us to have a submenu. + CreateSubmenu(); + + canceled_ = false; + + has_mnemonics_ = has_mnemonics; + + AddEmptyMenus(); + + if (!MenuController::GetActiveInstance()) { + // Only update the menu size if there are no menus showing, otherwise + // things may shift around. + UpdateMenuPartSizes(has_icons_); + } + + font_ = GetMenuFont(); + + BOOL show_cues; + show_mnemonics = + (SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &show_cues, 0) && + show_cues == TRUE); +} + +int MenuItemView::GetDrawStringFlags() { + int flags = 0; + if (UILayoutIsRightToLeft()) + flags |= ChromeCanvas::TEXT_ALIGN_RIGHT; + else + flags |= ChromeCanvas::TEXT_ALIGN_LEFT; + + if (has_mnemonics_) { + if (show_mnemonics) + flags |= ChromeCanvas::SHOW_PREFIX; + else + flags |= ChromeCanvas::HIDE_PREFIX; + } + return flags; +} + +void MenuItemView::AddEmptyMenus() { + DCHECK(HasSubmenu()); + if (submenu_->GetChildViewCount() == 0) { + submenu_->AddChildView(0, new EmptyMenuMenuItem(this)); + } else { + for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; + ++i) { + MenuItemView* child = submenu_->GetMenuItemAt(i); + if (child->HasSubmenu()) + child->AddEmptyMenus(); + } + } +} + +void MenuItemView::RemoveEmptyMenus() { + DCHECK(HasSubmenu()); + // Iterate backwards as we may end up removing views, which alters the child + // view count. + for (int i = submenu_->GetChildViewCount() - 1; i >= 0; --i) { + View* child = submenu_->GetChildViewAt(i); + if (child->GetID() == MenuItemView::kMenuItemViewID) { + MenuItemView* menu_item = static_cast<MenuItemView*>(child); + if (menu_item->HasSubmenu()) + menu_item->RemoveEmptyMenus(); + } else if (child->GetID() == EmptyMenuMenuItem::kEmptyMenuItemViewID) { + submenu_->RemoveChildView(child); + } + } +} + +void MenuItemView::AdjustBoundsForRTLUI(RECT* rect) const { + gfx::Rect mirrored_rect(*rect); + mirrored_rect.set_x(MirroredLeftPointForRect(mirrored_rect)); + *rect = mirrored_rect.ToRECT(); +} + +void MenuItemView::Paint(ChromeCanvas* canvas, bool for_drag) { + bool render_selection = + (!for_drag && IsSelected() && + parent_menu_item_->GetSubmenu()->GetShowSelection(this)); + int state = render_selection ? MPI_HOT : + (IsEnabled() ? MPI_NORMAL : MPI_DISABLED); + HDC dc = canvas->beginPlatformPaint(); + + // The gutter is rendered before the background. + if (render_gutter && !for_drag) { + RECT gutter_bounds = { label_start - kGutterToLabel - gutter_width, 0, 0, + height() }; + gutter_bounds.right = gutter_bounds.left + gutter_width; + AdjustBoundsForRTLUI(&gutter_bounds); + NativeTheme::instance()->PaintMenuGutter(dc, MENU_POPUPGUTTER, MPI_NORMAL, + &gutter_bounds); + } + + // Render the background. + if (!for_drag) { + RECT item_bounds = { 0, 0, width(), height() }; + AdjustBoundsForRTLUI(&item_bounds); + NativeTheme::instance()->PaintMenuItemBackground( + NativeTheme::MENU, dc, MENU_POPUPITEM, state, render_selection, + &item_bounds); + } + + int icon_x = kItemLeftMargin; + int top_margin = GetTopMargin(); + int bottom_margin = GetBottomMargin(); + int icon_y = top_margin + (height() - kItemTopMargin - + bottom_margin - check_height) / 2; + int icon_height = check_height; + int icon_width = check_width; + + if (type_ == CHECKBOX && GetDelegate()->IsItemChecked(GetCommand())) { + // Draw the check background. + RECT check_bg_bounds = { 0, 0, icon_x + icon_width, height() }; + const int bg_state = IsEnabled() ? MCB_NORMAL : MCB_DISABLED; + AdjustBoundsForRTLUI(&check_bg_bounds); + NativeTheme::instance()->PaintMenuCheckBackground( + NativeTheme::MENU, dc, MENU_POPUPCHECKBACKGROUND, bg_state, + &check_bg_bounds); + + // And the check. + RECT check_bounds = { icon_x, icon_y, icon_x + icon_width, + icon_y + icon_height }; + const int check_state = IsEnabled() ? MC_CHECKMARKNORMAL : + MC_CHECKMARKDISABLED; + AdjustBoundsForRTLUI(&check_bounds); + NativeTheme::instance()->PaintMenuCheck( + NativeTheme::MENU, dc, MENU_POPUPCHECK, check_state, &check_bounds, + render_selection); + } + + // Render the foreground. + // Menu color is specific to Vista, fallback to classic colors if can't + // get color. + int default_sys_color = render_selection ? COLOR_HIGHLIGHTTEXT : + (IsEnabled() ? COLOR_MENUTEXT : COLOR_GRAYTEXT); + SkColor fg_color = NativeTheme::instance()->GetThemeColorWithDefault( + NativeTheme::MENU, MENU_POPUPITEM, state, TMT_TEXTCOLOR, + default_sys_color); + int width = this->width() - item_right_margin - label_start; + ChromeFont& font = GetRootMenuItem()->font_; + gfx::Rect text_bounds(label_start, top_margin, width, font.height()); + text_bounds.set_x(MirroredLeftPointForRect(text_bounds)); + if (for_drag) { + // With different themes, it's difficult to tell what the correct foreground + // and background colors are for the text to draw the correct halo. Instead, + // just draw black on white, which will look good in most cases. + canvas->DrawStringWithHalo(GetTitle(), font, 0x00000000, 0xFFFFFFFF, + text_bounds.x(), text_bounds.y(), + text_bounds.width(), text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + } else { + canvas->DrawStringInt(GetTitle(), font, fg_color, + text_bounds.x(), text_bounds.y(), text_bounds.width(), + text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + } + + if (icon_.width() > 0) { + gfx::Rect icon_bounds(kItemLeftMargin, + top_margin + (height() - top_margin - + bottom_margin - icon_.height()) / 2, + icon_.width(), + icon_.height()); + icon_bounds.set_x(MirroredLeftPointForRect(icon_bounds)); + canvas->DrawBitmapInt(icon_, icon_bounds.x(), icon_bounds.y()); + } + + if (HasSubmenu()) { + int state_id = IsEnabled() ? MSM_NORMAL : MSM_DISABLED; + RECT arrow_bounds = { + this->width() - item_right_margin + kLabelToArrowPadding, + 0, + 0, + height() + }; + arrow_bounds.right = arrow_bounds.left + arrow_width; + AdjustBoundsForRTLUI(&arrow_bounds); + + // If our sub menus open from right to left (which is the case when the + // locale is RTL) then we should make sure the menu arrow points to the + // right direction. + NativeTheme::MenuArrowDirection arrow_direction; + if (UILayoutIsRightToLeft()) + arrow_direction = NativeTheme::LEFT_POINTING_ARROW; + else + arrow_direction = NativeTheme::RIGHT_POINTING_ARROW; + + NativeTheme::instance()->PaintMenuArrow( + NativeTheme::MENU, dc, MENU_POPUPSUBMENU, state_id, &arrow_bounds, + arrow_direction, render_selection); + } + canvas->endPlatformPaint(); +} + +void MenuItemView::DestroyAllMenuHosts() { + if (!HasSubmenu()) + return; + + submenu_->Close(); + for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; + ++i) { + submenu_->GetMenuItemAt(i)->DestroyAllMenuHosts(); + } +} + +int MenuItemView::GetTopMargin() { + MenuItemView* root = GetRootMenuItem(); + return root && root->has_icons_ ? kItemTopMargin : kItemNoIconTopMargin; +} + +int MenuItemView::GetBottomMargin() { + MenuItemView* root = GetRootMenuItem(); + return root && root->has_icons_ ? + kItemBottomMargin : kItemNoIconBottomMargin; +} + +// MenuController ------------------------------------------------------------ + +// static +MenuController* MenuController::active_instance_ = NULL; + +// static +MenuController* MenuController::GetActiveInstance() { + return active_instance_; +} + +#ifdef DEBUG_MENU +static int instance_count = 0; +static int nested_depth = 0; +#endif + +MenuItemView* MenuController::Run(HWND parent, + MenuItemView* root, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition position, + int* result_mouse_event_flags) { + exit_all_ = false; + possible_drag_ = false; + + bool nested_menu = showing_; + if (showing_) { + // Only support nesting of blocking_run menus, nesting of + // blocking/non-blocking shouldn't be needed. + DCHECK(blocking_run_); + + // We're already showing, push the current state. + menu_stack_.push_back(state_); + + // The context menu should be owned by the same parent. + DCHECK(owner_ == parent); + } else { + showing_ = true; + } + + // Reset current state. + pending_state_ = State(); + state_ = State(); + pending_state_.initial_bounds = bounds; + if (bounds.height() > 1) { + // Inset the bounds slightly, otherwise drag coordinates don't line up + // nicely and menus close prematurely. + pending_state_.initial_bounds.Inset(0, 1); + } + pending_state_.anchor = position; + owner_ = parent; + + // Calculate the bounds of the monitor we'll show menus on. Do this once to + // avoid repeated system queries for the info. + POINT initial_loc = { bounds.x(), bounds.y() }; + HMONITOR monitor = MonitorFromPoint(initial_loc, MONITOR_DEFAULTTONEAREST); + if (monitor) { + MONITORINFO mi = {0}; + mi.cbSize = sizeof(mi); + GetMonitorInfo(monitor, &mi); + // Menus appear over the taskbar. + pending_state_.monitor_bounds = gfx::Rect(mi.rcMonitor); + } + + // Set the selection, which opens the initial menu. + SetSelection(root, true, true); + + if (!blocking_run_) { + // Start the timer to hide the menu. This is needed as we get no + // notification when the drag has finished. + StartCancelAllTimer(); + return NULL; + } + +#ifdef DEBUG_MENU + nested_depth++; + DLOG(INFO) << " entering nested loop, depth=" << nested_depth; +#endif + + MessageLoopForUI* loop = MessageLoopForUI::current(); + if (MenuItemView::allow_task_nesting_during_run_) { + bool did_allow_task_nesting = loop->NestableTasksAllowed(); + loop->SetNestableTasksAllowed(true); + loop->Run(this); + loop->SetNestableTasksAllowed(did_allow_task_nesting); + } else { + loop->Run(this); + } + +#ifdef DEBUG_MENU + nested_depth--; + DLOG(INFO) << " exiting nested loop, depth=" << nested_depth; +#endif + + // Close any open menus. + SetSelection(NULL, false, true); + + if (nested_menu) { + DCHECK(!menu_stack_.empty()); + // We're running from within a menu, restore the previous state. + // The menus are already showing, so we don't have to show them. + state_ = menu_stack_.back(); + pending_state_ = menu_stack_.back(); + menu_stack_.pop_back(); + } else { + showing_ = false; + did_capture_ = false; + } + + MenuItemView* result = result_; + // In case we're nested, reset result_. + result_ = NULL; + + if (result_mouse_event_flags) + *result_mouse_event_flags = result_mouse_event_flags_; + + if (nested_menu && result) { + // We're nested and about to return a value. The caller might enter another + // blocking loop. We need to make sure all menus are hidden before that + // happens otherwise the menus will stay on screen. + CloseAllNestedMenus(); + + // Set exit_all_ to true, which makes sure all nested loops exit + // immediately. + exit_all_ = true; + } + + return result; +} + +void MenuController::SetSelection(MenuItemView* menu_item, + bool open_submenu, + bool update_immediately) { + size_t paths_differ_at = 0; + std::vector<MenuItemView*> current_path; + std::vector<MenuItemView*> new_path; + BuildPathsAndCalculateDiff(pending_state_.item, menu_item, ¤t_path, + &new_path, &paths_differ_at); + + size_t current_size = current_path.size(); + size_t new_size = new_path.size(); + + // Notify the old path it isn't selected. + for (size_t i = paths_differ_at; i < current_size; ++i) + current_path[i]->SetSelected(false); + + // Notify the new path it is selected. + for (size_t i = paths_differ_at; i < new_size; ++i) + new_path[i]->SetSelected(true); + + if (menu_item && menu_item->GetDelegate()) + menu_item->GetDelegate()->SelectionChanged(menu_item); + + pending_state_.item = menu_item; + pending_state_.submenu_open = open_submenu; + + // Stop timers. + StopShowTimer(); + StopCancelAllTimer(); + + if (update_immediately) + CommitPendingSelection(); + else + StartShowTimer(); +} + +void MenuController::Cancel(bool all) { + if (!showing_) { + // This occurs if we're in the process of notifying the delegate for a drop + // and the delegate cancels us. + return; + } + + MenuItemView* selected = state_.item; + exit_all_ = all; + + // Hide windows immediately. + SetSelection(NULL, false, true); + + if (!blocking_run_) { + // If we didn't block the caller we need to notify the menu, which + // triggers deleting us. + DCHECK(selected); + showing_ = false; + selected->GetRootMenuItem()->DropMenuClosed(true); + // WARNING: the call to MenuClosed deletes us. + return; + } +} + +void MenuController::OnMousePressed(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMousePressed source=" << source; +#endif + if (!blocking_run_) + return; + + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + if (part.is_scroll()) + return; // Ignore presses on scroll buttons. + + if (part.type == MenuPart::NONE || + (part.type == MenuPart::MENU_ITEM && part.menu && + part.menu->GetRootMenuItem() != state_.item->GetRootMenuItem())) { + // Mouse wasn't pressed over any menu, or the active menu, cancel. + + // We're going to close and we own the mouse capture. We need to repost the + // mouse down, otherwise the window the user clicked on won't get the + // event. + RepostEvent(source, event); + + // And close. + Cancel(true); + return; + } + + bool open_submenu = false; + if (!part.menu) { + part.menu = source->GetMenuItem(); + open_submenu = true; + } else { + if (part.menu->GetDelegate()->CanDrag(part.menu)) { + possible_drag_ = true; + press_x_ = event.x(); + press_y_ = event.y(); + } + if (part.menu->HasSubmenu()) + open_submenu = true; + } + // On a press we immediately commit the selection, that way a submenu + // pops up immediately rather than after a delay. + SetSelection(part.menu, open_submenu, true); +} + +void MenuController::OnMouseDragged(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMouseDragged source=" << source; +#endif + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + UpdateScrolling(part); + + if (!blocking_run_) + return; + + if (possible_drag_) { + if (View::ExceededDragThreshold(event.x() - press_x_, + event.y() - press_y_)) { + MenuItemView* item = state_.item; + DCHECK(item); + // Points are in the coordinates of the submenu, need to map to that of + // the selected item. Additionally source may not be the parent of + // the selected item, so need to map to screen first then to item. + gfx::Point press_loc(press_x_, press_y_); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &press_loc); + View::ConvertPointToView(NULL, item, &press_loc); + gfx::Point drag_loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &drag_loc); + View::ConvertPointToView(NULL, item, &drag_loc); + ChromeCanvas canvas(item->width(), item->height(), false); + item->Paint(&canvas, true); + + scoped_refptr<OSExchangeData> data(new OSExchangeData); + item->GetDelegate()->WriteDragData(item, data.get()); + drag_utils::SetDragImageOnDataObject(canvas, item->width(), + item->height(), press_loc.x(), + press_loc.y(), data); + + scoped_refptr<BaseDragSource> drag_source(new BaseDragSource); + int drag_ops = item->GetDelegate()->GetDragOperations(item); + DWORD effects; + StopScrolling(); + DoDragDrop(data, drag_source, + DragDropTypes::DragOperationToDropEffect(drag_ops), + &effects); + if (GetActiveInstance() == this) { + if (showing_) { + // We're still showing, close all menus. + CloseAllNestedMenus(); + Cancel(true); + } // else case, drop was on us. + } // else case, someone canceled us, don't do anything + } + return; + } + if (part.type == MenuPart::MENU_ITEM) { + if (!part.menu) + part.menu = source->GetMenuItem(); + SetSelection(part.menu ? part.menu : state_.item, true, false); + } +} + +void MenuController::OnMouseReleased(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMouseReleased source=" << source; +#endif + if (!blocking_run_) + return; + + DCHECK(state_.item); + possible_drag_ = false; + DCHECK(blocking_run_); + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + if (event.IsRightMouseButton() && (part.type == MenuPart::MENU_ITEM && + part.menu)) { + // Set the selection immediately, making sure the submenu is only open + // if it already was. + bool open_submenu = (state_.item == pending_state_.item && + state_.submenu_open); + SetSelection(pending_state_.item, open_submenu, true); + gfx::Point loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &loc); + + // If we open a context menu just return now + if (part.menu->GetDelegate()->ShowContextMenu( + part.menu, part.menu->GetCommand(), loc.x(), loc.y(), true)) + return; + } + + if (!part.is_scroll() && part.menu && !part.menu->HasSubmenu()) { + if (part.menu->GetDelegate()->IsTriggerableEvent(event)) { + Accept(part.menu, event.GetFlags()); + return; + } + } else if (part.type == MenuPart::MENU_ITEM) { + // User either clicked on empty space, or a menu that has children. + SetSelection(part.menu ? part.menu : state_.item, true, true); + } +} + +void MenuController::OnMouseMoved(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMouseMoved source=" << source; +#endif + if (showing_submenu_) + return; + + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + + UpdateScrolling(part); + + if (!blocking_run_) + return; + + if (part.type == MenuPart::MENU_ITEM && part.menu) { + SetSelection(part.menu, true, false); + } else if (!part.is_scroll() && pending_state_.item && + (!pending_state_.item->HasSubmenu() || + !pending_state_.item->GetSubmenu()->IsShowing())) { + // On exit if the user hasn't selected an item with a submenu, move the + // selection back to the parent menu item. + SetSelection(pending_state_.item->GetParentMenuItem(), true, false); + } +} + +void MenuController::OnMouseEntered(SubmenuView* source, + const MouseEvent& event) { + // MouseEntered is always followed by a mouse moved, so don't need to + // do anything here. +} + +bool MenuController::CanDrop(SubmenuView* source, const OSExchangeData& data) { + return source->GetMenuItem()->GetDelegate()->CanDrop(source->GetMenuItem(), + data); +} + +void MenuController::OnDragEntered(SubmenuView* source, + const DropTargetEvent& event) { + valid_drop_coordinates_ = false; +} + +int MenuController::OnDragUpdated(SubmenuView* source, + const DropTargetEvent& event) { + StopCancelAllTimer(); + + gfx::Point screen_loc(event.location()); + View::ConvertPointToScreen(source, &screen_loc); + if (valid_drop_coordinates_ && screen_loc.x() == drop_x_ && + screen_loc.y() == drop_y_) { + return last_drop_operation_; + } + drop_x_ = screen_loc.x(); + drop_y_ = screen_loc.y(); + valid_drop_coordinates_ = true; + + MenuItemView* menu_item = GetMenuItemAt(source, event.x(), event.y()); + bool over_empty_menu = false; + if (!menu_item) { + // See if we're over an empty menu. + menu_item = GetEmptyMenuItemAt(source, event.x(), event.y()); + if (menu_item) + over_empty_menu = true; + } + MenuDelegate::DropPosition drop_position = MenuDelegate::DROP_NONE; + int drop_operation = DragDropTypes::DRAG_NONE; + if (menu_item) { + gfx::Point menu_item_loc(event.location()); + View::ConvertPointToView(source, menu_item, &menu_item_loc); + MenuItemView* query_menu_item; + if (!over_empty_menu) { + int menu_item_height = menu_item->height(); + if (menu_item->HasSubmenu() && + (menu_item_loc.y() > kDropBetweenPixels && + menu_item_loc.y() < (menu_item_height - kDropBetweenPixels))) { + drop_position = MenuDelegate::DROP_ON; + } else if (menu_item_loc.y() < menu_item_height / 2) { + drop_position = MenuDelegate::DROP_BEFORE; + } else { + drop_position = MenuDelegate::DROP_AFTER; + } + query_menu_item = menu_item; + } else { + query_menu_item = menu_item->GetParentMenuItem(); + drop_position = MenuDelegate::DROP_ON; + } + drop_operation = menu_item->GetDelegate()->GetDropOperation( + query_menu_item, event, &drop_position); + + if (menu_item->HasSubmenu()) { + // The menu has a submenu, schedule the submenu to open. + SetSelection(menu_item, true, false); + } else { + SetSelection(menu_item, false, false); + } + + if (drop_position == MenuDelegate::DROP_NONE || + drop_operation == DragDropTypes::DRAG_NONE) { + menu_item = NULL; + } + } else { + SetSelection(source->GetMenuItem(), true, false); + } + SetDropMenuItem(menu_item, drop_position); + last_drop_operation_ = drop_operation; + return drop_operation; +} + +void MenuController::OnDragExited(SubmenuView* source) { + StartCancelAllTimer(); + + if (drop_target_) { + StopShowTimer(); + SetDropMenuItem(NULL, MenuDelegate::DROP_NONE); + } +} + +int MenuController::OnPerformDrop(SubmenuView* source, + const DropTargetEvent& event) { + DCHECK(drop_target_); + // NOTE: the delegate may delete us after invoking OnPerformDrop, as such + // we don't call cancel here. + + MenuItemView* item = state_.item; + DCHECK(item); + + MenuItemView* drop_target = drop_target_; + MenuDelegate::DropPosition drop_position = drop_position_; + + // Close all menus, including any nested menus. + SetSelection(NULL, false, true); + CloseAllNestedMenus(); + + // Set state such that we exit. + showing_ = false; + exit_all_ = true; + + if (!IsBlockingRun()) + item->GetRootMenuItem()->DropMenuClosed(false); + + // WARNING: the call to MenuClosed deletes us. + + // If over an empty menu item, drop occurs on the parent. + if (drop_target->GetID() == EmptyMenuMenuItem::kEmptyMenuItemViewID) + drop_target = drop_target->GetParentMenuItem(); + + return drop_target->GetDelegate()->OnPerformDrop( + drop_target, drop_position, event); +} + +void MenuController::OnDragEnteredScrollButton(SubmenuView* source, + bool is_up) { + MenuPart part; + part.type = is_up ? MenuPart::SCROLL_UP : MenuPart::SCROLL_DOWN; + part.submenu = source; + UpdateScrolling(part); + + // Do this to force the selection to hide. + SetDropMenuItem(source->GetMenuItemAt(0), MenuDelegate::DROP_NONE); + + StopCancelAllTimer(); +} + +void MenuController::OnDragExitedScrollButton(SubmenuView* source) { + StartCancelAllTimer(); + SetDropMenuItem(NULL, MenuDelegate::DROP_NONE); + StopScrolling(); +} + +// static +void MenuController::SetActiveInstance(MenuController* controller) { + active_instance_ = controller; +} + +bool MenuController::Dispatch(const MSG& msg) { + DCHECK(blocking_run_); + + if (exit_all_) { + // We must translate/dispatch the message here, otherwise we would drop + // the message on the floor. + TranslateMessage(&msg); + DispatchMessage(&msg); + return false; + } + + // NOTE: we don't get WM_ACTIVATE or anything else interesting in here. + switch (msg.message) { + case WM_CONTEXTMENU: { + MenuItemView* item = pending_state_.item; + if (item && item->GetRootMenuItem() != item) { + gfx::Point screen_loc(0, item->height()); + View::ConvertPointToScreen(item, &screen_loc); + item->GetDelegate()->ShowContextMenu( + item, item->GetCommand(), screen_loc.x(), screen_loc.y(), false); + } + return true; + } + + // NOTE: focus wasn't changed when the menu was shown. As such, don't + // dispatch key events otherwise the focused window will get the events. + case WM_KEYDOWN: + return OnKeyDown(msg); + + case WM_CHAR: + return OnChar(msg); + + case WM_KEYUP: + return true; + + case WM_SYSKEYUP: + // We may have been shown on a system key, as such don't do anything + // here. If another system key is pushed we'll get a WM_SYSKEYDOWN and + // close the menu. + return true; + + case WM_CANCELMODE: + case WM_SYSKEYDOWN: + // Exit immediately on system keys. + Cancel(true); + return false; + + default: + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + return !exit_all_; +} + +bool MenuController::OnKeyDown(const MSG& msg) { + DCHECK(blocking_run_); + + switch (msg.wParam) { + case VK_UP: + IncrementSelection(-1); + break; + + case VK_DOWN: + IncrementSelection(1); + break; + + // Handling of VK_RIGHT and VK_LEFT is different depending on the UI + // layout. + case VK_RIGHT: + if (l10n_util::TextDirection() == l10n_util::RIGHT_TO_LEFT) + CloseSubmenu(); + else + OpenSubmenuChangeSelectionIfCan(); + break; + + case VK_LEFT: + if (l10n_util::TextDirection() == l10n_util::RIGHT_TO_LEFT) + OpenSubmenuChangeSelectionIfCan(); + else + CloseSubmenu(); + break; + + case VK_RETURN: + if (pending_state_.item) { + if (pending_state_.item->HasSubmenu()) { + OpenSubmenuChangeSelectionIfCan(); + } else if (pending_state_.item->IsEnabled()) { + Accept(pending_state_.item, 0); + return false; + } + } + break; + + case VK_ESCAPE: + if (!state_.item->GetParentMenuItem() || + (!state_.item->GetParentMenuItem()->GetParentMenuItem() && + (!state_.item->HasSubmenu() || + !state_.item->GetSubmenu()->IsShowing()))) { + // User pressed escape and only one menu is shown, cancel it. + Cancel(false); + return false; + } else { + CloseSubmenu(); + } + break; + + case VK_APPS: + break; + + default: + TranslateMessage(&msg); + break; + } + return true; +} + +bool MenuController::OnChar(const MSG& msg) { + DCHECK(blocking_run_); + + return !SelectByChar(static_cast<wchar_t>(msg.wParam)); +} + +MenuController::MenuController(bool blocking) + : blocking_run_(blocking), + showing_(false), + exit_all_(false), + did_capture_(false), + result_(NULL), + drop_target_(NULL), + owner_(NULL), + possible_drag_(false), + valid_drop_coordinates_(false), + showing_submenu_(false), + result_mouse_event_flags_(0) { +#ifdef DEBUG_MENU + instance_count++; + DLOG(INFO) << "created MC, count=" << instance_count; +#endif +} + +MenuController::~MenuController() { + DCHECK(!showing_); + StopShowTimer(); + StopCancelAllTimer(); +#ifdef DEBUG_MENU + instance_count--; + DLOG(INFO) << "destroyed MC, count=" << instance_count; +#endif +} + +void MenuController::Accept(MenuItemView* item, int mouse_event_flags) { + DCHECK(IsBlockingRun()); + result_ = item; + exit_all_ = true; + result_mouse_event_flags_ = mouse_event_flags; +} + +void MenuController::CloseAllNestedMenus() { + for (std::list<State>::iterator i = menu_stack_.begin(); + i != menu_stack_.end(); ++i) { + MenuItemView* item = i->item; + MenuItemView* last_item = item; + while (item) { + CloseMenu(item); + last_item = item; + item = item->GetParentMenuItem(); + } + i->submenu_open = false; + i->item = last_item; + } +} + +MenuItemView* MenuController::GetMenuItemAt(View* source, int x, int y) { + View* child_under_mouse = source->GetViewForPoint(gfx::Point(x, y)); + if (child_under_mouse && child_under_mouse->IsEnabled() && + child_under_mouse->GetID() == MenuItemView::kMenuItemViewID) { + return static_cast<MenuItemView*>(child_under_mouse); + } + return NULL; +} + +MenuItemView* MenuController::GetEmptyMenuItemAt(View* source, int x, int y) { + View* child_under_mouse = source->GetViewForPoint(gfx::Point(x, y)); + if (child_under_mouse && + child_under_mouse->GetID() == EmptyMenuMenuItem::kEmptyMenuItemViewID) { + return static_cast<MenuItemView*>(child_under_mouse); + } + return NULL; +} + +bool MenuController::IsScrollButtonAt(SubmenuView* source, + int x, + int y, + MenuPart::Type* part) { + MenuScrollViewContainer* scroll_view = source->GetScrollViewContainer(); + View* child_under_mouse = scroll_view->GetViewForPoint(gfx::Point(x, y)); + if (child_under_mouse && child_under_mouse->IsEnabled()) { + if (child_under_mouse == scroll_view->scroll_up_button()) { + *part = MenuPart::SCROLL_UP; + return true; + } + if (child_under_mouse == scroll_view->scroll_down_button()) { + *part = MenuPart::SCROLL_DOWN; + return true; + } + } + return false; +} + +MenuController::MenuPart MenuController::GetMenuPartByScreenCoordinate( + SubmenuView* source, + int source_x, + int source_y) { + MenuPart part; + + gfx::Point screen_loc(source_x, source_y); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); + + MenuItemView* item = state_.item; + while (item) { + if (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + GetMenuPartByScreenCoordinateImpl(item->GetSubmenu(), screen_loc, + &part)) { + return part; + } + item = item->GetParentMenuItem(); + } + + return part; +} + +bool MenuController::GetMenuPartByScreenCoordinateImpl( + SubmenuView* menu, + const gfx::Point& screen_loc, + MenuPart* part) { + // Is the mouse over the scroll buttons? + gfx::Point scroll_view_loc = screen_loc; + View* scroll_view_container = menu->GetScrollViewContainer(); + View::ConvertPointToView(NULL, scroll_view_container, &scroll_view_loc); + if (scroll_view_loc.x() < 0 || + scroll_view_loc.x() >= scroll_view_container->width() || + scroll_view_loc.y() < 0 || + scroll_view_loc.y() >= scroll_view_container->height()) { + // Point isn't contained in menu. + return false; + } + if (IsScrollButtonAt(menu, scroll_view_loc.x(), scroll_view_loc.y(), + &(part->type))) { + part->submenu = menu; + return true; + } + + // Not over the scroll button. Check the actual menu. + if (DoesSubmenuContainLocation(menu, screen_loc)) { + gfx::Point menu_loc = screen_loc; + View::ConvertPointToView(NULL, menu, &menu_loc); + part->menu = GetMenuItemAt(menu, menu_loc.x(), menu_loc.y()); + part->type = MenuPart::MENU_ITEM; + return true; + } + + // While the mouse isn't over a menu item or the scroll buttons of menu, it + // is contained by menu and so we return true. If we didn't return true other + // menus would be searched, even though they are likely obscured by us. + return true; +} + +bool MenuController::DoesSubmenuContainLocation(SubmenuView* submenu, + const gfx::Point& screen_loc) { + gfx::Point view_loc = screen_loc; + View::ConvertPointToView(NULL, submenu, &view_loc); + gfx::Rect vis_rect = submenu->GetVisibleBounds(); + return vis_rect.Contains(view_loc.x(), view_loc.y()); +} + +void MenuController::CommitPendingSelection() { + StopShowTimer(); + + size_t paths_differ_at = 0; + std::vector<MenuItemView*> current_path; + std::vector<MenuItemView*> new_path; + BuildPathsAndCalculateDiff(state_.item, pending_state_.item, ¤t_path, + &new_path, &paths_differ_at); + + // Hide the old menu. + for (size_t i = paths_differ_at; i < current_path.size(); ++i) { + if (current_path[i]->HasSubmenu()) { + current_path[i]->GetSubmenu()->Hide(); + } + } + + // Copy pending to state_, making sure to preserve the direction menus were + // opened. + std::list<bool> pending_open_direction; + state_.open_leading.swap(pending_open_direction); + state_ = pending_state_; + state_.open_leading.swap(pending_open_direction); + + int menu_depth = MenuDepth(state_.item); + if (menu_depth == 0) { + state_.open_leading.clear(); + } else { + int cached_size = static_cast<int>(state_.open_leading.size()); + DCHECK(menu_depth >= 0); + while (cached_size-- >= menu_depth) + state_.open_leading.pop_back(); + } + + if (!state_.item) { + // Nothing to select. + StopScrolling(); + return; + } + + // Open all the submenus preceeding the last menu item (last menu item is + // handled next). + if (new_path.size() > 1) { + for (std::vector<MenuItemView*>::iterator i = new_path.begin(); + i != new_path.end() - 1; ++i) { + OpenMenu(*i); + } + } + + if (state_.submenu_open) { + // The submenu should be open, open the submenu if the item has a submenu. + if (state_.item->HasSubmenu()) { + OpenMenu(state_.item); + } else { + state_.submenu_open = false; + } + } else if (state_.item->HasSubmenu() && + state_.item->GetSubmenu()->IsShowing()) { + state_.item->GetSubmenu()->Hide(); + } + + if (scroll_task_.get() && scroll_task_->submenu()) { + // Stop the scrolling if none of the elements of the selection contain + // the menu being scrolled. + bool found = false; + MenuItemView* item = state_.item; + while (item && !found) { + found = (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + item->GetSubmenu() == scroll_task_->submenu()); + item = item->GetParentMenuItem(); + } + if (!found) + StopScrolling(); + } +} + +void MenuController::CloseMenu(MenuItemView* item) { + DCHECK(item); + if (!item->HasSubmenu()) + return; + item->GetSubmenu()->Hide(); +} + +void MenuController::OpenMenu(MenuItemView* item) { + DCHECK(item); + if (item->GetSubmenu()->IsShowing()) { + return; + } + + bool prefer_leading = + state_.open_leading.empty() ? true : state_.open_leading.back(); + bool resulting_direction; + gfx::Rect bounds = + CalculateMenuBounds(item, prefer_leading, &resulting_direction); + state_.open_leading.push_back(resulting_direction); + bool do_capture = (!did_capture_ && blocking_run_); + showing_submenu_ = true; + item->GetSubmenu()->ShowAt(owner_, bounds, do_capture); + showing_submenu_ = false; + did_capture_ = true; +} + +void MenuController::BuildPathsAndCalculateDiff( + MenuItemView* old_item, + MenuItemView* new_item, + std::vector<MenuItemView*>* old_path, + std::vector<MenuItemView*>* new_path, + size_t* first_diff_at) { + DCHECK(old_path && new_path && first_diff_at); + BuildMenuItemPath(old_item, old_path); + BuildMenuItemPath(new_item, new_path); + + size_t common_size = std::min(old_path->size(), new_path->size()); + + // Find the first difference between the two paths, when the loop + // returns, diff_i is the first index where the two paths differ. + for (size_t i = 0; i < common_size; ++i) { + if ((*old_path)[i] != (*new_path)[i]) { + *first_diff_at = i; + return; + } + } + + *first_diff_at = common_size; +} + +void MenuController::BuildMenuItemPath(MenuItemView* item, + std::vector<MenuItemView*>* path) { + if (!item) + return; + BuildMenuItemPath(item->GetParentMenuItem(), path); + path->push_back(item); +} + +void MenuController::StartShowTimer() { + show_timer_.Start(TimeDelta::FromMilliseconds(kShowDelay), this, + &MenuController::CommitPendingSelection); +} + +void MenuController::StopShowTimer() { + show_timer_.Stop(); +} + +void MenuController::StartCancelAllTimer() { + cancel_all_timer_.Start(TimeDelta::FromMilliseconds(kCloseOnExitTime), + this, &MenuController::CancelAll); +} + +void MenuController::StopCancelAllTimer() { + cancel_all_timer_.Stop(); +} + +gfx::Rect MenuController::CalculateMenuBounds(MenuItemView* item, + bool prefer_leading, + bool* is_leading) { + DCHECK(item); + + SubmenuView* submenu = item->GetSubmenu(); + DCHECK(submenu); + + gfx::Size pref = submenu->GetScrollViewContainer()->GetPreferredSize(); + + // Don't let the menu go to wide. This is some where between what IE and FF + // do. + pref.set_width(std::min(pref.width(), kMaxMenuWidth)); + if (!state_.monitor_bounds.IsEmpty()) + pref.set_width(std::min(pref.width(), state_.monitor_bounds.width())); + + // Assume we can honor prefer_leading. + *is_leading = prefer_leading; + + int x, y; + + if (!item->GetParentMenuItem()) { + // First item, position relative to initial location. + x = state_.initial_bounds.x(); + y = state_.initial_bounds.bottom(); + if (state_.anchor == MenuItemView::TOPRIGHT) + x = x + state_.initial_bounds.width() - pref.width(); + if (!state_.monitor_bounds.IsEmpty() && + y + pref.height() > state_.monitor_bounds.bottom()) { + // The menu doesn't fit on screen. If the first location is above the + // half way point, show from the mouse location to bottom of screen. + // Otherwise show from the top of the screen to the location of the mouse. + // While odd, this behavior matches IE. + if (y < (state_.monitor_bounds.y() + + state_.monitor_bounds.height() / 2)) { + pref.set_height(std::min(pref.height(), + state_.monitor_bounds.bottom() - y)); + } else { + pref.set_height(std::min(pref.height(), + state_.initial_bounds.y() - state_.monitor_bounds.y())); + y = state_.initial_bounds.y() - pref.height(); + } + } + } else { + // Not the first menu; position it relative to the bounds of the menu + // item. + gfx::Point item_loc; + View::ConvertPointToScreen(item, &item_loc); + + // We must make sure we take into account the UI layout. If the layout is + // RTL, then a 'leading' menu is positioned to the left of the parent menu + // item and not to the right. + bool layout_is_rtl = item->UILayoutIsRightToLeft(); + bool create_on_the_right = (prefer_leading && !layout_is_rtl) || + (!prefer_leading && layout_is_rtl); + + if (create_on_the_right) { + x = item_loc.x() + item->width() - kSubmenuHorizontalInset; + if (state_.monitor_bounds.width() != 0 && + x + pref.width() > state_.monitor_bounds.right()) { + if (layout_is_rtl) + *is_leading = true; + else + *is_leading = false; + x = item_loc.x() - pref.width() + kSubmenuHorizontalInset; + } + } else { + x = item_loc.x() - pref.width() + kSubmenuHorizontalInset; + if (state_.monitor_bounds.width() != 0 && x < state_.monitor_bounds.x()) { + if (layout_is_rtl) + *is_leading = false; + else + *is_leading = true; + x = item_loc.x() + item->width() - kSubmenuHorizontalInset; + } + } + y = item_loc.y() - kSubmenuBorderSize; + if (state_.monitor_bounds.width() != 0) { + pref.set_height(std::min(pref.height(), state_.monitor_bounds.height())); + if (y + pref.height() > state_.monitor_bounds.bottom()) + y = state_.monitor_bounds.bottom() - pref.height(); + if (y < state_.monitor_bounds.y()) + y = state_.monitor_bounds.y(); + } + } + + if (state_.monitor_bounds.width() != 0) { + if (x + pref.width() > state_.monitor_bounds.right()) + x = state_.monitor_bounds.right() - pref.width(); + if (x < state_.monitor_bounds.x()) + x = state_.monitor_bounds.x(); + } + return gfx::Rect(x, y, pref.width(), pref.height()); +} + +// static +int MenuController::MenuDepth(MenuItemView* item) { + if (!item) + return 0; + return MenuDepth(item->GetParentMenuItem()) + 1; +} + +void MenuController::IncrementSelection(int delta) { + MenuItemView* item = pending_state_.item; + DCHECK(item); + if (pending_state_.submenu_open && item->HasSubmenu() && + item->GetSubmenu()->IsShowing()) { + // A menu is selected and open, but none of its children are selected, + // select the first menu item. + if (item->GetSubmenu()->GetMenuItemCount()) { + SetSelection(item->GetSubmenu()->GetMenuItemAt(0), false, false); + ScrollToVisible(item->GetSubmenu()->GetMenuItemAt(0)); + return; // return so else case can fall through. + } + } + if (item->GetParentMenuItem()) { + MenuItemView* parent = item->GetParentMenuItem(); + int parent_count = parent->GetSubmenu()->GetMenuItemCount(); + if (parent_count > 1) { + for (int i = 0; i < parent_count; ++i) { + if (parent->GetSubmenu()->GetMenuItemAt(i) == item) { + int next_index = (i + delta + parent_count) % parent_count; + ScrollToVisible(parent->GetSubmenu()->GetMenuItemAt(next_index)); + SetSelection(parent->GetSubmenu()->GetMenuItemAt(next_index), false, + false); + break; + } + } + } + } +} + +void MenuController::OpenSubmenuChangeSelectionIfCan() { + MenuItemView* item = pending_state_.item; + if (item->HasSubmenu()) { + if (item->GetSubmenu()->GetMenuItemCount() > 0) { + SetSelection(item->GetSubmenu()->GetMenuItemAt(0), false, true); + } else { + // No menu items, just show the sub-menu. + SetSelection(item, true, true); + } + } +} + +void MenuController::CloseSubmenu() { + MenuItemView* item = state_.item; + DCHECK(item); + if (!item->GetParentMenuItem()) + return; + if (item->HasSubmenu() && item->GetSubmenu()->IsShowing()) { + SetSelection(item, false, true); + } else if (item->GetParentMenuItem()->GetParentMenuItem()) { + SetSelection(item->GetParentMenuItem(), false, true); + } +} + +bool MenuController::IsMenuWindow(MenuItemView* item, HWND window) { + if (!item) + return false; + return ((item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + item->GetSubmenu()->GetWidget()->GetNativeView() == window) || + IsMenuWindow(item->GetParentMenuItem(), window)); +} + +bool MenuController::SelectByChar(wchar_t character) { + wchar_t char_array[1] = { character }; + wchar_t key = l10n_util::ToLower(char_array)[0]; + MenuItemView* item = pending_state_.item; + if (!item->HasSubmenu() || !item->GetSubmenu()->IsShowing()) + item = item->GetParentMenuItem(); + DCHECK(item); + DCHECK(item->HasSubmenu()); + SubmenuView* submenu = item->GetSubmenu(); + DCHECK(submenu); + int menu_item_count = submenu->GetMenuItemCount(); + if (!menu_item_count) + return false; + for (int i = 0; i < menu_item_count; ++i) { + MenuItemView* child = submenu->GetMenuItemAt(i); + if (child->GetMnemonic() == key && child->IsEnabled()) { + Accept(child, 0); + return true; + } + } + + // No matching mnemonic, search through items that don't have mnemonic + // based on first character of the title. + int first_match = -1; + bool has_multiple = false; + int next_match = -1; + int index_of_item = -1; + for (int i = 0; i < menu_item_count; ++i) { + MenuItemView* child = submenu->GetMenuItemAt(i); + if (!child->GetMnemonic() && child->IsEnabled()) { + std::wstring lower_title = l10n_util::ToLower(child->GetTitle()); + if (child == pending_state_.item) + index_of_item = i; + if (lower_title.length() && lower_title[0] == key) { + if (first_match == -1) + first_match = i; + else + has_multiple = true; + if (next_match == -1 && index_of_item != -1 && i > index_of_item) + next_match = i; + } + } + } + if (first_match != -1) { + if (!has_multiple) { + if (submenu->GetMenuItemAt(first_match)->HasSubmenu()) { + SetSelection(submenu->GetMenuItemAt(first_match), true, false); + } else { + Accept(submenu->GetMenuItemAt(first_match), 0); + return true; + } + } else if (index_of_item == -1 || next_match == -1) { + SetSelection(submenu->GetMenuItemAt(first_match), false, false); + } else { + SetSelection(submenu->GetMenuItemAt(next_match), false, false); + } + } + return false; +} + +void MenuController::RepostEvent(SubmenuView* source, + const MouseEvent& event) { + gfx::Point screen_loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); + HWND window = WindowFromPoint(screen_loc.ToPOINT()); + if (window) { +#ifdef DEBUG_MENU + DLOG(INFO) << "RepostEvent on press"; +#endif + + // Release the capture. + SubmenuView* submenu = state_.item->GetRootMenuItem()->GetSubmenu(); + submenu->ReleaseCapture(); + + if (submenu->host() && submenu->host()->GetNativeView() && + GetWindowThreadProcessId(submenu->host()->GetNativeView(), NULL) != + GetWindowThreadProcessId(window, NULL)) { + // Even though we have mouse capture, windows generates a mouse event + // if the other window is in a separate thread. Don't generate an event in + // this case else the target window can get double events leading to bad + // behavior. + return; + } + + // Convert the coordinates to the target window. + RECT window_bounds; + GetWindowRect(window, &window_bounds); + int window_x = screen_loc.x() - window_bounds.left; + int window_y = screen_loc.y() - window_bounds.top; + + // Determine whether the click was in the client area or not. + // NOTE: WM_NCHITTEST coordinates are relative to the screen. + LRESULT nc_hit_result = SendMessage(window, WM_NCHITTEST, 0, + MAKELPARAM(screen_loc.x(), + screen_loc.y())); + const bool in_client_area = (nc_hit_result == HTCLIENT); + + // TODO(sky): this isn't right. The event to generate should correspond + // with the event we just got. MouseEvent only tells us what is down, + // which may differ. Need to add ability to get changed button from + // MouseEvent. + int event_type; + if (event.IsLeftMouseButton()) + event_type = in_client_area ? WM_LBUTTONDOWN : WM_NCLBUTTONDOWN; + else if (event.IsMiddleMouseButton()) + event_type = in_client_area ? WM_MBUTTONDOWN : WM_NCMBUTTONDOWN; + else if (event.IsRightMouseButton()) + event_type = in_client_area ? WM_RBUTTONDOWN : WM_NCRBUTTONDOWN; + else + event_type = 0; // Unknown mouse press. + + if (event_type) { + if (in_client_area) { + PostMessage(window, event_type, event.GetWindowsFlags(), + MAKELPARAM(window_x, window_y)); + } else { + PostMessage(window, event_type, nc_hit_result, + MAKELPARAM(screen_loc.x(), screen_loc.y())); + } + } + } +} + +void MenuController::SetDropMenuItem( + MenuItemView* new_target, + MenuDelegate::DropPosition new_position) { + if (new_target == drop_target_ && new_position == drop_position_) + return; + + if (drop_target_) { + drop_target_->GetParentMenuItem()->GetSubmenu()->SetDropMenuItem( + NULL, MenuDelegate::DROP_NONE); + } + drop_target_ = new_target; + drop_position_ = new_position; + if (drop_target_) { + drop_target_->GetParentMenuItem()->GetSubmenu()->SetDropMenuItem( + drop_target_, drop_position_); + } +} + +void MenuController::UpdateScrolling(const MenuPart& part) { + if (!part.is_scroll() && !scroll_task_.get()) + return; + + if (!scroll_task_.get()) + scroll_task_.reset(new MenuScrollTask()); + scroll_task_->Update(part); +} + +void MenuController::StopScrolling() { + scroll_task_.reset(NULL); +} + +} // namespace views diff --git a/views/controls/menu/chrome_menu.h b/views/controls/menu/chrome_menu.h new file mode 100644 index 0000000..02caaed --- /dev/null +++ b/views/controls/menu/chrome_menu.h @@ -0,0 +1,948 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_MENU_CHROME_MENU_H_ +#define VIEWS_CONTROLS_MENU_CHROME_MENU_H_ + +#include <list> +#include <vector> + +#include "app/drag_drop_types.h" +#include "app/gfx/chrome_font.h" +#include "base/gfx/point.h" +#include "base/gfx/rect.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "skia/include/SkBitmap.h" +#include "views/controls/menu/controller.h" +#include "views/view.h" + +namespace views { + +class WidgetWin; +class MenuController; +class MenuItemView; +class SubmenuView; + +namespace { +class MenuHost; +class MenuHostRootView; +class MenuScrollTask; +class MenuScrollViewContainer; +} + +// MenuDelegate -------------------------------------------------------------- + +// Delegate for the menu. + +class MenuDelegate : Controller { + public: + // Used during drag and drop to indicate where the drop indicator should + // be rendered. + enum DropPosition { + // Indicates a drop is not allowed here. + DROP_NONE, + + // Indicates the drop should occur before the item. + DROP_BEFORE, + + // Indicates the drop should occur after the item. + DROP_AFTER, + + // Indicates the drop should occur on the item. + DROP_ON + }; + + // Whether or not an item should be shown as checked. + // TODO(sky): need checked support. + virtual bool IsItemChecked(int id) const { + return false; + } + + // The string shown for the menu item. This is only invoked when an item is + // added with an empty label. + virtual std::wstring GetLabel(int id) const { + return std::wstring(); + } + + // Shows the context menu with the specified id. This is invoked when the + // user does the appropriate gesture to show a context menu. The id + // identifies the id of the menu to show the context menu for. + // is_mouse_gesture is true if this is the result of a mouse gesture. + // If this is not the result of a mouse gesture x/y is the recommended + // location to display the content menu at. In either case, x/y is in + // screen coordinates. + // Returns true if a context menu was displayed, otherwise false + virtual bool ShowContextMenu(MenuItemView* source, + int id, + int x, + int y, + bool is_mouse_gesture) { + return false; + } + + // Controller + virtual bool SupportsCommand(int id) const { + return true; + } + virtual bool IsCommandEnabled(int id) const { + return true; + } + virtual bool GetContextualLabel(int id, std::wstring* out) const { + return false; + } + virtual void ExecuteCommand(int id) { + } + + // Executes the specified command. mouse_event_flags give the flags of the + // mouse event that triggered this to be invoked (views::MouseEvent + // flags). mouse_event_flags is 0 if this is triggered by a user gesture + // other than a mouse event. + virtual void ExecuteCommand(int id, int mouse_event_flags) { + ExecuteCommand(id); + } + + // Returns true if the specified mouse event is one the user can use + // to trigger, or accept, the mouse. Defaults to left or right mouse buttons. + virtual bool IsTriggerableEvent(const MouseEvent& e) { + return e.IsLeftMouseButton() || e.IsRightMouseButton(); + } + + // Invoked to determine if drops can be accepted for a submenu. This is + // ONLY invoked for menus that have submenus and indicates whether or not + // a drop can occur on any of the child items of the item. For example, + // consider the following menu structure: + // + // A + // B + // C + // + // Where A has a submenu with children B and C. This is ONLY invoked for + // A, not B and C. + // + // To restrict which children can be dropped on override GetDropOperation. + virtual bool CanDrop(MenuItemView* menu, const OSExchangeData& data) { + return false; + } + + // Returns the drop operation for the specified target menu item. This is + // only invoked if CanDrop returned true for the parent menu. position + // is set based on the location of the mouse, reset to specify a different + // position. + // + // If a drop should not be allowed, returned DragDropTypes::DRAG_NONE. + virtual int GetDropOperation(MenuItemView* item, + const DropTargetEvent& event, + DropPosition* position) { + NOTREACHED() << "If you override CanDrop, you need to override this too"; + return DragDropTypes::DRAG_NONE; + } + + // Invoked to perform the drop operation. This is ONLY invoked if + // canDrop returned true for the parent menu item, and GetDropOperation + // returned an operation other than DragDropTypes::DRAG_NONE. + // + // menu indicates the menu the drop occurred on. + virtual int OnPerformDrop(MenuItemView* menu, + DropPosition position, + const DropTargetEvent& event) { + NOTREACHED() << "If you override CanDrop, you need to override this too"; + return DragDropTypes::DRAG_NONE; + } + + // Invoked to determine if it is possible for the user to drag the specified + // menu item. + virtual bool CanDrag(MenuItemView* menu) { + return false; + } + + // Invoked to write the data for a drag operation to data. sender is the + // MenuItemView being dragged. + virtual void WriteDragData(MenuItemView* sender, OSExchangeData* data) { + NOTREACHED() << "If you override CanDrag, you must override this too."; + } + + // Invoked to determine the drag operations for a drag session of sender. + // See DragDropTypes for possible values. + virtual int GetDragOperations(MenuItemView* sender) { + NOTREACHED() << "If you override CanDrag, you must override this too."; + return 0; + } + + // Notification the menu has closed. This is only sent when running the + // menu for a drop. + virtual void DropMenuClosed(MenuItemView* menu) { + } + + // Notification that the user has highlighted the specified item. + virtual void SelectionChanged(MenuItemView* menu) { + } +}; + +// MenuItemView -------------------------------------------------------------- + +// MenuItemView represents a single menu item with a label and optional icon. +// Each MenuItemView may also contain a submenu, which in turn may contain +// any number of child MenuItemViews. +// +// To use a menu create an initial MenuItemView using the constructor that +// takes a MenuDelegate, then create any number of child menu items by way +// of the various AddXXX methods. +// +// MenuItemView is itself a View, which means you can add Views to each +// MenuItemView. This normally NOT want you want, rather add other child Views +// to the submenu of the MenuItemView. +// +// There are two ways to show a MenuItemView: +// 1. Use RunMenuAt. This blocks the caller, executing the selected command +// on success. +// 2. Use RunMenuForDropAt. This is intended for use during a drop session +// and does NOT block the caller. Instead the delegate is notified when the +// menu closes via the DropMenuClosed method. + +class MenuItemView : public View { + friend class MenuController; + + public: + // ID used to identify menu items. + static const int kMenuItemViewID; + + // If true SetNestableTasksAllowed(true) is invoked before MessageLoop::Run + // is invoked. This is only useful for testing and defaults to false. + static bool allow_task_nesting_during_run_; + + // Different types of menu items. + enum Type { + NORMAL, + SUBMENU, + CHECKBOX, + RADIO, + SEPARATOR + }; + + // Where the menu should be anchored to. + enum AnchorPosition { + TOPLEFT, + TOPRIGHT + }; + + // Constructor for use with the top level menu item. This menu is never + // shown to the user, rather its use as the parent for all menu items. + explicit MenuItemView(MenuDelegate* delegate); + + virtual ~MenuItemView(); + + // Run methods. See description above class for details. Both Run methods take + // a rectangle, which is used to position the menu. |has_mnemonics| indicates + // whether the items have mnemonics. Mnemonics are identified by way of the + // character following the '&'. + void RunMenuAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor, + bool has_mnemonics); + void RunMenuForDropAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor); + + // Hides and cancels the menu. This does nothing if the menu is not open. + void Cancel(); + + // Adds an item to this menu. + // item_id The id of the item, used to identify it in delegate callbacks + // or (if delegate is NULL) to identify the command associated + // with this item with the controller specified in the ctor. Note + // that this value should not be 0 as this has a special meaning + // ("NULL command, no item selected") + // label The text label shown. + // type The type of item. + void AppendMenuItem(int item_id, + const std::wstring& label, + Type type) { + AppendMenuItemInternal(item_id, label, SkBitmap(), type); + } + + // Append a submenu to this menu. + // The returned pointer is owned by this menu. + MenuItemView* AppendSubMenu(int item_id, + const std::wstring& label) { + return AppendMenuItemInternal(item_id, label, SkBitmap(), SUBMENU); + } + + // Append a submenu with an icon to this menu. + // The returned pointer is owned by this menu. + MenuItemView* AppendSubMenuWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon) { + return AppendMenuItemInternal(item_id, label, icon, SUBMENU); + } + + // This is a convenience for standard text label menu items where the label + // is provided with this call. + void AppendMenuItemWithLabel(int item_id, + const std::wstring& label) { + AppendMenuItem(item_id, label, NORMAL); + } + + // This is a convenience for text label menu items where the label is + // provided by the delegate. + void AppendDelegateMenuItem(int item_id) { + AppendMenuItem(item_id, std::wstring(), NORMAL); + } + + // Adds a separator to this menu + void AppendSeparator() { + AppendMenuItemInternal(0, std::wstring(), SkBitmap(), SEPARATOR); + } + + // Appends a menu item with an icon. This is for the menu item which + // needs an icon. Calling this function forces the Menu class to draw + // the menu, instead of relying on Windows. + void AppendMenuItemWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon) { + AppendMenuItemInternal(item_id, label, icon, NORMAL); + } + + // Returns the view that contains child menu items. If the submenu has + // not been creates, this creates it. + virtual SubmenuView* CreateSubmenu(); + + // Returns true if this menu item has a submenu. + virtual bool HasSubmenu() const { return (submenu_ != NULL); } + + // Returns the view containing child menu items. + virtual SubmenuView* GetSubmenu() const { return submenu_; } + + // Returns the parent menu item. + MenuItemView* GetParentMenuItem() const { return parent_menu_item_; } + + // Sets the font. + void SetFont(const ChromeFont& font) { font_ = font; } + + // Sets the title + void SetTitle(const std::wstring& title) { + title_ = title; + } + + // Returns the title. + const std::wstring& GetTitle() const { return title_; } + + // Sets whether this item is selected. This is invoked as the user moves + // the mouse around the menu while open. + void SetSelected(bool selected); + + // Returns true if the item is selected. + bool IsSelected() const { return selected_; } + + // Sets the icon for the descendant identified by item_id. + void SetIcon(const SkBitmap& icon, int item_id); + + // Sets the icon of this menu item. + void SetIcon(const SkBitmap& icon); + + // Returns the icon. + const SkBitmap& GetIcon() const { return icon_; } + + // Sets the command id of this menu item. + void SetCommand(int command) { command_ = command; } + + // Returns the command id of this item. + int GetCommand() const { return command_; } + + // Paints the menu item. + virtual void Paint(ChromeCanvas* canvas); + + // Returns the preferred size of this item. + virtual gfx::Size GetPreferredSize(); + + // Returns the object responsible for controlling showing the menu. + MenuController* GetMenuController(); + + // Returns the delegate. This returns the delegate of the root menu item. + MenuDelegate* GetDelegate(); + + // Returns the root parent, or this if this has no parent. + MenuItemView* GetRootMenuItem(); + + // Returns the mnemonic for this MenuItemView, or 0 if this MenuItemView + // doesn't have a mnemonic. + wchar_t GetMnemonic(); + + // Do we have icons? This only has effect on the top menu. Turning this on + // makes the menus slightly wider and taller. + void set_has_icons(bool has_icons) { + has_icons_ = has_icons; + } + + protected: + // Creates a MenuItemView. This is used by the various AddXXX methods. + MenuItemView(MenuItemView* parent, int command, Type type); + + private: + // Called by the two constructors to initialize this menu item. + void Init(MenuItemView* parent, + int command, + MenuItemView::Type type, + MenuDelegate* delegate); + + // All the AddXXX methods funnel into this. + MenuItemView* AppendMenuItemInternal(int item_id, + const std::wstring& label, + const SkBitmap& icon, + Type type); + + // Returns the descendant with the specified command. + MenuItemView* GetDescendantByID(int id); + + // Invoked by the MenuController when the menu closes as the result of + // drag and drop run. + void DropMenuClosed(bool notify_delegate); + + // The RunXXX methods call into this to set up the necessary state before + // running. + void PrepareForRun(bool has_mnemonics); + + // Returns the flags passed to DrawStringInt. + int GetDrawStringFlags(); + + // If this menu item has no children a child is added showing it has no + // children. Otherwise AddEmtpyMenuIfNecessary is recursively invoked on + // child menu items that have children. + void AddEmptyMenus(); + + // Undoes the work of AddEmptyMenus. + void RemoveEmptyMenus(); + + // Given bounds within our View, this helper routine mirrors the bounds if + // necessary. + void AdjustBoundsForRTLUI(RECT* rect) const; + + // Actual paint implementation. If for_drag is true, portions of the menu + // are not rendered. + void Paint(ChromeCanvas* canvas, bool for_drag); + + // Destroys the window used to display this menu and recursively destroys + // the windows used to display all descendants. + void DestroyAllMenuHosts(); + + // Returns the various margins. + int GetTopMargin(); + int GetBottomMargin(); + + // The delegate. This is only valid for the root menu item. You shouldn't + // use this directly, instead use GetDelegate() which walks the tree as + // as necessary. + MenuDelegate* delegate_; + + // Returns the controller for the run operation, or NULL if the menu isn't + // showing. + MenuController* controller_; + + // Used to detect when Cancel was invoked. + bool canceled_; + + // Our parent. + MenuItemView* parent_menu_item_; + + // Type of menu. NOTE: MenuItemView doesn't itself represent SEPARATOR, + // that is handled by an entirely different view class. + Type type_; + + // Whether we're selected. + bool selected_; + + // Command id. + int command_; + + // Submenu, created via CreateSubmenu. + SubmenuView* submenu_; + + // Font. + ChromeFont font_; + + // Title. + std::wstring title_; + + // Icon. + SkBitmap icon_; + + // Does the title have a mnemonic? + bool has_mnemonics_; + + bool has_icons_; + + DISALLOW_EVIL_CONSTRUCTORS(MenuItemView); +}; + +// SubmenuView ---------------------------------------------------------------- + +// SubmenuView is the parent of all menu items. +// +// SubmenuView has the following responsibilities: +// . It positions and sizes all child views (any type of View may be added, +// not just MenuItemViews). +// . Forwards the appropriate events to the MenuController. This allows the +// MenuController to update the selection as the user moves the mouse around. +// . Renders the drop indicator during a drop operation. +// . Shows and hides the window (a WidgetWin) when the menu is shown on +// screen. +// +// SubmenuView is itself contained in a MenuScrollViewContainer. +// MenuScrollViewContainer handles showing as much of the SubmenuView as the +// screen allows. If the SubmenuView is taller than the screen, scroll buttons +// are provided that allow the user to see all the menu items. +class SubmenuView : public View { + public: + // Creates a SubmenuView for the specified menu item. + explicit SubmenuView(MenuItemView* parent); + ~SubmenuView(); + + // Returns the number of child views that are MenuItemViews. + // MenuItemViews are identified by ID. + int GetMenuItemCount(); + + // Returns the MenuItemView at the specified index. + MenuItemView* GetMenuItemAt(int index); + + // Positions and sizes the child views. This tiles the views vertically, + // giving each child the available width. + virtual void Layout(); + virtual gfx::Size GetPreferredSize(); + + // View method. Overriden to schedule a paint. We do this so that when + // scrolling occurs, everything is repainted correctly. + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + + // Painting. + void PaintChildren(ChromeCanvas* canvas); + + // Drag and drop methods. These are forwarded to the MenuController. + virtual bool CanDrop(const OSExchangeData& data); + virtual void OnDragEntered(const DropTargetEvent& event); + virtual int OnDragUpdated(const DropTargetEvent& event); + virtual void OnDragExited(); + virtual int OnPerformDrop(const DropTargetEvent& event); + + // Scrolls on menu item boundaries. + virtual bool OnMouseWheel(const MouseWheelEvent& e); + + // Returns true if the menu is showing. + bool IsShowing(); + + // Shows the menu at the specified location. Coordinates are in screen + // coordinates. max_width gives the max width the view should be. + void ShowAt(HWND parent, const gfx::Rect& bounds, bool do_capture); + + // Closes the menu, destroying the host. + void Close(); + + // Hides the hosting window. + // + // The hosting window is hidden first, then deleted (Close) when the menu is + // done running. This is done to avoid deletion ordering dependencies. In + // particular, during drag and drop (and when a modal dialog is shown as + // a result of choosing a context menu) it is possible that an event is + // being processed by the host, so that host is on the stack when we need to + // close the window. If we closed the window immediately (and deleted it), + // when control returned back to host we would crash as host was deleted. + void Hide(); + + // If mouse capture was grabbed, it is released. Does nothing if mouse was + // not captured. + void ReleaseCapture(); + + // Returns the parent menu item we're showing children for. + MenuItemView* GetMenuItem() const { return parent_menu_item_; } + + // Overriden to return true. This prevents tab from doing anything. + virtual bool CanProcessTabKeyEvents() { return true; } + + // Set the drop item and position. + void SetDropMenuItem(MenuItemView* item, + MenuDelegate::DropPosition position); + + // Returns whether the selection should be shown for the specified item. + // The selection is NOT shown during drag and drop when the drop is over + // the menu. + bool GetShowSelection(MenuItemView* item); + + // Returns the container for the SubmenuView. + MenuScrollViewContainer* GetScrollViewContainer(); + + // Returns the host of the menu. Returns NULL if not showing. + MenuHost* host() const { return host_; } + + private: + // Paints the drop indicator. This is only invoked if item is non-NULL and + // position is not DROP_NONE. + void PaintDropIndicator(ChromeCanvas* canvas, + MenuItemView* item, + MenuDelegate::DropPosition position); + + void SchedulePaintForDropIndicator(MenuItemView* item, + MenuDelegate::DropPosition position); + + // Calculates the location of th edrop indicator. + gfx::Rect CalculateDropIndicatorBounds(MenuItemView* item, + MenuDelegate::DropPosition position); + + // Parent menu item. + MenuItemView* parent_menu_item_; + + // WidgetWin subclass used to show the children. + MenuHost* host_; + + // If non-null, indicates a drop is in progress and drop_item is the item + // the drop is over. + MenuItemView* drop_item_; + + // Position of the drop. + MenuDelegate::DropPosition drop_position_; + + // Ancestor of the SubmenuView, lazily created. + MenuScrollViewContainer* scroll_view_container_; + + DISALLOW_EVIL_CONSTRUCTORS(SubmenuView); +}; + +// MenuController ------------------------------------------------------------- + +// MenuController manages showing, selecting and drag/drop for menus. +// All relevant events are forwarded to the MenuController from SubmenuView +// and MenuHost. + +class MenuController : public MessageLoopForUI::Dispatcher { + public: + friend class MenuHostRootView; + friend class MenuItemView; + friend class MenuScrollTask; + + // If a menu is currently active, this returns the controller for it. + static MenuController* GetActiveInstance(); + + // Runs the menu at the specified location. If the menu was configured to + // block, the selected item is returned. If the menu does not block this + // returns NULL immediately. + MenuItemView* Run(HWND parent, + MenuItemView* root, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition position, + int* mouse_event_flags); + + // Whether or not Run blocks. + bool IsBlockingRun() const { return blocking_run_; } + + // Sets the selection to menu_item, a value of NULL unselects everything. + // If open_submenu is true and menu_item has a submenu, the submenu is shown. + // If update_immediately is true, submenus are opened immediately, otherwise + // submenus are only opened after a timer fires. + // + // Internally this updates pending_state_ immediatley, and if + // update_immediately is true, CommitPendingSelection is invoked to + // show/hide submenus and update state_. + void SetSelection(MenuItemView* menu_item, + bool open_submenu, + bool update_immediately); + + // Cancels the current Run. If all is true, any nested loops are canceled + // as well. This immediately hides all menus. + void Cancel(bool all); + + // An alternative to Cancel(true) that can be used with a OneShotTimer. + void CancelAll() { return Cancel(true); } + + // Various events, forwarded from the submenu. + // + // NOTE: the coordinates of the events are in that of the + // MenuScrollViewContainer. + void OnMousePressed(SubmenuView* source, const MouseEvent& event); + void OnMouseDragged(SubmenuView* source, const MouseEvent& event); + void OnMouseReleased(SubmenuView* source, const MouseEvent& event); + void OnMouseMoved(SubmenuView* source, const MouseEvent& event); + void OnMouseEntered(SubmenuView* source, const MouseEvent& event); + bool CanDrop(SubmenuView* source, const OSExchangeData& data); + void OnDragEntered(SubmenuView* source, const DropTargetEvent& event); + int OnDragUpdated(SubmenuView* source, const DropTargetEvent& event); + void OnDragExited(SubmenuView* source); + int OnPerformDrop(SubmenuView* source, const DropTargetEvent& event); + + // Invoked from the scroll buttons of the MenuScrollViewContainer. + void OnDragEnteredScrollButton(SubmenuView* source, bool is_up); + void OnDragExitedScrollButton(SubmenuView* source); + + private: + // Tracks selection information. + struct State { + State() : item(NULL), submenu_open(false) {} + + // The selected menu item. + MenuItemView* item; + + // If item has a submenu this indicates if the submenu is showing. + bool submenu_open; + + // Bounds passed to the run menu. Used for positioning the first menu. + gfx::Rect initial_bounds; + + // Position of the initial menu. + MenuItemView::AnchorPosition anchor; + + // The direction child menus have opened in. + std::list<bool> open_leading; + + // Bounds for the monitor we're showing on. + gfx::Rect monitor_bounds; + }; + + // Used by GetMenuPartByScreenCoordinate to indicate the menu part at a + // particular location. + struct MenuPart { + // Type of part. + enum Type { + NONE, + MENU_ITEM, + SCROLL_UP, + SCROLL_DOWN + }; + + MenuPart() : type(NONE), menu(NULL), submenu(NULL) {} + + // Convenience for testing type == SCROLL_DOWN or type == SCROLL_UP. + bool is_scroll() const { return type == SCROLL_DOWN || type == SCROLL_UP; } + + // Type of part. + Type type; + + // If type is MENU_ITEM, this is the menu item the mouse is over, otherwise + // this is NULL. + // NOTE: if type is MENU_ITEM and the mouse is not over a valid menu item + // but is over a menu (for example, the mouse is over a separator or + // empty menu), this is NULL. + MenuItemView* menu; + + // If type is SCROLL_*, this is the submenu the mouse is over. + SubmenuView* submenu; + }; + + // Sets the active MenuController. + static void SetActiveInstance(MenuController* controller); + + // Dispatcher method. This returns true if the menu was canceled, or + // if the message is such that the menu should be closed. + virtual bool Dispatch(const MSG& msg); + + // Key processing. The return value of these is returned from Dispatch. + // In other words, if these return false (which they do if escape was + // pressed, or a matching mnemonic was found) the message loop returns. + bool OnKeyDown(const MSG& msg); + bool OnChar(const MSG& msg); + + // Creates a MenuController. If blocking is true, Run blocks the caller + explicit MenuController(bool blocking); + + ~MenuController(); + + // Invoked when the user accepts the selected item. This is only used + // when blocking. This schedules the loop to quit. + void Accept(MenuItemView* item, int mouse_event_flags); + + // Closes all menus, including any menus of nested invocations of Run. + void CloseAllNestedMenus(); + + // Gets the enabled menu item at the specified location. + // If over_any_menu is non-null it is set to indicate whether the location + // is over any menu. It is possible for this to return NULL, but + // over_any_menu to be true. For example, the user clicked on a separator. + MenuItemView* GetMenuItemAt(View* menu, int x, int y); + + // If there is an empty menu item at the specified location, it is returned. + MenuItemView* GetEmptyMenuItemAt(View* source, int x, int y); + + // Returns true if the coordinate is over the scroll buttons of the + // SubmenuView's MenuScrollViewContainer. If true is returned, part is set to + // indicate which scroll button the coordinate is. + bool IsScrollButtonAt(SubmenuView* source, + int x, + int y, + MenuPart::Type* part); + + // Returns the target for the mouse event. + MenuPart GetMenuPartByScreenCoordinate(SubmenuView* source, + int source_x, + int source_y); + + // Implementation of GetMenuPartByScreenCoordinate for a single menu. Returns + // true if the supplied SubmenuView contains the location in terms of the + // screen. If it does, part is set appropriately and true is returned. + bool GetMenuPartByScreenCoordinateImpl(SubmenuView* menu, + const gfx::Point& screen_loc, + MenuPart* part); + + // Returns true if the SubmenuView contains the specified location. This does + // NOT included the scroll buttons, only the submenu view. + bool DoesSubmenuContainLocation(SubmenuView* submenu, + const gfx::Point& screen_loc); + + // Opens/Closes the necessary menus such that state_ matches that of + // pending_state_. This is invoked if submenus are not opened immediately, + // but after a delay. + void CommitPendingSelection(); + + // If item has a submenu, it is closed. This does NOT update the selection + // in anyway. + void CloseMenu(MenuItemView* item); + + // If item has a submenu, it is opened. This does NOT update the selection + // in anyway. + void OpenMenu(MenuItemView* item); + + // Builds the paths of the two menu items into the two paths, and + // sets first_diff_at to the location of the first difference between the + // two paths. + void BuildPathsAndCalculateDiff(MenuItemView* old_item, + MenuItemView* new_item, + std::vector<MenuItemView*>* old_path, + std::vector<MenuItemView*>* new_path, + size_t* first_diff_at); + + // Builds the path for the specified item. + void BuildMenuItemPath(MenuItemView* item, std::vector<MenuItemView*>* path); + + // Starts/stops the timer that commits the pending state to state + // (opens/closes submenus). + void StartShowTimer(); + void StopShowTimer(); + + // Starts/stops the timer cancel the menu. This is used during drag and + // drop when the drop enters/exits the menu. + void StartCancelAllTimer(); + void StopCancelAllTimer(); + + // Calculates the bounds of the menu to show. is_leading is set to match the + // direction the menu opened in. + gfx::Rect CalculateMenuBounds(MenuItemView* item, + bool prefer_leading, + bool* is_leading); + + // Returns the depth of the menu. + static int MenuDepth(MenuItemView* item); + + // Selects the next/previous menu item. + void IncrementSelection(int delta); + + // If the selected item has a submenu and it isn't currently open, the + // the selection is changed such that the menu opens immediately. + void OpenSubmenuChangeSelectionIfCan(); + + // If possible, closes the submenu. + void CloseSubmenu(); + + // Returns true if window is the window used to show item, or any of + // items ancestors. + bool IsMenuWindow(MenuItemView* item, HWND window); + + // Selects by mnemonic, and if that doesn't work tries the first character of + // the title. Returns true if a match was selected and the menu should exit. + bool SelectByChar(wchar_t key); + + // If there is a window at the location of the event, a new mouse event is + // generated and posted to it. + void RepostEvent(SubmenuView* source, const MouseEvent& event); + + // Sets the drop target to new_item. + void SetDropMenuItem(MenuItemView* new_item, + MenuDelegate::DropPosition position); + + // Starts/stops scrolling as appropriate. part gives the part the mouse is + // over. + void UpdateScrolling(const MenuPart& part); + + // Stops scrolling. + void StopScrolling(); + + // The active instance. + static MenuController* active_instance_; + + // If true, Run blocks. If false, Run doesn't block and this is used for + // drag and drop. Note that the semantics for drag and drop are slightly + // different: cancel timer is kicked off any time the drag moves outside the + // menu, mouse events do nothing... + bool blocking_run_; + + // If true, we're showing. + bool showing_; + + // If true, all nested run loops should be exited. + bool exit_all_; + + // Whether we did a capture. We do a capture only if we're blocking and + // the mouse was down when Run. + bool did_capture_; + + // As the user drags the mouse around pending_state_ changes immediately. + // When the user stops moving/dragging the mouse (or clicks the mouse) + // pending_state_ is committed to state_, potentially resulting in + // opening or closing submenus. This gives a slight delayed effect to + // submenus as the user moves the mouse around. This is done so that as the + // user moves the mouse all submenus don't immediately pop. + State pending_state_; + State state_; + + // If the user accepted the selection, this is the result. + MenuItemView* result_; + + // The mouse event flags when the user clicked on a menu. Is 0 if the + // user did not use the mousee to select the menu. + int result_mouse_event_flags_; + + // If not empty, it means we're nested. When Run is invoked from within + // Run, the current state (state_) is pushed onto menu_stack_. This allows + // MenuController to restore the state when the nested run returns. + std::list<State> menu_stack_; + + // As the mouse moves around submenus are not opened immediately. Instead + // they open after this timer fires. + base::OneShotTimer<MenuController> show_timer_; + + // Used to invoke CancelAll(). This is used during drag and drop to hide the + // menu after the mouse moves out of the of the menu. This is necessitated by + // the lack of an ability to detect when the drag has completed from the drop + // side. + base::OneShotTimer<MenuController> cancel_all_timer_; + + // Drop target. + MenuItemView* drop_target_; + MenuDelegate::DropPosition drop_position_; + + // Owner of child windows. + HWND owner_; + + // Indicates a possible drag operation. + bool possible_drag_; + + // Location the mouse was pressed at. Used to detect d&d. + int press_x_; + int press_y_; + + // We get a slew of drag updated messages as the mouse is over us. To avoid + // continually processing whether we can drop, we cache the coordinates. + bool valid_drop_coordinates_; + int drop_x_; + int drop_y_; + int last_drop_operation_; + + // If true, we're in the middle of invoking ShowAt on a submenu. + bool showing_submenu_; + + // Task for scrolling the menu. If non-null indicates a scroll is currently + // underway. + scoped_ptr<MenuScrollTask> scroll_task_; + + DISALLOW_EVIL_CONSTRUCTORS(MenuController); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_MENU_CHROME_MENU_H_ diff --git a/views/controls/menu/controller.h b/views/controls/menu/controller.h new file mode 100644 index 0000000..6a693cc --- /dev/null +++ b/views/controls/menu/controller.h @@ -0,0 +1,33 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_MENU_CONTROLLER_H_ +#define VIEWS_CONTROLS_MENU_CONTROLLER_H_ + +#include <string> + +// TODO(beng): remove this interface and fold it into MenuDelegate. + +class Controller { + public: + virtual ~Controller() { } + + // Whether or not a command is supported by this controller. + virtual bool SupportsCommand(int id) const = 0; + + // Whether or not a command is enabled. + virtual bool IsCommandEnabled(int id) const = 0; + + // Assign the provided string with a contextual label. Returns true if a + // contextual label exists and false otherwise. This method can be used when + // implementing a menu or button that needs to have a different label + // depending on the context. If this method returns false, the default + // label used when creating the button or menu is used. + virtual bool GetContextualLabel(int id, std::wstring* out) const = 0; + + // Executes a command. + virtual void ExecuteCommand(int id) = 0; +}; + +#endif // VIEWS_CONTROLS_MENU_CONTROLLER_H_ diff --git a/views/controls/menu/menu.cc b/views/controls/menu/menu.cc new file mode 100644 index 0000000..1597da5 --- /dev/null +++ b/views/controls/menu/menu.cc @@ -0,0 +1,626 @@ +// Copyright (c) 2006-2008 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/controls/menu/menu.h" + +#include <atlbase.h> +#include <atlapp.h> +#include <atlwin.h> +#include <atlcrack.h> +#include <atlframe.h> +#include <atlmisc.h> +#include <string> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/l10n_util.h" +#include "app/l10n_util_win.h" +#include "base/gfx/rect.h" +#include "base/logging.h" +#include "base/stl_util-inl.h" +#include "base/string_util.h" +#include "views/accelerator.h" + +const SkBitmap* Menu::Delegate::kEmptyIcon = 0; + +// The width of an icon, including the pixels between the icon and +// the item label. +static const int kIconWidth = 23; +// Margins between the top of the item and the label. +static const int kItemTopMargin = 3; +// Margins between the bottom of the item and the label. +static const int kItemBottomMargin = 4; +// Margins between the left of the item and the icon. +static const int kItemLeftMargin = 4; +// Margins between the right of the item and the label. +static const int kItemRightMargin = 10; +// The width for displaying the sub-menu arrow. +static const int kArrowWidth = 10; + +// Current active MenuHostWindow. If NULL, no menu is active. +static MenuHostWindow* active_host_window = NULL; + +// The data of menu items needed to display. +struct Menu::ItemData { + std::wstring label; + SkBitmap icon; + bool submenu; +}; + +namespace { + +static int ChromeGetMenuItemID(HMENU hMenu, int pos) { + // The built-in Windows ::GetMenuItemID doesn't work for submenus, + // so here's our own implementation. + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_ID; + GetMenuItemInfo(hMenu, pos, TRUE, &mii); + return mii.wID; +} + +// MenuHostWindow ------------------------------------------------------------- + +// MenuHostWindow is the HWND the HMENU is parented to. MenuHostWindow is used +// to intercept right clicks on the HMENU and notify the delegate as well as +// for drawing icons. +// +class MenuHostWindow : public CWindowImpl<MenuHostWindow, CWindow, + CWinTraits<WS_CHILD>> { + public: + MenuHostWindow(Menu* menu, HWND parent_window) : menu_(menu) { + int extended_style = 0; + // If the menu needs to be created with a right-to-left UI layout, we must + // set the appropriate RTL flags (such as WS_EX_LAYOUTRTL) property for the + // underlying HWND. + if (menu_->delegate_->IsRightToLeftUILayout()) + extended_style |= l10n_util::GetExtendedStyles(); + Create(parent_window, gfx::Rect().ToRECT(), 0, 0, extended_style); + } + + ~MenuHostWindow() { + DestroyWindow(); + } + + DECLARE_FRAME_WND_CLASS(L"MenuHostWindow", NULL); + BEGIN_MSG_MAP(MenuHostWindow); + MSG_WM_RBUTTONUP(OnRButtonUp) + MSG_WM_MEASUREITEM(OnMeasureItem) + MSG_WM_DRAWITEM(OnDrawItem) + END_MSG_MAP(); + + private: + // NOTE: I really REALLY tried to use WM_MENURBUTTONUP, but I ran into + // two problems in using it: + // 1. It doesn't contain the coordinates of the mouse. + // 2. It isn't invoked for menuitems representing a submenu that have children + // menu items (not empty). + + void OnRButtonUp(UINT w_param, const CPoint& loc) { + int id; + if (menu_->delegate_ && FindMenuIDByLocation(menu_, loc, &id)) + menu_->delegate_->ShowContextMenu(menu_, id, loc.x, loc.y, true); + } + + void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* lpmis) { + Menu::ItemData* data = reinterpret_cast<Menu::ItemData*>(lpmis->itemData); + if (data != NULL) { + ChromeFont font; + lpmis->itemWidth = font.GetStringWidth(data->label) + kIconWidth + + kItemLeftMargin + kItemRightMargin - + GetSystemMetrics(SM_CXMENUCHECK); + if (data->submenu) + lpmis->itemWidth += kArrowWidth; + // If the label contains an accelerator, make room for tab. + if (data->label.find(L'\t') != std::wstring::npos) + lpmis->itemWidth += font.GetStringWidth(L" "); + lpmis->itemHeight = font.height() + kItemBottomMargin + kItemTopMargin; + } else { + // Measure separator size. + lpmis->itemHeight = GetSystemMetrics(SM_CYMENU) / 2; + lpmis->itemWidth = 0; + } + } + + void OnDrawItem(UINT wParam, DRAWITEMSTRUCT* lpdis) { + HDC hDC = lpdis->hDC; + COLORREF prev_bg_color, prev_text_color; + + // Set background color and text color + if (lpdis->itemState & ODS_SELECTED) { + prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_HIGHLIGHT)); + prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_HIGHLIGHTTEXT)); + } else { + prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_MENU)); + if (lpdis->itemState & ODS_DISABLED) + prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_GRAYTEXT)); + else + prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_MENUTEXT)); + } + + if (lpdis->itemData) { + Menu::ItemData* data = + reinterpret_cast<Menu::ItemData*>(lpdis->itemData); + + // Draw the background. + HBRUSH hbr = CreateSolidBrush(GetBkColor(hDC)); + FillRect(hDC, &lpdis->rcItem, hbr); + DeleteObject(hbr); + + // Draw the label. + RECT rect = lpdis->rcItem; + rect.top += kItemTopMargin; + // Should we add kIconWidth only when icon.width() != 0 ? + rect.left += kItemLeftMargin + kIconWidth; + rect.right -= kItemRightMargin; + UINT format = DT_TOP | DT_SINGLELINE; + // Check whether the mnemonics should be underlined. + BOOL underline_mnemonics; + SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &underline_mnemonics, 0); + if (!underline_mnemonics) + format |= DT_HIDEPREFIX; + ChromeFont font; + HGDIOBJ old_font = static_cast<HFONT>(SelectObject(hDC, font.hfont())); + int fontsize = font.FontSize(); + + // If an accelerator is specified (with a tab delimiting the rest of the + // label from the accelerator), we have to justify the fist part on the + // left and the accelerator on the right. + // TODO(jungshik): This will break in RTL UI. Currently, he/ar use the + // window system UI font and will not hit here. + std::wstring label = data->label; + std::wstring accel; + std::wstring::size_type tab_pos = label.find(L'\t'); + if (tab_pos != std::wstring::npos) { + accel = label.substr(tab_pos); + label = label.substr(0, tab_pos); + } + DrawTextEx(hDC, const_cast<wchar_t*>(label.data()), + static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL); + if (!accel.empty()) + DrawTextEx(hDC, const_cast<wchar_t*>(accel.data()), + static_cast<int>(accel.size()), &rect, + format | DT_RIGHT, NULL); + SelectObject(hDC, old_font); + + // Draw the icon after the label, otherwise it would be covered + // by the label. + if (data->icon.width() != 0 && data->icon.height() != 0) { + ChromeCanvas canvas(data->icon.width(), data->icon.height(), false); + canvas.drawColor(SK_ColorBLACK, SkPorterDuff::kClear_Mode); + canvas.DrawBitmapInt(data->icon, 0, 0); + canvas.getTopPlatformDevice().drawToHDC(hDC, lpdis->rcItem.left + + kItemLeftMargin, lpdis->rcItem.top + (lpdis->rcItem.bottom - + lpdis->rcItem.top - data->icon.height()) / 2, NULL); + } + + } else { + // Draw the separator + lpdis->rcItem.top += (lpdis->rcItem.bottom - lpdis->rcItem.top) / 3; + DrawEdge(hDC, &lpdis->rcItem, EDGE_ETCHED, BF_TOP); + } + + SetBkColor(hDC, prev_bg_color); + SetTextColor(hDC, prev_text_color); + } + + bool FindMenuIDByLocation(Menu* menu, const CPoint& loc, int* id) { + int index = MenuItemFromPoint(NULL, menu->menu_, loc); + if (index != -1) { + *id = ChromeGetMenuItemID(menu->menu_, index); + return true; + } else { + for (std::vector<Menu*>::iterator i = menu->submenus_.begin(); + i != menu->submenus_.end(); ++i) { + if (FindMenuIDByLocation(*i, loc, id)) + return true; + } + } + return false; + } + + // The menu that created us. + Menu* menu_; + + DISALLOW_EVIL_CONSTRUCTORS(MenuHostWindow); +}; + +} // namespace + +bool Menu::Delegate::IsRightToLeftUILayout() const { + return l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT; +} + +const SkBitmap& Menu::Delegate::GetEmptyIcon() const { + if (kEmptyIcon == NULL) + kEmptyIcon = new SkBitmap(); + return *kEmptyIcon; +} + +Menu::Menu(Delegate* delegate, AnchorPoint anchor, HWND owner) + : delegate_(delegate), + menu_(CreatePopupMenu()), + anchor_(anchor), + owner_(owner), + is_menu_visible_(false), + owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL)) { + DCHECK(delegate_); +} + +Menu::Menu(Menu* parent) + : delegate_(parent->delegate_), + menu_(CreatePopupMenu()), + anchor_(parent->anchor_), + owner_(parent->owner_), + is_menu_visible_(false), + owner_draw_(parent->owner_draw_) { +} + +Menu::Menu(HMENU hmenu) + : delegate_(NULL), + menu_(hmenu), + anchor_(TOPLEFT), + owner_(NULL), + is_menu_visible_(false), + owner_draw_(false) { + DCHECK(menu_); +} + +Menu::~Menu() { + STLDeleteContainerPointers(submenus_.begin(), submenus_.end()); + STLDeleteContainerPointers(item_data_.begin(), item_data_.end()); + DestroyMenu(menu_); +} + +UINT Menu::GetStateFlagsForItemID(int item_id) const { + // Use the delegate to get enabled and checked state. + UINT flags = + delegate_->IsCommandEnabled(item_id) ? MFS_ENABLED : MFS_DISABLED; + + if (delegate_->IsItemChecked(item_id)) + flags |= MFS_CHECKED; + + if (delegate_->IsItemDefault(item_id)) + flags |= MFS_DEFAULT; + + return flags; +} + +void Menu::AddMenuItemInternal(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon, + HMENU submenu, + MenuItemType type) { + DCHECK(type != SEPARATOR) << "Call AddSeparator instead!"; + + if (label.empty() && !delegate_) { + // No label and no delegate; don't add an empty menu. + // It appears under some circumstance we're getting an empty label + // (l10n_util::GetString(IDS_TASK_MANAGER) returns ""). This shouldn't + // happen, but I'm working over the crash here. + NOTREACHED(); + return; + } + + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE | MIIM_ID; + if (submenu) { + mii.fMask |= MIIM_SUBMENU; + mii.hSubMenu = submenu; + } + + // Set the type and ID. + if (!owner_draw_) { + mii.fType = MFT_STRING; + mii.fMask |= MIIM_STRING; + } else { + mii.fType = MFT_OWNERDRAW; + } + + if (type == RADIO) + mii.fType |= MFT_RADIOCHECK; + + mii.wID = item_id; + + // Set the item data. + Menu::ItemData* data = new ItemData; + item_data_.push_back(data); + data->submenu = submenu != NULL; + + std::wstring actual_label(label.empty() ? + delegate_->GetLabel(item_id) : label); + + // Find out if there is a shortcut we need to append to the label. + views::Accelerator accelerator(0, false, false, false); + if (delegate_ && delegate_->GetAcceleratorInfo(item_id, &accelerator)) { + actual_label += L'\t'; + actual_label += accelerator.GetShortcutText(); + } + labels_.push_back(actual_label); + + if (owner_draw_) { + if (icon.width() != 0 && icon.height() != 0) + data->icon = icon; + else + data->icon = delegate_->GetIcon(item_id); + } else { + mii.dwTypeData = const_cast<wchar_t*>(labels_.back().c_str()); + } + + InsertMenuItem(menu_, index, TRUE, &mii); +} + +void Menu::AppendMenuItem(int item_id, + const std::wstring& label, + MenuItemType type) { + AddMenuItem(-1, item_id, label, type); +} + +void Menu::AddMenuItem(int index, + int item_id, + const std::wstring& label, + MenuItemType type) { + if (type == SEPARATOR) + AddSeparator(index); + else + AddMenuItemInternal(index, item_id, label, SkBitmap(), NULL, type); +} + +Menu* Menu::AppendSubMenu(int item_id, const std::wstring& label) { + return AddSubMenu(-1, item_id, label); +} + +Menu* Menu::AddSubMenu(int index, int item_id, const std::wstring& label) { + return AddSubMenuWithIcon(index, item_id, label, SkBitmap()); +} + +Menu* Menu::AppendSubMenuWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon) { + return AddSubMenuWithIcon(-1, item_id, label, icon); +} + +Menu* Menu::AddSubMenuWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon) { + if (!owner_draw_ && icon.width() != 0 && icon.height() != 0) + owner_draw_ = true; + + Menu* submenu = new Menu(this); + submenus_.push_back(submenu); + AddMenuItemInternal(index, item_id, label, icon, submenu->menu_, NORMAL); + return submenu; +} + +void Menu::AppendMenuItemWithLabel(int item_id, const std::wstring& label) { + AddMenuItemWithLabel(-1, item_id, label); +} + +void Menu::AddMenuItemWithLabel(int index, int item_id, + const std::wstring& label) { + AddMenuItem(index, item_id, label, Menu::NORMAL); +} + +void Menu::AppendDelegateMenuItem(int item_id) { + AddDelegateMenuItem(-1, item_id); +} + +void Menu::AddDelegateMenuItem(int index, int item_id) { + AddMenuItem(index, item_id, std::wstring(), Menu::NORMAL); +} + +void Menu::AppendSeparator() { + AddSeparator(-1); +} + +void Menu::AddSeparator(int index) { + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR; + InsertMenuItem(menu_, index, TRUE, &mii); +} + +void Menu::AppendMenuItemWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon) { + AddMenuItemWithIcon(-1, item_id, label, icon); +} + +void Menu::AddMenuItemWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon) { + if (!owner_draw_) + owner_draw_ = true; + AddMenuItemInternal(index, item_id, label, icon, NULL, Menu::NORMAL); +} + +void Menu::EnableMenuItemByID(int item_id, bool enabled) { + UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED; + EnableMenuItem(menu_, item_id, MF_BYCOMMAND | enable_flags); +} + +void Menu::EnableMenuItemAt(int index, bool enabled) { + UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED; + EnableMenuItem(menu_, index, MF_BYPOSITION | enable_flags); +} + +void Menu::SetMenuLabel(int item_id, const std::wstring& label) { + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STRING; + mii.dwTypeData = const_cast<wchar_t*>(label.c_str()); + mii.cch = static_cast<UINT>(label.size()); + SetMenuItemInfo(menu_, item_id, false, &mii); +} + +DWORD Menu::GetTPMAlignFlags() const { + // The manner in which we handle the menu alignment depends on whether or not + // the menu is displayed within a mirrored view. If the UI is mirrored, the + // alignment needs to be fliped so that instead of aligning the menu to the + // right of the point, we align it to the left and vice versa. + DWORD align_flags = TPM_TOPALIGN; + switch (anchor_) { + case TOPLEFT: + if (delegate_->IsRightToLeftUILayout()) { + align_flags |= TPM_RIGHTALIGN; + } else { + align_flags |= TPM_LEFTALIGN; + } + break; + + case TOPRIGHT: + if (delegate_->IsRightToLeftUILayout()) { + align_flags |= TPM_LEFTALIGN; + } else { + align_flags |= TPM_RIGHTALIGN; + } + break; + + default: + NOTREACHED(); + return 0; + } + return align_flags; +} + +bool Menu::SetIcon(const SkBitmap& icon, int item_id) { + if (!owner_draw_) + owner_draw_ = true; + + const int num_items = GetMenuItemCount(menu_); + int sep_count = 0; + for (int i = 0; i < num_items; ++i) { + if (!(GetMenuState(menu_, i, MF_BYPOSITION) & MF_SEPARATOR)) { + if (ChromeGetMenuItemID(menu_, i) == item_id) { + item_data_[i - sep_count]->icon = icon; + // When the menu is running, we use SetMenuItemInfo to let Windows + // update the item information so that the icon being displayed + // could change immediately. + if (active_host_window) { + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE | MIIM_DATA; + mii.fType = MFT_OWNERDRAW; + mii.dwItemData = + reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]); + SetMenuItemInfo(menu_, item_id, false, &mii); + } + return true; + } + } else { + ++sep_count; + } + } + + // Continue searching for the item in submenus. + for (size_t i = 0; i < submenus_.size(); ++i) { + if (submenus_[i]->SetIcon(icon, item_id)) + return true; + } + + return false; +} + +void Menu::SetMenuInfo() { + const int num_items = GetMenuItemCount(menu_); + int sep_count = 0; + for (int i = 0; i < num_items; ++i) { + MENUITEMINFO mii_info; + mii_info.cbSize = sizeof(mii_info); + // Get the menu's original type. + mii_info.fMask = MIIM_FTYPE; + GetMenuItemInfo(menu_, i, MF_BYPOSITION, &mii_info); + // Set item states. + if (!(mii_info.fType & MF_SEPARATOR)) { + const int id = ChromeGetMenuItemID(menu_, i); + + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STATE | MIIM_FTYPE | MIIM_DATA | MIIM_STRING; + // We also need MFT_STRING for owner drawn items in order to let Windows + // handle the accelerators for us. + mii.fType = MFT_STRING; + if (owner_draw_) + mii.fType |= MFT_OWNERDRAW; + // If the menu originally has radiocheck type, we should follow it. + if (mii_info.fType & MFT_RADIOCHECK) + mii.fType |= MFT_RADIOCHECK; + mii.fState = GetStateFlagsForItemID(id); + + // Validate the label. If there is a contextual label, use it, otherwise + // default to the static label + std::wstring label; + if (!delegate_->GetContextualLabel(id, &label)) + label = labels_[i - sep_count]; + + if (owner_draw_) { + item_data_[i - sep_count]->label = label; + mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]); + } + mii.dwTypeData = const_cast<wchar_t*>(label.c_str()); + mii.cch = static_cast<UINT>(label.size()); + SetMenuItemInfo(menu_, i, true, &mii); + } else { + // Set data for owner drawn separators. Set dwItemData NULL to indicate + // a separator. + if (owner_draw_) { + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR | MFT_OWNERDRAW; + mii.dwItemData = NULL; + SetMenuItemInfo(menu_, i, true, &mii); + } + ++sep_count; + } + } + + for (size_t i = 0; i < submenus_.size(); ++i) + submenus_[i]->SetMenuInfo(); +} + +void Menu::RunMenuAt(int x, int y) { + SetMenuInfo(); + + delegate_->MenuWillShow(); + + // NOTE: we don't use TPM_RIGHTBUTTON here as it breaks selecting by way of + // press, drag, release. See bugs 718 and 8560. + UINT flags = + GetTPMAlignFlags() | TPM_LEFTBUTTON | TPM_RETURNCMD | TPM_RECURSE; + is_menu_visible_ = true; + DCHECK(owner_); + // In order for context menus on menus to work, the context menu needs to + // share the same window as the first menu is parented to. + bool created_host = false; + if (!active_host_window) { + created_host = true; + active_host_window = new MenuHostWindow(this, owner_); + } + UINT selected_id = + TrackPopupMenuEx(menu_, flags, x, y, active_host_window->m_hWnd, NULL); + if (created_host) { + delete active_host_window; + active_host_window = NULL; + } + is_menu_visible_ = false; + + // Execute the chosen command + if (selected_id != 0) + delegate_->ExecuteCommand(selected_id); +} + +void Menu::Cancel() { + DCHECK(is_menu_visible_); + EndMenu(); +} + +int Menu::ItemCount() { + return GetMenuItemCount(menu_); +} diff --git a/views/controls/menu/menu.h b/views/controls/menu/menu.h new file mode 100644 index 0000000..0be9126 --- /dev/null +++ b/views/controls/menu/menu.h @@ -0,0 +1,355 @@ +// Copyright (c) 2006-2008 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 CONTROLS_MENU_VIEWS_MENU_H_ +#define CONTROLS_MENU_VIEWS_MENU_H_ + +#include <windows.h> + +#include <vector> + +#include "base/basictypes.h" +#include "views/controls/menu/controller.h" + +class SkBitmap; + +namespace { +class MenuHostWindow; +} + +namespace views { +class Accelerator; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Menu class +// +// A wrapper around a Win32 HMENU handle that provides convenient APIs for +// menu construction, display and subsequent command execution. +// +/////////////////////////////////////////////////////////////////////////////// +class Menu { + friend class MenuHostWindow; + + public: + ///////////////////////////////////////////////////////////////////////////// + // + // Delegate Interface + // + // Classes implement this interface to tell the menu system more about each + // item as it is created. + // + ///////////////////////////////////////////////////////////////////////////// + class Delegate : public Controller { + public: + virtual ~Delegate() { } + + // Whether or not an item should be shown as checked. + virtual bool IsItemChecked(int id) const { + return false; + } + + // Whether or not an item should be shown as the default (using bold). + // There can only be one default menu item. + virtual bool IsItemDefault(int id) const { + return false; + } + + // The string shown for the menu item. + virtual std::wstring GetLabel(int id) const { + return std::wstring(); + } + + // The delegate needs to implement this function if it wants to display + // the shortcut text next to each menu item. If there is an accelerator + // for a given item id, the implementor must return it. + virtual bool GetAcceleratorInfo(int id, views::Accelerator* accel) { + return false; + } + + // The icon shown for the menu item. + virtual const SkBitmap& GetIcon(int id) const { + return GetEmptyIcon(); + } + + // The number of items to show in the menu + virtual int GetItemCount() const { + return 0; + } + + // Whether or not an item is a separator. + virtual bool IsItemSeparator(int id) const { + return false; + } + + // Shows the context menu with the specified id. This is invoked when the + // user does the appropriate gesture to show a context menu. The id + // identifies the id of the menu to show the context menu for. + // is_mouse_gesture is true if this is the result of a mouse gesture. + // If this is not the result of a mouse gesture x/y is the recommended + // location to display the content menu at. In either case, x/y is in + // screen coordinates. + virtual void ShowContextMenu(Menu* source, + int id, + int x, + int y, + bool is_mouse_gesture) { + } + + // Whether an item has an icon. + virtual bool HasIcon(int id) const { + return false; + } + + // Notification that the menu is about to be popped up. + virtual void MenuWillShow() { + } + + // Whether to create a right-to-left menu. The default implementation + // returns true if the locale's language is a right-to-left language (such + // as Hebrew) and false otherwise. This is generally the right behavior + // since there is no reason to show left-to-right menus for right-to-left + // locales. However, subclasses can override this behavior so that the menu + // is a right-to-left menu only if the view's layout is right-to-left + // (since the view can use a different layout than the locale's language + // layout). + virtual bool IsRightToLeftUILayout() const; + + // Controller + virtual bool SupportsCommand(int id) const { + return true; + } + virtual bool IsCommandEnabled(int id) const { + return true; + } + virtual bool GetContextualLabel(int id, std::wstring* out) const { + return false; + } + virtual void ExecuteCommand(int id) { + } + + protected: + // Returns an empty icon. Will initialize kEmptyIcon if it hasn't been + // initialized. + const SkBitmap& GetEmptyIcon() const; + + private: + // Will be initialized to an icon of 0 width and 0 height when first using. + // An empty icon means we don't need to draw it. + static const SkBitmap* kEmptyIcon; + }; + + // This class is a helper that simply wraps a controller and forwards all + // state and execution actions to it. Use this when you're not defining your + // own custom delegate, but just hooking a context menu to some existing + // controller elsewhere. + class BaseControllerDelegate : public Delegate { + public: + explicit BaseControllerDelegate(Controller* wrapped) + : controller_(wrapped) { + } + + // Overridden from Menu::Delegate + virtual bool SupportsCommand(int id) const { + return controller_->SupportsCommand(id); + } + virtual bool IsCommandEnabled(int id) const { + return controller_->IsCommandEnabled(id); + } + virtual void ExecuteCommand(int id) { + controller_->ExecuteCommand(id); + } + virtual bool GetContextualLabel(int id, std::wstring* out) const { + return controller_->GetContextualLabel(id, out); + } + + private: + // The internal controller that we wrap to forward state and execution + // actions to. + Controller* controller_; + + DISALLOW_COPY_AND_ASSIGN(BaseControllerDelegate); + }; + + // How this popup should align itself relative to the point it is run at. + enum AnchorPoint { + TOPLEFT, + TOPRIGHT + }; + + // Different types of menu items + enum MenuItemType { + NORMAL, + CHECKBOX, + RADIO, + SEPARATOR + }; + + // Construct a Menu using the specified controller to determine command + // state. + // delegate A Menu::Delegate implementation that provides more + // information about the Menu presentation. + // anchor An alignment hint for the popup menu. + // owner The window that the menu is being brought up relative + // to. Not actually used for anything but must not be + // NULL. + Menu(Delegate* delegate, AnchorPoint anchor, HWND owner); + // Alternatively, a Menu object can be constructed wrapping an existing + // HMENU. This can be used to use the convenience methods to insert + // menu items and manage label string ownership. However this kind of + // Menu object cannot use the delegate. + explicit Menu(HMENU hmenu); + virtual ~Menu(); + + void set_delegate(Delegate* delegate) { delegate_ = delegate; } + + // Adds an item to this menu. + // item_id The id of the item, used to identify it in delegate callbacks + // or (if delegate is NULL) to identify the command associated + // with this item with the controller specified in the ctor. Note + // that this value should not be 0 as this has a special meaning + // ("NULL command, no item selected") + // label The text label shown. + // type The type of item. + void AppendMenuItem(int item_id, + const std::wstring& label, + MenuItemType type); + void AddMenuItem(int index, + int item_id, + const std::wstring& label, + MenuItemType type); + + // Append a submenu to this menu. + // The returned pointer is owned by this menu. + Menu* AppendSubMenu(int item_id, + const std::wstring& label); + Menu* AddSubMenu(int index, int item_id, const std::wstring& label); + + // Append a submenu with an icon to this menu + // The returned pointer is owned by this menu. + // Unless the icon is empty, calling this function forces the Menu class + // to draw the menu, instead of relying on Windows. + Menu* AppendSubMenuWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon); + Menu* AddSubMenuWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon); + + // This is a convenience for standard text label menu items where the label + // is provided with this call. + void AppendMenuItemWithLabel(int item_id, const std::wstring& label); + void AddMenuItemWithLabel(int index, int item_id, const std::wstring& label); + + // This is a convenience for text label menu items where the label is + // provided by the delegate. + void AppendDelegateMenuItem(int item_id); + void AddDelegateMenuItem(int index, int item_id); + + // Adds a separator to this menu + void AppendSeparator(); + void AddSeparator(int index); + + // Appends a menu item with an icon. This is for the menu item which + // needs an icon. Calling this function forces the Menu class to draw + // the menu, instead of relying on Windows. + void AppendMenuItemWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon); + void AddMenuItemWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon); + + // Enables or disables the item with the specified id. + void EnableMenuItemByID(int item_id, bool enabled); + void EnableMenuItemAt(int index, bool enabled); + + // Sets menu label at specified index. + void SetMenuLabel(int item_id, const std::wstring& label); + + // Sets an icon for an item with a given item_id. Calling this function + // also forces the Menu class to draw the menu, instead of relying on Windows. + // Returns false if the item with |item_id| is not found. + bool SetIcon(const SkBitmap& icon, int item_id); + + // Shows the menu, blocks until the user dismisses the menu or selects an + // item, and executes the command for the selected item (if any). + // Warning: Blocking call. Will implicitly run a message loop. + void RunMenuAt(int x, int y); + + // Cancels the menu. + virtual void Cancel(); + + // Returns the number of menu items. + int ItemCount(); + + protected: + // The delegate that is being used to get information about the presentation. + Delegate* delegate_; + + private: + // The data of menu items needed to display. + struct ItemData; + + explicit Menu(Menu* parent); + + void AddMenuItemInternal(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon, + HMENU submenu, + MenuItemType type); + + // Sets menu information before displaying, including sub-menus. + void SetMenuInfo(); + + // Get all the state flags for the |fState| field of MENUITEMINFO for the + // item with the specified id. |delegate| is consulted if non-NULL about + // the state of the item in preference to |controller_|. + UINT GetStateFlagsForItemID(int item_id) const; + + // Gets the Win32 TPM alignment flags for the specified AnchorPoint. + DWORD GetTPMAlignFlags() const; + + // The Win32 Menu Handle we wrap + HMENU menu_; + + // The window that would receive WM_COMMAND messages when the user selects + // an item from the menu. + HWND owner_; + + // This list is used to store the default labels for the menu items. + // We may use contextual labels when RunMenu is called, so we must save + // a copy of default ones here. + std::vector<std::wstring> labels_; + + // A flag to indicate whether this menu will be drawn by the Menu class. + // If it's true, all the menu items will be owner drawn. Otherwise, + // all the drawing will be done by Windows. + bool owner_draw_; + + // How this popup menu should be aligned relative to the point it is run at. + AnchorPoint anchor_; + + // This list is to store the string labels and icons to display. It's used + // when owner_draw_ is true. We give MENUITEMINFO pointers to these + // structures to specify what we'd like to draw. If owner_draw_ is false, + // we only give MENUITEMINFO pointers to the labels_. + // The label member of the ItemData structure comes from either labels_ or + // the GetContextualLabel. + std::vector<ItemData*> item_data_; + + // Our sub-menus, if any. + std::vector<Menu*> submenus_; + + // Whether the menu is visible. + bool is_menu_visible_; + + DISALLOW_COPY_AND_ASSIGN(Menu); +}; + +#endif // CONTROLS_MENU_VIEWS_MENU_H_ diff --git a/views/controls/menu/view_menu_delegate.h b/views/controls/menu/view_menu_delegate.h new file mode 100644 index 0000000..bb72ed3 --- /dev/null +++ b/views/controls/menu/view_menu_delegate.h @@ -0,0 +1,34 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ +#define VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ + +#include "base/gfx/native_widget_types.h" + +namespace views { + +class View; + +//////////////////////////////////////////////////////////////////////////////// +// +// ViewMenuDelegate +// +// An interface that allows a component to tell a View about a menu that it +// has constructed that the view can show (e.g. for MenuButton views, or as a +// context menu.) +// +//////////////////////////////////////////////////////////////////////////////// +class ViewMenuDelegate { + public: + // Create and show a menu at the specified position. Source is the view the + // ViewMenuDelegate was set on. + virtual void RunMenu(View* source, + const CPoint& pt, + gfx::NativeView hwnd) = 0; +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ diff --git a/views/controls/message_box_view.cc b/views/controls/message_box_view.cc new file mode 100644 index 0000000..d9c45c2 --- /dev/null +++ b/views/controls/message_box_view.cc @@ -0,0 +1,209 @@ +// Copyright (c) 2006-2008 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/controls/message_box_view.h" + +#include "app/l10n_util.h" +#include "app/message_box_flags.h" +#include "base/clipboard.h" +#include "base/message_loop.h" +#include "base/scoped_clipboard_writer.h" +#include "base/string_util.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/views/standard_layout.h" +#include "views/controls/button/checkbox.h" +#include "views/window/client_view.h" +#include "grit/generated_resources.h" + +static const int kDefaultMessageWidth = 320; + +/////////////////////////////////////////////////////////////////////////////// +// MessageBoxView, public: + +MessageBoxView::MessageBoxView(int dialog_flags, + const std::wstring& message, + const std::wstring& default_prompt, + int message_width) + : message_label_(new views::Label(message)), + prompt_field_(NULL), + icon_(NULL), + checkbox_(NULL), + message_width_(message_width), + focus_grabber_factory_(this) { + Init(dialog_flags, default_prompt); +} + +MessageBoxView::MessageBoxView(int dialog_flags, + const std::wstring& message, + const std::wstring& default_prompt) + : message_label_(new views::Label(message)), + prompt_field_(NULL), + icon_(NULL), + checkbox_(NULL), + message_width_(kDefaultMessageWidth), + focus_grabber_factory_(this) { + Init(dialog_flags, default_prompt); +} + +std::wstring MessageBoxView::GetInputText() { + if (prompt_field_) + return prompt_field_->GetText(); + return EmptyWString(); +} + +bool MessageBoxView::IsCheckBoxSelected() { + return checkbox_ ? checkbox_->checked() : false; +} + +void MessageBoxView::SetIcon(const SkBitmap& icon) { + if (!icon_) + icon_ = new views::ImageView(); + icon_->SetImage(icon); + icon_->SetBounds(0, 0, icon.width(), icon.height()); + ResetLayoutManager(); +} + +void MessageBoxView::SetCheckBoxLabel(const std::wstring& label) { + if (!checkbox_) + checkbox_ = new views::Checkbox(label); + else + checkbox_->SetLabel(label); + ResetLayoutManager(); +} + +void MessageBoxView::SetCheckBoxSelected(bool selected) { + if (!checkbox_) + return; + checkbox_->SetChecked(selected); +} + +/////////////////////////////////////////////////////////////////////////////// +// MessageBoxView, views::View overrides: + +void MessageBoxView::ViewHierarchyChanged(bool is_add, + views::View* parent, + views::View* child) { + if (child == this && is_add) { + if (prompt_field_) + prompt_field_->SelectAll(); + } +} + +bool MessageBoxView::AcceleratorPressed( + const views::Accelerator& accelerator) { + // We only accepts Ctrl-C. + DCHECK(accelerator.GetKeyCode() == 'C' && accelerator.IsCtrlDown()); + + Clipboard* clipboard = g_browser_process->clipboard(); + if (!clipboard) + return false; + + ScopedClipboardWriter scw(clipboard); + scw.WriteText(message_label_->GetText()); + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +// MessageBoxView, private: + +void MessageBoxView::Init(int dialog_flags, + const std::wstring& default_prompt) { + message_label_->SetMultiLine(true); + message_label_->SetAllowCharacterBreak(true); + if (dialog_flags & MessageBoxFlags::kAutoDetectAlignment) { + // Determine the alignment and directionality based on the first character + // with strong directionality. + l10n_util::TextDirection direction = + l10n_util::GetFirstStrongCharacterDirection(message_label_->GetText()); + views::Label::Alignment alignment; + if (direction == l10n_util::RIGHT_TO_LEFT) + alignment = views::Label::ALIGN_RIGHT; + else + alignment = views::Label::ALIGN_LEFT; + // In addition, we should set the RTL alignment mode as + // AUTO_DETECT_ALIGNMENT so that the alignment will not be flipped around + // in RTL locales. + message_label_->SetRTLAlignmentMode(views::Label::AUTO_DETECT_ALIGNMENT); + message_label_->SetHorizontalAlignment(alignment); + } else { + message_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); + } + + if (dialog_flags & MessageBoxFlags::kFlagHasPromptField) { + prompt_field_ = new views::TextField; + prompt_field_->SetText(default_prompt); + } + + ResetLayoutManager(); +} + +void MessageBoxView::ResetLayoutManager() { + using views::GridLayout; + using views::ColumnSet; + + // Initialize the Grid Layout Manager used for this dialog box. + GridLayout* layout = CreatePanelGridLayout(this); + SetLayoutManager(layout); + + gfx::Size icon_size; + if (icon_) + icon_size = icon_->GetPreferredSize(); + + // Add the column set for the message displayed at the top of the dialog box. + // And an icon, if one has been set. + const int message_column_view_set_id = 0; + ColumnSet* column_set = layout->AddColumnSet(message_column_view_set_id); + if (icon_) { + column_set->AddColumn(GridLayout::LEADING, GridLayout::LEADING, 0, + GridLayout::FIXED, icon_size.width(), + icon_size.height()); + column_set->AddPaddingColumn(0, kUnrelatedControlHorizontalSpacing); + } + column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1, + GridLayout::FIXED, message_width_, 0); + + // Column set for prompt textfield, if one has been set. + const int textfield_column_view_set_id = 1; + if (prompt_field_) { + column_set = layout->AddColumnSet(textfield_column_view_set_id); + if (icon_) { + column_set->AddPaddingColumn(0, + icon_size.width() + kUnrelatedControlHorizontalSpacing); + } + column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1, + GridLayout::USE_PREF, 0, 0); + } + + // Column set for checkbox, if one has been set. + const int checkbox_column_view_set_id = 2; + if (checkbox_) { + column_set = layout->AddColumnSet(checkbox_column_view_set_id); + if (icon_) { + column_set->AddPaddingColumn(0, + icon_size.width() + kUnrelatedControlHorizontalSpacing); + } + column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1, + GridLayout::USE_PREF, 0, 0); + } + + layout->StartRow(0, message_column_view_set_id); + if (icon_) + layout->AddView(icon_); + + layout->AddView(message_label_); + + if (prompt_field_) { + layout->AddPaddingRow(0, kRelatedControlVerticalSpacing); + layout->StartRow(0, textfield_column_view_set_id); + layout->AddView(prompt_field_); + } + + if (checkbox_) { + layout->AddPaddingRow(0, kRelatedControlVerticalSpacing); + layout->StartRow(0, checkbox_column_view_set_id); + layout->AddView(checkbox_); + } + + layout->AddPaddingRow(0, kRelatedControlVerticalSpacing); +} diff --git a/views/controls/message_box_view.h b/views/controls/message_box_view.h new file mode 100644 index 0000000..6356d84 --- /dev/null +++ b/views/controls/message_box_view.h @@ -0,0 +1,94 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_MESSAGE_BOX_VIEW_VIEW_H_ +#define VIEWS_CONTROLS_MESSAGE_BOX_VIEW_VIEW_H_ + +#include <string> + +#include "base/task.h" +#include "views/controls/image_view.h" +#include "views/controls/label.h" +#include "views/controls/text_field.h" +#include "views/view.h" + +namespace views { +class Checkbox; +} + +// This class displays the contents of a message box. It is intended for use +// within a constrained window, and has options for a message, prompt, OK +// and Cancel buttons. +class MessageBoxView : public views::View { + public: + MessageBoxView(int dialog_flags, + const std::wstring& message, + const std::wstring& default_prompt, + int message_width); + + MessageBoxView(int dialog_flags, + const std::wstring& message, + const std::wstring& default_prompt); + + // Returns the text box. + views::TextField* text_box() { return prompt_field_; } + + // Returns user entered data in the prompt field. + std::wstring GetInputText(); + + // Returns true if a checkbox is selected, false otherwise. (And false if + // the message box has no checkbox.) + bool IsCheckBoxSelected(); + + // Adds |icon| to the upper left of the message box or replaces the current + // icon. To start out, the message box has no icon. + void SetIcon(const SkBitmap& icon); + + // Adds a checkbox with the specified label to the message box if this is the + // first call. Otherwise, it changes the label of the current checkbox. To + // start, the message box has no checkbox until this function is called. + void SetCheckBoxLabel(const std::wstring& label); + + // Sets the state of the check-box. + void SetCheckBoxSelected(bool selected); + + protected: + // Layout and Painting functions. + virtual void ViewHierarchyChanged(bool is_add, + views::View* parent, + views::View* child); + + // Handles Ctrl-C and writes the message in the system clipboard. + virtual bool AcceleratorPressed(const views::Accelerator& accelerator); + + private: + // Sets up the layout manager and initializes the prompt field. This should + // only be called once, from the constructor. + void Init(int dialog_flags, const std::wstring& default_prompt); + + // Sets up the layout manager based on currently initialized views. Should be + // called when a view is initialized or changed. + void ResetLayoutManager(); + + // Message for the message box. + views::Label* message_label_; + + // Input text field for the message box. + views::TextField* prompt_field_; + + // Icon displayed in the upper left corner of the message box. + views::ImageView* icon_; + + // Checkbox for the message box. + views::Checkbox* checkbox_; + + // Maximum width of the message label. + int message_width_; + + ScopedRunnableMethodFactory<MessageBoxView> focus_grabber_factory_; + + DISALLOW_EVIL_CONSTRUCTORS(MessageBoxView); +}; + +#endif // VIEWS_CONTROLS_MESSAGE_BOX_VIEW_VIEW_H_ diff --git a/views/controls/native_control.cc b/views/controls/native_control.cc new file mode 100644 index 0000000..ce37193 --- /dev/null +++ b/views/controls/native_control.cc @@ -0,0 +1,385 @@ +// Copyright (c) 2006-2008 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/controls/native_control.h" + +#include <atlbase.h> +#include <atlapp.h> +#include <atlcrack.h> +#include <atlframe.h> + +#include "app/l10n_util_win.h" +#include "base/logging.h" +#include "base/win_util.h" +#include "views/background.h" +#include "views/border.h" +#include "views/controls/hwnd_view.h" +#include "views/focus/focus_manager.h" +#include "views/widget/widget.h" +#include "base/gfx/native_theme.h" + +namespace views { + +// Maps to the original WNDPROC for the controller window before we subclassed +// it. +static const wchar_t* const kHandlerKey = + L"__CONTROL_ORIGINAL_MESSAGE_HANDLER__"; + +// Maps to the NativeControl. +static const wchar_t* const kNativeControlKey = L"__NATIVE_CONTROL__"; + +class NativeControlContainer : public CWindowImpl<NativeControlContainer, + CWindow, + CWinTraits<WS_CHILD | WS_CLIPSIBLINGS | + WS_CLIPCHILDREN>> { + public: + + explicit NativeControlContainer(NativeControl* parent) : parent_(parent), + control_(NULL) { + Create(parent->GetWidget()->GetNativeView()); + ::ShowWindow(m_hWnd, SW_SHOW); + } + + virtual ~NativeControlContainer() { + } + + // NOTE: If you add a new message, be sure and verify parent_ is valid before + // calling into parent_. + DECLARE_FRAME_WND_CLASS(L"ChromeViewsNativeControlContainer", NULL); + BEGIN_MSG_MAP(NativeControlContainer); + MSG_WM_CREATE(OnCreate); + MSG_WM_ERASEBKGND(OnEraseBkgnd); + MSG_WM_PAINT(OnPaint); + MSG_WM_SIZE(OnSize); + MSG_WM_NOTIFY(OnNotify); + MSG_WM_COMMAND(OnCommand); + MSG_WM_DESTROY(OnDestroy); + MSG_WM_CONTEXTMENU(OnContextMenu); + MSG_WM_CTLCOLORBTN(OnCtlColorBtn); + MSG_WM_CTLCOLORSTATIC(OnCtlColorStatic) + END_MSG_MAP(); + + HWND GetControl() { + return control_; + } + + // Called when the parent is getting deleted. This control stays around until + // it gets the OnFinalMessage call. + void ResetParent() { + parent_ = NULL; + } + + void OnFinalMessage(HWND hwnd) { + if (parent_) + parent_->NativeControlDestroyed(); + delete this; + } + private: + + LRESULT OnCreate(LPCREATESTRUCT create_struct) { + TRACK_HWND_CREATION(m_hWnd); + + control_ = parent_->CreateNativeControl(m_hWnd); + TRACK_HWND_CREATION(control_); + + FocusManager::InstallFocusSubclass(control_, parent_); + + // We subclass the control hwnd so we get the WM_KEYDOWN messages. + WNDPROC original_handler = + win_util::SetWindowProc(control_, + &NativeControl::NativeControlWndProc); + SetProp(control_, kHandlerKey, original_handler); + SetProp(control_, kNativeControlKey , parent_); + + ::ShowWindow(control_, SW_SHOW); + return 1; + } + + LRESULT OnEraseBkgnd(HDC dc) { + return 1; + } + + void OnPaint(HDC ignore) { + PAINTSTRUCT ps; + HDC dc = ::BeginPaint(*this, &ps); + ::EndPaint(*this, &ps); + } + + void OnSize(int type, const CSize& sz) { + ::MoveWindow(control_, 0, 0, sz.cx, sz.cy, TRUE); + } + + LRESULT OnCommand(UINT code, int id, HWND source) { + return parent_ ? parent_->OnCommand(code, id, source) : 0; + } + + LRESULT OnNotify(int w_param, LPNMHDR l_param) { + if (parent_) + return parent_->OnNotify(w_param, l_param); + else + return 0; + } + + void OnDestroy() { + if (parent_) + parent_->OnDestroy(); + TRACK_HWND_DESTRUCTION(m_hWnd); + } + + void OnContextMenu(HWND window, const CPoint& location) { + if (parent_) + parent_->OnContextMenu(location); + } + + // We need to find an ancestor with a non-null background, and + // ask it for a (solid color) brush that approximates + // the background. The caller will use this when drawing + // the native control as a background color, particularly + // for radiobuttons and XP style pushbuttons. + LRESULT OnCtlColor(UINT msg, HDC dc, HWND control) { + const View *ancestor = parent_; + while (ancestor) { + const Background *background = ancestor->background(); + if (background) { + HBRUSH brush = background->GetNativeControlBrush(); + if (brush) + return reinterpret_cast<LRESULT>(brush); + } + ancestor = ancestor->GetParent(); + } + + // COLOR_BTNFACE is the default for dialog box backgrounds. + return reinterpret_cast<LRESULT>(GetSysColorBrush(COLOR_BTNFACE)); + } + + LRESULT OnCtlColorBtn(HDC dc, HWND control) { + return OnCtlColor(WM_CTLCOLORBTN, dc, control); + } + + LRESULT OnCtlColorStatic(HDC dc, HWND control) { + return OnCtlColor(WM_CTLCOLORSTATIC, dc, control); + } + + NativeControl* parent_; + HWND control_; + DISALLOW_EVIL_CONSTRUCTORS(NativeControlContainer); +}; + +NativeControl::NativeControl() : hwnd_view_(NULL), + container_(NULL), + fixed_width_(-1), + horizontal_alignment_(CENTER), + fixed_height_(-1), + vertical_alignment_(CENTER) { + enabled_ = true; + focusable_ = true; +} + +NativeControl::~NativeControl() { + if (container_) { + container_->ResetParent(); + ::DestroyWindow(*container_); + } +} + +void NativeControl::ValidateNativeControl() { + if (hwnd_view_ == NULL) { + hwnd_view_ = new HWNDView(); + AddChildView(hwnd_view_); + } + + if (!container_ && IsVisible()) { + container_ = new NativeControlContainer(this); + hwnd_view_->Attach(*container_); + if (!enabled_) + EnableWindow(GetNativeControlHWND(), enabled_); + + // This message ensures that the focus border is shown. + ::SendMessage(container_->GetControl(), + WM_CHANGEUISTATE, + MAKELPARAM(UIS_CLEAR, UISF_HIDEFOCUS), + 0); + } +} + +void NativeControl::ViewHierarchyChanged(bool is_add, View *parent, + View *child) { + if (is_add && GetWidget()) { + ValidateNativeControl(); + Layout(); + } +} + +void NativeControl::Layout() { + if (!container_ && GetWidget()) + ValidateNativeControl(); + + if (hwnd_view_) { + gfx::Rect lb = GetLocalBounds(false); + + int x = lb.x(); + int y = lb.y(); + int width = lb.width(); + int height = lb.height(); + if (fixed_width_ > 0) { + width = std::min(fixed_width_, width); + switch (horizontal_alignment_) { + case LEADING: + // Nothing to do. + break; + case CENTER: + x += (lb.width() - width) / 2; + break; + case TRAILING: + x = x + lb.width() - width; + break; + default: + NOTREACHED(); + } + } + + if (fixed_height_ > 0) { + height = std::min(fixed_height_, height); + switch (vertical_alignment_) { + case LEADING: + // Nothing to do. + break; + case CENTER: + y += (lb.height() - height) / 2; + break; + case TRAILING: + y = y + lb.height() - height; + break; + default: + NOTREACHED(); + } + } + + hwnd_view_->SetBounds(x, y, width, height); + } +} + +void NativeControl::OnContextMenu(const CPoint& location) { + if (!GetContextMenuController()) + return; + + int x = location.x; + int y = location.y; + bool is_mouse = true; + if (x == -1 && y == -1) { + gfx::Point point = GetKeyboardContextMenuLocation(); + x = point.x(); + y = point.y(); + is_mouse = false; + } + ShowContextMenu(x, y, is_mouse); +} + +void NativeControl::Focus() { + if (container_) { + DCHECK(container_->GetControl()); + ::SetFocus(container_->GetControl()); + } +} + +HWND NativeControl::GetNativeControlHWND() { + if (container_) + return container_->GetControl(); + else + return NULL; +} + +void NativeControl::NativeControlDestroyed() { + if (hwnd_view_) + hwnd_view_->Detach(); + container_ = NULL; +} + +void NativeControl::SetVisible(bool f) { + if (f != IsVisible()) { + View::SetVisible(f); + if (!f && container_) { + ::DestroyWindow(*container_); + } else if (f && !container_) { + ValidateNativeControl(); + } + } +} + +void NativeControl::SetEnabled(bool enabled) { + if (enabled_ != enabled) { + View::SetEnabled(enabled); + if (GetNativeControlHWND()) { + EnableWindow(GetNativeControlHWND(), enabled_); + } + } +} + +void NativeControl::Paint(ChromeCanvas* canvas) { +} + +void NativeControl::VisibilityChanged(View* starting_from, bool is_visible) { + SetVisible(is_visible); +} + +void NativeControl::SetFixedWidth(int width, Alignment alignment) { + DCHECK(width > 0); + fixed_width_ = width; + horizontal_alignment_ = alignment; +} + +void NativeControl::SetFixedHeight(int height, Alignment alignment) { + DCHECK(height > 0); + fixed_height_ = height; + vertical_alignment_ = alignment; +} + +DWORD NativeControl::GetAdditionalExStyle() const { + // If the UI for the view is mirrored, we should make sure we add the + // extended window style for a right-to-left layout so the subclass creates + // a mirrored HWND for the underlying control. + DWORD ex_style = 0; + if (UILayoutIsRightToLeft()) + ex_style |= l10n_util::GetExtendedStyles(); + + return ex_style; +} + +DWORD NativeControl::GetAdditionalRTLStyle() const { + // If the UI for the view is mirrored, we should make sure we add the + // extended window style for a right-to-left layout so the subclass creates + // a mirrored HWND for the underlying control. + DWORD ex_style = 0; + if (UILayoutIsRightToLeft()) + ex_style |= l10n_util::GetExtendedTooltipStyles(); + + return ex_style; +} + +// static +LRESULT CALLBACK NativeControl::NativeControlWndProc(HWND window, UINT message, + WPARAM w_param, + LPARAM l_param) { + HANDLE original_handler = GetProp(window, kHandlerKey); + DCHECK(original_handler); + NativeControl* native_control = + static_cast<NativeControl*>(GetProp(window, kNativeControlKey)); + DCHECK(native_control); + + if (message == WM_KEYDOWN && native_control->NotifyOnKeyDown()) { + if (native_control->OnKeyDown(static_cast<int>(w_param))) + return 0; + } else if (message == WM_DESTROY) { + win_util::SetWindowProc(window, + reinterpret_cast<WNDPROC>(original_handler)); + RemoveProp(window, kHandlerKey); + RemoveProp(window, kNativeControlKey); + TRACK_HWND_DESTRUCTION(window); + } + + return CallWindowProc(reinterpret_cast<WNDPROC>(original_handler), window, + message, w_param, l_param); +} + +} // namespace views diff --git a/views/controls/native_control.h b/views/controls/native_control.h new file mode 100644 index 0000000..0573168 --- /dev/null +++ b/views/controls/native_control.h @@ -0,0 +1,132 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_NATIVE_CONTROL_H_ +#define VIEWS_CONTROLS_NATIVE_CONTROL_H_ + +#include <windows.h> + +#include "views/view.h" + +namespace views { + +class HWNDView; +class NativeControlContainer; + +//////////////////////////////////////////////////////////////////////////////// +// +// NativeControl is an abstract view that is used to implement views wrapping +// native controls. Subclasses can simply implement CreateNativeControl() to +// wrap a new kind of control +// +//////////////////////////////////////////////////////////////////////////////// +class NativeControl : public View { + public: + enum Alignment { + LEADING = 0, + CENTER, + TRAILING }; + + NativeControl(); + virtual ~NativeControl(); + + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + virtual void Layout(); + + // Overridden to properly set the native control state. + virtual void SetVisible(bool f); + virtual void SetEnabled(bool enabled); + + // Overridden to do nothing. + virtual void Paint(ChromeCanvas* canvas); + protected: + friend class NativeControlContainer; + + // Overridden by sub-classes to create the windows control which is wrapped + virtual HWND CreateNativeControl(HWND parent_container) = 0; + + // Invoked when the native control sends a WM_NOTIFY message to its parent + virtual LRESULT OnNotify(int w_param, LPNMHDR l_param) = 0; + + // Invoked when the native control sends a WM_COMMAND message to its parent + virtual LRESULT OnCommand(UINT code, int id, HWND source) { return 0; } + + // Invoked when the appropriate gesture for a context menu is issued. + virtual void OnContextMenu(const CPoint& location); + + // Overridden so to set the native focus to the native control. + virtual void Focus(); + + // Invoked when the native control sends a WM_DESTORY message to its parent. + virtual void OnDestroy() { } + + // Return the native control + virtual HWND GetNativeControlHWND(); + + // Invoked by the native windows control when it has been destroyed. This is + // invoked AFTER WM_DESTORY has been sent. Any window commands send to the + // HWND will most likely fail. + void NativeControlDestroyed(); + + // Overridden so that the control properly reflects parent's visibility. + virtual void VisibilityChanged(View* starting_from, bool is_visible); + + // Controls that have fixed sizes should call these methods to specify the + // actual size and how they should be aligned within their parent. + void SetFixedWidth(int width, Alignment alignment); + void SetFixedHeight(int height, Alignment alignment); + + // Derived classes interested in receiving key down notification should + // override this method and return true. In which case OnKeyDown is called + // when a key down message is sent to the control. + // Note that this method is called at the time of the control creation: the + // behavior will not change if the returned value changes after the control + // has been created. + virtual bool NotifyOnKeyDown() const { return false; } + + // Invoked when a key is pressed on the control (if NotifyOnKeyDown returns + // true). Should return true if the key message was processed, false + // otherwise. + virtual bool OnKeyDown(int virtual_key_code) { return false; } + + // Returns additional extended style flags. When subclasses call + // CreateWindowEx in order to create the underlying control, they must OR the + // ExStyle parameter with the value returned by this function. + // + // We currently use this method in order to add flags such as WS_EX_LAYOUTRTL + // to the HWND for views with right-to-left UI layout. + DWORD GetAdditionalExStyle() const; + + // TODO(xji): we use the following temporary function as we transition the + // various native controls to use the right set of RTL flags. This function + // will go away (and be replaced by GetAdditionalExStyle()) once all the + // controls are properly transitioned. + DWORD GetAdditionalRTLStyle() const; + + // This variable is protected to provide subclassers direct access. However + // subclassers should always check for NULL since this variable is only + // initialized in ValidateNativeControl(). + HWNDView* hwnd_view_; + + // Fixed size information. -1 for a size means no fixed size. + int fixed_width_; + Alignment horizontal_alignment_; + int fixed_height_; + Alignment vertical_alignment_; + + private: + + void ValidateNativeControl(); + + static LRESULT CALLBACK NativeControlWndProc(HWND window, UINT message, + WPARAM w_param, LPARAM l_param); + + NativeControlContainer* container_; + + DISALLOW_COPY_AND_ASSIGN(NativeControl); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_NATIVE_CONTROL_H_ diff --git a/views/controls/native_control_win.cc b/views/controls/native_control_win.cc new file mode 100644 index 0000000..0c1baf8 --- /dev/null +++ b/views/controls/native_control_win.cc @@ -0,0 +1,201 @@ +// Copyright (c) 2009 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/controls/native_control_win.h" + +#include "app/l10n_util_win.h" +#include "base/logging.h" +#include "base/win_util.h" + +namespace views { + +// static +const wchar_t* NativeControlWin::kNativeControlWinKey = + L"__NATIVE_CONTROL_WIN__"; + +static const wchar_t* kNativeControlOriginalWndProcKey = + L"__NATIVE_CONTROL_ORIGINAL_WNDPROC__"; + +// static +WNDPROC NativeControlWin::original_wndproc_ = NULL; + +//////////////////////////////////////////////////////////////////////////////// +// NativeControlWin, public: + +NativeControlWin::NativeControlWin() : HWNDView() { +} + +NativeControlWin::~NativeControlWin() { + HWND hwnd = GetHWND(); + if (hwnd) { + // Destroy the hwnd if it still exists. Otherwise we won't have shut things + // down correctly, leading to leaking and crashing if another message + // comes in for the hwnd. + Detach(); + DestroyWindow(hwnd); + } +} + +bool NativeControlWin::ProcessMessage(UINT message, WPARAM w_param, + LPARAM l_param, LRESULT* result) { + switch (message) { + case WM_CONTEXTMENU: + ShowContextMenu(gfx::Point(LOWORD(l_param), HIWORD(l_param))); + *result = 0; + return true; + case WM_CTLCOLORBTN: + case WM_CTLCOLORSTATIC: + *result = GetControlColor(message, reinterpret_cast<HDC>(w_param), + GetHWND()); + return true; + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeControlWin, View overrides: + +void NativeControlWin::SetEnabled(bool enabled) { + if (IsEnabled() != enabled) { + View::SetEnabled(enabled); + if (GetHWND()) + EnableWindow(GetHWND(), IsEnabled()); + } +} + +void NativeControlWin::ViewHierarchyChanged(bool is_add, View* parent, + View* child) { + // Create the HWND when we're added to a valid Widget. Many controls need a + // parent HWND to function properly. + if (is_add && GetWidget() && !GetHWND()) + CreateNativeControl(); + + // Call the base class to hide the view if we're being removed. + HWNDView::ViewHierarchyChanged(is_add, parent, child); +} + +void NativeControlWin::VisibilityChanged(View* starting_from, bool is_visible) { + if (!is_visible) { + // We destroy the child control HWND when we become invisible because of the + // performance cost of maintaining many HWNDs. + HWND hwnd = GetHWND(); + Detach(); + DestroyWindow(hwnd); + } else if (!GetHWND()) { + CreateNativeControl(); + } +} + +void NativeControlWin::Focus() { + DCHECK(GetHWND()); + SetFocus(GetHWND()); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeControlWin, protected: + +void NativeControlWin::ShowContextMenu(const gfx::Point& location) { + if (!GetContextMenuController()) + return; + + int x = location.x(); + int y = location.y(); + bool is_mouse = true; + if (x == -1 && y == -1) { + gfx::Point point = GetKeyboardContextMenuLocation(); + x = point.x(); + y = point.y(); + is_mouse = false; + } + View::ShowContextMenu(x, y, is_mouse); +} + +void NativeControlWin::NativeControlCreated(HWND native_control) { + TRACK_HWND_CREATION(native_control); + + // Associate this object with the control's HWND so that WidgetWin can find + // this object when it receives messages from it. + SetProp(native_control, kNativeControlWinKey, this); + + // Subclass the window so we can monitor for key presses. + original_wndproc_ = + win_util::SetWindowProc(native_control, + &NativeControlWin::NativeControlWndProc); + SetProp(native_control, kNativeControlOriginalWndProcKey, original_wndproc_); + + Attach(native_control); + // GetHWND() is now valid. + + // Update the newly created HWND with any resident enabled state. + EnableWindow(GetHWND(), IsEnabled()); + + // This message ensures that the focus border is shown. + SendMessage(GetHWND(), WM_CHANGEUISTATE, + MAKEWPARAM(UIS_CLEAR, UISF_HIDEFOCUS), 0); +} + +DWORD NativeControlWin::GetAdditionalExStyle() const { + // If the UI for the view is mirrored, we should make sure we add the + // extended window style for a right-to-left layout so the subclass creates + // a mirrored HWND for the underlying control. + DWORD ex_style = 0; + if (UILayoutIsRightToLeft()) + ex_style |= l10n_util::GetExtendedStyles(); + + return ex_style; +} + +DWORD NativeControlWin::GetAdditionalRTLStyle() const { + // If the UI for the view is mirrored, we should make sure we add the + // extended window style for a right-to-left layout so the subclass creates + // a mirrored HWND for the underlying control. + DWORD ex_style = 0; + if (UILayoutIsRightToLeft()) + ex_style |= l10n_util::GetExtendedTooltipStyles(); + + return ex_style; +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeControlWin, private: + +LRESULT NativeControlWin::GetControlColor(UINT message, HDC dc, HWND sender) { + View *ancestor = this; + while (ancestor) { + const Background* background = ancestor->background(); + if (background) { + HBRUSH brush = background->GetNativeControlBrush(); + if (brush) + return reinterpret_cast<LRESULT>(brush); + } + ancestor = ancestor->GetParent(); + } + + // COLOR_BTNFACE is the default for dialog box backgrounds. + return reinterpret_cast<LRESULT>(GetSysColorBrush(COLOR_BTNFACE)); +} + +// static +LRESULT NativeControlWin::NativeControlWndProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param) { + NativeControlWin* native_control = + static_cast<NativeControlWin*>(GetProp(window, kNativeControlWinKey)); + DCHECK(native_control); + + if (message == WM_KEYDOWN && native_control->NotifyOnKeyDown()) { + if (native_control->OnKeyDown(static_cast<int>(w_param))) + return 0; + } else if (message == WM_DESTROY) { + win_util::SetWindowProc(window, native_control->original_wndproc_); + RemoveProp(window, kNativeControlWinKey); + TRACK_HWND_DESTRUCTION(window); + } + + return CallWindowProc(native_control->original_wndproc_, window, message, + w_param, l_param); +} + +} // namespace views diff --git a/views/controls/native_control_win.h b/views/controls/native_control_win.h new file mode 100644 index 0000000..6f9923f --- /dev/null +++ b/views/controls/native_control_win.h @@ -0,0 +1,100 @@ +// Copyright (c) 2009 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 VIEWS_CONTROLS_NATIVE_CONTROL_WIN_H_ +#define VIEWS_CONTROLS_NATIVE_CONTROL_WIN_H_ + +#include "views/controls/hwnd_view.h" + +namespace views { + +// A View that hosts a native Windows control. +class NativeControlWin : public HWNDView { + public: + static const wchar_t* kNativeControlWinKey; + + NativeControlWin(); + virtual ~NativeControlWin(); + + // Called by the containing WidgetWin when a message is received from the HWND + // created by an object derived from NativeControlWin. Derived classes MUST + // call _this_ version of the function if they override it and do not handle + // all of the messages listed in widget_win.cc ProcessNativeControlWinMessage. + // Returns true if the message was handled, with a valid result in |result|. + // Returns false if the message was not handled. + virtual bool ProcessMessage(UINT message, + WPARAM w_param, + LPARAM l_param, + LRESULT* result); + + // Called by our subclassed window procedure when a WM_KEYDOWN message is + // received by the HWND created by an object derived from NativeControlWin. + // Returns true if the key was processed, false otherwise. + virtual bool OnKeyDown(int vkey) { return false; } + + // Overridden from View: + virtual void SetEnabled(bool enabled); + + protected: + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + virtual void VisibilityChanged(View* starting_from, bool is_visible); + virtual void Focus(); + + // Called by the containing WidgetWin when a WM_CONTEXTMENU message is + // received from the HWND created by an object derived from NativeControlWin. + virtual void ShowContextMenu(const gfx::Point& location); + + // Derived classes interested in receiving key down notification should + // override this method and return true. In which case OnKeyDown is called + // when a key down message is sent to the control. + // Note that this method is called at the time of the control creation: the + // behavior will not change if the returned value changes after the control + // has been created. + virtual bool NotifyOnKeyDown() const { return false; } + + // Called when the NativeControlWin is attached to a View hierarchy with a + // valid Widget. The NativeControlWin should use this opportunity to create + // its associated HWND. + virtual void CreateNativeControl() = 0; + + // MUST be called by the subclass implementation of |CreateNativeControl| + // immediately after creating the control HWND, otherwise it won't be attached + // to the HWNDView and will be effectively orphaned. + virtual void NativeControlCreated(HWND native_control); + + // Returns additional extended style flags. When subclasses call + // CreateWindowEx in order to create the underlying control, they must OR the + // ExStyle parameter with the value returned by this function. + // + // We currently use this method in order to add flags such as WS_EX_LAYOUTRTL + // to the HWND for views with right-to-left UI layout. + DWORD GetAdditionalExStyle() const; + + // TODO(xji): we use the following temporary function as we transition the + // various native controls to use the right set of RTL flags. This function + // will go away (and be replaced by GetAdditionalExStyle()) once all the + // controls are properly transitioned. + DWORD GetAdditionalRTLStyle() const; + + private: + // Called by the containing WidgetWin when a message of type WM_CTLCOLORBTN or + // WM_CTLCOLORSTATIC is sent from the HWND created by an object dreived from + // NativeControlWin. + LRESULT GetControlColor(UINT message, HDC dc, HWND sender); + + // Our subclass window procedure for the attached control. + static LRESULT CALLBACK NativeControlWndProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param); + + // The window procedure before we subclassed. + static WNDPROC original_wndproc_; + + DISALLOW_COPY_AND_ASSIGN(NativeControlWin); +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_NATIVE_CONTROL_WIN_H_ diff --git a/views/controls/native_view_host.cc b/views/controls/native_view_host.cc new file mode 100644 index 0000000..3fc5fba --- /dev/null +++ b/views/controls/native_view_host.cc @@ -0,0 +1,74 @@ +// Copyright (c) 2009 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/controls/native_view_host.h" + +#include "views/widget/widget.h" +#include "base/logging.h" + +namespace views { + +NativeViewHost::NativeViewHost() + : native_view_(NULL), + installed_clip_(false), + fast_resize_(false), + focus_view_(NULL) { + // The native widget is placed relative to the root. As such, we need to + // know when the position of any ancestor changes, or our visibility relative + // to other views changed as it'll effect our position relative to the root. + SetNotifyWhenVisibleBoundsInRootChanges(true); +} + +NativeViewHost::~NativeViewHost() { +} + +gfx::Size NativeViewHost::GetPreferredSize() { + return preferred_size_; +} + +void NativeViewHost::Layout() { + if (!native_view_) + return; + + // Since widgets know nothing about the View hierarchy (they are direct + // children of the Widget that hosts our View hierarchy) they need to be + // positioned in the coordinate system of the Widget, not the current + // view. + gfx::Point top_left; + ConvertPointToWidget(this, &top_left); + + gfx::Rect vis_bounds = GetVisibleBounds(); + bool visible = !vis_bounds.IsEmpty(); + + if (visible && !fast_resize_) { + if (vis_bounds.size() != size()) { + // Only a portion of the Widget is really visible. + int x = vis_bounds.x(); + int y = vis_bounds.y(); + InstallClip(x, y, vis_bounds.width(), vis_bounds.height()); + installed_clip_ = true; + } else if (installed_clip_) { + // The whole widget is visible but we installed a clip on the widget, + // uninstall it. + UninstallClip(); + installed_clip_ = false; + } + } + + if (visible) { + ShowWidget(top_left.x(), top_left.y(), width(), height()); + } else { + HideWidget(); + } +} + +void NativeViewHost::VisibilityChanged(View* starting_from, bool is_visible) { + Layout(); +} + +void NativeViewHost::VisibleBoundsInRootChanged() { + Layout(); +} + +} // namespace views diff --git a/views/controls/native_view_host.h b/views/controls/native_view_host.h new file mode 100644 index 0000000..99b85b6 --- /dev/null +++ b/views/controls/native_view_host.h @@ -0,0 +1,103 @@ +// Copyright (c) 2009 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 VIEWS_CONTROLS_NATIVE_VIEW_HOST_H_ +#define VIEWS_CONTROLS_NATIVE_VIEW_HOST_H_ + +#include <string> + +#include "views/view.h" + +#include "base/gfx/native_widget_types.h" + +namespace views { + +// Base class for embedding native widgets in a view. +class NativeViewHost : public View { + public: + NativeViewHost(); + virtual ~NativeViewHost(); + + void set_preferred_size(const gfx::Size& size) { preferred_size_ = size; } + + // Returns the preferred size set via set_preferred_size. + virtual gfx::Size GetPreferredSize(); + + // Overriden to invoke Layout. + virtual void VisibilityChanged(View* starting_from, bool is_visible); + + // Invokes any of InstallClip, UninstallClip, ShowWidget or HideWidget + // depending upon what portion of the widget is view in the parent. + virtual void Layout(); + + // A NativeViewHost has an associated focus View so that the focus of the + // native control and of the View are kept in sync. In simple cases where the + // NativeViewHost directly wraps a native window as is, the associated view + // is this View. In other cases where the NativeViewHost is part of another + // view (such as TextField), the actual View is not the NativeViewHost and + // this method must be called to set that. + // This method must be called before Attach(). + void SetAssociatedFocusView(View* view) { focus_view_ = view; } + View* associated_focus_view() { return focus_view_; } + + void set_fast_resize(bool fast_resize) { fast_resize_ = fast_resize; } + bool fast_resize() const { return fast_resize_; } + + // The embedded native view. + gfx::NativeView native_view() const { return native_view_; } + + protected: + // Notification that our visible bounds relative to the root has changed. + // Invokes Layout to make sure the widget is positioned correctly. + virtual void VisibleBoundsInRootChanged(); + + // Sets the native view. Subclasses will typically invoke Layout after setting + // the widget. + void set_native_view(gfx::NativeView widget) { native_view_ = widget; } + + // Installs a clip on the native widget. + virtual void InstallClip(int x, int y, int w, int h) = 0; + + // Removes the clip installed on the native widget by way of InstallClip. + virtual void UninstallClip() = 0; + + // Shows the widget at the specified position (relative to the parent widget). + virtual void ShowWidget(int x, int y, int w, int h) = 0; + + // Hides the widget. NOTE: this may be invoked when the widget is already + // hidden. + virtual void HideWidget() = 0; + + void set_installed_clip(bool installed_clip) { + installed_clip_ = installed_clip; + } + bool installed_clip() const { return installed_clip_; } + + private: + gfx::NativeView native_view_; + + // The preferred size of this View + gfx::Size preferred_size_; + + // Have we installed a region on the HWND used to clip to only the visible + // portion of the HWND? + bool installed_clip_; + + // Fast resizing will move the hwnd and clip its window region, this will + // result in white areas and will not resize the content (so scrollbars + // will be all wrong and content will flow offscreen). Only use this + // when you're doing extremely quick, high-framerate vertical resizes + // and don't care about accuracy. Make sure you do a real resize at the + // end. USE WITH CAUTION. + bool fast_resize_; + + // The view that should be given focus when this NativeViewHost is focused. + View* focus_view_; + + DISALLOW_COPY_AND_ASSIGN(NativeViewHost); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_NATIVE_VIEW_HOST_H_ diff --git a/views/controls/scroll_view.cc b/views/controls/scroll_view.cc new file mode 100644 index 0000000..680b623 --- /dev/null +++ b/views/controls/scroll_view.cc @@ -0,0 +1,517 @@ +// Copyright (c) 2006-2008 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/controls/scroll_view.h" + +#include "app/resource_bundle.h" +#include "base/logging.h" +#include "grit/theme_resources.h" +#include "views/controls/scrollbar/native_scroll_bar.h" +#include "views/widget/root_view.h" + +namespace views { + +const char* const ScrollView::kViewClassName = "views/ScrollView"; + +// Viewport contains the contents View of the ScrollView. +class Viewport : public View { + public: + Viewport() {} + virtual ~Viewport() {} + + virtual void ScrollRectToVisible(int x, int y, int width, int height) { + if (!GetChildViewCount() || !GetParent()) + return; + + View* contents = GetChildViewAt(0); + x -= contents->x(); + y -= contents->y(); + static_cast<ScrollView*>(GetParent())->ScrollContentsRegionToBeVisible( + x, y, width, height); + } + + private: + DISALLOW_EVIL_CONSTRUCTORS(Viewport); +}; + + +ScrollView::ScrollView() { + Init(new NativeScrollBar(true), new NativeScrollBar(false), NULL); +} + +ScrollView::ScrollView(ScrollBar* horizontal_scrollbar, + ScrollBar* vertical_scrollbar, + View* resize_corner) { + Init(horizontal_scrollbar, vertical_scrollbar, resize_corner); +} + +ScrollView::~ScrollView() { + // If scrollbars are currently not used, delete them + if (!horiz_sb_->GetParent()) { + delete horiz_sb_; + } + + if (!vert_sb_->GetParent()) { + delete vert_sb_; + } + + if (resize_corner_ && !resize_corner_->GetParent()) { + delete resize_corner_; + } +} + +void ScrollView::SetContents(View* a_view) { + if (contents_ && contents_ != a_view) { + viewport_->RemoveChildView(contents_); + delete contents_; + contents_ = NULL; + } + + if (a_view) { + contents_ = a_view; + viewport_->AddChildView(contents_); + } + + Layout(); +} + +View* ScrollView::GetContents() const { + return contents_; +} + +void ScrollView::Init(ScrollBar* horizontal_scrollbar, + ScrollBar* vertical_scrollbar, + View* resize_corner) { + DCHECK(horizontal_scrollbar && vertical_scrollbar); + + contents_ = NULL; + horiz_sb_ = horizontal_scrollbar; + vert_sb_ = vertical_scrollbar; + resize_corner_ = resize_corner; + + viewport_ = new Viewport(); + AddChildView(viewport_); + + // Don't add the scrollbars as children until we discover we need them + // (ShowOrHideScrollBar). + horiz_sb_->SetVisible(false); + horiz_sb_->SetController(this); + vert_sb_->SetVisible(false); + vert_sb_->SetController(this); + if (resize_corner_) + resize_corner_->SetVisible(false); +} + +// Make sure that a single scrollbar is created and visible as needed +void ScrollView::SetControlVisibility(View* control, bool should_show) { + if (!control) + return; + if (should_show) { + if (!control->IsVisible()) { + AddChildView(control); + control->SetVisible(true); + } + } else { + RemoveChildView(control); + control->SetVisible(false); + } +} + +void ScrollView::ComputeScrollBarsVisibility(const gfx::Size& vp_size, + const gfx::Size& content_size, + bool* horiz_is_shown, + bool* vert_is_shown) const { + // Try to fit both ways first, then try vertical bar only, then horizontal + // bar only, then defaults to both shown. + if (content_size.width() <= vp_size.width() && + content_size.height() <= vp_size.height()) { + *horiz_is_shown = false; + *vert_is_shown = false; + } else if (content_size.width() <= vp_size.width() - GetScrollBarWidth()) { + *horiz_is_shown = false; + *vert_is_shown = true; + } else if (content_size.height() <= vp_size.height() - GetScrollBarHeight()) { + *horiz_is_shown = true; + *vert_is_shown = false; + } else { + *horiz_is_shown = true; + *vert_is_shown = true; + } +} + +void ScrollView::Layout() { + // Most views will want to auto-fit the available space. Most of them want to + // use the all available width (without overflowing) and only overflow in + // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc. + // Other views want to fit in both ways. An example is PrintView. To make both + // happy, assume a vertical scrollbar but no horizontal scrollbar. To + // override this default behavior, the inner view has to calculate the + // available space, used ComputeScrollBarsVisibility() to use the same + // calculation that is done here and sets its bound to fit within. + gfx::Rect viewport_bounds = GetLocalBounds(true); + // Realign it to 0 so it can be used as-is for SetBounds(). + viewport_bounds.set_origin(gfx::Point(0, 0)); + // viewport_size is the total client space available. + gfx::Size viewport_size = viewport_bounds.size(); + if (viewport_bounds.IsEmpty()) { + // There's nothing to layout. + return; + } + + // Assumes a vertical scrollbar since most the current views are designed for + // this. + int horiz_sb_height = GetScrollBarHeight(); + int vert_sb_width = GetScrollBarWidth(); + viewport_bounds.set_width(viewport_bounds.width() - vert_sb_width); + // Update the bounds right now so the inner views can fit in it. + viewport_->SetBounds(viewport_bounds); + + // Give contents_ a chance to update its bounds if it depends on the + // viewport. + if (contents_) + contents_->Layout(); + + bool should_layout_contents = false; + bool horiz_sb_required = false; + bool vert_sb_required = false; + if (contents_) { + gfx::Size content_size = contents_->size(); + ComputeScrollBarsVisibility(viewport_size, + content_size, + &horiz_sb_required, + &vert_sb_required); + } + bool resize_corner_required = resize_corner_ && horiz_sb_required && + vert_sb_required; + // Take action. + SetControlVisibility(horiz_sb_, horiz_sb_required); + SetControlVisibility(vert_sb_, vert_sb_required); + SetControlVisibility(resize_corner_, resize_corner_required); + + // Non-default. + if (horiz_sb_required) { + viewport_bounds.set_height(viewport_bounds.height() - horiz_sb_height); + should_layout_contents = true; + } + // Default. + if (!vert_sb_required) { + viewport_bounds.set_width(viewport_bounds.width() + vert_sb_width); + should_layout_contents = true; + } + + if (horiz_sb_required) { + horiz_sb_->SetBounds(0, + viewport_bounds.bottom(), + viewport_bounds.right(), + horiz_sb_height); + } + if (vert_sb_required) { + vert_sb_->SetBounds(viewport_bounds.right(), + 0, + vert_sb_width, + viewport_bounds.bottom()); + } + if (resize_corner_required) { + // Show the resize corner. + resize_corner_->SetBounds(viewport_bounds.right(), + viewport_bounds.bottom(), + vert_sb_width, + horiz_sb_height); + } + + // Update to the real client size with the visible scrollbars. + viewport_->SetBounds(viewport_bounds); + if (should_layout_contents && contents_) + contents_->Layout(); + + CheckScrollBounds(); + SchedulePaint(); + UpdateScrollBarPositions(); +} + +int ScrollView::CheckScrollBounds(int viewport_size, + int content_size, + int current_pos) { + int max = std::max(content_size - viewport_size, 0); + if (current_pos < 0) + current_pos = 0; + else if (current_pos > max) + current_pos = max; + return current_pos; +} + +void ScrollView::CheckScrollBounds() { + if (contents_) { + int x, y; + + x = CheckScrollBounds(viewport_->width(), + contents_->width(), + -contents_->x()); + y = CheckScrollBounds(viewport_->height(), + contents_->height(), + -contents_->y()); + + // This is no op if bounds are the same + contents_->SetBounds(-x, -y, contents_->width(), contents_->height()); + } +} + +gfx::Rect ScrollView::GetVisibleRect() const { + if (!contents_) + return gfx::Rect(); + + const int x = + (horiz_sb_ && horiz_sb_->IsVisible()) ? horiz_sb_->GetPosition() : 0; + const int y = + (vert_sb_ && vert_sb_->IsVisible()) ? vert_sb_->GetPosition() : 0; + return gfx::Rect(x, y, viewport_->width(), viewport_->height()); +} + +void ScrollView::ScrollContentsRegionToBeVisible(int x, + int y, + int width, + int height) { + if (!contents_ || ((!horiz_sb_ || !horiz_sb_->IsVisible()) && + (!vert_sb_ || !vert_sb_->IsVisible()))) { + return; + } + + // Figure out the maximums for this scroll view. + const int contents_max_x = + std::max(viewport_->width(), contents_->width()); + const int contents_max_y = + std::max(viewport_->height(), contents_->height()); + + // Make sure x and y are within the bounds of [0,contents_max_*]. + x = std::max(0, std::min(contents_max_x, x)); + y = std::max(0, std::min(contents_max_y, y)); + + // Figure out how far and down the rectangle will go taking width + // and height into account. This will be "clipped" by the viewport. + const int max_x = std::min(contents_max_x, + x + std::min(width, viewport_->width())); + const int max_y = std::min(contents_max_y, + y + std::min(height, + viewport_->height())); + + // See if the rect is already visible. Note the width is (max_x - x) + // and the height is (max_y - y) to take into account the clipping of + // either viewport or the content size. + const gfx::Rect vis_rect = GetVisibleRect(); + if (vis_rect.Contains(gfx::Rect(x, y, max_x - x, max_y - y))) + return; + + // Shift contents_'s X and Y so that the region is visible. If we + // need to shift up or left from where we currently are then we need + // to get it so that the content appears in the upper/left + // corner. This is done by setting the offset to -X or -Y. For down + // or right shifts we need to make sure it appears in the + // lower/right corner. This is calculated by taking max_x or max_y + // and scaling it back by the size of the viewport. + const int new_x = + (vis_rect.x() > x) ? x : std::max(0, max_x - viewport_->width()); + const int new_y = + (vis_rect.y() > y) ? y : std::max(0, max_y - viewport_->height()); + + contents_->SetX(-new_x); + contents_->SetY(-new_y); + UpdateScrollBarPositions(); +} + +void ScrollView::UpdateScrollBarPositions() { + if (!contents_) { + return; + } + + if (horiz_sb_->IsVisible()) { + int vw = viewport_->width(); + int cw = contents_->width(); + int origin = contents_->x(); + horiz_sb_->Update(vw, cw, -origin); + } + if (vert_sb_->IsVisible()) { + int vh = viewport_->height(); + int ch = contents_->height(); + int origin = contents_->y(); + vert_sb_->Update(vh, ch, -origin); + } +} + +// TODO(ACW). We should really use ScrollWindowEx as needed +void ScrollView::ScrollToPosition(ScrollBar* source, int position) { + if (!contents_) + return; + + if (source == horiz_sb_ && horiz_sb_->IsVisible()) { + int vw = viewport_->width(); + int cw = contents_->width(); + int origin = contents_->x(); + if (-origin != position) { + int max_pos = std::max(0, cw - vw); + if (position < 0) + position = 0; + else if (position > max_pos) + position = max_pos; + contents_->SetX(-position); + contents_->SchedulePaint(contents_->GetLocalBounds(true), true); + } + } else if (source == vert_sb_ && vert_sb_->IsVisible()) { + int vh = viewport_->height(); + int ch = contents_->height(); + int origin = contents_->y(); + if (-origin != position) { + int max_pos = std::max(0, ch - vh); + if (position < 0) + position = 0; + else if (position > max_pos) + position = max_pos; + contents_->SetY(-position); + contents_->SchedulePaint(contents_->GetLocalBounds(true), true); + } + } +} + +int ScrollView::GetScrollIncrement(ScrollBar* source, bool is_page, + bool is_positive) { + bool is_horizontal = source->IsHorizontal(); + int amount = 0; + View* view = GetContents(); + if (view) { + if (is_page) + amount = view->GetPageScrollIncrement(this, is_horizontal, is_positive); + else + amount = view->GetLineScrollIncrement(this, is_horizontal, is_positive); + if (amount > 0) + return amount; + } + // No view, or the view didn't return a valid amount. + if (is_page) + return is_horizontal ? viewport_->width() : viewport_->height(); + return is_horizontal ? viewport_->width() / 5 : viewport_->height() / 5; +} + +void ScrollView::ViewHierarchyChanged(bool is_add, View *parent, View *child) { + if (is_add) { + RootView* rv = GetRootView(); + if (rv) { + rv->SetDefaultKeyboardHandler(this); + rv->SetFocusOnMousePressed(true); + } + } +} + +bool ScrollView::OnKeyPressed(const KeyEvent& event) { + bool processed = false; + + // Give vertical scrollbar priority + if (vert_sb_->IsVisible()) { + processed = vert_sb_->OnKeyPressed(event); + } + + if (!processed && horiz_sb_->IsVisible()) { + processed = horiz_sb_->OnKeyPressed(event); + } + return processed; +} + +bool ScrollView::OnMouseWheel(const MouseWheelEvent& e) { + bool processed = false; + + // Give vertical scrollbar priority + if (vert_sb_->IsVisible()) { + processed = vert_sb_->OnMouseWheel(e); + } + + if (!processed && horiz_sb_->IsVisible()) { + processed = horiz_sb_->OnMouseWheel(e); + } + return processed; +} + +std::string ScrollView::GetClassName() const { + return kViewClassName; +} + +int ScrollView::GetScrollBarWidth() const { + return vert_sb_->GetLayoutSize(); +} + +int ScrollView::GetScrollBarHeight() const { + return horiz_sb_->GetLayoutSize(); +} + +// VariableRowHeightScrollHelper ---------------------------------------------- + +VariableRowHeightScrollHelper::VariableRowHeightScrollHelper( + Controller* controller) : controller_(controller) { +} + +VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() { +} + +int VariableRowHeightScrollHelper::GetPageScrollIncrement( + ScrollView* scroll_view, bool is_horizontal, bool is_positive) { + if (is_horizontal) + return 0; + // y coordinate is most likely negative. + int y = abs(scroll_view->GetContents()->y()); + int vis_height = scroll_view->GetContents()->GetParent()->height(); + if (is_positive) { + // Align the bottom most row to the top of the view. + int bottom = std::min(scroll_view->GetContents()->height() - 1, + y + vis_height); + RowInfo bottom_row_info = GetRowInfo(bottom); + // If 0, ScrollView will provide a default value. + return std::max(0, bottom_row_info.origin - y); + } else { + // Align the row on the previous page to to the top of the view. + int last_page_y = y - vis_height; + RowInfo last_page_info = GetRowInfo(std::max(0, last_page_y)); + if (last_page_y != last_page_info.origin) + return std::max(0, y - last_page_info.origin - last_page_info.height); + return std::max(0, y - last_page_info.origin); + } +} + +int VariableRowHeightScrollHelper::GetLineScrollIncrement( + ScrollView* scroll_view, bool is_horizontal, bool is_positive) { + if (is_horizontal) + return 0; + // y coordinate is most likely negative. + int y = abs(scroll_view->GetContents()->y()); + RowInfo row = GetRowInfo(y); + if (is_positive) { + return row.height - (y - row.origin); + } else if (y == row.origin) { + row = GetRowInfo(std::max(0, row.origin - 1)); + return y - row.origin; + } else { + return y - row.origin; + } +} + +VariableRowHeightScrollHelper::RowInfo + VariableRowHeightScrollHelper::GetRowInfo(int y) { + return controller_->GetRowInfo(y); +} + +// FixedRowHeightScrollHelper ----------------------------------------------- + +FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin, + int row_height) + : VariableRowHeightScrollHelper(NULL), + top_margin_(top_margin), + row_height_(row_height) { + DCHECK(row_height > 0); +} + +VariableRowHeightScrollHelper::RowInfo + FixedRowHeightScrollHelper::GetRowInfo(int y) { + if (y < top_margin_) + return RowInfo(0, top_margin_); + return RowInfo((y - top_margin_) / row_height_ * row_height_ + top_margin_, + row_height_); +} + +} // namespace views diff --git a/views/controls/scroll_view.h b/views/controls/scroll_view.h new file mode 100644 index 0000000..5a578cd --- /dev/null +++ b/views/controls/scroll_view.h @@ -0,0 +1,207 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_SCROLL_VIEW_H_ +#define VIEWS_CONTROLS_SCROLL_VIEW_H_ + +#include "views/controls/scrollbar/scroll_bar.h" + +namespace views { + +///////////////////////////////////////////////////////////////////////////// +// +// ScrollView class +// +// A ScrollView is used to make any View scrollable. The view is added to +// a viewport which takes care of clipping. +// +// In this current implementation both horizontal and vertical scrollbars are +// added as needed. +// +// The scrollview supports keyboard UI and mousewheel. +// +///////////////////////////////////////////////////////////////////////////// + +class ScrollView : public View, + public ScrollBarController { + public: + static const char* const kViewClassName; + + ScrollView(); + // Initialize with specific views. resize_corner is optional. + ScrollView(ScrollBar* horizontal_scrollbar, + ScrollBar* vertical_scrollbar, + View* resize_corner); + virtual ~ScrollView(); + + // Set the contents. Any previous contents will be deleted. The contents + // is the view that needs to scroll. + void SetContents(View* a_view); + View* GetContents() const; + + // Overridden to layout the viewport and scrollbars. + virtual void Layout(); + + // Returns the visible region of the content View. + gfx::Rect GetVisibleRect() const; + + // Scrolls the minimum amount necessary to make the specified rectangle + // visible, in the coordinates of the contents view. The specified rectangle + // is constrained by the bounds of the contents view. This has no effect if + // the contents have not been set. + // + // Client code should use ScrollRectToVisible, which invokes this + // appropriately. + void ScrollContentsRegionToBeVisible(int x, int y, int width, int height); + + // ScrollBarController. + // NOTE: this is intended to be invoked by the ScrollBar, and NOT general + // client code. + // See also ScrollRectToVisible. + virtual void ScrollToPosition(ScrollBar* source, int position); + + // Returns the amount to scroll relative to the visible bounds. This invokes + // either GetPageScrollIncrement or GetLineScrollIncrement to determine the + // amount to scroll. If the view returns 0 (or a negative value) a default + // value is used. + virtual int GetScrollIncrement(ScrollBar* source, + bool is_page, + bool is_positive); + + // Overridden to setup keyboard ui when the view hierarchy changes + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + + // Keyboard events + virtual bool OnKeyPressed(const KeyEvent& event); + virtual bool OnMouseWheel(const MouseWheelEvent& e); + + virtual std::string GetClassName() const; + + // Retrieves the vertical scrollbar width. + int GetScrollBarWidth() const; + + // Retrieves the horizontal scrollbar height. + int GetScrollBarHeight() const; + + // Computes the visibility of both scrollbars, taking in account the view port + // and content sizes. + void ComputeScrollBarsVisibility(const gfx::Size& viewport_size, + const gfx::Size& content_size, + bool* horiz_is_shown, + bool* vert_is_shown) const; + + ScrollBar* horizontal_scroll_bar() const { return horiz_sb_; } + + ScrollBar* vertical_scroll_bar() const { return vert_sb_; } + + private: + // Initialize the ScrollView. resize_corner is optional. + void Init(ScrollBar* horizontal_scrollbar, + ScrollBar* vertical_scrollbar, + View* resize_corner); + + // Shows or hides the scrollbar/resize_corner based on the value of + // |should_show|. + void SetControlVisibility(View* control, bool should_show); + + // Update the scrollbars positions given viewport and content sizes. + void UpdateScrollBarPositions(); + + // Make sure the content is not scrolled out of bounds + void CheckScrollBounds(); + + // Make sure the content is not scrolled out of bounds in one dimension + int CheckScrollBounds(int viewport_size, int content_size, int current_pos); + + // The clipping viewport. Content is added to that view. + View* viewport_; + + // The current contents + View* contents_; + + // Horizontal scrollbar. + ScrollBar* horiz_sb_; + + // Vertical scrollbar. + ScrollBar* vert_sb_; + + // Resize corner. + View* resize_corner_; + + DISALLOW_EVIL_CONSTRUCTORS(ScrollView); +}; + +// VariableRowHeightScrollHelper is intended for views that contain rows of +// varying height. To use a VariableRowHeightScrollHelper create one supplying +// a Controller and delegate GetPageScrollIncrement and GetLineScrollIncrement +// to the helper. VariableRowHeightScrollHelper calls back to the +// Controller to determine row boundaries. +class VariableRowHeightScrollHelper { + public: + // The origin and height of a row. + struct RowInfo { + RowInfo(int origin, int height) : origin(origin), height(height) {} + + // Origin of the row. + int origin; + + // Height of the row. + int height; + }; + + // Used to determine row boundaries. + class Controller { + public: + // Returns the origin and size of the row at the specified location. + virtual VariableRowHeightScrollHelper::RowInfo GetRowInfo(int y) = 0; + }; + + // Creates a new VariableRowHeightScrollHelper. Controller is + // NOT deleted by this VariableRowHeightScrollHelper. + explicit VariableRowHeightScrollHelper(Controller* controller); + virtual ~VariableRowHeightScrollHelper(); + + // Delegate the View methods of the same name to these. The scroll amount is + // determined by querying the Controller for the appropriate row to scroll + // to. + int GetPageScrollIncrement(ScrollView* scroll_view, + bool is_horizontal, bool is_positive); + int GetLineScrollIncrement(ScrollView* scroll_view, + bool is_horizontal, bool is_positive); + + protected: + // Returns the row information for the row at the specified location. This + // calls through to the method of the same name on the controller. + virtual RowInfo GetRowInfo(int y); + + private: + Controller* controller_; + + DISALLOW_EVIL_CONSTRUCTORS(VariableRowHeightScrollHelper); +}; + +// FixedRowHeightScrollHelper is intended for views that contain fixed height +// height rows. To use a FixedRowHeightScrollHelper delegate +// GetPageScrollIncrement and GetLineScrollIncrement to it. +class FixedRowHeightScrollHelper : public VariableRowHeightScrollHelper { + public: + // Creates a FixedRowHeightScrollHelper. top_margin gives the distance from + // the top of the view to the first row, and may be 0. row_height gives the + // height of each row. + FixedRowHeightScrollHelper(int top_margin, int row_height); + + protected: + // Calculates the bounds of the row from the top margin and row height. + virtual RowInfo GetRowInfo(int y); + + private: + int top_margin_; + int row_height_; + + DISALLOW_EVIL_CONSTRUCTORS(FixedRowHeightScrollHelper); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_SCROLL_VIEW_H_ diff --git a/views/controls/scrollbar/bitmap_scroll_bar.cc b/views/controls/scrollbar/bitmap_scroll_bar.cc new file mode 100644 index 0000000..4a93782 --- /dev/null +++ b/views/controls/scrollbar/bitmap_scroll_bar.cc @@ -0,0 +1,703 @@ +// Copyright (c) 2006-2008 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/controls/scrollbar/bitmap_scroll_bar.h" + +#include "app/gfx/chrome_canvas.h" +#include "app/l10n_util.h" +#include "base/message_loop.h" +#include "grit/generated_resources.h" +#include "skia/include/SkBitmap.h" +#include "views/controls/menu/menu.h" +#include "views/controls/scroll_view.h" +#include "views/widget/widget.h" + +#undef min +#undef max + +namespace views { + +namespace { + +// The distance the mouse can be dragged outside the bounds of the thumb during +// dragging before the scrollbar will snap back to its regular position. +static const int kScrollThumbDragOutSnap = 100; + +/////////////////////////////////////////////////////////////////////////////// +// +// AutorepeatButton +// +// A button that activates on mouse pressed rather than released, and that +// continues to fire the clicked action as the mouse button remains pressed +// down on the button. +// +/////////////////////////////////////////////////////////////////////////////// +class AutorepeatButton : public ImageButton { + public: + AutorepeatButton(ButtonListener* listener) + : ImageButton(listener), + repeater_(NewCallback<AutorepeatButton>(this, + &AutorepeatButton::NotifyClick)) { + } + virtual ~AutorepeatButton() {} + + protected: + virtual bool OnMousePressed(const MouseEvent& event) { + Button::NotifyClick(event.GetFlags()); + repeater_.Start(); + return true; + } + + virtual void OnMouseReleased(const MouseEvent& event, bool canceled) { + repeater_.Stop(); + View::OnMouseReleased(event, canceled); + } + + private: + void NotifyClick() { + Button::NotifyClick(0); + } + + // The repeat controller that we use to repeatedly click the button when the + // mouse button is down. + RepeatController repeater_; + + DISALLOW_EVIL_CONSTRUCTORS(AutorepeatButton); +}; + +/////////////////////////////////////////////////////////////////////////////// +// +// BitmapScrollBarThumb +// +// A view that acts as the thumb in the scroll bar track that the user can +// drag to scroll the associated contents view within the viewport. +// +/////////////////////////////////////////////////////////////////////////////// +class BitmapScrollBarThumb : public View { + public: + explicit BitmapScrollBarThumb(BitmapScrollBar* scroll_bar) + : scroll_bar_(scroll_bar), + drag_start_position_(-1), + mouse_offset_(-1), + state_(CustomButton::BS_NORMAL) { + } + virtual ~BitmapScrollBarThumb() { } + + // Sets the size (width or height) of the thumb to the specified value. + void SetSize(int size) { + // Make sure the thumb is never sized smaller than its minimum possible + // display size. + gfx::Size prefsize = GetPreferredSize(); + size = std::max(size, + static_cast<int>(scroll_bar_->IsHorizontal() ? + prefsize.width() : prefsize.height())); + gfx::Rect thumb_bounds = bounds(); + if (scroll_bar_->IsHorizontal()) { + thumb_bounds.set_width(size); + } else { + thumb_bounds.set_height(size); + } + SetBounds(thumb_bounds); + } + + // Retrieves the size (width or height) of the thumb. + int GetSize() const { + if (scroll_bar_->IsHorizontal()) + return width(); + return height(); + } + + // Sets the position of the thumb on the x or y axis. + void SetPosition(int position) { + gfx::Rect thumb_bounds = bounds(); + gfx::Rect track_bounds = scroll_bar_->GetTrackBounds(); + if (scroll_bar_->IsHorizontal()) { + thumb_bounds.set_x(track_bounds.x() + position); + } else { + thumb_bounds.set_x(track_bounds.y() + position); + } + SetBounds(thumb_bounds); + } + + // Gets the position of the thumb on the x or y axis. + int GetPosition() const { + gfx::Rect track_bounds = scroll_bar_->GetTrackBounds(); + if (scroll_bar_->IsHorizontal()) + return x() - track_bounds.x(); + return y() - track_bounds.y(); + } + + // View overrides: + virtual gfx::Size GetPreferredSize() { + return gfx::Size(background_bitmap()->width(), + start_cap_bitmap()->height() + + end_cap_bitmap()->height() + + grippy_bitmap()->height()); + } + + protected: + // View overrides: + virtual void Paint(ChromeCanvas* canvas) { + canvas->DrawBitmapInt(*start_cap_bitmap(), 0, 0); + int top_cap_height = start_cap_bitmap()->height(); + int bottom_cap_height = end_cap_bitmap()->height(); + int thumb_body_height = height() - top_cap_height - bottom_cap_height; + canvas->TileImageInt(*background_bitmap(), 0, top_cap_height, + background_bitmap()->width(), thumb_body_height); + canvas->DrawBitmapInt(*end_cap_bitmap(), 0, + height() - bottom_cap_height); + + // Paint the grippy over the track. + int grippy_x = (width() - grippy_bitmap()->width()) / 2; + int grippy_y = (thumb_body_height - grippy_bitmap()->height()) / 2; + canvas->DrawBitmapInt(*grippy_bitmap(), grippy_x, grippy_y); + } + + virtual void OnMouseEntered(const MouseEvent& event) { + SetState(CustomButton::BS_HOT); + } + + virtual void OnMouseExited(const MouseEvent& event) { + SetState(CustomButton::BS_NORMAL); + } + + virtual bool OnMousePressed(const MouseEvent& event) { + mouse_offset_ = scroll_bar_->IsHorizontal() ? event.x() : event.y(); + drag_start_position_ = GetPosition(); + SetState(CustomButton::BS_PUSHED); + return true; + } + + virtual bool OnMouseDragged(const MouseEvent& event) { + // If the user moves the mouse more than |kScrollThumbDragOutSnap| outside + // the bounds of the thumb, the scrollbar will snap the scroll back to the + // point it was at before the drag began. + if (scroll_bar_->IsHorizontal()) { + if ((event.y() < y() - kScrollThumbDragOutSnap) || + (event.y() > (y() + height() + kScrollThumbDragOutSnap))) { + scroll_bar_->ScrollToThumbPosition(drag_start_position_, false); + return true; + } + } else { + if ((event.x() < x() - kScrollThumbDragOutSnap) || + (event.x() > (x() + width() + kScrollThumbDragOutSnap))) { + scroll_bar_->ScrollToThumbPosition(drag_start_position_, false); + return true; + } + } + if (scroll_bar_->IsHorizontal()) { + int thumb_x = event.x() - mouse_offset_; + scroll_bar_->ScrollToThumbPosition(x() + thumb_x, false); + } else { + int thumb_y = event.y() - mouse_offset_; + scroll_bar_->ScrollToThumbPosition(y() + thumb_y, false); + } + return true; + } + + virtual void OnMouseReleased(const MouseEvent& event, + bool canceled) { + SetState(CustomButton::BS_HOT); + View::OnMouseReleased(event, canceled); + } + + private: + // Returns the bitmap rendered at the start of the thumb. + SkBitmap* start_cap_bitmap() const { + return scroll_bar_->images_[BitmapScrollBar::THUMB_START_CAP][state_]; + } + + // Returns the bitmap rendered at the end of the thumb. + SkBitmap* end_cap_bitmap() const { + return scroll_bar_->images_[BitmapScrollBar::THUMB_END_CAP][state_]; + } + + // Returns the bitmap that is tiled in the background of the thumb between + // the start and the end caps. + SkBitmap* background_bitmap() const { + return scroll_bar_->images_[BitmapScrollBar::THUMB_MIDDLE][state_]; + } + + // Returns the bitmap that is rendered in the middle of the thumb + // transparently over the background bitmap. + SkBitmap* grippy_bitmap() const { + return scroll_bar_->images_[BitmapScrollBar::THUMB_GRIPPY] + [CustomButton::BS_NORMAL]; + } + + // Update our state and schedule a repaint when the mouse moves over us. + void SetState(CustomButton::ButtonState state) { + state_ = state; + SchedulePaint(); + } + + // The BitmapScrollBar that owns us. + BitmapScrollBar* scroll_bar_; + + int drag_start_position_; + + // The position of the mouse on the scroll axis relative to the top of this + // View when the drag started. + int mouse_offset_; + + // The current state of the thumb button. + CustomButton::ButtonState state_; + + DISALLOW_EVIL_CONSTRUCTORS(BitmapScrollBarThumb); +}; + +} // anonymous namespace + +/////////////////////////////////////////////////////////////////////////////// +// BitmapScrollBar, public: + +BitmapScrollBar::BitmapScrollBar(bool horizontal, bool show_scroll_buttons) + : contents_size_(0), + contents_scroll_offset_(0), + prev_button_(new AutorepeatButton(this)), + next_button_(new AutorepeatButton(this)), + thumb_(new BitmapScrollBarThumb(this)), + thumb_track_state_(CustomButton::BS_NORMAL), + last_scroll_amount_(SCROLL_NONE), + repeater_(NewCallback<BitmapScrollBar>(this, + &BitmapScrollBar::TrackClicked)), + context_menu_mouse_position_(0), + show_scroll_buttons_(show_scroll_buttons), + ScrollBar(horizontal) { + if (!show_scroll_buttons_) { + prev_button_->SetVisible(false); + next_button_->SetVisible(false); + } + + AddChildView(prev_button_); + AddChildView(next_button_); + AddChildView(thumb_); + + SetContextMenuController(this); + prev_button_->SetContextMenuController(this); + next_button_->SetContextMenuController(this); + thumb_->SetContextMenuController(this); +} + +gfx::Rect BitmapScrollBar::GetTrackBounds() const { + gfx::Size prefsize = prev_button_->GetPreferredSize(); + if (IsHorizontal()) { + if (!show_scroll_buttons_) + prefsize.set_width(0); + int new_width = + std::max(0, static_cast<int>(width() - (prefsize.width() * 2))); + gfx::Rect track_bounds(prefsize.width(), 0, new_width, prefsize.height()); + return track_bounds; + } + if (!show_scroll_buttons_) + prefsize.set_height(0); + gfx::Rect track_bounds(0, prefsize.height(), prefsize.width(), + std::max(0, height() - (prefsize.height() * 2))); + return track_bounds; +} + +void BitmapScrollBar::SetImage(ScrollBarPart part, + CustomButton::ButtonState state, + SkBitmap* bitmap) { + DCHECK(part < PART_COUNT); + DCHECK(state < CustomButton::BS_COUNT); + switch (part) { + case PREV_BUTTON: + prev_button_->SetImage(state, bitmap); + break; + case NEXT_BUTTON: + next_button_->SetImage(state, bitmap); + break; + case THUMB_START_CAP: + case THUMB_MIDDLE: + case THUMB_END_CAP: + case THUMB_GRIPPY: + case THUMB_TRACK: + images_[part][state] = bitmap; + break; + } +} + +void BitmapScrollBar::ScrollByAmount(ScrollAmount amount) { + ScrollBarController* controller = GetController(); + int offset = contents_scroll_offset_; + switch (amount) { + case SCROLL_START: + offset = GetMinPosition(); + break; + case SCROLL_END: + offset = GetMaxPosition(); + break; + case SCROLL_PREV_LINE: + offset -= controller->GetScrollIncrement(this, false, false); + offset = std::max(GetMinPosition(), offset); + break; + case SCROLL_NEXT_LINE: + offset += controller->GetScrollIncrement(this, false, true); + offset = std::min(GetMaxPosition(), offset); + break; + case SCROLL_PREV_PAGE: + offset -= controller->GetScrollIncrement(this, true, false); + offset = std::max(GetMinPosition(), offset); + break; + case SCROLL_NEXT_PAGE: + offset += controller->GetScrollIncrement(this, true, true); + offset = std::min(GetMaxPosition(), offset); + break; + } + contents_scroll_offset_ = offset; + ScrollContentsToOffset(); +} + +void BitmapScrollBar::ScrollToThumbPosition(int thumb_position, + bool scroll_to_middle) { + contents_scroll_offset_ = + CalculateContentsOffset(thumb_position, scroll_to_middle); + if (contents_scroll_offset_ < GetMinPosition()) { + contents_scroll_offset_ = GetMinPosition(); + } else if (contents_scroll_offset_ > GetMaxPosition()) { + contents_scroll_offset_ = GetMaxPosition(); + } + ScrollContentsToOffset(); + SchedulePaint(); +} + +void BitmapScrollBar::ScrollByContentsOffset(int contents_offset) { + contents_scroll_offset_ -= contents_offset; + if (contents_scroll_offset_ < GetMinPosition()) { + contents_scroll_offset_ = GetMinPosition(); + } else if (contents_scroll_offset_ > GetMaxPosition()) { + contents_scroll_offset_ = GetMaxPosition(); + } + ScrollContentsToOffset(); +} + +void BitmapScrollBar::TrackClicked() { + if (last_scroll_amount_ != SCROLL_NONE) + ScrollByAmount(last_scroll_amount_); +} + +/////////////////////////////////////////////////////////////////////////////// +// BitmapScrollBar, View implementation: + +gfx::Size BitmapScrollBar::GetPreferredSize() { + // In this case, we're returning the desired width of the scrollbar and its + // minimum allowable height. + gfx::Size button_prefsize = prev_button_->GetPreferredSize(); + return gfx::Size(button_prefsize.width(), button_prefsize.height() * 2); +} + +void BitmapScrollBar::Paint(ChromeCanvas* canvas) { + // Paint the track. + gfx::Rect track_bounds = GetTrackBounds(); + canvas->TileImageInt(*images_[THUMB_TRACK][thumb_track_state_], + track_bounds.x(), track_bounds.y(), + track_bounds.width(), track_bounds.height()); +} + +void BitmapScrollBar::Layout() { + // Size and place the two scroll buttons. + if (show_scroll_buttons_) { + gfx::Size prefsize = prev_button_->GetPreferredSize(); + prev_button_->SetBounds(0, 0, prefsize.width(), prefsize.height()); + prefsize = next_button_->GetPreferredSize(); + if (IsHorizontal()) { + next_button_->SetBounds(width() - prefsize.width(), 0, prefsize.width(), + prefsize.height()); + } else { + next_button_->SetBounds(0, height() - prefsize.height(), prefsize.width(), + prefsize.height()); + } + } else { + prev_button_->SetBounds(0, 0, 0, 0); + next_button_->SetBounds(0, 0, 0, 0); + } + + // Size and place the thumb + gfx::Size thumb_prefsize = thumb_->GetPreferredSize(); + gfx::Rect track_bounds = GetTrackBounds(); + + // Preserve the height/width of the thumb (depending on orientation) as set + // by the last call to |Update|, but coerce the width/height to be the + // appropriate value for the bitmaps provided. + if (IsHorizontal()) { + thumb_->SetBounds(thumb_->x(), thumb_->y(), thumb_->width(), + thumb_prefsize.height()); + } else { + thumb_->SetBounds(thumb_->x(), thumb_->y(), thumb_prefsize.width(), + thumb_->height()); + } + + // Hide the thumb if the track isn't tall enough to display even a tiny + // thumb. The user can only use the mousewheel, scroll buttons or keyboard + // in this scenario. + if ((IsHorizontal() && (track_bounds.width() < thumb_prefsize.width()) || + (!IsHorizontal() && (track_bounds.height() < thumb_prefsize.height())))) { + thumb_->SetVisible(false); + } else if (!thumb_->IsVisible()) { + thumb_->SetVisible(true); + } +} + +bool BitmapScrollBar::OnMousePressed(const MouseEvent& event) { + if (event.IsOnlyLeftMouseButton()) { + SetThumbTrackState(CustomButton::BS_PUSHED); + gfx::Rect thumb_bounds = thumb_->bounds(); + if (IsHorizontal()) { + if (event.x() < thumb_bounds.x()) { + last_scroll_amount_ = SCROLL_PREV_PAGE; + } else if (event.x() > thumb_bounds.right()) { + last_scroll_amount_ = SCROLL_NEXT_PAGE; + } + } else { + if (event.y() < thumb_bounds.y()) { + last_scroll_amount_ = SCROLL_PREV_PAGE; + } else if (event.y() > thumb_bounds.bottom()) { + last_scroll_amount_ = SCROLL_NEXT_PAGE; + } + } + TrackClicked(); + repeater_.Start(); + } + return true; +} + +void BitmapScrollBar::OnMouseReleased(const MouseEvent& event, bool canceled) { + SetThumbTrackState(CustomButton::BS_NORMAL); + repeater_.Stop(); + View::OnMouseReleased(event, canceled); +} + +bool BitmapScrollBar::OnMouseWheel(const MouseWheelEvent& event) { + ScrollByContentsOffset(event.GetOffset()); + return true; +} + +bool BitmapScrollBar::OnKeyPressed(const KeyEvent& event) { + ScrollAmount amount = SCROLL_NONE; + switch(event.GetCharacter()) { + case VK_UP: + if (!IsHorizontal()) + amount = SCROLL_PREV_LINE; + break; + case VK_DOWN: + if (!IsHorizontal()) + amount = SCROLL_NEXT_LINE; + break; + case VK_LEFT: + if (IsHorizontal()) + amount = SCROLL_PREV_LINE; + break; + case VK_RIGHT: + if (IsHorizontal()) + amount = SCROLL_NEXT_LINE; + break; + case VK_PRIOR: + amount = SCROLL_PREV_PAGE; + break; + case VK_NEXT: + amount = SCROLL_NEXT_PAGE; + break; + case VK_HOME: + amount = SCROLL_START; + break; + case VK_END: + amount = SCROLL_END; + break; + } + if (amount != SCROLL_NONE) { + ScrollByAmount(amount); + return true; + } + return false; +} + +/////////////////////////////////////////////////////////////////////////////// +// BitmapScrollBar, ContextMenuController implementation: + +enum ScrollBarContextMenuCommands { + ScrollBarContextMenuCommand_ScrollHere = 1, + ScrollBarContextMenuCommand_ScrollStart, + ScrollBarContextMenuCommand_ScrollEnd, + ScrollBarContextMenuCommand_ScrollPageUp, + ScrollBarContextMenuCommand_ScrollPageDown, + ScrollBarContextMenuCommand_ScrollPrev, + ScrollBarContextMenuCommand_ScrollNext +}; + +void BitmapScrollBar::ShowContextMenu(View* source, + int x, + int y, + bool is_mouse_gesture) { + Widget* widget = GetWidget(); + gfx::Rect widget_bounds; + widget->GetBounds(&widget_bounds, true); + gfx::Point temp_pt(x - widget_bounds.x(), y - widget_bounds.y()); + View::ConvertPointFromWidget(this, &temp_pt); + context_menu_mouse_position_ = IsHorizontal() ? temp_pt.x() : temp_pt.y(); + + Menu menu(this, Menu::TOPLEFT, GetWidget()->GetNativeView()); + menu.AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollHere); + menu.AppendSeparator(); + menu.AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollStart); + menu.AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollEnd); + menu.AppendSeparator(); + menu.AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPageUp); + menu.AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPageDown); + menu.AppendSeparator(); + menu.AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPrev); + menu.AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollNext); + menu.RunMenuAt(x, y); +} + +/////////////////////////////////////////////////////////////////////////////// +// BitmapScrollBar, Menu::Delegate implementation: + +std::wstring BitmapScrollBar::GetLabel(int id) const { + switch (id) { + case ScrollBarContextMenuCommand_ScrollHere: + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLHERE); + case ScrollBarContextMenuCommand_ScrollStart: + if (IsHorizontal()) + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLLEFTEDGE); + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLHOME); + case ScrollBarContextMenuCommand_ScrollEnd: + if (IsHorizontal()) + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLRIGHTEDGE); + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLEND); + case ScrollBarContextMenuCommand_ScrollPageUp: + if (IsHorizontal()) + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLPAGEUP); + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLPAGEUP); + case ScrollBarContextMenuCommand_ScrollPageDown: + if (IsHorizontal()) + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLPAGEDOWN); + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLPAGEDOWN); + case ScrollBarContextMenuCommand_ScrollPrev: + if (IsHorizontal()) + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLLEFT); + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLUP); + case ScrollBarContextMenuCommand_ScrollNext: + if (IsHorizontal()) + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLRIGHT); + return l10n_util::GetString(IDS_SCROLLBAR_CXMENU_SCROLLDOWN); + } + NOTREACHED() << "Invalid BitmapScrollBar Context Menu command!"; + return L""; +} + +bool BitmapScrollBar::IsCommandEnabled(int id) const { + switch (id) { + case ScrollBarContextMenuCommand_ScrollPageUp: + case ScrollBarContextMenuCommand_ScrollPageDown: + return !IsHorizontal(); + } + return true; +} + +void BitmapScrollBar::ExecuteCommand(int id) { + switch (id) { + case ScrollBarContextMenuCommand_ScrollHere: + ScrollToThumbPosition(context_menu_mouse_position_, true); + break; + case ScrollBarContextMenuCommand_ScrollStart: + ScrollByAmount(SCROLL_START); + break; + case ScrollBarContextMenuCommand_ScrollEnd: + ScrollByAmount(SCROLL_END); + break; + case ScrollBarContextMenuCommand_ScrollPageUp: + ScrollByAmount(SCROLL_PREV_PAGE); + break; + case ScrollBarContextMenuCommand_ScrollPageDown: + ScrollByAmount(SCROLL_NEXT_PAGE); + break; + case ScrollBarContextMenuCommand_ScrollPrev: + ScrollByAmount(SCROLL_PREV_LINE); + break; + case ScrollBarContextMenuCommand_ScrollNext: + ScrollByAmount(SCROLL_NEXT_LINE); + break; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// BitmapScrollBar, ButtonListener implementation: + +void BitmapScrollBar::ButtonPressed(Button* sender) { + if (sender == prev_button_) { + ScrollByAmount(SCROLL_PREV_LINE); + } else if (sender == next_button_) { + ScrollByAmount(SCROLL_NEXT_LINE); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// BitmapScrollBar, ScrollBar implementation: + +void BitmapScrollBar::Update(int viewport_size, int content_size, + int contents_scroll_offset) { + ScrollBar::Update(viewport_size, content_size, contents_scroll_offset); + + // Make sure contents_size is always > 0 to avoid divide by zero errors in + // calculations throughout this code. + contents_size_ = std::max(1, content_size); + + if (content_size < 0) + content_size = 0; + if (contents_scroll_offset < 0) + contents_scroll_offset = 0; + if (contents_scroll_offset > content_size) + contents_scroll_offset = content_size; + + // Thumb Height and Thumb Pos. + // The height of the thumb is the ratio of the Viewport height to the + // content size multiplied by the height of the thumb track. + double ratio = static_cast<double>(viewport_size) / contents_size_; + int thumb_size = static_cast<int>(ratio * GetTrackSize()); + thumb_->SetSize(thumb_size); + + int thumb_position = CalculateThumbPosition(contents_scroll_offset); + thumb_->SetPosition(thumb_position); +} + +int BitmapScrollBar::GetLayoutSize() const { + gfx::Size prefsize = prev_button_->GetPreferredSize(); + return IsHorizontal() ? prefsize.height() : prefsize.width(); +} + +int BitmapScrollBar::GetPosition() const { + return thumb_->GetPosition(); +} + +/////////////////////////////////////////////////////////////////////////////// +// BitmapScrollBar, private: + +void BitmapScrollBar::ScrollContentsToOffset() { + GetController()->ScrollToPosition(this, contents_scroll_offset_); + thumb_->SetPosition(CalculateThumbPosition(contents_scroll_offset_)); +} + +int BitmapScrollBar::GetTrackSize() const { + gfx::Rect track_bounds = GetTrackBounds(); + return IsHorizontal() ? track_bounds.width() : track_bounds.height(); +} + +int BitmapScrollBar::CalculateThumbPosition(int contents_scroll_offset) const { + return (contents_scroll_offset * GetTrackSize()) / contents_size_; +} + +int BitmapScrollBar::CalculateContentsOffset(int thumb_position, + bool scroll_to_middle) const { + if (scroll_to_middle) + thumb_position = thumb_position - (thumb_->GetSize() / 2); + return (thumb_position * contents_size_) / GetTrackSize(); +} + +void BitmapScrollBar::SetThumbTrackState(CustomButton::ButtonState state) { + thumb_track_state_ = state; + SchedulePaint(); +} + +} // namespace views diff --git a/views/controls/scrollbar/bitmap_scroll_bar.h b/views/controls/scrollbar/bitmap_scroll_bar.h new file mode 100644 index 0000000..45e3535 --- /dev/null +++ b/views/controls/scrollbar/bitmap_scroll_bar.h @@ -0,0 +1,192 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_SCROLLBAR_BITMAP_SCROLL_BAR_H_ +#define VIEWS_CONTROLS_SCROLLBAR_BITMAP_SCROLL_BAR_H_ + +#include "views/controls/button/image_button.h" +#include "views/controls/menu/menu.h" +#include "views/controls/scrollbar/scroll_bar.h" +#include "views/repeat_controller.h" + +namespace views { + +namespace { +class BitmapScrollBarThumb; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// BitmapScrollBar +// +// A ScrollBar subclass that implements a scroll bar rendered using bitmaps +// that the user provides. There are bitmaps for the up and down buttons, as +// well as for the thumb and track. This is intended for creating UIs that +// have customized, non-native appearances, like floating HUDs etc. +// +// Maybe TODO(beng): (Cleanup) If we need to, we may want to factor rendering +// out of this altogether and have the user supply +// Background impls for each component, and just use those +// to render, so that for example we get native theme +// rendering. +// +/////////////////////////////////////////////////////////////////////////////// +class BitmapScrollBar : public ScrollBar, + public ButtonListener, + public ContextMenuController, + public Menu::Delegate { + public: + BitmapScrollBar(bool horizontal, bool show_scroll_buttons); + virtual ~BitmapScrollBar() { } + + // Get the bounds of the "track" area that the thumb is free to slide within. + gfx::Rect GetTrackBounds() const; + + // A list of parts that the user may supply bitmaps for. + enum ScrollBarPart { + // The button used to represent scrolling up/left by 1 line. + PREV_BUTTON = 0, + // The button used to represent scrolling down/right by 1 line. + // IMPORTANT: The code assumes the prev and next + // buttons have equal width and equal height. + NEXT_BUTTON, + // The top/left segment of the thumb on the scrollbar. + THUMB_START_CAP, + // The tiled background image of the thumb. + THUMB_MIDDLE, + // The bottom/right segment of the thumb on the scrollbar. + THUMB_END_CAP, + // The grippy that is rendered in the center of the thumb. + THUMB_GRIPPY, + // The tiled background image of the thumb track. + THUMB_TRACK, + PART_COUNT + }; + + // Sets the bitmap to be rendered for the specified part and state. + void SetImage(ScrollBarPart part, + CustomButton::ButtonState state, + SkBitmap* bitmap); + + // An enumeration of different amounts of incremental scroll, representing + // events sent from different parts of the UI/keyboard. + enum ScrollAmount { + SCROLL_NONE = 0, + SCROLL_START, + SCROLL_END, + SCROLL_PREV_LINE, + SCROLL_NEXT_LINE, + SCROLL_PREV_PAGE, + SCROLL_NEXT_PAGE, + }; + + // Scroll the contents by the specified type (see ScrollAmount above). + void ScrollByAmount(ScrollAmount amount); + + // Scroll the contents to the appropriate position given the supplied + // position of the thumb (thumb track coordinates). If |scroll_to_middle| is + // true, then the conversion assumes |thumb_position| is in the middle of the + // thumb rather than the top. + void ScrollToThumbPosition(int thumb_position, bool scroll_to_middle); + + // Scroll the contents by the specified offset (contents coordinates). + void ScrollByContentsOffset(int contents_offset); + + // View overrides: + virtual gfx::Size GetPreferredSize(); + virtual void Paint(ChromeCanvas* canvas); + virtual void Layout(); + virtual bool OnMousePressed(const MouseEvent& event); + virtual void OnMouseReleased(const MouseEvent& event, bool canceled); + virtual bool OnMouseWheel(const MouseWheelEvent& event); + virtual bool OnKeyPressed(const KeyEvent& event); + + // BaseButton::ButtonListener overrides: + virtual void ButtonPressed(Button* sender); + + // ScrollBar overrides: + virtual void Update(int viewport_size, + int content_size, + int contents_scroll_offset); + virtual int GetLayoutSize() const; + virtual int GetPosition() const; + + // ContextMenuController overrides. + virtual void ShowContextMenu(View* source, + int x, + int y, + bool is_mouse_gesture); + + // Menu::Delegate overrides: + virtual std::wstring GetLabel(int id) const; + virtual bool IsCommandEnabled(int id) const; + virtual void ExecuteCommand(int id); + + private: + // Called when the mouse is pressed down in the track area. + void TrackClicked(); + + // Responsible for scrolling the contents and also updating the UI to the + // current value of the Scroll Offset. + void ScrollContentsToOffset(); + + // Returns the size (width or height) of the track area of the ScrollBar. + int GetTrackSize() const; + + // Calculate the position of the thumb within the track based on the + // specified scroll offset of the contents. + int CalculateThumbPosition(int contents_scroll_offset) const; + + // Calculates the current value of the contents offset (contents coordinates) + // based on the current thumb position (thumb track coordinates). See + // |ScrollToThumbPosition| for an explanation of |scroll_to_middle|. + int CalculateContentsOffset(int thumb_position, + bool scroll_to_middle) const; + + // Called when the state of the thumb track changes (e.g. by the user + // pressing the mouse button down in it). + void SetThumbTrackState(CustomButton::ButtonState state); + + // The thumb needs to be able to access the part images. + friend BitmapScrollBarThumb; + SkBitmap* images_[PART_COUNT][CustomButton::BS_COUNT]; + + // The size of the scrolled contents, in pixels. + int contents_size_; + + // The current amount the contents is offset by in the viewport. + int contents_scroll_offset_; + + // Up/Down/Left/Right buttons and the Thumb. + ImageButton* prev_button_; + ImageButton* next_button_; + BitmapScrollBarThumb* thumb_; + + // The state of the scrollbar track. Typically, the track will highlight when + // the user presses the mouse on them (during page scrolling). + CustomButton::ButtonState thumb_track_state_; + + // The last amount of incremental scroll that this scrollbar performed. This + // is accessed by the callbacks for the auto-repeat up/down buttons to know + // what direction to repeatedly scroll in. + ScrollAmount last_scroll_amount_; + + // An instance of a RepeatController which scrolls the scrollbar continuously + // as the user presses the mouse button down on the up/down buttons or the + // track. + RepeatController repeater_; + + // The position of the mouse within the scroll bar when the context menu + // was invoked. + int context_menu_mouse_position_; + + // True if the scroll buttons at each end of the scroll bar should be shown. + bool show_scroll_buttons_; + + DISALLOW_EVIL_CONSTRUCTORS(BitmapScrollBar); +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_SCROLLBAR_BITMAP_SCROLL_BAR_H_ diff --git a/views/controls/scrollbar/native_scroll_bar.cc b/views/controls/scrollbar/native_scroll_bar.cc new file mode 100644 index 0000000..52ddd91 --- /dev/null +++ b/views/controls/scrollbar/native_scroll_bar.cc @@ -0,0 +1,357 @@ +// Copyright (c) 2006-2008 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/controls/scrollbar/native_scroll_bar.h" + +#include <atlbase.h> +#include <atlapp.h> +#include <atlwin.h> +#include <atlcrack.h> +#include <atlframe.h> +#include <atlmisc.h> +#include <string> + +#include "base/message_loop.h" +#include "views/controls/hwnd_view.h" +#include "views/widget/widget.h" + +namespace views { + +///////////////////////////////////////////////////////////////////////////// +// +// ScrollBarContainer +// +// Since windows scrollbar only send notifications to their parent hwnd, we +// use instance of this class to wrap native scrollbars. +// +///////////////////////////////////////////////////////////////////////////// +class ScrollBarContainer : public CWindowImpl<ScrollBarContainer, + CWindow, + CWinTraits<WS_CHILD>> { + public: + ScrollBarContainer(ScrollBar* parent) : parent_(parent), + scrollbar_(NULL) { + Create(parent->GetWidget()->GetNativeView()); + ::ShowWindow(m_hWnd, SW_SHOW); + } + + virtual ~ScrollBarContainer() { + } + + DECLARE_FRAME_WND_CLASS(L"ChromeViewsScrollBarContainer", NULL); + BEGIN_MSG_MAP(ScrollBarContainer); + MSG_WM_CREATE(OnCreate); + MSG_WM_ERASEBKGND(OnEraseBkgnd); + MSG_WM_PAINT(OnPaint); + MSG_WM_SIZE(OnSize); + MSG_WM_HSCROLL(OnHorizScroll); + MSG_WM_VSCROLL(OnVertScroll); + END_MSG_MAP(); + + HWND GetScrollBarHWND() { + return scrollbar_; + } + + // Invoked when the scrollwheel is used + void ScrollWithOffset(int o) { + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + ::GetScrollInfo(scrollbar_, SB_CTL, &si); + int pos = si.nPos - o; + + if (pos < parent_->GetMinPosition()) + pos = parent_->GetMinPosition(); + else if (pos > parent_->GetMaxPosition()) + pos = parent_->GetMaxPosition(); + + ScrollBarController* sbc = parent_->GetController(); + sbc->ScrollToPosition(parent_, pos); + + si.nPos = pos; + si.fMask = SIF_POS; + ::SetScrollInfo(scrollbar_, SB_CTL, &si, TRUE); + } + + private: + + LRESULT OnCreate(LPCREATESTRUCT create_struct) { + scrollbar_ = CreateWindow(L"SCROLLBAR", + L"", + WS_CHILD | (parent_->IsHorizontal() ? + SBS_HORZ : SBS_VERT), + 0, + 0, + parent_->width(), + parent_->height(), + m_hWnd, + NULL, + NULL, + NULL); + ::ShowWindow(scrollbar_, SW_SHOW); + return 1; + } + + LRESULT OnEraseBkgnd(HDC dc) { + return 1; + } + + void OnPaint(HDC ignore) { + PAINTSTRUCT ps; + HDC dc = ::BeginPaint(*this, &ps); + ::EndPaint(*this, &ps); + } + + void OnSize(int type, const CSize& sz) { + ::SetWindowPos(scrollbar_, + 0, + 0, + 0, + sz.cx, + sz.cy, + SWP_DEFERERASE | + SWP_NOACTIVATE | + SWP_NOCOPYBITS | + SWP_NOOWNERZORDER | + SWP_NOSENDCHANGING | + SWP_NOZORDER); + } + + void OnScroll(int code, HWND source, bool is_horizontal) { + int pos; + + if (code == SB_ENDSCROLL) { + return; + } + + // If we receive an event from the scrollbar, make the view + // component focused so we actually get mousewheel events. + if (source != NULL) { + Widget* widget = parent_->GetWidget(); + if (widget && widget->GetNativeView() != GetFocus()) { + parent_->RequestFocus(); + } + } + + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_POS | SIF_TRACKPOS; + ::GetScrollInfo(scrollbar_, SB_CTL, &si); + pos = si.nPos; + + ScrollBarController* sbc = parent_->GetController(); + + switch (code) { + case SB_BOTTOM: // case SB_RIGHT: + pos = parent_->GetMaxPosition(); + break; + case SB_TOP: // case SB_LEFT: + pos = parent_->GetMinPosition(); + break; + case SB_LINEDOWN: // case SB_LINERIGHT: + pos += sbc->GetScrollIncrement(parent_, false, true); + pos = std::min(parent_->GetMaxPosition(), pos); + break; + case SB_LINEUP: // case SB_LINELEFT: + pos -= sbc->GetScrollIncrement(parent_, false, false); + pos = std::max(parent_->GetMinPosition(), pos); + break; + case SB_PAGEDOWN: // case SB_PAGERIGHT: + pos += sbc->GetScrollIncrement(parent_, true, true); + pos = std::min(parent_->GetMaxPosition(), pos); + break; + case SB_PAGEUP: // case SB_PAGELEFT: + pos -= sbc->GetScrollIncrement(parent_, true, false); + pos = std::max(parent_->GetMinPosition(), pos); + break; + case SB_THUMBPOSITION: + case SB_THUMBTRACK: + pos = si.nTrackPos; + if (pos < parent_->GetMinPosition()) + pos = parent_->GetMinPosition(); + else if (pos > parent_->GetMaxPosition()) + pos = parent_->GetMaxPosition(); + break; + default: + break; + } + + sbc->ScrollToPosition(parent_, pos); + + si.nPos = pos; + si.fMask = SIF_POS; + ::SetScrollInfo(scrollbar_, SB_CTL, &si, TRUE); + + // Note: the system scrollbar modal loop doesn't give a chance + // to our message_loop so we need to call DidProcessMessage() + // manually. + // + // Sadly, we don't know what message has been processed. We may + // want to remove the message from DidProcessMessage() + MSG dummy; + dummy.hwnd = NULL; + dummy.message = 0; + MessageLoopForUI::current()->DidProcessMessage(dummy); + } + + // note: always ignore 2nd param as it is 16 bits + void OnHorizScroll(int n_sb_code, int ignore, HWND source) { + OnScroll(n_sb_code, source, true); + } + + // note: always ignore 2nd param as it is 16 bits + void OnVertScroll(int n_sb_code, int ignore, HWND source) { + OnScroll(n_sb_code, source, false); + } + + + + ScrollBar* parent_; + HWND scrollbar_; +}; + +NativeScrollBar::NativeScrollBar(bool is_horiz) + : sb_view_(NULL), + sb_container_(NULL), + ScrollBar(is_horiz) { +} + +NativeScrollBar::~NativeScrollBar() { + if (sb_container_) { + // We always destroy the scrollbar container explicitly to cover all + // cases including when the container is no longer connected to a + // widget tree. + ::DestroyWindow(*sb_container_); + delete sb_container_; + } +} + +void NativeScrollBar::ViewHierarchyChanged(bool is_add, View *parent, + View *child) { + Widget* widget; + if (is_add && (widget = GetWidget()) && !sb_view_) { + sb_view_ = new HWNDView(); + AddChildView(sb_view_); + sb_container_ = new ScrollBarContainer(this); + sb_view_->Attach(*sb_container_); + Layout(); + } +} + +void NativeScrollBar::Layout() { + if (sb_view_) + sb_view_->SetBounds(GetLocalBounds(true)); +} + +gfx::Size NativeScrollBar::GetPreferredSize() { + if (IsHorizontal()) + return gfx::Size(0, GetLayoutSize()); + return gfx::Size(GetLayoutSize(), 0); +} + +void NativeScrollBar::Update(int viewport_size, + int content_size, + int current_pos) { + ScrollBar::Update(viewport_size, content_size, current_pos); + if (!sb_container_) + return; + + if (content_size < 0) + content_size = 0; + + if (current_pos < 0) + current_pos = 0; + + if (current_pos > content_size) + current_pos = content_size; + + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_DISABLENOSCROLL | SIF_POS | SIF_RANGE | SIF_PAGE; + si.nMin = 0; + si.nMax = content_size; + si.nPos = current_pos; + si.nPage = viewport_size; + ::SetScrollInfo(sb_container_->GetScrollBarHWND(), + SB_CTL, + &si, + TRUE); +} + +int NativeScrollBar::GetLayoutSize() const { + return ::GetSystemMetrics(IsHorizontal() ? SM_CYHSCROLL : SM_CYVSCROLL); +} + +int NativeScrollBar::GetPosition() const { + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + GetScrollInfo(sb_container_->GetScrollBarHWND(), SB_CTL, &si); + return si.nPos; +} + +bool NativeScrollBar::OnMouseWheel(const MouseWheelEvent& e) { + if (!sb_container_) { + return false; + } + + sb_container_->ScrollWithOffset(e.GetOffset()); + return true; +} + +bool NativeScrollBar::OnKeyPressed(const KeyEvent& event) { + if (!sb_container_) { + return false; + } + int code = -1; + switch(event.GetCharacter()) { + case VK_UP: + if (!IsHorizontal()) + code = SB_LINEUP; + break; + case VK_PRIOR: + code = SB_PAGEUP; + break; + case VK_NEXT: + code = SB_PAGEDOWN; + break; + case VK_DOWN: + if (!IsHorizontal()) + code = SB_LINEDOWN; + break; + case VK_HOME: + code = SB_TOP; + break; + case VK_END: + code = SB_BOTTOM; + break; + case VK_LEFT: + if (IsHorizontal()) + code = SB_LINELEFT; + break; + case VK_RIGHT: + if (IsHorizontal()) + code = SB_LINERIGHT; + break; + } + if (code != -1) { + ::SendMessage(*sb_container_, + IsHorizontal() ? WM_HSCROLL : WM_VSCROLL, + MAKELONG(static_cast<WORD>(code), 0), 0L); + return true; + } + return false; +} + +//static +int NativeScrollBar::GetHorizontalScrollBarHeight() { + return ::GetSystemMetrics(SM_CYHSCROLL); +} + +//static +int NativeScrollBar::GetVerticalScrollBarWidth() { + return ::GetSystemMetrics(SM_CXVSCROLL); +} + +} // namespace views diff --git a/views/controls/scrollbar/native_scroll_bar.h b/views/controls/scrollbar/native_scroll_bar.h new file mode 100644 index 0000000..2747bce --- /dev/null +++ b/views/controls/scrollbar/native_scroll_bar.h @@ -0,0 +1,67 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_SCROLLBAR_NATIVE_SCROLLBAR_H_ +#define VIEWS_CONTROLS_SCROLLBAR_NATIVE_SCROLLBAR_H_ + +#include "build/build_config.h" + +#include "views/controls/scrollbar/scroll_bar.h" + +namespace views { + +class HWNDView; +class ScrollBarContainer; + +///////////////////////////////////////////////////////////////////////////// +// +// NativeScrollBar +// +// A View subclass that wraps a Native Windows scrollbar control. +// +// A scrollbar is either horizontal or vertical. +// +///////////////////////////////////////////////////////////////////////////// +class NativeScrollBar : public ScrollBar { + public: + + // Create new scrollbar, either horizontal or vertical + explicit NativeScrollBar(bool is_horiz); + virtual ~NativeScrollBar(); + + // Overridden for layout purpose + virtual void Layout(); + virtual gfx::Size GetPreferredSize(); + + // Overridden for keyboard UI purpose + virtual bool OnKeyPressed(const KeyEvent& event); + virtual bool OnMouseWheel(const MouseWheelEvent& e); + + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + + // Overridden from ScrollBar + virtual void Update(int viewport_size, int content_size, int current_pos); + virtual int GetLayoutSize() const; + virtual int GetPosition() const; + + // Return the system sizes + static int GetHorizontalScrollBarHeight(); + static int GetVerticalScrollBarWidth(); + + private: +#if defined(OS_WIN) + // The sb_view_ takes care of keeping sb_container in sync with the + // view hierarchy + HWNDView* sb_view_; +#endif // defined(OS_WIN) + + // sb_container_ is a custom hwnd that we use to wrap the real + // windows scrollbar. We need to do this to get the scroll events + // without having to do anything special in the high level hwnd. + ScrollBarContainer* sb_container_; +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_SCROLLBAR_NATIVE_SCROLLBAR_H_ diff --git a/views/controls/scrollbar/scroll_bar.cc b/views/controls/scrollbar/scroll_bar.cc new file mode 100644 index 0000000..a475c44 --- /dev/null +++ b/views/controls/scrollbar/scroll_bar.cc @@ -0,0 +1,47 @@ +// Copyright (c) 2006-2008 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/controls/scrollbar/scroll_bar.h" + +namespace views { + +///////////////////////////////////////////////////////////////////////////// +// +// ScrollBar implementation +// +///////////////////////////////////////////////////////////////////////////// + +ScrollBar::ScrollBar(bool is_horiz) : is_horiz_(is_horiz), + controller_(NULL), + max_pos_(0) { +} + +ScrollBar::~ScrollBar() { +} + +bool ScrollBar::IsHorizontal() const { + return is_horiz_; +} + +void ScrollBar::SetController(ScrollBarController* controller) { + controller_ = controller; +} + +ScrollBarController* ScrollBar::GetController() const { + return controller_; +} + +void ScrollBar::Update(int viewport_size, int content_size, int current_pos) { + max_pos_ = std::max(0, content_size - viewport_size); +} + +int ScrollBar::GetMaxPosition() const { + return max_pos_; +} + +int ScrollBar::GetMinPosition() const { + return 0; +} + +} // namespace views diff --git a/views/controls/scrollbar/scroll_bar.h b/views/controls/scrollbar/scroll_bar.h new file mode 100644 index 0000000..36a9d2e --- /dev/null +++ b/views/controls/scrollbar/scroll_bar.h @@ -0,0 +1,102 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_SCROLLBAR_SCROLLBAR_H_ +#define VIEWS_CONTROLS_SCROLLBAR_SCROLLBAR_H_ + +#include "views/view.h" +#include "views/event.h" + +namespace views { + +class ScrollBar; + +///////////////////////////////////////////////////////////////////////////// +// +// ScrollBarController +// +// ScrollBarController defines the method that should be implemented to +// receive notification from a scrollbar +// +///////////////////////////////////////////////////////////////////////////// +class ScrollBarController { + public: + + // Invoked by the scrollbar when the scrolling position changes + // This method typically implements the actual scrolling. + // + // The provided position is expressed in pixels. It is the new X or Y + // position which is in the GetMinPosition() / GetMaxPosition range. + virtual void ScrollToPosition(ScrollBar* source, int position) = 0; + + // Returns the amount to scroll. The amount to scroll may be requested in + // two different amounts. If is_page is true the 'page scroll' amount is + // requested. The page scroll amount typically corresponds to the + // visual size of the view. If is_page is false, the 'line scroll' amount + // is being requested. The line scroll amount typically corresponds to the + // size of one row/column. + // + // The return value should always be positive. A value <= 0 results in + // scrolling by a fixed amount. + virtual int GetScrollIncrement(ScrollBar* source, + bool is_page, + bool is_positive) = 0; +}; + +///////////////////////////////////////////////////////////////////////////// +// +// ScrollBar +// +// A View subclass to wrap to implement a ScrollBar. Our current windows +// version simply wraps a native windows scrollbar. +// +// A scrollbar is either horizontal or vertical +// +///////////////////////////////////////////////////////////////////////////// +class ScrollBar : public View { + public: + virtual ~ScrollBar(); + + // Return whether this scrollbar is horizontal + bool IsHorizontal() const; + + // Set / Get the controller + void SetController(ScrollBarController* controller); + ScrollBarController* GetController() const; + + // Update the scrollbar appearance given a viewport size, content size and + // current position + virtual void Update(int viewport_size, int content_size, int current_pos); + + // Return the max and min positions + int GetMaxPosition() const; + int GetMinPosition() const; + + // Returns the position of the scrollbar. + virtual int GetPosition() const = 0; + + // Get the width or height of this scrollbar, for use in layout calculations. + // For a vertical scrollbar, this is the width of the scrollbar, likewise it + // is the height for a horizontal scrollbar. + virtual int GetLayoutSize() const = 0; + + protected: + // Create new scrollbar, either horizontal or vertical. These are protected + // since you need to be creating either a NativeScrollBar or a + // BitmapScrollBar. + ScrollBar(bool is_horiz); + + private: + const bool is_horiz_; + + // Current controller + ScrollBarController* controller_; + + // properties + int max_pos_; +}; + +} // namespace views + +#endif // #ifndef VIEWS_CONTROLS_SCROLLBAR_SCROLLBAR_H_ diff --git a/views/controls/separator.cc b/views/controls/separator.cc new file mode 100644 index 0000000..f331ce8 --- /dev/null +++ b/views/controls/separator.cc @@ -0,0 +1,37 @@ +// Copyright (c) 2006-2008 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/controls/separator.h" + +#include "views/controls/hwnd_view.h" + +namespace views { + +static const int kSeparatorSize = 2; + +Separator::Separator() { + SetFocusable(false); +} + +Separator::~Separator() { +} + +HWND Separator::CreateNativeControl(HWND parent_container) { + SetFixedHeight(kSeparatorSize, CENTER); + + return ::CreateWindowEx(GetAdditionalExStyle(), L"STATIC", L"", + WS_CHILD | SS_ETCHEDHORZ | SS_SUNKEN, + 0, 0, width(), height(), + parent_container, NULL, NULL, NULL); +} + +LRESULT Separator::OnNotify(int w_param, LPNMHDR l_param) { + return 0; +} + +gfx::Size Separator::GetPreferredSize() { + return gfx::Size(width(), fixed_height_); +} + +} // namespace views diff --git a/views/controls/separator.h b/views/controls/separator.h new file mode 100644 index 0000000..866beda --- /dev/null +++ b/views/controls/separator.h @@ -0,0 +1,34 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_SEPARATOR_H_ +#define VIEWS_CONTROLS_SEPARATOR_H_ + +#include "views/controls/native_control.h" + +namespace views { + +// The Separator class is a view that shows a line used to visually separate +// other views. The current implementation is only horizontal. + +class Separator : public NativeControl { + public: + Separator(); + virtual ~Separator(); + + // NativeControl overrides: + virtual HWND CreateNativeControl(HWND parent_container); + virtual LRESULT OnNotify(int w_param, LPNMHDR l_param); + + // View overrides: + virtual gfx::Size GetPreferredSize(); + + private: + + DISALLOW_EVIL_CONSTRUCTORS(Separator); +}; + +} // namespace views + +#endif // #define VIEWS_CONTROLS_SEPARATOR_H_ diff --git a/views/controls/single_split_view.cc b/views/controls/single_split_view.cc new file mode 100644 index 0000000..4558ffd --- /dev/null +++ b/views/controls/single_split_view.cc @@ -0,0 +1,116 @@ +// Copyright (c) 2006-2008 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/controls/single_split_view.h" + +#include "app/gfx/chrome_canvas.h" +#include "skia/ext/skia_utils_win.h" +#include "views/background.h" + +namespace views { + +// Size of the divider in pixels. +static const int kDividerSize = 4; + +SingleSplitView::SingleSplitView(View* leading, View* trailing) + : divider_x_(-1) { + AddChildView(leading); + AddChildView(trailing); + set_background( + views::Background::CreateSolidBackground( + skia::COLORREFToSkColor(GetSysColor(COLOR_3DFACE)))); +} + +void SingleSplitView::Layout() { + if (GetChildViewCount() != 2) + return; + + View* leading = GetChildViewAt(0); + View* trailing = GetChildViewAt(1); + if (divider_x_ < 0) + divider_x_ = (width() - kDividerSize) / 2; + else + divider_x_ = std::min(divider_x_, width() - kDividerSize); + leading->SetBounds(0, 0, divider_x_, height()); + trailing->SetBounds(divider_x_ + kDividerSize, 0, + width() - divider_x_ - kDividerSize, height()); + + SchedulePaint(); + + // Invoke super's implementation so that the children are layed out. + View::Layout(); +} + +gfx::Size SingleSplitView::GetPreferredSize() { + int width = 0; + int height = 0; + for (int i = 0; i < 2 && i < GetChildViewCount(); ++i) { + View* view = GetChildViewAt(i); + gfx::Size pref = view->GetPreferredSize(); + width += pref.width(); + height = std::max(height, pref.height()); + } + width += kDividerSize; + return gfx::Size(width, height); +} + +HCURSOR SingleSplitView::GetCursorForPoint(Event::EventType event_type, + int x, + int y) { + if (IsPointInDivider(x)) { + static HCURSOR resize_cursor = LoadCursor(NULL, IDC_SIZEWE); + return resize_cursor; + } + return NULL; +} + +bool SingleSplitView::OnMousePressed(const MouseEvent& event) { + if (!IsPointInDivider(event.x())) + return false; + drag_info_.initial_mouse_x = event.x(); + drag_info_.initial_divider_x = divider_x_; + return true; +} + +bool SingleSplitView::OnMouseDragged(const MouseEvent& event) { + if (GetChildViewCount() < 2) + return false; + + int delta_x = event.x() - drag_info_.initial_mouse_x; + if (UILayoutIsRightToLeft()) + delta_x *= -1; + // Honor the minimum size when resizing. + int new_width = std::max(GetChildViewAt(0)->GetMinimumSize().width(), + drag_info_.initial_divider_x + delta_x); + + // And don't let the view get bigger than our width. + new_width = std::min(width() - kDividerSize, new_width); + + if (new_width != divider_x_) { + set_divider_x(new_width); + Layout(); + } + return true; +} + +void SingleSplitView::OnMouseReleased(const MouseEvent& event, bool canceled) { + if (GetChildViewCount() < 2) + return; + + if (canceled && drag_info_.initial_divider_x != divider_x_) { + set_divider_x(drag_info_.initial_divider_x); + Layout(); + } +} + +bool SingleSplitView::IsPointInDivider(int x) { + if (GetChildViewCount() < 2) + return false; + + int divider_relative_x = + x - GetChildViewAt(UILayoutIsRightToLeft() ? 1 : 0)->width(); + return (divider_relative_x >= 0 && divider_relative_x < kDividerSize); +} + +} // namespace views diff --git a/views/controls/single_split_view.h b/views/controls/single_split_view.h new file mode 100644 index 0000000..a531800 --- /dev/null +++ b/views/controls/single_split_view.h @@ -0,0 +1,57 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_SINGLE_SPLIT_VIEW_H_ +#define VIEWS_CONTROLS_SINGLE_SPLIT_VIEW_H_ + +#include "views/view.h" + +namespace views { + +// SingleSplitView lays out two views horizontally. A splitter exists between +// the two views that the user can drag around to resize the views. +class SingleSplitView : public views::View { + public: + SingleSplitView(View* leading, View* trailing); + + virtual void Layout(); + + // SingleSplitView's preferred size is the sum of the preferred widths + // and the max of the heights. + virtual gfx::Size GetPreferredSize(); + + // Overriden to return a resize cursor when over the divider. + virtual HCURSOR GetCursorForPoint(Event::EventType event_type, int x, int y); + + void set_divider_x(int divider_x) { divider_x_ = divider_x; } + int divider_x() { return divider_x_; } + + protected: + virtual bool OnMousePressed(const MouseEvent& event); + virtual bool OnMouseDragged(const MouseEvent& event); + virtual void OnMouseReleased(const MouseEvent& event, bool canceled); + + private: + // Returns true if |x| is over the divider. + bool IsPointInDivider(int x); + + // Used to track drag info. + struct DragInfo { + // The initial coordinate of the mouse when the user started the drag. + int initial_mouse_x; + // The initial position of the divider when the user started the drag. + int initial_divider_x; + }; + + DragInfo drag_info_; + + // Position of the divider. + int divider_x_; + + DISALLOW_COPY_AND_ASSIGN(SingleSplitView); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_SINGLE_SPLIT_VIEW_H_ diff --git a/views/controls/tabbed_pane.cc b/views/controls/tabbed_pane.cc new file mode 100644 index 0000000..401cb79 --- /dev/null +++ b/views/controls/tabbed_pane.cc @@ -0,0 +1,264 @@ +// Copyright (c) 2006-2008 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/controls/tabbed_pane.h" + +#include <vssym32.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/l10n_util_win.h" +#include "app/resource_bundle.h" +#include "base/gfx/native_theme.h" +#include "base/logging.h" +#include "base/stl_util-inl.h" +#include "skia/ext/skia_utils_win.h" +#include "skia/include/SkColor.h" +#include "views/background.h" +#include "views/fill_layout.h" +#include "views/widget/root_view.h" +#include "views/widget/widget_win.h" + +namespace views { + +// A background object that paints the tab panel background which may be +// rendered by the system visual styles system. +class TabBackground : public Background { + public: + explicit TabBackground() { + // TMT_FILLCOLORHINT returns a color value that supposedly + // approximates the texture drawn by PaintTabPanelBackground. + SkColor tab_page_color = + gfx::NativeTheme::instance()->GetThemeColorWithDefault( + gfx::NativeTheme::TAB, TABP_BODY, 0, TMT_FILLCOLORHINT, + COLOR_3DFACE); + SetNativeControlColor(tab_page_color); + } + virtual ~TabBackground() {} + + virtual void Paint(ChromeCanvas* canvas, View* view) const { + HDC dc = canvas->beginPlatformPaint(); + RECT r = {0, 0, view->width(), view->height()}; + gfx::NativeTheme::instance()->PaintTabPanelBackground(dc, &r); + canvas->endPlatformPaint(); + } + + private: + DISALLOW_EVIL_CONSTRUCTORS(TabBackground); +}; + +TabbedPane::TabbedPane() : content_window_(NULL), listener_(NULL) { +} + +TabbedPane::~TabbedPane() { + // We own the tab views, let's delete them. + STLDeleteContainerPointers(tab_views_.begin(), tab_views_.end()); +} + +void TabbedPane::SetListener(Listener* listener) { + listener_ = listener; +} + +void TabbedPane::AddTab(const std::wstring& title, View* contents) { + AddTabAtIndex(static_cast<int>(tab_views_.size()), title, contents, true); +} + +void TabbedPane::AddTabAtIndex(int index, + const std::wstring& title, + View* contents, + bool select_if_first_tab) { + DCHECK(index <= static_cast<int>(tab_views_.size())); + contents->SetParentOwned(false); + tab_views_.insert(tab_views_.begin() + index, contents); + + TCITEM tcitem; + tcitem.mask = TCIF_TEXT; + + // If the locale is RTL, we set the TCIF_RTLREADING so that BiDi text is + // rendered properly on the tabs. + if (UILayoutIsRightToLeft()) { + tcitem.mask |= TCIF_RTLREADING; + } + + tcitem.pszText = const_cast<wchar_t*>(title.c_str()); + int result = TabCtrl_InsertItem(tab_control_, index, &tcitem); + DCHECK(result != -1); + + if (!contents->background()) { + contents->set_background(new TabBackground); + } + + if (tab_views_.size() == 1 && select_if_first_tab) { + // If this is the only tab displayed, make sure the contents is set. + content_window_->GetRootView()->AddChildView(contents); + } + + // The newly added tab may have made the contents window smaller. + ResizeContents(tab_control_); +} + +View* TabbedPane::RemoveTabAtIndex(int index) { + int tab_count = static_cast<int>(tab_views_.size()); + DCHECK(index >= 0 && index < tab_count); + + if (index < (tab_count - 1)) { + // Select the next tab. + SelectTabAt(index + 1); + } else { + // We are the last tab, select the previous one. + if (index > 0) { + SelectTabAt(index - 1); + } else { + // That was the last tab. Remove the contents. + content_window_->GetRootView()->RemoveAllChildViews(false); + } + } + TabCtrl_DeleteItem(tab_control_, index); + + // The removed tab may have made the contents window bigger. + ResizeContents(tab_control_); + + std::vector<View*>::iterator iter = tab_views_.begin() + index; + View* removed_tab = *iter; + tab_views_.erase(iter); + + return removed_tab; +} + +void TabbedPane::SelectTabAt(int index) { + DCHECK((index >= 0) && (index < static_cast<int>(tab_views_.size()))); + TabCtrl_SetCurSel(tab_control_, index); + DoSelectTabAt(index); +} + +void TabbedPane::SelectTabForContents(const View* contents) { + SelectTabAt(GetIndexForContents(contents)); +} + +int TabbedPane::GetTabCount() { + return TabCtrl_GetItemCount(tab_control_); +} + +HWND TabbedPane::CreateNativeControl(HWND parent_container) { + // Create the tab control. + // + // Note that we don't follow the common convention for NativeControl + // subclasses and we don't pass the value returned from + // NativeControl::GetAdditionalExStyle() as the dwExStyle parameter. Here is + // why: on RTL locales, if we pass NativeControl::GetAdditionalExStyle() when + // we basically tell Windows to create our HWND with the WS_EX_LAYOUTRTL. If + // we do that, then the HWND we create for |content_window_| below will + // inherit the WS_EX_LAYOUTRTL property and this will result in the contents + // being flipped, which is not what we want (because we handle mirroring in + // views without the use of Windows' support for mirroring). Therefore, + // we initially create our HWND without the aforementioned property and we + // explicitly set this property our child is created. This way, on RTL + // locales, our tabs will be nicely rendered from right to left (by virtue of + // Windows doing the right thing with the TabbedPane HWND) and each tab + // contents will use an RTL layout correctly (by virtue of the mirroring + // infrastructure in views doing the right thing with each View we put + // in the tab). + tab_control_ = ::CreateWindowEx(0, + WC_TABCONTROL, + L"", + WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE, + 0, 0, width(), height(), + parent_container, NULL, NULL, NULL); + + HFONT font = ResourceBundle::GetSharedInstance(). + GetFont(ResourceBundle::BaseFont).hfont(); + SendMessage(tab_control_, WM_SETFONT, reinterpret_cast<WPARAM>(font), FALSE); + + // Create the view container which is a child of the TabControl. + content_window_ = new WidgetWin(); + content_window_->Init(tab_control_, gfx::Rect(), false); + + // Explicitly setting the WS_EX_LAYOUTRTL property for the HWND (see above + // for a thorough explanation regarding why we waited until |content_window_| + // if created before we set this property for the tabbed pane's HWND). + if (UILayoutIsRightToLeft()) { + l10n_util::HWNDSetRTLLayout(tab_control_); + } + + RootView* root_view = content_window_->GetRootView(); + root_view->SetLayoutManager(new FillLayout()); + DWORD sys_color = ::GetSysColor(COLOR_3DHILIGHT); + SkColor color = SkColorSetRGB(GetRValue(sys_color), GetGValue(sys_color), + GetBValue(sys_color)); + root_view->set_background(Background::CreateSolidBackground(color)); + + content_window_->SetFocusTraversableParentView(this); + ResizeContents(tab_control_); + return tab_control_; +} + +LRESULT TabbedPane::OnNotify(int w_param, LPNMHDR l_param) { + if (static_cast<LPNMHDR>(l_param)->code == TCN_SELCHANGE) { + int selected_tab = TabCtrl_GetCurSel(tab_control_); + DCHECK(selected_tab != -1); + DoSelectTabAt(selected_tab); + return TRUE; + } + return FALSE; +} + +void TabbedPane::DoSelectTabAt(int index) { + RootView* content_root = content_window_->GetRootView(); + + // Clear the focus if the focused view was on the tab. + FocusManager* focus_manager = GetFocusManager(); + DCHECK(focus_manager); + View* focused_view = focus_manager->GetFocusedView(); + if (focused_view && content_root->IsParentOf(focused_view)) + focus_manager->ClearFocus(); + + content_root->RemoveAllChildViews(false); + content_root->AddChildView(tab_views_[index]); + content_root->Layout(); + if (listener_) + listener_->TabSelectedAt(index); +} + +int TabbedPane::GetIndexForContents(const View* contents) const { + std::vector<View*>::const_iterator i = + std::find(tab_views_.begin(), tab_views_.end(), contents); + DCHECK(i != tab_views_.end()); + return static_cast<int>(i - tab_views_.begin()); +} + +void TabbedPane::Layout() { + NativeControl::Layout(); + ResizeContents(GetNativeControlHWND()); +} + +RootView* TabbedPane::GetContentsRootView() { + return content_window_->GetRootView(); +} + +FocusTraversable* TabbedPane::GetFocusTraversable() { + return content_window_; +} + +void TabbedPane::ViewHierarchyChanged(bool is_add, View *parent, View *child) { + NativeControl::ViewHierarchyChanged(is_add, parent, child); + + if (is_add && (child == this) && content_window_) { + // We have been added to a view hierarchy, update the FocusTraversable + // parent. + content_window_->SetFocusTraversableParent(GetRootView()); + } +} + +void TabbedPane::ResizeContents(HWND tab_control) { + DCHECK(tab_control); + CRect content_bounds; + if (!GetClientRect(tab_control, &content_bounds)) + return; + TabCtrl_AdjustRect(tab_control, FALSE, &content_bounds); + content_window_->MoveWindow(content_bounds.left, content_bounds.top, + content_bounds.Width(), content_bounds.Height(), + TRUE); +} + +} // namespace views diff --git a/views/controls/tabbed_pane.h b/views/controls/tabbed_pane.h new file mode 100644 index 0000000..528a2d5 --- /dev/null +++ b/views/controls/tabbed_pane.h @@ -0,0 +1,94 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_TABBED_PANE_H_ +#define VIEWS_CONTROLS_TABBED_PANE_H_ + +#include "views/controls/native_control.h" + +namespace views { + +// The TabbedPane class is a view that shows tabs. When the user clicks on a +// tab, the associated view is displayed. +// TODO (jcampan): implement GetPreferredSize(). +class WidgetWin; + +class TabbedPane : public NativeControl { + public: + TabbedPane(); + virtual ~TabbedPane(); + + // An interface an object can implement to be notified about events within + // the TabbedPane. + class Listener { + public: + // Called when the tab at the specified |index| is selected by the user. + virtual void TabSelectedAt(int index) = 0; + }; + void SetListener(Listener* listener); + + // Adds a new tab at the end of this TabbedPane with the specified |title|. + // |contents| is the view displayed when the tab is selected and is owned by + // the TabbedPane. + void AddTab(const std::wstring& title, View* contents); + + // Adds a new tab at the specified |index| with the specified |title|. + // |contents| is the view displayed when the tab is selected and is owned by + // the TabbedPane. If |select_if_first_tab| is true and the tabbed pane is + // currently empty, the new tab is selected. If you pass in false for + // |select_if_first_tab| you need to explicitly invoke SelectTabAt, otherwise + // the tabbed pane will not have a valid selection. + void AddTabAtIndex(int index, + const std::wstring& title, + View* contents, + bool select_if_first_tab); + + // Removes the tab at the specified |index| and returns the associated content + // view. The caller becomes the owner of the returned view. + View* RemoveTabAtIndex(int index); + + // Selects the tab at the specified |index|, which must be valid. + void SelectTabAt(int index); + + // Selects the tab containing the specified |contents|, which must be valid. + void SelectTabForContents(const View* contents); + + // Returns the number of tabs. + int GetTabCount(); + + virtual HWND CreateNativeControl(HWND parent_container); + virtual LRESULT OnNotify(int w_param, LPNMHDR l_param); + + virtual void Layout(); + + virtual RootView* GetContentsRootView(); + virtual FocusTraversable* GetFocusTraversable(); + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + + private: + // Changes the contents view to the view associated with the tab at |index|. + void DoSelectTabAt(int index); + + // Returns the index of the tab containing the specified |contents|. + int GetIndexForContents(const View* contents) const; + + void ResizeContents(HWND tab_control); + + HWND tab_control_; + + // The views associated with the different tabs. + std::vector<View*> tab_views_; + + // The window displayed in the tab. + WidgetWin* content_window_; + + // The listener we notify about tab selection changes. + Listener* listener_; + + DISALLOW_EVIL_CONSTRUCTORS(TabbedPane); +}; + +} // namespace views + +#endif // #define VIEWS_CONTROLS_TABBED_PANE_H_ diff --git a/views/controls/table/group_table_view.cc b/views/controls/table/group_table_view.cc new file mode 100644 index 0000000..5e6a155 --- /dev/null +++ b/views/controls/table/group_table_view.cc @@ -0,0 +1,193 @@ +// Copyright (c) 2006-2008 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/controls/table/group_table_view.h" + +#include "app/gfx/chrome_canvas.h" +#include "base/message_loop.h" +#include "base/task.h" + +namespace views { + +static const COLORREF kSeparatorLineColor = RGB(208, 208, 208); +static const int kSeparatorLineThickness = 1; + +const char GroupTableView::kViewClassName[] = "views/GroupTableView"; + +GroupTableView::GroupTableView(GroupTableModel* model, + const std::vector<TableColumn>& columns, + TableTypes table_type, + bool single_selection, + bool resizable_columns, + bool autosize_columns) + : TableView(model, columns, table_type, false, resizable_columns, + autosize_columns), + model_(model), + sync_selection_factory_(this) { +} + +GroupTableView::~GroupTableView() { +} + +void GroupTableView::SyncSelection() { + int index = 0; + int row_count = model_->RowCount(); + GroupRange group_range; + while (index < row_count) { + model_->GetGroupRangeForItem(index, &group_range); + if (group_range.length == 1) { + // No synching required for single items. + index++; + } else { + // We need to select the group if at least one item is selected. + bool should_select = false; + for (int i = group_range.start; + i < group_range.start + group_range.length; ++i) { + if (IsItemSelected(i)) { + should_select = true; + break; + } + } + if (should_select) { + for (int i = group_range.start; + i < group_range.start + group_range.length; ++i) { + SetSelectedState(i, true); + } + } + index += group_range.length; + } + } +} + +void GroupTableView::OnKeyDown(unsigned short virtual_keycode) { + // In a list view, multiple items can be selected but only one item has the + // focus. This creates a problem when the arrow keys are used for navigating + // between items in the list view. An example will make this more clear: + // + // Suppose we have 5 items in the list view, and three of these items are + // part of one group: + // + // Index0: ItemA (No Group) + // Index1: ItemB (GroupX) + // Index2: ItemC (GroupX) + // Index3: ItemD (GroupX) + // Index4: ItemE (No Group) + // + // When GroupX is selected (say, by clicking on ItemD with the mouse), + // GroupTableView::SyncSelection() will make sure ItemB, ItemC and ItemD are + // selected. Also, the item with the focus will be ItemD (simply because + // this is the item the user happened to click on). If then the UP arrow is + // pressed once, the focus will be switched to ItemC and not to ItemA and the + // end result is that we are stuck in GroupX even though the intention was to + // switch to ItemA. + // + // For that exact reason, we need to set the focus appropriately when we + // detect that one of the arrow keys is pressed. Thus, when it comes time + // for the list view control to actually switch the focus, the right item + // will be selected. + if ((virtual_keycode != VK_UP) && (virtual_keycode != VK_DOWN)) { + TableView::OnKeyDown(virtual_keycode); + return; + } + + // We start by finding the index of the item with the focus. If no item + // currently has the focus, then this routine doesn't do anything. + int focused_index; + int row_count = model_->RowCount(); + for (focused_index = 0; focused_index < row_count; focused_index++) { + if (ItemHasTheFocus(focused_index)) { + break; + } + } + + if (focused_index == row_count) { + return; + } + DCHECK_LT(focused_index, row_count); + + // Nothing to do if the item which has the focus is not part of a group. + GroupRange group_range; + model_->GetGroupRangeForItem(focused_index, &group_range); + if (group_range.length == 1) { + return; + } + + // If the user pressed the UP key, then the focus should be set to the + // topmost element in the group. If the user pressed the DOWN key, the focus + // should be set to the bottommost element. + if (virtual_keycode == VK_UP) { + SetFocusOnItem(group_range.start); + } else { + DCHECK_EQ(virtual_keycode, VK_DOWN); + SetFocusOnItem(group_range.start + group_range.length - 1); + } +} + +void GroupTableView::PrepareForSort() { + GroupRange range; + int row_count = RowCount(); + model_index_to_range_start_map_.clear(); + for (int model_row = 0; model_row < row_count;) { + model_->GetGroupRangeForItem(model_row, &range); + for (int range_counter = 0; range_counter < range.length; range_counter++) + model_index_to_range_start_map_[range_counter + model_row] = model_row; + model_row += range.length; + } +} + +int GroupTableView::CompareRows(int model_row1, int model_row2) { + int range1 = model_index_to_range_start_map_[model_row1]; + int range2 = model_index_to_range_start_map_[model_row2]; + if (range1 == range2) { + // The two rows are in the same group, sort so that items in the same group + // always appear in the same order. + return model_row1 - model_row2; + } + // Sort by the first entry of each of the groups. + return TableView::CompareRows(range1, range2); +} + +void GroupTableView::OnSelectedStateChanged() { + // The goal is to make sure all items for a same group are in a consistent + // state in term of selection. When a user clicks an item, several selection + // messages are sent, possibly including unselecting all currently selected + // items. For that reason, we post a task to be performed later, after all + // selection messages have been processed. In the meantime we just ignore all + // selection notifications. + if (sync_selection_factory_.empty()) { + MessageLoop::current()->PostTask(FROM_HERE, + sync_selection_factory_.NewRunnableMethod( + &GroupTableView::SyncSelection)); + } + TableView::OnSelectedStateChanged(); +} + +// Draws the line separator betweens the groups. +void GroupTableView::PostPaint(int model_row, int column, bool selected, + const CRect& bounds, HDC hdc) { + GroupRange group_range; + model_->GetGroupRangeForItem(model_row, &group_range); + + // We always paint a vertical line at the end of the last cell. + HPEN hPen = CreatePen(PS_SOLID, kSeparatorLineThickness, kSeparatorLineColor); + HPEN hPenOld = (HPEN) SelectObject(hdc, hPen); + int x = static_cast<int>(bounds.right - kSeparatorLineThickness); + MoveToEx(hdc, x, bounds.top, NULL); + LineTo(hdc, x, bounds.bottom); + + // We paint a separator line after the last item of a group. + if (model_row == (group_range.start + group_range.length - 1)) { + int y = static_cast<int>(bounds.bottom - kSeparatorLineThickness); + MoveToEx(hdc, 0, y, NULL); + LineTo(hdc, bounds.Width(), y); + } + SelectObject(hdc, hPenOld); + DeleteObject(hPen); +} + +std::string GroupTableView::GetClassName() const { + return kViewClassName; +} + +} // namespace views diff --git a/views/controls/table/group_table_view.h b/views/controls/table/group_table_view.h new file mode 100644 index 0000000..d128759 --- /dev/null +++ b/views/controls/table/group_table_view.h @@ -0,0 +1,82 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_TABLE_GROUP_TABLE_VIEW_H_ +#define VIEWS_CONTROLS_TABLE_GROUP_TABLE_VIEW_H_ + +#include "base/task.h" +#include "views/controls/table/table_view.h" + +// The GroupTableView adds grouping to the TableView class. +// It allows to have groups of rows that act as a single row from the selection +// perspective. Groups are visually separated by a horizontal line. + +namespace views { + +struct GroupRange { + int start; + int length; +}; + +// The model driving the GroupTableView. +class GroupTableModel : public TableModel { + public: + // Populates the passed range with the first row/last row (included) + // that this item belongs to. + virtual void GetGroupRangeForItem(int item, GroupRange* range) = 0; +}; + +class GroupTableView : public TableView { + public: + // The view class name. + static const char kViewClassName[]; + + GroupTableView(GroupTableModel* model, + const std::vector<TableColumn>& columns, + TableTypes table_type, bool single_selection, + bool resizable_columns, bool autosize_columns); + virtual ~GroupTableView(); + + virtual std::string GetClassName() const; + + protected: + // Notification from the ListView that the selected state of an item has + // changed. + void OnSelectedStateChanged(); + + // Extra-painting required to draw the separator line between groups. + virtual bool ImplementPostPaint() { return true; } + virtual void PostPaint(int model_row, int column, bool selected, + const CRect& bounds, HDC device_context); + + // In order to make keyboard navigation possible (using the Up and Down + // keys), we must take action when an arrow key is pressed. The reason we + // need to process this message has to do with the manner in which the focus + // needs to be set on a group item when a group is selected. + virtual void OnKeyDown(unsigned short virtual_keycode); + + // Overriden to make sure rows in the same group stay grouped together. + virtual int CompareRows(int model_row1, int model_row2); + + // Updates model_index_to_range_start_map_ from the model. + virtual void PrepareForSort(); + + private: + // Make the selection of group consistent. + void SyncSelection(); + + GroupTableModel* model_; + + // A factory to make the selection consistent among groups. + ScopedRunnableMethodFactory<GroupTableView> sync_selection_factory_; + + // Maps from model row to start of group. + std::map<int,int> model_index_to_range_start_map_; + + DISALLOW_COPY_AND_ASSIGN(GroupTableView); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TABLE_GROUP_TABLE_VIEW_H_ diff --git a/views/controls/table/table_view.cc b/views/controls/table/table_view.cc new file mode 100644 index 0000000..f8c7303 --- /dev/null +++ b/views/controls/table/table_view.cc @@ -0,0 +1,1570 @@ +// Copyright (c) 2006-2008 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/controls/table/table_view.h" + +#include <algorithm> +#include <windowsx.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/favicon_size.h" +#include "app/gfx/icon_util.h" +#include "app/l10n_util_win.h" +#include "app/resource_bundle.h" +#include "base/string_util.h" +#include "base/win_util.h" +#include "skia/ext/skia_utils_win.h" +#include "skia/include/SkBitmap.h" +#include "skia/include/SkColorFilter.h" +#include "views/controls/hwnd_view.h" + +namespace views { + +// Added to column width to prevent truncation. +const int kListViewTextPadding = 15; +// Additional column width necessary if column has icons. +const int kListViewIconWidthAndPadding = 18; + +// TableModel ----------------------------------------------------------------- + +// static +const int TableView::kImageSize = 18; + +// Used for sorting. +static Collator* collator = NULL; + +SkBitmap TableModel::GetIcon(int row) { + return SkBitmap(); +} + +int TableModel::CompareValues(int row1, int row2, int column_id) { + DCHECK(row1 >= 0 && row1 < RowCount() && + row2 >= 0 && row2 < RowCount()); + std::wstring value1 = GetText(row1, column_id); + std::wstring value2 = GetText(row2, column_id); + Collator* collator = GetCollator(); + + if (collator) { + UErrorCode compare_status = U_ZERO_ERROR; + UCollationResult compare_result = collator->compare( + static_cast<const UChar*>(value1.c_str()), + static_cast<int>(value1.length()), + static_cast<const UChar*>(value2.c_str()), + static_cast<int>(value2.length()), + compare_status); + DCHECK(U_SUCCESS(compare_status)); + return compare_result; + } + NOTREACHED(); + return 0; +} + +Collator* TableModel::GetCollator() { + if (!collator) { + UErrorCode create_status = U_ZERO_ERROR; + collator = Collator::createInstance(create_status); + if (!U_SUCCESS(create_status)) { + collator = NULL; + NOTREACHED(); + } + } + return collator; +} + +// TableView ------------------------------------------------------------------ + +TableView::TableView(TableModel* model, + const std::vector<TableColumn>& columns, + TableTypes table_type, + bool single_selection, + bool resizable_columns, + bool autosize_columns) + : model_(model), + table_view_observer_(NULL), + visible_columns_(), + all_columns_(), + column_count_(static_cast<int>(columns.size())), + table_type_(table_type), + single_selection_(single_selection), + ignore_listview_change_(false), + custom_colors_enabled_(false), + sized_columns_(false), + autosize_columns_(autosize_columns), + resizable_columns_(resizable_columns), + list_view_(NULL), + header_original_handler_(NULL), + original_handler_(NULL), + table_view_wrapper_(this), + custom_cell_font_(NULL), + content_offset_(0) { + for (std::vector<TableColumn>::const_iterator i = columns.begin(); + i != columns.end(); ++i) { + AddColumn(*i); + visible_columns_.push_back(i->id); + } +} + +TableView::~TableView() { + if (list_view_) { + if (model_) + model_->SetObserver(NULL); + } + if (custom_cell_font_) + DeleteObject(custom_cell_font_); +} + +void TableView::SetModel(TableModel* model) { + if (model == model_) + return; + + if (list_view_ && model_) + model_->SetObserver(NULL); + model_ = model; + if (list_view_ && model_) + model_->SetObserver(this); + if (list_view_) + OnModelChanged(); +} + +void TableView::SetSortDescriptors(const SortDescriptors& sort_descriptors) { + if (!sort_descriptors_.empty()) { + ResetColumnSortImage(sort_descriptors_[0].column_id, + NO_SORT); + } + sort_descriptors_ = sort_descriptors; + if (!sort_descriptors_.empty()) { + ResetColumnSortImage( + sort_descriptors_[0].column_id, + sort_descriptors_[0].ascending ? ASCENDING_SORT : DESCENDING_SORT); + } + if (!list_view_) + return; + + // For some reason we have to turn off/on redraw, otherwise the display + // isn't updated when done. + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + + UpdateItemsLParams(0, 0); + + SortItemsAndUpdateMapping(); + + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); +} + +void TableView::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + if (!list_view_) + return; + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + Layout(); + if ((!sized_columns_ || autosize_columns_) && width() > 0) { + sized_columns_ = true; + ResetColumnSizes(); + } + UpdateContentOffset(); + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); +} + +int TableView::RowCount() { + if (!list_view_) + return 0; + return ListView_GetItemCount(list_view_); +} + +int TableView::SelectedRowCount() { + if (!list_view_) + return 0; + return ListView_GetSelectedCount(list_view_); +} + +void TableView::Select(int model_row) { + if (!list_view_) + return; + + DCHECK(model_row >= 0 && model_row < RowCount()); + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + ignore_listview_change_ = true; + + // Unselect everything. + ListView_SetItemState(list_view_, -1, 0, LVIS_SELECTED); + + // Select the specified item. + int view_row = model_to_view(model_row); + ListView_SetItemState(list_view_, view_row, LVIS_SELECTED | LVIS_FOCUSED, + LVIS_SELECTED | LVIS_FOCUSED); + + // Make it visible. + ListView_EnsureVisible(list_view_, view_row, FALSE); + ignore_listview_change_ = false; + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); + if (table_view_observer_) + table_view_observer_->OnSelectionChanged(); +} + +void TableView::SetSelectedState(int model_row, bool state) { + if (!list_view_) + return; + + DCHECK(model_row >= 0 && model_row < RowCount()); + + ignore_listview_change_ = true; + + // Select the specified item. + ListView_SetItemState(list_view_, model_to_view(model_row), + state ? LVIS_SELECTED : 0, LVIS_SELECTED); + + ignore_listview_change_ = false; +} + +void TableView::SetFocusOnItem(int model_row) { + if (!list_view_) + return; + + DCHECK(model_row >= 0 && model_row < RowCount()); + + ignore_listview_change_ = true; + + // Set the focus to the given item. + ListView_SetItemState(list_view_, model_to_view(model_row), LVIS_FOCUSED, + LVIS_FOCUSED); + + ignore_listview_change_ = false; +} + +int TableView::FirstSelectedRow() { + if (!list_view_) + return -1; + + int view_row = ListView_GetNextItem(list_view_, -1, LVNI_ALL | LVIS_SELECTED); + return view_row == -1 ? -1 : view_to_model(view_row); +} + +bool TableView::IsItemSelected(int model_row) { + if (!list_view_) + return false; + + DCHECK(model_row >= 0 && model_row < RowCount()); + return (ListView_GetItemState(list_view_, model_to_view(model_row), + LVIS_SELECTED) == LVIS_SELECTED); +} + +bool TableView::ItemHasTheFocus(int model_row) { + if (!list_view_) + return false; + + DCHECK(model_row >= 0 && model_row < RowCount()); + return (ListView_GetItemState(list_view_, model_to_view(model_row), + LVIS_FOCUSED) == LVIS_FOCUSED); +} + +TableView::iterator TableView::SelectionBegin() { + return TableView::iterator(this, LastSelectedViewIndex()); +} + +TableView::iterator TableView::SelectionEnd() { + return TableView::iterator(this, -1); +} + +void TableView::OnItemsChanged(int start, int length) { + if (!list_view_) + return; + + if (length == -1) { + DCHECK(start >= 0); + length = model_->RowCount() - start; + } + int row_count = RowCount(); + DCHECK(start >= 0 && length > 0 && start + length <= row_count); + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + if (table_type_ == ICON_AND_TEXT) { + // The redraw event does not include the icon in the clip rect, preventing + // our icon from being repainted. So far the only way I could find around + // this is to change the image for the item. Even if the image does not + // exist, it causes the clip rect to include the icon's bounds so we can + // paint it in the post paint event. + LVITEM lv_item; + memset(&lv_item, 0, sizeof(LVITEM)); + lv_item.mask = LVIF_IMAGE; + for (int i = start; i < start + length; ++i) { + // Retrieve the current icon index. + lv_item.iItem = model_to_view(i); + BOOL r = ListView_GetItem(list_view_, &lv_item); + DCHECK(r); + // Set the current icon index to the other image. + lv_item.iImage = (lv_item.iImage + 1) % 2; + DCHECK((lv_item.iImage == 0) || (lv_item.iImage == 1)); + r = ListView_SetItem(list_view_, &lv_item); + DCHECK(r); + } + } + UpdateListViewCache(start, length, false); + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); +} + +void TableView::OnModelChanged() { + if (!list_view_) + return; + + int current_row_count = ListView_GetItemCount(list_view_); + if (current_row_count > 0) + OnItemsRemoved(0, current_row_count); + if (model_ && model_->RowCount()) + OnItemsAdded(0, model_->RowCount()); +} + +void TableView::OnItemsAdded(int start, int length) { + if (!list_view_) + return; + + DCHECK(start >= 0 && length > 0 && start <= RowCount()); + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + UpdateListViewCache(start, length, true); + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); +} + +void TableView::OnItemsRemoved(int start, int length) { + if (!list_view_) + return; + + if (start < 0 || length < 0 || start + length > RowCount()) { + NOTREACHED(); + return; + } + + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + + bool had_selection = (SelectedRowCount() > 0); + int old_row_count = RowCount(); + if (start == 0 && length == RowCount()) { + // Everything was removed. + ListView_DeleteAllItems(list_view_); + view_to_model_.reset(NULL); + model_to_view_.reset(NULL); + } else { + // Only a portion of the data was removed. + if (is_sorted()) { + int new_row_count = model_->RowCount(); + std::vector<int> view_items_to_remove; + view_items_to_remove.reserve(length); + // Iterate through the elements, updating the view_to_model_ mapping + // as well as collecting the rows that need to be deleted. + for (int i = 0, removed_count = 0; i < old_row_count; ++i) { + int model_index = view_to_model(i); + if (model_index >= start) { + if (model_index < start + length) { + // This item was removed. + view_items_to_remove.push_back(i); + model_index = -1; + } else { + model_index -= length; + } + } + if (model_index >= 0) { + view_to_model_[i - static_cast<int>(view_items_to_remove.size())] = + model_index; + } + } + + // Update the model_to_view mapping from the updated view_to_model + // mapping. + for (int i = 0; i < new_row_count; ++i) + model_to_view_[view_to_model_[i]] = i; + + // And finally delete the items. We do this backwards as the items were + // added ordered smallest to largest. + for (int i = length - 1; i >= 0; --i) + ListView_DeleteItem(list_view_, view_items_to_remove[i]); + } else { + for (int i = 0; i < length; ++i) + ListView_DeleteItem(list_view_, start); + } + } + + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); + + // If the row count goes to zero and we had a selection LVN_ITEMCHANGED isn't + // invoked, so we handle it here. + // + // When the model is set to NULL all the rows are removed. We don't notify + // the delegate in this case as setting the model to NULL is usually done as + // the last step before being deleted and callers shouldn't have to deal with + // getting a selection change when the model is being reset. + if (model_ && table_view_observer_ && had_selection && RowCount() == 0) + table_view_observer_->OnSelectionChanged(); +} + +void TableView::AddColumn(const TableColumn& col) { + DCHECK(all_columns_.count(col.id) == 0); + all_columns_[col.id] = col; +} + +void TableView::SetColumns(const std::vector<TableColumn>& columns) { + // Remove the currently visible columns. + while (!visible_columns_.empty()) + SetColumnVisibility(visible_columns_.front(), false); + + all_columns_.clear(); + for (std::vector<TableColumn>::const_iterator i = columns.begin(); + i != columns.end(); ++i) { + AddColumn(*i); + } + + // Remove any sort descriptors that are no longer valid. + SortDescriptors sort = sort_descriptors(); + for (SortDescriptors::iterator i = sort.begin(); i != sort.end();) { + if (all_columns_.count(i->column_id) == 0) + i = sort.erase(i); + else + ++i; + } + sort_descriptors_ = sort; +} + +void TableView::OnColumnsChanged() { + column_count_ = static_cast<int>(visible_columns_.size()); + ResetColumnSizes(); +} + +void TableView::SetColumnVisibility(int id, bool is_visible) { + bool changed = false; + for (std::vector<int>::iterator i = visible_columns_.begin(); + i != visible_columns_.end(); ++i) { + if (*i == id) { + if (is_visible) { + // It's already visible, bail out early. + return; + } else { + int index = static_cast<int>(i - visible_columns_.begin()); + // This could be called before the native list view has been created + // (in CreateNativeControl, called when the view is added to a + // Widget). In that case since the column is not in + // visible_columns_ it will not be added later on when it is created. + if (list_view_) + SendMessage(list_view_, LVM_DELETECOLUMN, index, 0); + visible_columns_.erase(i); + changed = true; + break; + } + } + } + if (is_visible) { + visible_columns_.push_back(id); + TableColumn& column = all_columns_[id]; + InsertColumn(column, column_count_); + if (column.min_visible_width == 0) { + // ListView_GetStringWidth must be padded or else truncation will occur. + column.min_visible_width = ListView_GetStringWidth(list_view_, + column.title.c_str()) + + kListViewTextPadding; + } + changed = true; + } + if (changed) + OnColumnsChanged(); +} + +void TableView::SetVisibleColumns(const std::vector<int>& columns) { + size_t old_count = visible_columns_.size(); + size_t new_count = columns.size(); + // remove the old columns + if (list_view_) { + for (std::vector<int>::reverse_iterator i = visible_columns_.rbegin(); + i != visible_columns_.rend(); ++i) { + int index = static_cast<int>(i - visible_columns_.rend()); + SendMessage(list_view_, LVM_DELETECOLUMN, index, 0); + } + } + visible_columns_ = columns; + // Insert the new columns. + if (list_view_) { + for (std::vector<int>::iterator i = visible_columns_.begin(); + i != visible_columns_.end(); ++i) { + int index = static_cast<int>(i - visible_columns_.end()); + InsertColumn(all_columns_[*i], index); + } + } + OnColumnsChanged(); +} + +bool TableView::IsColumnVisible(int id) const { + for (std::vector<int>::const_iterator i = visible_columns_.begin(); + i != visible_columns_.end(); ++i) + if (*i == id) { + return true; + } + return false; +} + +const TableColumn& TableView::GetColumnAtPosition(int pos) { + return all_columns_[visible_columns_[pos]]; +} + +bool TableView::HasColumn(int id) { + return all_columns_.count(id) > 0; +} + +gfx::Point TableView::GetKeyboardContextMenuLocation() { + int first_selected = FirstSelectedRow(); + int y = height() / 2; + if (first_selected != -1) { + RECT cell_bounds; + RECT client_rect; + if (ListView_GetItemRect(GetNativeControlHWND(), first_selected, + &cell_bounds, LVIR_BOUNDS) && + GetClientRect(GetNativeControlHWND(), &client_rect) && + cell_bounds.bottom >= 0 && cell_bounds.bottom < client_rect.bottom) { + y = cell_bounds.bottom; + } + } + gfx::Point screen_loc(0, y); + if (UILayoutIsRightToLeft()) + screen_loc.set_x(width()); + ConvertPointToScreen(this, &screen_loc); + return screen_loc; +} + +void TableView::SetCustomColorsEnabled(bool custom_colors_enabled) { + custom_colors_enabled_ = custom_colors_enabled; +} + +bool TableView::GetCellColors(int model_row, + int column, + ItemColor* foreground, + ItemColor* background, + LOGFONT* logfont) { + return false; +} + +static int GetViewIndexFromMouseEvent(HWND window, LPARAM l_param) { + int x = GET_X_LPARAM(l_param); + int y = GET_Y_LPARAM(l_param); + LVHITTESTINFO hit_info = {0}; + hit_info.pt.x = x; + hit_info.pt.y = y; + return ListView_HitTest(window, &hit_info); +} + +// static +LRESULT CALLBACK TableView::TableWndProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param) { + TableView* table_view = reinterpret_cast<TableViewWrapper*>( + GetWindowLongPtr(window, GWLP_USERDATA))->table_view; + + // Is the mouse down on the table? + static bool in_mouse_down = false; + // Should we select on mouse up? + static bool select_on_mouse_up = false; + + // If the mouse is down, this is the location of the mouse down message. + static int mouse_down_x, mouse_down_y; + + switch (message) { + case WM_CONTEXTMENU: { + // This addresses two problems seen with context menus in right to left + // locales: + // 1. The mouse coordinates in the l_param were occasionally wrong in + // weird ways. This is most often seen when right clicking on the + // list-view twice in a row. + // 2. Right clicking on the icon would show the scrollbar menu. + // + // As a work around this uses the position of the cursor and ignores + // the position supplied in the l_param. + if (table_view->UILayoutIsRightToLeft() && + (GET_X_LPARAM(l_param) != -1 || GET_Y_LPARAM(l_param) != -1)) { + CPoint screen_point; + GetCursorPos(&screen_point); + CPoint table_point = screen_point; + CRect client_rect; + if (ScreenToClient(window, &table_point) && + GetClientRect(window, &client_rect) && + client_rect.PtInRect(table_point)) { + // The point is over the client area of the table, handle it ourself. + // But first select the row if it isn't already selected. + LVHITTESTINFO hit_info = {0}; + hit_info.pt.x = table_point.x; + hit_info.pt.y = table_point.y; + int view_index = ListView_HitTest(window, &hit_info); + if (view_index != -1) { + int model_index = table_view->view_to_model(view_index); + if (!table_view->IsItemSelected(model_index)) + table_view->Select(model_index); + } + table_view->OnContextMenu(screen_point); + return 0; // So that default processing doesn't occur. + } + } + // else case: default handling is fine, so break and let the default + // handler service the request (which will likely calls us back with + // OnContextMenu). + break; + } + + case WM_CANCELMODE: { + if (in_mouse_down) { + in_mouse_down = false; + return 0; + } + break; + } + + case WM_ERASEBKGND: + // We make WM_ERASEBKGND do nothing (returning 1 indicates we handled + // the request). We do this so that the table view doesn't flicker during + // resizing. + return 1; + + case WM_PAINT: { + LRESULT result = CallWindowProc(table_view->original_handler_, window, + message, w_param, l_param); + table_view->PostPaint(); + return result; + } + + case WM_KEYDOWN: { + if (!table_view->single_selection_ && w_param == 'A' && + GetKeyState(VK_CONTROL) < 0 && table_view->RowCount() > 0) { + // Select everything. + ListView_SetItemState(window, -1, LVIS_SELECTED, LVIS_SELECTED); + // And make the first row focused. + ListView_SetItemState(window, 0, LVIS_FOCUSED, LVIS_FOCUSED); + return 0; + } else if (w_param == VK_DELETE && table_view->table_view_observer_) { + table_view->table_view_observer_->OnTableViewDelete(table_view); + return 0; + } + // else case: fall through to default processing. + break; + } + + case WM_LBUTTONDBLCLK: { + if (w_param == MK_LBUTTON) + table_view->OnDoubleClick(); + return 0; + } + + case WM_LBUTTONUP: { + if (in_mouse_down) { + in_mouse_down = false; + ReleaseCapture(); + SetFocus(window); + if (select_on_mouse_up) { + int view_index = GetViewIndexFromMouseEvent(window, l_param); + if (view_index != -1) + table_view->Select(table_view->view_to_model(view_index)); + } + return 0; + } + break; + } + + case WM_LBUTTONDOWN: { + // ListView treats clicking on an area outside the text of a column as + // drag to select. This is confusing when the selection is shown across + // the whole row. For this reason we override the default handling for + // mouse down/move/up and treat the whole row as draggable. That is, no + // matter where you click in the row we'll attempt to start dragging. + // + // Only do custom mouse handling if no other mouse buttons are down. + if ((w_param | (MK_LBUTTON | MK_CONTROL | MK_SHIFT)) == + (MK_LBUTTON | MK_CONTROL | MK_SHIFT)) { + if (in_mouse_down) + return 0; + + int view_index = GetViewIndexFromMouseEvent(window, l_param); + if (view_index != -1) { + table_view->ignore_listview_change_ = true; + in_mouse_down = true; + select_on_mouse_up = false; + mouse_down_x = GET_X_LPARAM(l_param); + mouse_down_y = GET_Y_LPARAM(l_param); + int model_index = table_view->view_to_model(view_index); + bool select = true; + if (w_param & MK_CONTROL) { + select = false; + if (!table_view->IsItemSelected(model_index)) { + if (table_view->single_selection_) { + // Single selection mode and the row isn't selected, select + // only it. + table_view->Select(model_index); + } else { + // Not single selection, add this row to the selection. + table_view->SetSelectedState(model_index, true); + } + } else { + // Remove this row from the selection. + table_view->SetSelectedState(model_index, false); + } + ListView_SetSelectionMark(window, view_index); + } else if (!table_view->single_selection_ && w_param & MK_SHIFT) { + int mark_view_index = ListView_GetSelectionMark(window); + if (mark_view_index != -1) { + // Unselect everything. + ListView_SetItemState(window, -1, 0, LVIS_SELECTED); + select = false; + + // Select from mark to mouse down location. + for (int i = std::min(view_index, mark_view_index), + max_i = std::max(view_index, mark_view_index); i <= max_i; + ++i) { + table_view->SetSelectedState(table_view->view_to_model(i), + true); + } + } + } + // Make the row the user clicked on the focused row. + ListView_SetItemState(window, view_index, LVIS_FOCUSED, + LVIS_FOCUSED); + if (select) { + if (!table_view->IsItemSelected(model_index)) { + // Clear all. + ListView_SetItemState(window, -1, 0, LVIS_SELECTED); + // And select the row the user clicked on. + table_view->SetSelectedState(model_index, true); + } else { + // The item is already selected, don't clear the state right away + // in case the user drags. Instead wait for mouse up, then only + // select the row the user clicked on. + select_on_mouse_up = true; + } + ListView_SetSelectionMark(window, view_index); + } + table_view->ignore_listview_change_ = false; + table_view->OnSelectedStateChanged(); + SetCapture(window); + return 0; + } + // else case, continue on to default handler + } + break; + } + + case WM_MOUSEMOVE: { + if (in_mouse_down) { + int x = GET_X_LPARAM(l_param); + int y = GET_Y_LPARAM(l_param); + if (View::ExceededDragThreshold(x - mouse_down_x, y - mouse_down_y)) { + // We're about to start drag and drop, which results in no mouse up. + // Release capture and reset state. + ReleaseCapture(); + in_mouse_down = false; + + NMLISTVIEW details; + memset(&details, 0, sizeof(details)); + details.hdr.code = LVN_BEGINDRAG; + SendMessage(::GetParent(window), WM_NOTIFY, 0, + reinterpret_cast<LPARAM>(&details)); + } + return 0; + } + break; + } + + default: + break; + } + DCHECK(table_view->original_handler_); + return CallWindowProc(table_view->original_handler_, window, message, w_param, + l_param); +} + +LRESULT CALLBACK TableView::TableHeaderWndProc(HWND window, UINT message, + WPARAM w_param, LPARAM l_param) { + TableView* table_view = reinterpret_cast<TableViewWrapper*>( + GetWindowLongPtr(window, GWLP_USERDATA))->table_view; + + switch (message) { + case WM_SETCURSOR: + if (!table_view->resizable_columns_) + // Prevents the cursor from changing to the resize cursor. + return TRUE; + break; + case WM_LBUTTONDBLCLK: + if (!table_view->resizable_columns_) + // Prevents the double-click on the column separator from auto-resizing + // the column. + return TRUE; + break; + default: + break; + } + DCHECK(table_view->header_original_handler_); + return CallWindowProc(table_view->header_original_handler_, + window, message, w_param, l_param); +} + +HWND TableView::CreateNativeControl(HWND parent_container) { + int style = WS_CHILD | LVS_REPORT | LVS_SHOWSELALWAYS; + if (single_selection_) + style |= LVS_SINGLESEL; + // If there's only one column and the title string is empty, don't show a + // header. + if (all_columns_.size() == 1) { + std::map<int, TableColumn>::const_iterator first = + all_columns_.begin(); + if (first->second.title.empty()) + style |= LVS_NOCOLUMNHEADER; + } + list_view_ = ::CreateWindowEx(WS_EX_CLIENTEDGE | GetAdditionalRTLStyle(), + WC_LISTVIEW, + L"", + style, + 0, 0, width(), height(), + parent_container, NULL, NULL, NULL); + + // Make the selection extend across the row. + // Reduce overdraw/flicker artifacts by double buffering. + DWORD list_view_style = LVS_EX_FULLROWSELECT; + if (win_util::GetWinVersion() > win_util::WINVERSION_2000) { + list_view_style |= LVS_EX_DOUBLEBUFFER; + } + if (table_type_ == CHECK_BOX_AND_TEXT) + list_view_style |= LVS_EX_CHECKBOXES; + ListView_SetExtendedListViewStyleEx(list_view_, 0, list_view_style); + l10n_util::AdjustUIFontForWindow(list_view_); + + // Add the columns. + for (std::vector<int>::iterator i = visible_columns_.begin(); + i != visible_columns_.end(); ++i) { + InsertColumn(all_columns_[*i], + static_cast<int>(i - visible_columns_.begin())); + } + + if (model_) + model_->SetObserver(this); + + // Add the groups. + if (model_ && model_->HasGroups() && + win_util::GetWinVersion() > win_util::WINVERSION_2000) { + ListView_EnableGroupView(list_view_, true); + + TableModel::Groups groups = model_->GetGroups(); + LVGROUP group = { 0 }; + group.cbSize = sizeof(LVGROUP); + group.mask = LVGF_HEADER | LVGF_ALIGN | LVGF_GROUPID; + group.uAlign = LVGA_HEADER_LEFT; + for (size_t i = 0; i < groups.size(); ++i) { + group.pszHeader = const_cast<wchar_t*>(groups[i].title.c_str()); + group.iGroupId = groups[i].id; + ListView_InsertGroup(list_view_, static_cast<int>(i), &group); + } + } + + // Set the # of rows. + if (model_) + UpdateListViewCache(0, model_->RowCount(), true); + + if (table_type_ == ICON_AND_TEXT) { + HIMAGELIST image_list = + ImageList_Create(kImageSize, kImageSize, ILC_COLOR32, 2, 2); + // We create 2 phony images because we are going to switch images at every + // refresh in order to force a refresh of the icon area (somehow the clip + // rect does not include the icon). + ChromeCanvas canvas(kImageSize, kImageSize, false); + // Make the background completely transparent. + canvas.drawColor(SK_ColorBLACK, SkPorterDuff::kClear_Mode); + HICON empty_icon = + IconUtil::CreateHICONFromSkBitmap(canvas.ExtractBitmap()); + ImageList_AddIcon(image_list, empty_icon); + ImageList_AddIcon(image_list, empty_icon); + DeleteObject(empty_icon); + ListView_SetImageList(list_view_, image_list, LVSIL_SMALL); + } + + if (!resizable_columns_) { + // To disable the resizing of columns we'll filter the events happening on + // the header. We also need to intercept the HDM_LAYOUT to size the header + // for the Chrome headers. + HWND header = ListView_GetHeader(list_view_); + DCHECK(header); + SetWindowLongPtr(header, GWLP_USERDATA, + reinterpret_cast<LONG_PTR>(&table_view_wrapper_)); + header_original_handler_ = win_util::SetWindowProc(header, + &TableView::TableHeaderWndProc); + } + + SetWindowLongPtr(list_view_, GWLP_USERDATA, + reinterpret_cast<LONG_PTR>(&table_view_wrapper_)); + original_handler_ = + win_util::SetWindowProc(list_view_, &TableView::TableWndProc); + + // Bug 964884: detach the IME attached to this window. + // We should attach IMEs only when we need to input CJK strings. + ::ImmAssociateContextEx(list_view_, NULL, 0); + + UpdateContentOffset(); + + return list_view_; +} + +void TableView::ToggleSortOrder(int column_id) { + SortDescriptors sort = sort_descriptors(); + if (!sort.empty() && sort[0].column_id == column_id) { + sort[0].ascending = !sort[0].ascending; + } else { + SortDescriptor descriptor(column_id, true); + sort.insert(sort.begin(), descriptor); + if (sort.size() > 2) { + // Only persist two sort descriptors. + sort.resize(2); + } + } + SetSortDescriptors(sort); +} + +void TableView::UpdateItemsLParams(int start, int length) { + LVITEM item; + memset(&item, 0, sizeof(LVITEM)); + item.mask = LVIF_PARAM; + int row_count = RowCount(); + for (int i = 0; i < row_count; ++i) { + item.iItem = i; + int model_index = view_to_model(i); + if (length > 0 && model_index >= start) + model_index += length; + item.lParam = static_cast<LPARAM>(model_index); + ListView_SetItem(list_view_, &item); + } +} + +void TableView::SortItemsAndUpdateMapping() { + if (!is_sorted()) { + ListView_SortItems(list_view_, &TableView::NaturalSortFunc, this); + view_to_model_.reset(NULL); + model_to_view_.reset(NULL); + return; + } + + PrepareForSort(); + + // Sort the items. + ListView_SortItems(list_view_, &TableView::SortFunc, this); + + // Cleanup the collator. + if (collator) { + delete collator; + collator = NULL; + } + + // Update internal mapping to match how items were actually sorted. + int row_count = RowCount(); + model_to_view_.reset(new int[row_count]); + view_to_model_.reset(new int[row_count]); + LVITEM item; + memset(&item, 0, sizeof(LVITEM)); + item.mask = LVIF_PARAM; + for (int i = 0; i < row_count; ++i) { + item.iItem = i; + ListView_GetItem(list_view_, &item); + int model_index = static_cast<int>(item.lParam); + view_to_model_[i] = model_index; + model_to_view_[model_index] = i; + } +} + +// static +int CALLBACK TableView::SortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param) { + int model_index_1 = static_cast<int>(model_index_1_p); + int model_index_2 = static_cast<int>(model_index_2_p); + TableView* table_view = reinterpret_cast<TableView*>(table_view_param); + return table_view->CompareRows(model_index_1, model_index_2); +} + +// static +int CALLBACK TableView::NaturalSortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param) { + return model_index_1_p - model_index_2_p; +} + +void TableView::ResetColumnSortImage(int column_id, SortDirection direction) { + if (!list_view_ || column_id == -1) + return; + + std::vector<int>::const_iterator i = + std::find(visible_columns_.begin(), visible_columns_.end(), column_id); + if (i == visible_columns_.end()) + return; + + HWND header = ListView_GetHeader(list_view_); + if (!header) + return; + + int column_index = static_cast<int>(i - visible_columns_.begin()); + HDITEM header_item; + memset(&header_item, 0, sizeof(header_item)); + header_item.mask = HDI_FORMAT; + Header_GetItem(header, column_index, &header_item); + header_item.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN); + if (direction == ASCENDING_SORT) + header_item.fmt |= HDF_SORTUP; + else if (direction == DESCENDING_SORT) + header_item.fmt |= HDF_SORTDOWN; + Header_SetItem(header, column_index, &header_item); +} + +void TableView::InsertColumn(const TableColumn& tc, int index) { + if (!list_view_) + return; + + LVCOLUMN column = { 0 }; + column.mask = LVCF_TEXT|LVCF_FMT; + column.pszText = const_cast<LPWSTR>(tc.title.c_str()); + switch (tc.alignment) { + case TableColumn::LEFT: + column.fmt = LVCFMT_LEFT; + break; + case TableColumn::RIGHT: + column.fmt = LVCFMT_RIGHT; + break; + case TableColumn::CENTER: + column.fmt = LVCFMT_CENTER; + break; + default: + NOTREACHED(); + } + if (tc.width != -1) { + column.mask |= LVCF_WIDTH; + column.cx = tc.width; + } + column.mask |= LVCF_SUBITEM; + // Sub-items are 1s indexed. + column.iSubItem = index + 1; + SendMessage(list_view_, LVM_INSERTCOLUMN, index, + reinterpret_cast<LPARAM>(&column)); + if (is_sorted() && sort_descriptors_[0].column_id == tc.id) { + ResetColumnSortImage( + tc.id, + sort_descriptors_[0].ascending ? ASCENDING_SORT : DESCENDING_SORT); + } +} + +LRESULT TableView::OnNotify(int w_param, LPNMHDR hdr) { + if (!model_) + return 0; + + switch (hdr->code) { + case NM_CUSTOMDRAW: { + // Draw notification. dwDragState indicates the current stage of drawing. + return OnCustomDraw(reinterpret_cast<NMLVCUSTOMDRAW*>(hdr)); + } + + case LVN_ITEMCHANGED: { + // Notification that the state of an item has changed. The state + // includes such things as whether the item is selected or checked. + NMLISTVIEW* state_change = reinterpret_cast<NMLISTVIEW*>(hdr); + if ((state_change->uChanged & LVIF_STATE) != 0) { + if ((state_change->uOldState & LVIS_SELECTED) != + (state_change->uNewState & LVIS_SELECTED)) { + // Selected state of the item changed. + OnSelectedStateChanged(); + } + if ((state_change->uOldState & LVIS_STATEIMAGEMASK) != + (state_change->uNewState & LVIS_STATEIMAGEMASK)) { + // Checked state of the item changed. + bool is_checked = + ((state_change->uNewState & LVIS_STATEIMAGEMASK) == + INDEXTOSTATEIMAGEMASK(2)); + OnCheckedStateChanged(view_to_model(state_change->iItem), + is_checked); + } + } + break; + } + + case HDN_BEGINTRACKW: + case HDN_BEGINTRACKA: + // Prevent clicks so columns cannot be resized. + if (!resizable_columns_) + return TRUE; + break; + + case NM_DBLCLK: + OnDoubleClick(); + break; + + // If we see a key down message, we need to invoke the OnKeyDown handler + // in order to give our class (or any subclass) and opportunity to perform + // a key down triggered action, if such action is necessary. + case LVN_KEYDOWN: { + NMLVKEYDOWN* key_down_message = reinterpret_cast<NMLVKEYDOWN*>(hdr); + OnKeyDown(key_down_message->wVKey); + break; + } + + case LVN_COLUMNCLICK: { + const TableColumn& column = GetColumnAtPosition( + reinterpret_cast<NMLISTVIEW*>(hdr)->iSubItem); + if (column.sortable) + ToggleSortOrder(column.id); + break; + } + + case LVN_MARQUEEBEGIN: // We don't want the marque selection. + return 1; + + default: + break; + } + return 0; +} + +void TableView::OnDestroy() { + if (table_type_ == ICON_AND_TEXT) { + HIMAGELIST image_list = + ListView_GetImageList(GetNativeControlHWND(), LVSIL_SMALL); + DCHECK(image_list); + if (image_list) + ImageList_Destroy(image_list); + } +} + +// Returns result, unless ascending is false in which case -result is returned. +static int SwapCompareResult(int result, bool ascending) { + return ascending ? result : -result; +} + +int TableView::CompareRows(int model_row1, int model_row2) { + if (model_->HasGroups()) { + // By default ListView sorts the elements regardless of groups. In such + // a situation the groups display only the items they contain. This results + // in the visual order differing from the item indices. I could not find + // a way to iterate over the visual order in this situation. As a workaround + // this forces the items to be sorted by groups as well, which means the + // visual order matches the item indices. + int g1 = model_->GetGroupID(model_row1); + int g2 = model_->GetGroupID(model_row2); + if (g1 != g2) + return g1 - g2; + } + int sort_result = model_->CompareValues( + model_row1, model_row2, sort_descriptors_[0].column_id); + if (sort_result == 0 && sort_descriptors_.size() > 1 && + sort_descriptors_[1].column_id != -1) { + // Try the secondary sort. + return SwapCompareResult( + model_->CompareValues(model_row1, model_row2, + sort_descriptors_[1].column_id), + sort_descriptors_[1].ascending); + } + return SwapCompareResult(sort_result, sort_descriptors_[0].ascending); +} + +int TableView::GetColumnWidth(int column_id) { + if (!list_view_) + return -1; + + std::vector<int>::const_iterator i = + std::find(visible_columns_.begin(), visible_columns_.end(), column_id); + if (i == visible_columns_.end()) + return -1; + + return ListView_GetColumnWidth( + list_view_, static_cast<int>(i - visible_columns_.begin())); +} + +LRESULT TableView::OnCustomDraw(NMLVCUSTOMDRAW* draw_info) { + switch (draw_info->nmcd.dwDrawStage) { + case CDDS_PREPAINT: { + return CDRF_NOTIFYITEMDRAW; + } + case CDDS_ITEMPREPAINT: { + // The list-view is about to paint an item, tell it we want to + // notified when it paints every subitem. + LRESULT r = CDRF_NOTIFYSUBITEMDRAW; + if (table_type_ == ICON_AND_TEXT) + r |= CDRF_NOTIFYPOSTPAINT; + return r; + } + case (CDDS_ITEMPREPAINT | CDDS_SUBITEM): { + // The list-view is painting a subitem. See if the colors should be + // changed from the default. + if (custom_colors_enabled_) { + // At this time, draw_info->clrText and draw_info->clrTextBk are not + // set. So we pass in an ItemColor to GetCellColors. If + // ItemColor.color_is_set is true, then we use the provided color. + ItemColor foreground = {0}; + ItemColor background = {0}; + + LOGFONT logfont; + GetObject(GetWindowFont(list_view_), sizeof(logfont), &logfont); + + if (GetCellColors(view_to_model( + static_cast<int>(draw_info->nmcd.dwItemSpec)), + draw_info->iSubItem, + &foreground, + &background, + &logfont)) { + // TODO(tc): Creating/deleting a font for every cell seems like a + // waste if the font hasn't changed. Maybe we should use a struct + // with a bool like we do with colors? + if (custom_cell_font_) + DeleteObject(custom_cell_font_); + l10n_util::AdjustUIFont(&logfont); + custom_cell_font_ = CreateFontIndirect(&logfont); + SelectObject(draw_info->nmcd.hdc, custom_cell_font_); + draw_info->clrText = foreground.color_is_set + ? skia::SkColorToCOLORREF(foreground.color) + : CLR_DEFAULT; + draw_info->clrTextBk = background.color_is_set + ? skia::SkColorToCOLORREF(background.color) + : CLR_DEFAULT; + return CDRF_NEWFONT; + } + } + return CDRF_DODEFAULT; + } + case CDDS_ITEMPOSTPAINT: { + DCHECK((table_type_ == ICON_AND_TEXT) || (ImplementPostPaint())); + int view_index = static_cast<int>(draw_info->nmcd.dwItemSpec); + // We get notifications for empty items, just ignore them. + if (view_index >= model_->RowCount()) + return CDRF_DODEFAULT; + int model_index = view_to_model(view_index); + LRESULT r = CDRF_DODEFAULT; + // First let's take care of painting the right icon. + if (table_type_ == ICON_AND_TEXT) { + SkBitmap image = model_->GetIcon(model_index); + if (!image.isNull()) { + // Get the rect that holds the icon. + CRect icon_rect, client_rect; + if (ListView_GetItemRect(list_view_, view_index, &icon_rect, + LVIR_ICON) && + GetClientRect(list_view_, &client_rect)) { + CRect intersection; + // Client rect includes the header but we need to make sure we don't + // paint into it. + client_rect.top += content_offset_; + // Make sure the region need to paint is visible. + if (intersection.IntersectRect(&icon_rect, &client_rect)) { + ChromeCanvas canvas(icon_rect.Width(), icon_rect.Height(), false); + + // It seems the state in nmcd.uItemState is not correct. + // We'll retrieve it explicitly. + int selected = ListView_GetItemState( + list_view_, view_index, LVIS_SELECTED | LVIS_DROPHILITED); + bool drop_highlight = ((selected & LVIS_DROPHILITED) != 0); + int bg_color_index; + if (!IsEnabled()) + bg_color_index = COLOR_3DFACE; + else if (drop_highlight) + bg_color_index = COLOR_HIGHLIGHT; + else if (selected) + bg_color_index = HasFocus() ? COLOR_HIGHLIGHT : COLOR_3DFACE; + else + bg_color_index = COLOR_WINDOW; + // NOTE: This may be invoked without the ListView filling in the + // background (or rather windows paints background, then invokes + // this twice). As such, we always fill in the background. + canvas.drawColor( + skia::COLORREFToSkColor(GetSysColor(bg_color_index)), + SkPorterDuff::kSrc_Mode); + // + 1 for padding (we declared the image as 18x18 in the list- + // view when they are 16x16 so we get an extra pixel of padding). + canvas.DrawBitmapInt(image, 0, 0, + image.width(), image.height(), + 1, 1, kFavIconSize, kFavIconSize, true); + + // Only paint the visible region of the icon. + RECT to_draw = { intersection.left - icon_rect.left, + intersection.top - icon_rect.top, + 0, 0 }; + to_draw.right = to_draw.left + + (intersection.right - intersection.left); + to_draw.bottom = to_draw.top + + (intersection.bottom - intersection.top); + canvas.getTopPlatformDevice().drawToHDC(draw_info->nmcd.hdc, + intersection.left, + intersection.top, + &to_draw); + r = CDRF_SKIPDEFAULT; + } + } + } + } + if (ImplementPostPaint()) { + CRect cell_rect; + if (ListView_GetItemRect(list_view_, view_index, &cell_rect, + LVIR_BOUNDS)) { + PostPaint(model_index, 0, false, cell_rect, draw_info->nmcd.hdc); + r = CDRF_SKIPDEFAULT; + } + } + return r; + } + default: + return CDRF_DODEFAULT; + } +} + +void TableView::UpdateListViewCache(int start, int length, bool add) { + ignore_listview_change_ = true; + UpdateListViewCache0(start, length, add); + ignore_listview_change_ = false; +} + +void TableView::ResetColumnSizes() { + if (!list_view_) + return; + + // See comment in TableColumn for what this does. + int width = this->width(); + CRect native_bounds; + if (GetClientRect(GetNativeControlHWND(), &native_bounds) && + native_bounds.Width() > 0) { + // Prefer the bounds of the window over our bounds, which may be different. + width = native_bounds.Width(); + } + + float percent = 0; + int fixed_width = 0; + int autosize_width = 0; + + for (std::vector<int>::const_iterator i = visible_columns_.begin(); + i != visible_columns_.end(); ++i) { + TableColumn& col = all_columns_[*i]; + int col_index = static_cast<int>(i - visible_columns_.begin()); + if (col.width == -1) { + if (col.percent > 0) { + percent += col.percent; + } else { + autosize_width += col.min_visible_width; + } + } else { + fixed_width += ListView_GetColumnWidth(list_view_, col_index); + } + } + + // Now do a pass to set the actual sizes of auto-sized and + // percent-sized columns. + int available_width = width - fixed_width - autosize_width; + for (std::vector<int>::const_iterator i = visible_columns_.begin(); + i != visible_columns_.end(); ++i) { + TableColumn& col = all_columns_[*i]; + if (col.width == -1) { + int col_index = static_cast<int>(i - visible_columns_.begin()); + if (col.percent > 0) { + if (available_width > 0) { + int col_width = + static_cast<int>(available_width * (col.percent / percent)); + available_width -= col_width; + percent -= col.percent; + ListView_SetColumnWidth(list_view_, col_index, col_width); + } + } else { + int col_width = col.min_visible_width; + // If no "percent" columns, the last column acts as one, if auto-sized. + if (percent == 0.f && available_width > 0 && + col_index == column_count_ - 1) { + col_width += available_width; + } + ListView_SetColumnWidth(list_view_, col_index, col_width); + } + } + } +} + +gfx::Size TableView::GetPreferredSize() { + return preferred_size_; +} + +void TableView::UpdateListViewCache0(int start, int length, bool add) { + if (is_sorted()) { + if (add) + UpdateItemsLParams(start, length); + else + UpdateItemsLParams(0, 0); + } + + LVITEM item = {0}; + int start_column = 0; + int max_row = start + length; + const bool has_groups = + (win_util::GetWinVersion() > win_util::WINVERSION_2000 && + model_->HasGroups()); + if (add) { + if (has_groups) + item.mask = LVIF_GROUPID; + item.mask |= LVIF_PARAM; + for (int i = start; i < max_row; ++i) { + item.iItem = i; + if (has_groups) + item.iGroupId = model_->GetGroupID(i); + item.lParam = i; + ListView_InsertItem(list_view_, &item); + } + } + + memset(&item, 0, sizeof(LVITEM)); + + // NOTE: I don't quite get why the iSubItem in the following is not offset + // by 1. According to the docs it should be offset by one, but that doesn't + // work. + if (table_type_ == CHECK_BOX_AND_TEXT) { + start_column = 1; + item.iSubItem = 0; + item.mask = LVIF_TEXT | LVIF_STATE; + item.stateMask = LVIS_STATEIMAGEMASK; + for (int i = start; i < max_row; ++i) { + std::wstring text = model_->GetText(i, visible_columns_[0]); + item.iItem = add ? i : model_to_view(i); + item.pszText = const_cast<LPWSTR>(text.c_str()); + item.state = INDEXTOSTATEIMAGEMASK(model_->IsChecked(i) ? 2 : 1); + ListView_SetItem(list_view_, &item); + } + } + + item.stateMask = 0; + item.mask = LVIF_TEXT; + if (table_type_ == ICON_AND_TEXT) { + item.mask |= LVIF_IMAGE; + } + for (int j = start_column; j < column_count_; ++j) { + TableColumn& col = all_columns_[visible_columns_[j]]; + int max_text_width = ListView_GetStringWidth(list_view_, col.title.c_str()); + for (int i = start; i < max_row; ++i) { + item.iItem = add ? i : model_to_view(i); + item.iSubItem = j; + std::wstring text = model_->GetText(i, visible_columns_[j]); + item.pszText = const_cast<LPWSTR>(text.c_str()); + item.iImage = 0; + ListView_SetItem(list_view_, &item); + + // Compute width in px, using current font. + int string_width = ListView_GetStringWidth(list_view_, item.pszText); + // The width of an icon belongs to the first column. + if (j == 0 && table_type_ == ICON_AND_TEXT) + string_width += kListViewIconWidthAndPadding; + max_text_width = std::max(string_width, max_text_width); + } + + // ListView_GetStringWidth must be padded or else truncation will occur + // (MSDN). 15px matches the Win32/LVSCW_AUTOSIZE_USEHEADER behavior. + max_text_width += kListViewTextPadding; + + // Protect against partial update. + if (max_text_width > col.min_visible_width || + (start == 0 && length == model_->RowCount())) { + col.min_visible_width = max_text_width; + } + } + + if (is_sorted()) { + // NOTE: As most of our tables are smallish I'm not going to optimize this. + // If our tables become large and frequently update, then it'll make sense + // to optimize this. + + SortItemsAndUpdateMapping(); + } +} + +void TableView::OnDoubleClick() { + if (!ignore_listview_change_ && table_view_observer_) { + table_view_observer_->OnDoubleClick(); + } +} + +void TableView::OnSelectedStateChanged() { + if (!ignore_listview_change_ && table_view_observer_) { + table_view_observer_->OnSelectionChanged(); + } +} + +void TableView::OnKeyDown(unsigned short virtual_keycode) { + if (!ignore_listview_change_ && table_view_observer_) { + table_view_observer_->OnKeyDown(virtual_keycode); + } +} + +void TableView::OnCheckedStateChanged(int model_row, bool is_checked) { + if (!ignore_listview_change_) + model_->SetChecked(model_row, is_checked); +} + +int TableView::PreviousSelectedViewIndex(int view_index) { + DCHECK(view_index >= 0); + if (!list_view_ || view_index <= 0) + return -1; + + int row_count = RowCount(); + if (row_count == 0) + return -1; // Empty table, nothing can be selected. + + // For some reason + // ListView_GetNextItem(list_view_,item, LVNI_SELECTED | LVNI_ABOVE) + // fails on Vista (always returns -1), so we iterate through the indices. + view_index = std::min(view_index, row_count); + while (--view_index >= 0 && !IsItemSelected(view_to_model(view_index))); + return view_index; +} + +int TableView::LastSelectedViewIndex() { + return PreviousSelectedViewIndex(RowCount()); +} + +void TableView::UpdateContentOffset() { + content_offset_ = 0; + + if (!list_view_) + return; + + HWND header = ListView_GetHeader(list_view_); + if (!header) + return; + + POINT origin = {0, 0}; + MapWindowPoints(header, list_view_, &origin, 1); + + CRect header_bounds; + GetWindowRect(header, &header_bounds); + + content_offset_ = origin.y + header_bounds.Height(); +} + +// +// TableSelectionIterator +// +TableSelectionIterator::TableSelectionIterator(TableView* view, + int view_index) + : table_view_(view), + view_index_(view_index) { + UpdateModelIndexFromViewIndex(); +} + +TableSelectionIterator& TableSelectionIterator::operator=( + const TableSelectionIterator& other) { + view_index_ = other.view_index_; + model_index_ = other.model_index_; + return *this; +} + +bool TableSelectionIterator::operator==(const TableSelectionIterator& other) { + return (other.view_index_ == view_index_); +} + +bool TableSelectionIterator::operator!=(const TableSelectionIterator& other) { + return (other.view_index_ != view_index_); +} + +TableSelectionIterator& TableSelectionIterator::operator++() { + view_index_ = table_view_->PreviousSelectedViewIndex(view_index_); + UpdateModelIndexFromViewIndex(); + return *this; +} + +int TableSelectionIterator::operator*() { + return model_index_; +} + +void TableSelectionIterator::UpdateModelIndexFromViewIndex() { + if (view_index_ == -1) + model_index_ = -1; + else + model_index_ = table_view_->view_to_model(view_index_); +} + +} // namespace views diff --git a/views/controls/table/table_view.h b/views/controls/table/table_view.h new file mode 100644 index 0000000..8061d27 --- /dev/null +++ b/views/controls/table/table_view.h @@ -0,0 +1,676 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_TABLE_TABLE_VIEW_H_ +#define VIEWS_CONTROLS_TABLE_TABLE_VIEW_H_ + +#include "build/build_config.h" + +#if defined(OS_WIN) +#include <windows.h> +#endif // defined(OS_WIN) + +#include <map> +#include <unicode/coll.h> +#include <unicode/uchar.h> +#include <vector> + +#include "app/l10n_util.h" +#include "base/logging.h" +#include "skia/include/SkColor.h" +#if defined(OS_WIN) +// TODO(port): remove the ifdef when native_control.h is ported. +#include "views/controls/native_control.h" +#endif // defined(OS_WIN) + +class SkBitmap; + +// A TableView is a view that displays multiple rows with any number of columns. +// TableView is driven by a TableModel. The model returns the contents +// to display. TableModel also has an Observer which is used to notify +// TableView of changes to the model so that the display may be updated +// appropriately. +// +// TableView itself has an observer that is notified when the selection +// changes. +// +// Tables may be sorted either by directly invoking SetSortDescriptors or by +// marking the column as sortable and the user doing a gesture to sort the +// contents. TableView itself maintains the sort so that the underlying model +// isn't effected. +// +// When a table is sorted the model coordinates do not necessarily match the +// view coordinates. All table methods are in terms of the model. If you need to +// convert to view coordinates use model_to_view. +// +// Sorting is done by a locale sensitive string sort. You can customize the +// sort by way of overriding CompareValues. +// +// TableView is a wrapper around the window type ListView in report mode. +namespace views { + +class ListView; +class ListViewParent; +class TableView; +struct TableColumn; + +// The cells in the first column of a table can contain: +// - only text +// - a small icon (16x16) and some text +// - a check box and some text +enum TableTypes { + TEXT_ONLY = 0, + ICON_AND_TEXT, + CHECK_BOX_AND_TEXT +}; + +// Any time the TableModel changes, it must notify its observer. +class TableModelObserver { + public: + // Invoked when the model has been completely changed. + virtual void OnModelChanged() = 0; + + // Invoked when a range of items has changed. + virtual void OnItemsChanged(int start, int length) = 0; + + // Invoked when new items are added. + virtual void OnItemsAdded(int start, int length) = 0; + + // Invoked when a range of items has been removed. + virtual void OnItemsRemoved(int start, int length) = 0; +}; + +// The model driving the TableView. +class TableModel { + public: + // See HasGroups, get GetGroupID for details as to how this is used. + struct Group { + // The title text for the group. + std::wstring title; + + // Unique id for the group. + int id; + }; + typedef std::vector<Group> Groups; + + // Number of rows in the model. + virtual int RowCount() = 0; + + // Returns the value at a particular location in text. + virtual std::wstring GetText(int row, int column_id) = 0; + + // Returns the small icon (16x16) that should be displayed in the first + // column before the text. This is only used when the TableView was created + // with the ICON_AND_TEXT table type. Returns an isNull() bitmap if there is + // no bitmap. + virtual SkBitmap GetIcon(int row); + + // Sets whether a particular row is checked. This is only invoked + // if the TableView was created with show_check_in_first_column true. + virtual void SetChecked(int row, bool is_checked) { + NOTREACHED(); + } + + // Returns whether a particular row is checked. This is only invoked + // if the TableView was created with show_check_in_first_column true. + virtual bool IsChecked(int row) { + return false; + } + + // Returns true if the TableView has groups. Groups provide a way to visually + // delineate the rows in a table view. When groups are enabled table view + // shows a visual separator for each group, followed by all the rows in + // the group. + // + // On win2k a visual separator is not rendered for the group headers. + virtual bool HasGroups() { return false; } + + // Returns the groups. + // This is only used if HasGroups returns true. + virtual Groups GetGroups() { + // If you override HasGroups to return true, you must override this as + // well. + NOTREACHED(); + return std::vector<Group>(); + } + + // Returns the group id of the specified row. + // This is only used if HasGroups returns true. + virtual int GetGroupID(int row) { + // If you override HasGroups to return true, you must override this as + // well. + NOTREACHED(); + return 0; + } + + // Sets the observer for the model. The TableView should NOT take ownership + // of the observer. + virtual void SetObserver(TableModelObserver* observer) = 0; + + // Compares the values in the column with id |column_id| for the two rows. + // Returns a value < 0, == 0 or > 0 as to whether the first value is + // <, == or > the second value. + // + // This implementation does a case insensitive locale specific string + // comparison. + virtual int CompareValues(int row1, int row2, int column_id); + + protected: + // Returns the collator used by CompareValues. + Collator* GetCollator(); +}; + +// TableColumn specifies the title, alignment and size of a particular column. +struct TableColumn { + enum Alignment { + LEFT, RIGHT, CENTER + }; + + TableColumn() + : id(0), + title(), + alignment(LEFT), + width(-1), + percent(), + min_visible_width(0), + sortable(false) { + } + TableColumn(int id, const std::wstring title, Alignment alignment, int width) + : id(id), + title(title), + alignment(alignment), + width(width), + percent(0), + min_visible_width(0), + sortable(false) { + } + TableColumn(int id, const std::wstring title, Alignment alignment, int width, + float percent) + : id(id), + title(title), + alignment(alignment), + width(width), + percent(percent), + min_visible_width(0), + sortable(false) { + } + // It's common (but not required) to use the title's IDS_* tag as the column + // id. In this case, the provided conveniences look up the title string on + // bahalf of the caller. + TableColumn(int id, Alignment alignment, int width) + : id(id), + alignment(alignment), + width(width), + percent(0), + min_visible_width(0), + sortable(false) { + title = l10n_util::GetString(id); + } + TableColumn(int id, Alignment alignment, int width, float percent) + : id(id), + alignment(alignment), + width(width), + percent(percent), + min_visible_width(0), + sortable(false) { + title = l10n_util::GetString(id); + } + + // A unique identifier for the column. + int id; + + // The title for the column. + std::wstring title; + + // Alignment for the content. + Alignment alignment; + + // The size of a column may be specified in two ways: + // 1. A fixed width. Set the width field to a positive number and the + // column will be given that width, in pixels. + // 2. As a percentage of the available width. If width is -1, and percent is + // > 0, the column is given a width of + // available_width * percent / total_percent. + // 3. If the width == -1 and percent == 0, the column is autosized based on + // the width of the column header text. + // + // Sizing is done in four passes. Fixed width columns are given + // their width, percentages are applied, autosized columns are autosized, + // and finally percentages are applied again taking into account the widths + // of autosized columns. + int width; + float percent; + + // The minimum width required for all items in this column + // (including the header) + // to be visible. + int min_visible_width; + + // Is this column sortable? Default is false + bool sortable; +}; + +// Returned from SelectionBegin/SelectionEnd +class TableSelectionIterator { + public: + TableSelectionIterator(TableView* view, int view_index); + TableSelectionIterator& operator=(const TableSelectionIterator& other); + bool operator==(const TableSelectionIterator& other); + bool operator!=(const TableSelectionIterator& other); + TableSelectionIterator& operator++(); + int operator*(); + + private: + void UpdateModelIndexFromViewIndex(); + + TableView* table_view_; + int view_index_; + + // The index in terms of the model. This is returned from the * operator. This + // is cached to avoid dependencies on the view_to_model mapping. + int model_index_; +}; + +// TableViewObserver is notified about the TableView selection. +class TableViewObserver { + public: + virtual ~TableViewObserver() {} + + // Invoked when the selection changes. + virtual void OnSelectionChanged() = 0; + + // Optional method invoked when the user double clicks on the table. + virtual void OnDoubleClick() {} + + // Optional method invoked when the user hits a key with the table in focus. + virtual void OnKeyDown(unsigned short virtual_keycode) {} + + // Invoked when the user presses the delete key. + virtual void OnTableViewDelete(TableView* table_view) {} +}; + +#if defined(OS_WIN) +// TODO(port): Port TableView. +class TableView : public NativeControl, + public TableModelObserver { + public: + typedef TableSelectionIterator iterator; + + // A helper struct for GetCellColors. Set |color_is_set| to true if color is + // set. See OnCustomDraw for more details on why we need this. + struct ItemColor { + bool color_is_set; + SkColor color; + }; + + // Describes a sorted column. + struct SortDescriptor { + SortDescriptor() : column_id(-1), ascending(true) {} + SortDescriptor(int column_id, bool ascending) + : column_id(column_id), + ascending(ascending) { } + + // ID of the sorted column. + int column_id; + + // Is the sort ascending? + bool ascending; + }; + + typedef std::vector<SortDescriptor> SortDescriptors; + + // Creates a new table using the model and columns specified. + // The table type applies to the content of the first column (text, icon and + // text, checkbox and text). + // When autosize_columns is true, columns always fill the available width. If + // false, columns are not resized when the table is resized. An extra empty + // column at the right fills the remaining space. + // When resizable_columns is true, users can resize columns by dragging the + // separator on the column header. NOTE: Right now this is always true. The + // code to set it false is still in place to be a base for future, better + // resizing behavior (see http://b/issue?id=874646 ), but no one uses or + // tests the case where this flag is false. + // Note that setting both resizable_columns and autosize_columns to false is + // probably not a good idea, as there is no way for the user to increase a + // column's size in that case. + TableView(TableModel* model, const std::vector<TableColumn>& columns, + TableTypes table_type, bool single_selection, + bool resizable_columns, bool autosize_columns); + virtual ~TableView(); + + // Assigns a new model to the table view, detaching the old one if present. + // If |model| is NULL, the table view cannot be used after this call. This + // should be called in the containing view's destructor to avoid destruction + // issues when the model needs to be deleted before the table. + void SetModel(TableModel* model); + TableModel* model() const { return model_; } + + // Resorts the contents. + void SetSortDescriptors(const SortDescriptors& sort_descriptors); + + // Current sort. + const SortDescriptors& sort_descriptors() const { return sort_descriptors_; } + + void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + + // Returns the number of rows in the TableView. + int RowCount(); + + // Returns the number of selected rows. + int SelectedRowCount(); + + // Selects the specified item, making sure it's visible. + void Select(int model_row); + + // Sets the selected state of an item (without sending any selection + // notifications). Note that this routine does NOT set the focus to the + // item at the given index. + void SetSelectedState(int model_row, bool state); + + // Sets the focus to the item at the given index. + void SetFocusOnItem(int model_row); + + // Returns the first selected row in terms of the model. + int FirstSelectedRow(); + + // Returns true if the item at the specified index is selected. + bool IsItemSelected(int model_row); + + // Returns true if the item at the specified index has the focus. + bool ItemHasTheFocus(int model_row); + + // Returns an iterator over the selection. The iterator proceeds from the + // last index to the first. + // + // NOTE: the iterator iterates over the visual order (but returns coordinates + // in terms of the model). + iterator SelectionBegin(); + iterator SelectionEnd(); + + // TableModelObserver methods. + virtual void OnModelChanged(); + virtual void OnItemsChanged(int start, int length); + virtual void OnItemsAdded(int start, int length); + virtual void OnItemsRemoved(int start, int length); + + void SetObserver(TableViewObserver* observer) { + table_view_observer_ = observer; + } + TableViewObserver* observer() const { return table_view_observer_; } + + // Replaces the set of known columns without changing the current visible + // columns. + void SetColumns(const std::vector<TableColumn>& columns); + void AddColumn(const TableColumn& col); + bool HasColumn(int id); + + // Sets which columns (by id) are displayed. All transient size and position + // information is lost. + void SetVisibleColumns(const std::vector<int>& columns); + void SetColumnVisibility(int id, bool is_visible); + bool IsColumnVisible(int id) const; + + // Resets the size of the columns based on the sizes passed to the + // constructor. Your normally needn't invoked this, it's done for you the + // first time the TableView is given a valid size. + void ResetColumnSizes(); + + // Sometimes we may want to size the TableView to a specific width and + // height. + virtual gfx::Size GetPreferredSize(); + void set_preferred_size(const gfx::Size& size) { preferred_size_ = size; } + + // Is the table sorted? + bool is_sorted() const { return !sort_descriptors_.empty(); } + + // Maps from the index in terms of the model to that of the view. + int model_to_view(int model_index) const { + return model_to_view_.get() ? model_to_view_[model_index] : model_index; + } + + // Maps from the index in terms of the view to that of the model. + int view_to_model(int view_index) const { + return view_to_model_.get() ? view_to_model_[view_index] : view_index; + } + + protected: + // Overriden to return the position of the first selected row. + virtual gfx::Point GetKeyboardContextMenuLocation(); + + // Subclasses that want to customize the colors of a particular row/column, + // must invoke this passing in true. The default value is false, such that + // GetCellColors is never invoked. + void SetCustomColorsEnabled(bool custom_colors_enabled); + + // Notification from the ListView that the selected state of an item has + // changed. + virtual void OnSelectedStateChanged(); + + // Notification from the ListView that the used double clicked the table. + virtual void OnDoubleClick(); + + // Subclasses can implement this method if they need to be notified of a key + // press event. Other wise, it appeals to table_view_observer_ + virtual void OnKeyDown(unsigned short virtual_keycode); + + // Invoked to customize the colors or font at a particular cell. If you + // change the colors or font, return true. This is only invoked if + // SetCustomColorsEnabled(true) has been invoked. + virtual bool GetCellColors(int model_row, + int column, + ItemColor* foreground, + ItemColor* background, + LOGFONT* logfont); + + // Subclasses that want to perform some custom painting (on top of the regular + // list view painting) should return true here and implement the PostPaint + // method. + virtual bool ImplementPostPaint() { return false; } + // Subclasses can implement in this method extra-painting for cells. + virtual void PostPaint(int model_row, int column, bool selected, + const CRect& bounds, HDC device_context) { } + virtual void PostPaint() {} + + virtual HWND CreateNativeControl(HWND parent_container); + + virtual LRESULT OnNotify(int w_param, LPNMHDR l_param); + + // Overriden to destroy the image list. + virtual void OnDestroy(); + + // Used to sort the two rows. Returns a value < 0, == 0 or > 0 indicating + // whether the row2 comes before row1, row2 is the same as row1 or row1 comes + // after row2. This invokes CompareValues on the model with the sorted column. + virtual int CompareRows(int model_row1, int model_row2); + + // Called before sorting. This does nothing and is intended for subclasses + // that need to cache state used during sorting. + virtual void PrepareForSort() {} + + // Returns the width of the specified column by id, or -1 if the column isn't + // visible. + int GetColumnWidth(int column_id); + + // Returns the offset from the top of the client area to the start of the + // content. + int content_offset() const { return content_offset_; } + + // Size (width and height) of images. + static const int kImageSize; + + private: + // Direction of a sort. + enum SortDirection { + ASCENDING_SORT, + DESCENDING_SORT, + NO_SORT + }; + + // We need this wrapper to pass the table view to the windows proc handler + // when subclassing the list view and list view header, as the reinterpret + // cast from GetWindowLongPtr would break the pointer if it is pointing to a + // subclass (in the OO sense of TableView). + struct TableViewWrapper { + explicit TableViewWrapper(TableView* view) : table_view(view) { } + TableView* table_view; + }; + + friend class ListViewParent; + friend class TableSelectionIterator; + + LRESULT OnCustomDraw(NMLVCUSTOMDRAW* draw_info); + + // Invoked when the user clicks on a column to toggle the sort order. If + // column_id is the primary sorted column the direction of the sort is + // toggled, otherwise column_id is made the primary sorted column. + void ToggleSortOrder(int column_id); + + // Updates the lparam of each of the list view items to be the model index. + // If length is > 0, all items with an index >= start get offset by length. + // This is used during sorting to determine how the items were sorted. + void UpdateItemsLParams(int start, int length); + + // Does the actual sort and updates the mappings (view_to_model and + // model_to_view) appropriately. + void SortItemsAndUpdateMapping(); + + // Method invoked by ListView to compare the two values. Invokes CompareRows. + static int CALLBACK SortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param); + + // Method invoked by ListView when sorting back to natural state. Returns + // model_index_1_p - model_index_2_p. + static int CALLBACK NaturalSortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param); + + // Resets the sort image displayed for the specified column. + void ResetColumnSortImage(int column_id, SortDirection direction); + + // Adds a new column. + void InsertColumn(const TableColumn& tc, int index); + + // Update headers and internal state after columns have changed + void OnColumnsChanged(); + + // Updates the ListView with values from the model. See UpdateListViewCache0 + // for a complete description. + // This turns off redrawing, and invokes UpdateListViewCache0 to do the + // actual updating. + void UpdateListViewCache(int start, int length, bool add); + + // Updates ListView with values from the model. + // If add is true, this adds length items starting at index start. + // If add is not true, the items are not added, the but the values in the + // range start - [start + length] are updated from the model. + void UpdateListViewCache0(int start, int length, bool add); + + // Notification from the ListView that the checked state of the item has + // changed. + void OnCheckedStateChanged(int model_row, bool is_checked); + + // Returns the index of the selected item before |view_index|, or -1 if + // |view_index| is the first selected item. + // + // WARNING: this returns coordinates in terms of the view, NOT the model. + int PreviousSelectedViewIndex(int view_index); + + // Returns the last selected view index in the table view, or -1 if the table + // is empty, or nothing is selected. + // + // WARNING: this returns coordinates in terms of the view, NOT the model. + int LastSelectedViewIndex(); + + // The TableColumn visible at position pos. + const TableColumn& GetColumnAtPosition(int pos); + + // Window procedure of the list view class. We subclass the list view to + // ignore WM_ERASEBKGND, which gives smoother painting during resizing. + static LRESULT CALLBACK TableWndProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param); + + // Window procedure of the header class. We subclass the header of the table + // to disable resizing of columns. + static LRESULT CALLBACK TableHeaderWndProc(HWND window, UINT message, + WPARAM w_param, LPARAM l_param); + + // Updates content_offset_ from the position of the header. + void UpdateContentOffset(); + + TableModel* model_; + TableTypes table_type_; + TableViewObserver* table_view_observer_; + + // An ordered list of id's into all_columns_ representing current visible + // columns. + std::vector<int> visible_columns_; + + // Mapping of an int id to a TableColumn representing all possible columns. + std::map<int, TableColumn> all_columns_; + + // Cached value of columns_.size() + int column_count_; + + // Selection mode. + bool single_selection_; + + // If true, any events that would normally be propagated to the observer + // are ignored. For example, if this is true and the selection changes in + // the listview, the observer is not notified. + bool ignore_listview_change_; + + // Reflects the value passed to SetCustomColorsEnabled. + bool custom_colors_enabled_; + + // Whether or not the columns have been sized in the ListView. This is + // set to true the first time Layout() is invoked and we have a valid size. + bool sized_columns_; + + // Whether or not columns should automatically be resized to fill the + // the available width when the list view is resized. + bool autosize_columns_; + + // Whether or not the user can resize columns. + bool resizable_columns_; + + // NOTE: While this has the name View in it, it's not a view. Rather it's + // a wrapper around the List-View window. + HWND list_view_; + + // The list view's header original proc handler. It is required when + // subclassing. + WNDPROC header_original_handler_; + + // Window procedure of the listview before we subclassed it. + WNDPROC original_handler_; + + // A wrapper around 'this' used when "subclassing" the list view and header. + TableViewWrapper table_view_wrapper_; + + // A custom font we use when overriding the font type for a specific cell. + HFONT custom_cell_font_; + + // The preferred size of the table view. + gfx::Size preferred_size_; + + int content_offset_; + + // Current sort. + SortDescriptors sort_descriptors_; + + // Mappings used when sorted. + scoped_array<int> view_to_model_; + scoped_array<int> model_to_view_; + + DISALLOW_COPY_AND_ASSIGN(TableView); +}; +#endif // defined(OS_WIN) + +} // namespace views + +#endif // VIEWS_CONTROLS_TABLE_TABLE_VIEW_H_ diff --git a/views/controls/table/table_view_unittest.cc b/views/controls/table/table_view_unittest.cc new file mode 100644 index 0000000..e0f08e2 --- /dev/null +++ b/views/controls/table/table_view_unittest.cc @@ -0,0 +1,381 @@ +// Copyright (c) 2006-2008 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 <vector> + +#include "base/message_loop.h" +#include "base/string_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "views/controls/table/table_view.h" +#include "views/window/window_delegate.h" +#include "views/window/window_win.h" + +using views::TableView; + +// TestTableModel -------------------------------------------------------------- + +// Trivial TableModel implementation that is backed by a vector of vectors. +// Provides methods for adding/removing/changing the contents that notify the +// observer appropriately. +// +// Initial contents are: +// 0, 1 +// 1, 1 +// 2, 2 +class TestTableModel : public views::TableModel { + public: + TestTableModel(); + + // Adds a new row at index |row| with values |c1_value| and |c2_value|. + void AddRow(int row, int c1_value, int c2_value); + + // Removes the row at index |row|. + void RemoveRow(int row); + + // Changes the values of the row at |row|. + void ChangeRow(int row, int c1_value, int c2_value); + + // TableModel + virtual int RowCount(); + virtual std::wstring GetText(int row, int column_id); + virtual void SetObserver(views::TableModelObserver* observer); + virtual int CompareValues(int row1, int row2, int column_id); + + private: + views::TableModelObserver* observer_; + + // The data. + std::vector<std::vector<int>> rows_; + + DISALLOW_COPY_AND_ASSIGN(TestTableModel); +}; + +TestTableModel::TestTableModel() : observer_(NULL) { + AddRow(0, 0, 1); + AddRow(1, 1, 1); + AddRow(2, 2, 2); +} + +void TestTableModel::AddRow(int row, int c1_value, int c2_value) { + DCHECK(row >= 0 && row <= static_cast<int>(rows_.size())); + std::vector<int> new_row; + new_row.push_back(c1_value); + new_row.push_back(c2_value); + rows_.insert(rows_.begin() + row, new_row); + if (observer_) + observer_->OnItemsAdded(row, 1); +} +void TestTableModel::RemoveRow(int row) { + DCHECK(row >= 0 && row <= static_cast<int>(rows_.size())); + rows_.erase(rows_.begin() + row); + if (observer_) + observer_->OnItemsRemoved(row, 1); +} + +void TestTableModel::ChangeRow(int row, int c1_value, int c2_value) { + DCHECK(row >= 0 && row < static_cast<int>(rows_.size())); + rows_[row][0] = c1_value; + rows_[row][1] = c2_value; + if (observer_) + observer_->OnItemsChanged(row, 1); +} + +int TestTableModel::RowCount() { + return static_cast<int>(rows_.size()); +} + +std::wstring TestTableModel::GetText(int row, int column_id) { + return IntToWString(rows_[row][column_id]); +} + +void TestTableModel::SetObserver(views::TableModelObserver* observer) { + observer_ = observer; +} + +int TestTableModel::CompareValues(int row1, int row2, int column_id) { + return rows_[row1][column_id] - rows_[row2][column_id]; +} + +// TableViewTest --------------------------------------------------------------- + +class TableViewTest : public testing::Test, views::WindowDelegate { + public: + virtual void SetUp(); + virtual void TearDown(); + + virtual views::View* GetContentsView() { + return table_; + } + + protected: + // Creates the model. + TestTableModel* CreateModel(); + + // Verifies the view order matches that of the supplied arguments. The + // arguments are in terms of the model. For example, values of '1, 0' indicate + // the model index at row 0 is 1 and the model index at row 1 is 0. + void VeriyViewOrder(int first, ...); + + // Verifies the selection matches the supplied arguments. The supplied + // arguments are in terms of this model. This uses the iterator returned by + // SelectionBegin. + void VerifySelectedRows(int first, ...); + + // Configures the state for the various multi-selection tests. + // This selects model rows 0 and 1, and if |sort| is true the first column + // is sorted in descending order. + void SetUpMultiSelectTestState(bool sort); + + scoped_ptr<TestTableModel> model_; + + // The table. This is owned by the window. + TableView* table_; + + private: + MessageLoopForUI message_loop_; + views::Window* window_; +}; + +void TableViewTest::SetUp() { + OleInitialize(NULL); + model_.reset(CreateModel()); + std::vector<views::TableColumn> columns; + columns.resize(2); + columns[0].id = 0; + columns[1].id = 1; + table_ = new TableView(model_.get(), columns, views::ICON_AND_TEXT, + false, false, false); + window_ = + views::Window::CreateChromeWindow(NULL, + gfx::Rect(100, 100, 512, 512), + this); +} + +void TableViewTest::TearDown() { + window_->Close(); + // Temporary workaround to avoid leak of RootView::pending_paint_task_. + message_loop_.RunAllPending(); + OleUninitialize(); +} + +void TableViewTest::VeriyViewOrder(int first, ...) { + va_list marker; + va_start(marker, first); + int value = first; + int index = 0; + for (int value = first, index = 0; value != -1; index++) { + ASSERT_EQ(value, table_->view_to_model(index)); + value = va_arg(marker, int); + } + va_end(marker); +} + +void TableViewTest::VerifySelectedRows(int first, ...) { + va_list marker; + va_start(marker, first); + int value = first; + int index = 0; + TableView::iterator selection_iterator = table_->SelectionBegin(); + for (int value = first, index = 0; value != -1; index++) { + ASSERT_TRUE(selection_iterator != table_->SelectionEnd()); + ASSERT_EQ(value, *selection_iterator); + value = va_arg(marker, int); + ++selection_iterator; + } + ASSERT_TRUE(selection_iterator == table_->SelectionEnd()); + va_end(marker); +} + +void TableViewTest::SetUpMultiSelectTestState(bool sort) { + // Select two rows. + table_->SetSelectedState(0, true); + table_->SetSelectedState(1, true); + + VerifySelectedRows(1, 0, -1); + if (!sort || HasFatalFailure()) + return; + + // Sort by first column descending. + TableView::SortDescriptors sd; + sd.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sd); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Make sure the two rows are sorted. + // NOTE: the order changed because iteration happens over view indices. + VerifySelectedRows(0, 1, -1); +} + +TestTableModel* TableViewTest::CreateModel() { + return new TestTableModel(); +} + +// NullModelTableViewTest ------------------------------------------------------ + +class NullModelTableViewTest : public TableViewTest { + protected: + // Creates the model. + TestTableModel* CreateModel() { + return NULL; + } +}; + +// Tests ----------------------------------------------------------------------- + +// Tests various sorting permutations. +TEST_F(TableViewTest, Sort) { + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Sort by second column ascending, first column descending. + sort.clear(); + sort.push_back(TableView::SortDescriptor(1, true)); + sort.push_back(TableView::SortDescriptor(0, false)); + sort[1].ascending = false; + table_->SetSortDescriptors(sort); + VeriyViewOrder(1, 0, 2, -1); + if (HasFatalFailure()) + return; + + // Clear the sort. + table_->SetSortDescriptors(TableView::SortDescriptors()); + VeriyViewOrder(0, 1, 2, -1); + if (HasFatalFailure()) + return; +} + +// Tests changing the model while sorted. +TEST_F(TableViewTest, SortThenChange) { + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + model_->ChangeRow(0, 3, 1); + VeriyViewOrder(0, 2, 1, -1); +} + +// Tests adding to the model while sorted. +TEST_F(TableViewTest, AddToSorted) { + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Add row so that it occurs first. + model_->AddRow(0, 5, -1); + VeriyViewOrder(0, 3, 2, 1, -1); + if (HasFatalFailure()) + return; + + // Add row so that it occurs last. + model_->AddRow(0, -1, -1); + VeriyViewOrder(1, 4, 3, 2, 0, -1); +} + +// Tests selection on sort. +TEST_F(TableViewTest, PersistSelectionOnSort) { + // Select row 0. + table_->Select(0); + + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Make sure 0 is still selected. + EXPECT_EQ(0, table_->FirstSelectedRow()); +} + +// Tests selection iterator with sort. +TEST_F(TableViewTest, PersistMultiSelectionOnSort) { + SetUpMultiSelectTestState(true); +} + +// Tests selection persists after a change when sorted with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnChangeWithSort) { + SetUpMultiSelectTestState(true); + if (HasFatalFailure()) + return; + + model_->ChangeRow(0, 3, 1); + + VerifySelectedRows(1, 0, -1); +} + +// Tests selection persists after a remove when sorted with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnRemoveWithSort) { + SetUpMultiSelectTestState(true); + if (HasFatalFailure()) + return; + + model_->RemoveRow(0); + + VerifySelectedRows(0, -1); +} + +// Tests selection persists after a add when sorted with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnAddWithSort) { + SetUpMultiSelectTestState(true); + if (HasFatalFailure()) + return; + + model_->AddRow(3, 4, 4); + + VerifySelectedRows(0, 1, -1); +} + +// Tests selection persists after a change with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnChange) { + SetUpMultiSelectTestState(false); + if (HasFatalFailure()) + return; + + model_->ChangeRow(0, 3, 1); + + VerifySelectedRows(1, 0, -1); +} + +// Tests selection persists after a remove with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnRemove) { + SetUpMultiSelectTestState(false); + if (HasFatalFailure()) + return; + + model_->RemoveRow(0); + + VerifySelectedRows(0, -1); +} + +// Tests selection persists after a add with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnAdd) { + SetUpMultiSelectTestState(false); + if (HasFatalFailure()) + return; + + model_->AddRow(3, 4, 4); + + VerifySelectedRows(1, 0, -1); +} + +TEST_F(NullModelTableViewTest, NullModel) { + // There's nothing explicit to test. If there is a bug in TableView relating + // to a NULL model we'll crash. +} diff --git a/views/controls/text_field.cc b/views/controls/text_field.cc new file mode 100644 index 0000000..2981282 --- /dev/null +++ b/views/controls/text_field.cc @@ -0,0 +1,1192 @@ +// Copyright (c) 2009 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/controls/text_field.h" + +#include <atlbase.h> +#include <atlapp.h> +#include <atlcrack.h> +#include <atlctrls.h> +#include <tom.h> // For ITextDocument, a COM interface to CRichEditCtrl +#include <vsstyle.h> + +#include "app/gfx/insets.h" +#include "app/l10n_util.h" +#include "app/l10n_util_win.h" +#include "base/clipboard.h" +#include "base/gfx/native_theme.h" +#include "base/scoped_clipboard_writer.h" +#include "base/string_util.h" +#include "base/win_util.h" +#include "chrome/browser/browser_process.h" +#include "chrome/common/win_util.h" +#include "grit/generated_resources.h" +#include "skia/ext/skia_utils_win.h" +#include "views/controls/hwnd_view.h" +#include "views/controls/menu/menu.h" +#include "views/focus/focus_util_win.h" +#include "views/widget/widget.h" + +using gfx::NativeTheme; + +namespace views { + +static const int kDefaultEditStyle = WS_CHILD | WS_VISIBLE; + +class TextField::Edit + : public CWindowImpl<TextField::Edit, CRichEditCtrl, + CWinTraits<kDefaultEditStyle> >, + public CRichEditCommands<TextField::Edit>, + public Menu::Delegate { + public: + DECLARE_WND_CLASS(L"ChromeViewsTextFieldEdit"); + + Edit(TextField* parent, bool draw_border); + ~Edit(); + + std::wstring GetText() const; + void SetText(const std::wstring& text); + void AppendText(const std::wstring& text); + + std::wstring GetSelectedText() const; + + // Selects all the text in the edit. Use this in place of SetSelAll() to + // avoid selecting the "phantom newline" at the end of the edit. + void SelectAll(); + + // Clears the selection within the edit field and sets the caret to the end. + void ClearSelection(); + + // Removes the border. + void RemoveBorder(); + + void SetEnabled(bool enabled); + + void SetBackgroundColor(COLORREF bg_color); + + // CWindowImpl + BEGIN_MSG_MAP(Edit) + MSG_WM_CHAR(OnChar) + MSG_WM_CONTEXTMENU(OnContextMenu) + MSG_WM_COPY(OnCopy) + MSG_WM_CREATE(OnCreate) + MSG_WM_CUT(OnCut) + MSG_WM_DESTROY(OnDestroy) + MESSAGE_HANDLER_EX(WM_IME_CHAR, OnImeChar) + MESSAGE_HANDLER_EX(WM_IME_STARTCOMPOSITION, OnImeStartComposition) + MESSAGE_HANDLER_EX(WM_IME_COMPOSITION, OnImeComposition) + MSG_WM_KEYDOWN(OnKeyDown) + MSG_WM_LBUTTONDBLCLK(OnLButtonDblClk) + MSG_WM_LBUTTONDOWN(OnLButtonDown) + MSG_WM_LBUTTONUP(OnLButtonUp) + MSG_WM_MBUTTONDOWN(OnNonLButtonDown) + MSG_WM_MOUSEMOVE(OnMouseMove) + MSG_WM_MOUSELEAVE(OnMouseLeave) + MESSAGE_HANDLER_EX(WM_MOUSEWHEEL, OnMouseWheel) + MSG_WM_NCCALCSIZE(OnNCCalcSize) + MSG_WM_NCPAINT(OnNCPaint) + MSG_WM_RBUTTONDOWN(OnNonLButtonDown) + MSG_WM_PASTE(OnPaste) + MSG_WM_SYSCHAR(OnSysChar) // WM_SYSxxx == WM_xxx with ALT down + MSG_WM_SYSKEYDOWN(OnKeyDown) + END_MSG_MAP() + + // Menu::Delegate + virtual bool IsCommandEnabled(int id) const; + virtual void ExecuteCommand(int id); + + private: + // This object freezes repainting of the edit until the object is destroyed. + // Some methods of the CRichEditCtrl draw synchronously to the screen. If we + // don't freeze, the user will see a rapid series of calls to these as + // flickers. + // + // Freezing the control while it is already frozen is permitted; the control + // will unfreeze once both freezes are released (the freezes stack). + class ScopedFreeze { + public: + ScopedFreeze(Edit* edit, ITextDocument* text_object_model); + ~ScopedFreeze(); + + private: + Edit* const edit_; + ITextDocument* const text_object_model_; + + DISALLOW_COPY_AND_ASSIGN(ScopedFreeze); + }; + + // message handlers + void OnChar(TCHAR key, UINT repeat_count, UINT flags); + void OnContextMenu(HWND window, const CPoint& point); + void OnCopy(); + LRESULT OnCreate(CREATESTRUCT* create_struct); + void OnCut(); + void OnDestroy(); + LRESULT OnImeChar(UINT message, WPARAM wparam, LPARAM lparam); + LRESULT OnImeStartComposition(UINT message, WPARAM wparam, LPARAM lparam); + LRESULT OnImeComposition(UINT message, WPARAM wparam, LPARAM lparam); + void OnKeyDown(TCHAR key, UINT repeat_count, UINT flags); + void OnLButtonDblClk(UINT keys, const CPoint& point); + void OnLButtonDown(UINT keys, const CPoint& point); + void OnLButtonUp(UINT keys, const CPoint& point); + void OnMouseLeave(); + LRESULT OnMouseWheel(UINT message, WPARAM w_param, LPARAM l_param); + void OnMouseMove(UINT keys, const CPoint& point); + int OnNCCalcSize(BOOL w_param, LPARAM l_param); + void OnNCPaint(HRGN region); + void OnNonLButtonDown(UINT keys, const CPoint& point); + void OnPaste(); + void OnSysChar(TCHAR ch, UINT repeat_count, UINT flags); + + // Helper function for OnChar() and OnKeyDown() that handles keystrokes that + // could change the text in the edit. + void HandleKeystroke(UINT message, TCHAR key, UINT repeat_count, UINT flags); + + // Every piece of code that can change the edit should call these functions + // before and after the change. These functions determine if anything + // meaningful changed, and do any necessary updating and notification. + void OnBeforePossibleChange(); + void OnAfterPossibleChange(); + + // Given an X coordinate in client coordinates, returns that coordinate + // clipped to be within the horizontal bounds of the visible text. + // + // This is used in our mouse handlers to work around quirky behaviors of the + // underlying CRichEditCtrl like not supporting triple-click when the user + // doesn't click on the text itself. + // + // |is_triple_click| should be true iff this is the third click of a triple + // click. Sadly, we need to clip slightly differently in this case. + LONG ClipXCoordToVisibleText(LONG x, bool is_triple_click) const; + + // Sets whether the mouse is in the edit. As necessary this redraws the + // edit. + void SetContainsMouse(bool contains_mouse); + + // Getter for the text_object_model_, used by the ScopedFreeze class. Note + // that the pointer returned here is only valid as long as the Edit is still + // alive. + ITextDocument* GetTextObjectModel() const; + + // We need to know if the user triple-clicks, so track double click points + // and times so we can see if subsequent clicks are actually triple clicks. + bool tracking_double_click_; + CPoint double_click_point_; + DWORD double_click_time_; + + // Used to discard unnecessary WM_MOUSEMOVE events after the first such + // unnecessary event. See detailed comments in OnMouseMove(). + bool can_discard_mousemove_; + + // The text of this control before a possible change. + std::wstring text_before_change_; + + // If true, the mouse is over the edit. + bool contains_mouse_; + + static bool did_load_library_; + + TextField* parent_; + + // The context menu for the edit. + scoped_ptr<Menu> context_menu_; + + // Border insets. + gfx::Insets content_insets_; + + // Whether the border is drawn. + bool draw_border_; + + // This interface is useful for accessing the CRichEditCtrl at a low level. + mutable CComQIPtr<ITextDocument> text_object_model_; + + // The position and the length of the ongoing composition string. + // These values are used for removing a composition string from a search + // text to emulate Firefox. + bool ime_discard_composition_; + int ime_composition_start_; + int ime_composition_length_; + + COLORREF bg_color_; + + DISALLOW_COPY_AND_ASSIGN(Edit); +}; + +/////////////////////////////////////////////////////////////////////////////// +// Helper classes + +TextField::Edit::ScopedFreeze::ScopedFreeze(TextField::Edit* edit, + ITextDocument* text_object_model) + : edit_(edit), + text_object_model_(text_object_model) { + // Freeze the screen. + if (text_object_model_) { + long count; + text_object_model_->Freeze(&count); + } +} + +TextField::Edit::ScopedFreeze::~ScopedFreeze() { + // Unfreeze the screen. + if (text_object_model_) { + long count; + text_object_model_->Unfreeze(&count); + if (count == 0) { + // We need to UpdateWindow() here instead of InvalidateRect() because, as + // far as I can tell, the edit likes to synchronously erase its background + // when unfreezing, thus requiring us to synchronously redraw if we don't + // want flicker. + edit_->UpdateWindow(); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// TextField::Edit + +bool TextField::Edit::did_load_library_ = false; + +TextField::Edit::Edit(TextField* parent, bool draw_border) + : parent_(parent), + tracking_double_click_(false), + double_click_time_(0), + can_discard_mousemove_(false), + contains_mouse_(false), + draw_border_(draw_border), + ime_discard_composition_(false), + ime_composition_start_(0), + ime_composition_length_(0), + bg_color_(0) { + if (!did_load_library_) + did_load_library_ = !!LoadLibrary(L"riched20.dll"); + + DWORD style = kDefaultEditStyle; + if (parent->GetStyle() & TextField::STYLE_PASSWORD) + style |= ES_PASSWORD; + + if (parent->read_only_) + style |= ES_READONLY; + + if (parent->GetStyle() & TextField::STYLE_MULTILINE) + style |= ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL; + else + style |= ES_AUTOHSCROLL; + // Make sure we apply RTL related extended window styles if necessary. + DWORD ex_style = l10n_util::GetExtendedStyles(); + + RECT r = {0, 0, parent_->width(), parent_->height()}; + Create(parent_->GetWidget()->GetNativeView(), r, NULL, style, ex_style); + + if (parent->GetStyle() & TextField::STYLE_LOWERCASE) { + DCHECK((parent->GetStyle() & TextField::STYLE_PASSWORD) == 0); + SetEditStyle(SES_LOWERCASE, SES_LOWERCASE); + } + + // Set up the text_object_model_. + CComPtr<IRichEditOle> ole_interface; + ole_interface.Attach(GetOleInterface()); + text_object_model_ = ole_interface; + + context_menu_.reset(new Menu(this, Menu::TOPLEFT, m_hWnd)); + context_menu_->AppendMenuItemWithLabel(IDS_UNDO, + l10n_util::GetString(IDS_UNDO)); + context_menu_->AppendSeparator(); + context_menu_->AppendMenuItemWithLabel(IDS_CUT, + l10n_util::GetString(IDS_CUT)); + context_menu_->AppendMenuItemWithLabel(IDS_COPY, + l10n_util::GetString(IDS_COPY)); + context_menu_->AppendMenuItemWithLabel(IDS_PASTE, + l10n_util::GetString(IDS_PASTE)); + context_menu_->AppendSeparator(); + context_menu_->AppendMenuItemWithLabel(IDS_SELECT_ALL, + l10n_util::GetString(IDS_SELECT_ALL)); +} + +TextField::Edit::~Edit() { +} + +std::wstring TextField::Edit::GetText() const { + int len = GetTextLength() + 1; + std::wstring str; + GetWindowText(WriteInto(&str, len), len); + return str; +} + +void TextField::Edit::SetText(const std::wstring& text) { + // Adjusting the string direction before setting the text in order to make + // sure both RTL and LTR strings are displayed properly. + std::wstring text_to_set; + if (!l10n_util::AdjustStringForLocaleDirection(text, &text_to_set)) + text_to_set = text; + if (parent_->GetStyle() & STYLE_LOWERCASE) + text_to_set = l10n_util::ToLower(text_to_set); + SetWindowText(text_to_set.c_str()); +} + +void TextField::Edit::AppendText(const std::wstring& text) { + int text_length = GetWindowTextLength(); + ::SendMessage(m_hWnd, TBM_SETSEL, true, MAKELPARAM(text_length, text_length)); + ::SendMessage(m_hWnd, EM_REPLACESEL, false, + reinterpret_cast<LPARAM>(text.c_str())); +} + +std::wstring TextField::Edit::GetSelectedText() const { + // Figure out the length of the selection. + long start; + long end; + GetSel(start, end); + + // Grab the selected text. + std::wstring str; + GetSelText(WriteInto(&str, end - start + 1)); + + return str; +} + +void TextField::Edit::SelectAll() { + // Select from the end to the front so that the first part of the text is + // always visible. + SetSel(GetTextLength(), 0); +} + +void TextField::Edit::ClearSelection() { + SetSel(GetTextLength(), GetTextLength()); +} + +void TextField::Edit::RemoveBorder() { + if (!draw_border_) + return; + + draw_border_ = false; + SetWindowPos(NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_FRAMECHANGED | SWP_NOACTIVATE | + SWP_NOOWNERZORDER | SWP_NOSIZE); +} + +void TextField::Edit::SetEnabled(bool enabled) { + SendMessage(parent_->GetNativeComponent(), WM_ENABLE, + static_cast<WPARAM>(enabled), 0); +} + +void TextField::Edit::SetBackgroundColor(COLORREF bg_color) { + CRichEditCtrl::SetBackgroundColor(bg_color); + bg_color_ = bg_color; +} + +bool TextField::Edit::IsCommandEnabled(int id) const { + switch (id) { + case IDS_UNDO: return !parent_->IsReadOnly() && !!CanUndo(); + case IDS_CUT: return !parent_->IsReadOnly() && + !parent_->IsPassword() && !!CanCut(); + case IDS_COPY: return !!CanCopy() && !parent_->IsPassword(); + case IDS_PASTE: return !parent_->IsReadOnly() && !!CanPaste(); + case IDS_SELECT_ALL: return !!CanSelectAll(); + default: NOTREACHED(); + return false; + } +} + +void TextField::Edit::ExecuteCommand(int id) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + switch (id) { + case IDS_UNDO: Undo(); break; + case IDS_CUT: Cut(); break; + case IDS_COPY: Copy(); break; + case IDS_PASTE: Paste(); break; + case IDS_SELECT_ALL: SelectAll(); break; + default: NOTREACHED(); break; + } + OnAfterPossibleChange(); +} + +void TextField::Edit::OnChar(TCHAR ch, UINT repeat_count, UINT flags) { + HandleKeystroke(GetCurrentMessage()->message, ch, repeat_count, flags); +} + +void TextField::Edit::OnContextMenu(HWND window, const CPoint& point) { + CPoint p(point); + if (point.x == -1 || point.y == -1) { + GetCaretPos(&p); + MapWindowPoints(HWND_DESKTOP, &p, 1); + } + context_menu_->RunMenuAt(p.x, p.y); +} + +void TextField::Edit::OnCopy() { + if (parent_->IsPassword()) + return; + + const std::wstring text(GetSelectedText()); + + if (!text.empty()) { + ScopedClipboardWriter scw(g_browser_process->clipboard()); + scw.WriteText(text); + } +} + +LRESULT TextField::Edit::OnCreate(CREATESTRUCT* create_struct) { + SetMsgHandled(FALSE); + TRACK_HWND_CREATION(m_hWnd); + return 0; +} + +void TextField::Edit::OnCut() { + if (parent_->IsReadOnly() || parent_->IsPassword()) + return; + + OnCopy(); + + // This replace selection will have no effect (even on the undo stack) if the + // current selection is empty. + ReplaceSel(L"", true); +} + +void TextField::Edit::OnDestroy() { + TRACK_HWND_DESTRUCTION(m_hWnd); +} + +LRESULT TextField::Edit::OnImeChar(UINT message, WPARAM wparam, LPARAM lparam) { + // http://crbug.com/7707: a rich-edit control may crash when it receives a + // WM_IME_CHAR message while it is processing a WM_IME_COMPOSITION message. + // Since view controls don't need WM_IME_CHAR messages, we prevent WM_IME_CHAR + // messages from being dispatched to view controls via the CallWindowProc() + // call. + return 0; +} + +LRESULT TextField::Edit::OnImeStartComposition(UINT message, + WPARAM wparam, + LPARAM lparam) { + // Users may press alt+shift or control+shift keys to change their keyboard + // layouts. So, we retrieve the input locale identifier everytime we start + // an IME composition. + int language_id = PRIMARYLANGID(GetKeyboardLayout(0)); + ime_discard_composition_ = + language_id == LANG_JAPANESE || language_id == LANG_CHINESE; + ime_composition_start_ = 0; + ime_composition_length_ = 0; + + return DefWindowProc(message, wparam, lparam); +} + +LRESULT TextField::Edit::OnImeComposition(UINT message, + WPARAM wparam, + LPARAM lparam) { + text_before_change_.clear(); + LRESULT result = DefWindowProc(message, wparam, lparam); + + ime_composition_start_ = 0; + ime_composition_length_ = 0; + if (ime_discard_composition_) { + // Call IMM32 functions to retrieve the position and the length of the + // ongoing composition string and notify the OnAfterPossibleChange() + // function that it should discard the composition string from a search + // string. We should not call IMM32 functions in the function because it + // is called when an IME is not composing a string. + HIMC imm_context = ImmGetContext(m_hWnd); + if (imm_context) { + CHARRANGE selection; + GetSel(selection); + const int cursor_position = + ImmGetCompositionString(imm_context, GCS_CURSORPOS, NULL, 0); + if (cursor_position >= 0) + ime_composition_start_ = selection.cpMin - cursor_position; + + const int composition_size = + ImmGetCompositionString(imm_context, GCS_COMPSTR, NULL, 0); + if (composition_size >= 0) + ime_composition_length_ = composition_size / sizeof(wchar_t); + + ImmReleaseContext(m_hWnd, imm_context); + } + } + + OnAfterPossibleChange(); + return result; +} + +void TextField::Edit::OnKeyDown(TCHAR key, UINT repeat_count, UINT flags) { + // NOTE: Annoyingly, ctrl-alt-<key> generates WM_KEYDOWN rather than + // WM_SYSKEYDOWN, so we need to check (flags & KF_ALTDOWN) in various places + // in this function even with a WM_SYSKEYDOWN handler. + + switch (key) { + case VK_RETURN: + // If we are multi-line, we want to let returns through so they start a + // new line. + if (parent_->IsMultiLine()) + break; + else + return; + // Hijacking Editing Commands + // + // We hijack the keyboard short-cuts for Cut, Copy, and Paste here so that + // they go through our clipboard routines. This allows us to be smarter + // about how we interact with the clipboard and avoid bugs in the + // CRichEditCtrl. If we didn't hijack here, the edit control would handle + // these internally with sending the WM_CUT, WM_COPY, or WM_PASTE messages. + // + // Cut: Shift-Delete and Ctrl-x are treated as cut. Ctrl-Shift-Delete and + // Ctrl-Shift-x are not treated as cut even though the underlying + // CRichTextEdit would treat them as such. + // Copy: Ctrl-v is treated as copy. Shift-Ctrl-v is not. + // Paste: Shift-Insert and Ctrl-v are tread as paste. Ctrl-Shift-Insert and + // Ctrl-Shift-v are not. + // + // This behavior matches most, but not all Windows programs, and largely + // conforms to what users expect. + + case VK_DELETE: + case 'X': + if ((flags & KF_ALTDOWN) || + (GetKeyState((key == 'X') ? VK_CONTROL : VK_SHIFT) >= 0)) + break; + if (GetKeyState((key == 'X') ? VK_SHIFT : VK_CONTROL) >= 0) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + Cut(); + OnAfterPossibleChange(); + } + return; + + case 'C': + if ((flags & KF_ALTDOWN) || (GetKeyState(VK_CONTROL) >= 0)) + break; + if (GetKeyState(VK_SHIFT) >= 0) + Copy(); + return; + + case VK_INSERT: + case 'V': + if ((flags & KF_ALTDOWN) || + (GetKeyState((key == 'V') ? VK_CONTROL : VK_SHIFT) >= 0)) + break; + if (GetKeyState((key == 'V') ? VK_SHIFT : VK_CONTROL) >= 0) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + Paste(); + OnAfterPossibleChange(); + } + return; + + case 0xbb: // Ctrl-'='. Triggers subscripting, even in plain text mode. + return; + + case VK_PROCESSKEY: + // This key event is consumed by an IME. + // We ignore this event because an IME sends WM_IME_COMPOSITION messages + // when it updates the CRichEditCtrl text. + return; + } + + // CRichEditCtrl changes its text on WM_KEYDOWN instead of WM_CHAR for many + // different keys (backspace, ctrl-v, ...), so we call this in both cases. + HandleKeystroke(GetCurrentMessage()->message, key, repeat_count, flags); +} + +void TextField::Edit::OnLButtonDblClk(UINT keys, const CPoint& point) { + // Save the double click info for later triple-click detection. + tracking_double_click_ = true; + double_click_point_ = point; + double_click_time_ = GetCurrentMessage()->time; + + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONDBLCLK, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y)); + OnAfterPossibleChange(); +} + +void TextField::Edit::OnLButtonDown(UINT keys, const CPoint& point) { + // Check for triple click, then reset tracker. Should be safe to subtract + // double_click_time_ from the current message's time even if the timer has + // wrapped in between. + const bool is_triple_click = tracking_double_click_ && + win_util::IsDoubleClick(double_click_point_, point, + GetCurrentMessage()->time - double_click_time_); + tracking_double_click_ = false; + + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONDOWN, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, is_triple_click), + point.y)); + OnAfterPossibleChange(); +} + +void TextField::Edit::OnLButtonUp(UINT keys, const CPoint& point) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONUP, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y)); + OnAfterPossibleChange(); +} + +void TextField::Edit::OnMouseLeave() { + SetContainsMouse(false); +} + +LRESULT TextField::Edit::OnMouseWheel(UINT message, + WPARAM w_param, LPARAM l_param) { + // Reroute the mouse-wheel to the window under the mouse pointer if + // applicable. + if (views::RerouteMouseWheel(m_hWnd, w_param, l_param)) + return 0; + return DefWindowProc(message, w_param, l_param);; +} + +void TextField::Edit::OnMouseMove(UINT keys, const CPoint& point) { + SetContainsMouse(true); + // Clamp the selection to the visible text so the user can't drag to select + // the "phantom newline". In theory we could achieve this by clipping the X + // coordinate, but in practice the edit seems to behave nondeterministically + // with similar sequences of clipped input coordinates fed to it. Maybe it's + // reading the mouse cursor position directly? + // + // This solution has a minor visual flaw, however: if there's a visible + // cursor at the edge of the text (only true when there's no selection), + // dragging the mouse around outside that edge repaints the cursor on every + // WM_MOUSEMOVE instead of allowing it to blink normally. To fix this, we + // special-case this exact case and discard the WM_MOUSEMOVE messages instead + // of passing them along. + // + // But even this solution has a flaw! (Argh.) In the case where the user + // has a selection that starts at the edge of the edit, and proceeds to the + // middle of the edit, and the user is dragging back past the start edge to + // remove the selection, there's a redraw problem where the change between + // having the last few bits of text still selected and having nothing + // selected can be slow to repaint (which feels noticeably strange). This + // occurs if you only let the edit receive a single WM_MOUSEMOVE past the + // edge of the text. I think on each WM_MOUSEMOVE the edit is repainting its + // previous state, then updating its internal variables to the new state but + // not repainting. To fix this, we allow one more WM_MOUSEMOVE through after + // the selection has supposedly been shrunk to nothing; this makes the edit + // redraw the selection quickly so it feels smooth. + CHARRANGE selection; + GetSel(selection); + const bool possibly_can_discard_mousemove = + (selection.cpMin == selection.cpMax) && + (((selection.cpMin == 0) && + (ClipXCoordToVisibleText(point.x, false) > point.x)) || + ((selection.cpMin == GetTextLength()) && + (ClipXCoordToVisibleText(point.x, false) < point.x))); + if (!can_discard_mousemove_ || !possibly_can_discard_mousemove) { + can_discard_mousemove_ = possibly_can_discard_mousemove; + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + // Force the Y coordinate to the center of the clip rect. The edit + // behaves strangely when the cursor is dragged vertically: if the cursor + // is in the middle of the text, drags inside the clip rect do nothing, + // and drags outside the clip rect act as if the cursor jumped to the + // left edge of the text. When the cursor is at the right edge, drags of + // just a few pixels vertically end up selecting the "phantom newline"... + // sometimes. + RECT r; + GetRect(&r); + DefWindowProc(WM_MOUSEMOVE, keys, + MAKELPARAM(point.x, (r.bottom - r.top) / 2)); + OnAfterPossibleChange(); + } +} + +int TextField::Edit::OnNCCalcSize(BOOL w_param, LPARAM l_param) { + content_insets_.Set(0, 0, 0, 0); + parent_->CalculateInsets(&content_insets_); + if (w_param) { + NCCALCSIZE_PARAMS* nc_params = + reinterpret_cast<NCCALCSIZE_PARAMS*>(l_param); + nc_params->rgrc[0].left += content_insets_.left(); + nc_params->rgrc[0].right -= content_insets_.right(); + nc_params->rgrc[0].top += content_insets_.top(); + nc_params->rgrc[0].bottom -= content_insets_.bottom(); + } else { + RECT* rect = reinterpret_cast<RECT*>(l_param); + rect->left += content_insets_.left(); + rect->right -= content_insets_.right(); + rect->top += content_insets_.top(); + rect->bottom -= content_insets_.bottom(); + } + return 0; +} + +void TextField::Edit::OnNCPaint(HRGN region) { + if (!draw_border_) + return; + + HDC hdc = GetWindowDC(); + + CRect window_rect; + GetWindowRect(&window_rect); + // Convert to be relative to 0x0. + window_rect.MoveToXY(0, 0); + + ExcludeClipRect(hdc, + window_rect.left + content_insets_.left(), + window_rect.top + content_insets_.top(), + window_rect.right - content_insets_.right(), + window_rect.bottom - content_insets_.bottom()); + + HBRUSH brush = CreateSolidBrush(bg_color_); + FillRect(hdc, &window_rect, brush); + DeleteObject(brush); + + int part; + int state; + + if (win_util::GetWinVersion() < win_util::WINVERSION_VISTA) { + part = EP_EDITTEXT; + + if (!parent_->IsEnabled()) + state = ETS_DISABLED; + else if (parent_->IsReadOnly()) + state = ETS_READONLY; + else if (!contains_mouse_) + state = ETS_NORMAL; + else + state = ETS_HOT; + } else { + part = EP_EDITBORDER_HVSCROLL; + + if (!parent_->IsEnabled()) + state = EPSHV_DISABLED; + else if (GetFocus() == m_hWnd) + state = EPSHV_FOCUSED; + else if (contains_mouse_) + state = EPSHV_HOT; + else + state = EPSHV_NORMAL; + // Vista doesn't appear to have a unique state for readonly. + } + + int classic_state = + (!parent_->IsEnabled() || parent_->IsReadOnly()) ? DFCS_INACTIVE : 0; + + NativeTheme::instance()->PaintTextField(hdc, part, state, classic_state, + &window_rect, bg_color_, false, + true); + + // NOTE: I tried checking the transparent property of the theme and invoking + // drawParentBackground, but it didn't seem to make a difference. + + ReleaseDC(hdc); +} + +void TextField::Edit::OnNonLButtonDown(UINT keys, const CPoint& point) { + // Interestingly, the edit doesn't seem to cancel triple clicking when the + // x-buttons (which usually means "thumb buttons") are pressed, so we only + // call this for M and R down. + tracking_double_click_ = false; + SetMsgHandled(false); +} + +void TextField::Edit::OnPaste() { + if (parent_->IsReadOnly()) + return; + + Clipboard* clipboard = g_browser_process->clipboard(); + + if (!clipboard->IsFormatAvailable(Clipboard::GetPlainTextWFormatType())) + return; + + std::wstring clipboard_str; + clipboard->ReadText(&clipboard_str); + if (!clipboard_str.empty()) { + std::wstring collapsed(CollapseWhitespace(clipboard_str, false)); + if (parent_->GetStyle() & STYLE_LOWERCASE) + collapsed = l10n_util::ToLower(collapsed); + // Force a Paste operation to trigger OnContentsChanged, even if identical + // contents are pasted into the text box. + text_before_change_.clear(); + ReplaceSel(collapsed.c_str(), true); + } +} + +void TextField::Edit::OnSysChar(TCHAR ch, UINT repeat_count, UINT flags) { + // Nearly all alt-<xxx> combos result in beeping rather than doing something + // useful, so we discard most. Exceptions: + // * ctrl-alt-<xxx>, which is sometimes important, generates WM_CHAR instead + // of WM_SYSCHAR, so it doesn't need to be handled here. + // * alt-space gets translated by the default WM_SYSCHAR handler to a + // WM_SYSCOMMAND to open the application context menu, so we need to allow + // it through. + if (ch == VK_SPACE) + SetMsgHandled(false); +} + +void TextField::Edit::HandleKeystroke(UINT message, + TCHAR key, + UINT repeat_count, + UINT flags) { + ScopedFreeze freeze(this, GetTextObjectModel()); + + TextField::Controller* controller = parent_->GetController(); + bool handled = false; + if (controller) { + handled = + controller->HandleKeystroke(parent_, message, key, repeat_count, flags); + } + + if (!handled) { + OnBeforePossibleChange(); + DefWindowProc(message, key, MAKELPARAM(repeat_count, flags)); + OnAfterPossibleChange(); + } +} + +void TextField::Edit::OnBeforePossibleChange() { + // Record our state. + text_before_change_ = GetText(); +} + +void TextField::Edit::OnAfterPossibleChange() { + // Prevent the user from selecting the "phantom newline" at the end of the + // edit. If they try, we just silently move the end of the selection back to + // the end of the real text. + CHARRANGE new_sel; + GetSel(new_sel); + const int length = GetTextLength(); + if (new_sel.cpMax > length) { + new_sel.cpMax = length; + if (new_sel.cpMin > length) + new_sel.cpMin = length; + SetSel(new_sel); + } + + std::wstring new_text(GetText()); + if (new_text != text_before_change_) { + if (ime_discard_composition_ && ime_composition_start_ >= 0 && + ime_composition_length_ > 0) { + // A string retrieved with a GetText() call contains a string being + // composed by an IME. We remove the composition string from this search + // string. + new_text.erase(ime_composition_start_, ime_composition_length_); + ime_composition_start_ = 0; + ime_composition_length_ = 0; + if (new_text.empty()) + return; + } + parent_->SyncText(); + if (parent_->GetController()) + parent_->GetController()->ContentsChanged(parent_, new_text); + } +} + +LONG TextField::Edit::ClipXCoordToVisibleText(LONG x, + bool is_triple_click) const { + // Clip the X coordinate to the left edge of the text. Careful: + // PosFromChar(0) may return a negative X coordinate if the beginning of the + // text has scrolled off the edit, so don't go past the clip rect's edge. + PARAFORMAT2 pf2; + GetParaFormat(pf2); + // Calculation of the clipped coordinate is more complicated if the paragraph + // layout is RTL layout, or if there is RTL characters inside the LTR layout + // paragraph. + bool ltr_text_in_ltr_layout = true; + if ((pf2.wEffects & PFE_RTLPARA) || + l10n_util::StringContainsStrongRTLChars(GetText())) { + ltr_text_in_ltr_layout = false; + } + const int length = GetTextLength(); + RECT r; + GetRect(&r); + // The values returned by PosFromChar() seem to refer always + // to the left edge of the character's bounding box. + const LONG first_position_x = PosFromChar(0).x; + LONG min_x = first_position_x; + if (!ltr_text_in_ltr_layout) { + for (int i = 1; i < length; ++i) + min_x = std::min(min_x, PosFromChar(i).x); + } + const LONG left_bound = std::max(r.left, min_x); + + // PosFromChar(length) is a phantom character past the end of the text. It is + // not necessarily a right bound; in RTL controls it may be a left bound. So + // treat it as a right bound only if it is to the right of the first + // character. + LONG right_bound = r.right; + LONG end_position_x = PosFromChar(length).x; + if (end_position_x >= first_position_x) { + right_bound = std::min(right_bound, end_position_x); // LTR case. + } + // For trailing characters that are 2 pixels wide of less (like "l" in some + // fonts), we have a problem: + // * Clicks on any pixel within the character will place the cursor before + // the character. + // * Clicks on the pixel just after the character will not allow triple- + // click to work properly (true for any last character width). + // So, we move to the last pixel of the character when this is a + // triple-click, and moving to one past the last pixel in all other + // scenarios. This way, all clicks that can move the cursor will place it at + // the end of the text, but triple-click will still work. + if (x < left_bound) { + return (is_triple_click && ltr_text_in_ltr_layout) ? left_bound - 1 : + left_bound; + } + if ((length == 0) || (x < right_bound)) + return x; + return is_triple_click ? (right_bound - 1) : right_bound; +} + +void TextField::Edit::SetContainsMouse(bool contains_mouse) { + if (contains_mouse == contains_mouse_) + return; + + contains_mouse_ = contains_mouse; + + if (!draw_border_) + return; + + if (contains_mouse_) { + // Register for notification when the mouse leaves. Need to do this so + // that we can reset contains mouse properly. + TRACKMOUSEEVENT tme; + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = m_hWnd; + tme.dwHoverTime = 0; + TrackMouseEvent(&tme); + } + RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_FRAME); +} + +ITextDocument* TextField::Edit::GetTextObjectModel() const { + if (!text_object_model_) { + CComPtr<IRichEditOle> ole_interface; + ole_interface.Attach(GetOleInterface()); + text_object_model_ = ole_interface; + } + return text_object_model_; +} + +///////////////////////////////////////////////////////////////////////////// +// TextField + +TextField::~TextField() { + if (edit_) { + // If the edit hwnd still exists, we need to destroy it explicitly. + if (*edit_) + edit_->DestroyWindow(); + delete edit_; + } +} + +void TextField::ViewHierarchyChanged(bool is_add, View* parent, View* child) { + Widget* widget; + + if (is_add && (widget = GetWidget())) { + // This notification is called from the AddChildView call below. Ignore it. + if (native_view_ && !edit_) + return; + + if (!native_view_) { + native_view_ = new HWNDView(); // Deleted from our superclass destructor + AddChildView(native_view_); + + // Maps the focus of the native control to the focus of this view. + native_view_->SetAssociatedFocusView(this); + } + + // If edit_ is invalid from a previous use. Reset it. + if (edit_ && !IsWindow(edit_->m_hWnd)) { + native_view_->Detach(); + delete edit_; + edit_ = NULL; + } + + if (!edit_) { + edit_ = new Edit(this, draw_border_); + edit_->SetFont(font_.hfont()); + native_view_->Attach(*edit_); + if (!text_.empty()) + edit_->SetText(text_); + UpdateEditBackgroundColor(); + Layout(); + } + } else if (!is_add && edit_ && IsWindow(edit_->m_hWnd)) { + edit_->SetParent(NULL); + } +} + +void TextField::Layout() { + if (native_view_) { + native_view_->SetBounds(GetLocalBounds(true)); + native_view_->Layout(); + } +} + +gfx::Size TextField::GetPreferredSize() { + gfx::Insets insets; + CalculateInsets(&insets); + return gfx::Size(font_.GetExpectedTextWidth(default_width_in_chars_) + + insets.width(), + num_lines_ * font_.height() + insets.height()); +} + +std::wstring TextField::GetText() const { + return text_; +} + +void TextField::SetText(const std::wstring& text) { + text_ = text; + if (edit_) + edit_->SetText(text); +} + +void TextField::AppendText(const std::wstring& text) { + text_ += text; + if (edit_) + edit_->AppendText(text); +} + +void TextField::CalculateInsets(gfx::Insets* insets) { + DCHECK(insets); + + if (!draw_border_) + return; + + // NOTE: One would think GetThemeMargins would return the insets we should + // use, but it doesn't. The margins returned by GetThemeMargins are always + // 0. + + // This appears to be the insets used by Windows. + insets->Set(3, 3, 3, 3); +} + +void TextField::SyncText() { + if (edit_) + text_ = edit_->GetText(); +} + +void TextField::SetController(Controller* controller) { + controller_ = controller; +} + +TextField::Controller* TextField::GetController() const { + return controller_; +} + +bool TextField::IsReadOnly() const { + return edit_ ? ((edit_->GetStyle() & ES_READONLY) != 0) : read_only_; +} + +bool TextField::IsPassword() const { + return GetStyle() & TextField::STYLE_PASSWORD; +} + +bool TextField::IsMultiLine() const { + return (style_ & STYLE_MULTILINE) != 0; +} + +void TextField::SetReadOnly(bool read_only) { + read_only_ = read_only; + if (edit_) { + edit_->SetReadOnly(read_only); + UpdateEditBackgroundColor(); + } +} + +void TextField::Focus() { + ::SetFocus(native_view_->GetHWND()); +} + +void TextField::SelectAll() { + if (edit_) + edit_->SelectAll(); +} + +void TextField::ClearSelection() const { + if (edit_) + edit_->ClearSelection(); +} + +HWND TextField::GetNativeComponent() { + return native_view_->GetHWND(); +} + +void TextField::SetBackgroundColor(SkColor color) { + background_color_ = color; + use_default_background_color_ = false; + UpdateEditBackgroundColor(); +} + +void TextField::SetDefaultBackgroundColor() { + use_default_background_color_ = true; + UpdateEditBackgroundColor(); +} + +void TextField::SetFont(const ChromeFont& font) { + font_ = font; + if (edit_) + edit_->SetFont(font.hfont()); +} + +ChromeFont TextField::GetFont() const { + return font_; +} + +bool TextField::SetHorizontalMargins(int left, int right) { + // SendMessage expects the two values to be packed into one using MAKELONG + // so we truncate to 16 bits if necessary. + return ERROR_SUCCESS == SendMessage(GetNativeComponent(), + (UINT) EM_SETMARGINS, + (WPARAM) EC_LEFTMARGIN | EC_RIGHTMARGIN, + (LPARAM) MAKELONG(left & 0xFFFF, + right & 0xFFFF)); +} + +void TextField::SetHeightInLines(int num_lines) { + DCHECK(IsMultiLine()); + num_lines_ = num_lines; +} + +void TextField::RemoveBorder() { + if (!draw_border_) + return; + + draw_border_ = false; + if (edit_) + edit_->RemoveBorder(); +} + +void TextField::SetEnabled(bool enabled) { + View::SetEnabled(enabled); + edit_->SetEnabled(enabled); +} + +bool TextField::IsFocusable() const { + return IsEnabled() && !IsReadOnly(); +} + +void TextField::AboutToRequestFocusFromTabTraversal(bool reverse) { + SelectAll(); +} + +bool TextField::ShouldLookupAccelerators(const KeyEvent& e) { + // TODO(hamaji): Figure out which keyboard combinations we need to add here, + // similar to LocationBarView::ShouldLookupAccelerators. + if (e.GetCharacter() == VK_BACK) + return false; // We'll handle BackSpace ourselves. + + // We don't translate accelerators for ALT + NumPad digit, they are used for + // entering special characters. + if (!e.IsAltDown()) + return true; + + return !win_util::IsNumPadDigit(e.GetCharacter(), e.IsExtendedKey()); +} + +void TextField::UpdateEditBackgroundColor() { + if (!edit_) + return; + + COLORREF bg_color; + if (!use_default_background_color_) + bg_color = skia::SkColorToCOLORREF(background_color_); + else + bg_color = GetSysColor(read_only_ ? COLOR_3DFACE : COLOR_WINDOW); + edit_->SetBackgroundColor(bg_color); +} + +} // namespace views diff --git a/views/controls/text_field.h b/views/controls/text_field.h new file mode 100644 index 0000000..3b030ba --- /dev/null +++ b/views/controls/text_field.h @@ -0,0 +1,208 @@ +// Copyright (c) 2009 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. + +// These classes define a text field widget that can be used in the views UI +// toolkit. + +#ifndef VIEWS_CONTROLS_TEXT_FIELD_H_ +#define VIEWS_CONTROLS_TEXT_FIELD_H_ + +#include <string> + +#include "app/gfx/chrome_font.h" +#include "base/basictypes.h" +#include "views/view.h" +#include "skia/include/SkColor.h" + +namespace views { + +class HWNDView; + +// This class implements a ChromeView that wraps a native text (edit) field. +class TextField : public View { + public: + // This defines the callback interface for other code to be notified of + // changes in the state of a text field. + class Controller { + public: + // This method is called whenever the text in the field changes. + virtual void ContentsChanged(TextField* sender, + const std::wstring& new_contents) = 0; + + // This method is called to get notified about keystrokes in the edit. + // This method returns true if the message was handled and should not be + // processed further. If it returns false the processing continues. + virtual bool HandleKeystroke(TextField* sender, + UINT message, TCHAR key, UINT repeat_count, + UINT flags) = 0; + }; + + enum StyleFlags { + STYLE_DEFAULT = 0, + STYLE_PASSWORD = 1<<0, + STYLE_MULTILINE = 1<<1, + STYLE_LOWERCASE = 1<<2 + }; + + TextField::TextField() + : native_view_(NULL), + edit_(NULL), + controller_(NULL), + style_(STYLE_DEFAULT), + read_only_(false), + default_width_in_chars_(0), + draw_border_(true), + use_default_background_color_(true), + num_lines_(1) { + SetFocusable(true); + } + explicit TextField::TextField(StyleFlags style) + : native_view_(NULL), + edit_(NULL), + controller_(NULL), + style_(style), + read_only_(false), + default_width_in_chars_(0), + draw_border_(true), + use_default_background_color_(true), + num_lines_(1) { + SetFocusable(true); + } + virtual ~TextField(); + + void ViewHierarchyChanged(bool is_add, View* parent, View* child); + + // Overridden for layout purposes + virtual void Layout(); + virtual gfx::Size GetPreferredSize(); + + // Controller accessors + void SetController(Controller* controller); + Controller* GetController() const; + + void SetReadOnly(bool read_only); + bool IsReadOnly() const; + + bool IsPassword() const; + + // Whether the text field is multi-line or not, must be set when the text + // field is created, using StyleFlags. + bool IsMultiLine() const; + + virtual bool IsFocusable() const; + virtual void AboutToRequestFocusFromTabTraversal(bool reverse); + + // Overridden from Chrome::View. + virtual bool ShouldLookupAccelerators(const KeyEvent& e); + + virtual HWND GetNativeComponent(); + + // Returns the text currently displayed in the text field. + std::wstring GetText() const; + + // Sets the text currently displayed in the text field. + void SetText(const std::wstring& text); + + // Appends the given string to the previously-existing text in the field. + void AppendText(const std::wstring& text); + + virtual void Focus(); + + // Causes the edit field to be fully selected. + void SelectAll(); + + // Clears the selection within the edit field and sets the caret to the end. + void ClearSelection() const; + + StyleFlags GetStyle() const { return style_; } + + void SetBackgroundColor(SkColor color); + void SetDefaultBackgroundColor(); + + // Set the font. + void SetFont(const ChromeFont& font); + + // Return the font used by this TextField. + ChromeFont GetFont() const; + + // Sets the left and right margin (in pixels) within the text box. On Windows + // this is accomplished by packing the left and right margin into a single + // 32 bit number, so the left and right margins are effectively 16 bits. + bool SetHorizontalMargins(int left, int right); + + // Should only be called on a multi-line text field. Sets how many lines of + // text can be displayed at once by this text field. + void SetHeightInLines(int num_lines); + + // Sets the default width of the text control. See default_width_in_chars_. + void set_default_width_in_chars(int default_width) { + default_width_in_chars_ = default_width; + } + + // Removes the border from the edit box, giving it a 2D look. + void RemoveBorder(); + + // Disable the edit control. + // NOTE: this does NOT change the read only property. + void SetEnabled(bool enabled); + + private: + class Edit; + + // Invoked by the edit control when the value changes. This method set + // the text_ member variable to the value contained in edit control. + // This is important because the edit control can be replaced if it has + // been deleted during a window close. + void SyncText(); + + // Reset the text field native control. + void ResetNativeControl(); + + // Resets the background color of the edit. + void UpdateEditBackgroundColor(); + + // This encapsulates the HWND of the native text field. + HWNDView* native_view_; + + // This inherits from the native text field. + Edit* edit_; + + // This is the current listener for events from this control. + Controller* controller_; + + StyleFlags style_; + + ChromeFont font_; + + // NOTE: this is temporary until we rewrite TextField to always work whether + // there is an HWND or not. + // Used if the HWND hasn't been created yet. + std::wstring text_; + + bool read_only_; + + // The default number of average characters for the width of this text field. + // This will be reported as the "desired size". Defaults to 0. + int default_width_in_chars_; + + // Whether the border is drawn. + bool draw_border_; + + SkColor background_color_; + + bool use_default_background_color_; + + // The number of lines of text this textfield displays at once. + int num_lines_; + + protected: + // Calculates the insets for the text field. + void CalculateInsets(gfx::Insets* insets); + + DISALLOW_COPY_AND_ASSIGN(TextField); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TEXT_FIELD_H_ diff --git a/views/controls/throbber.cc b/views/controls/throbber.cc new file mode 100644 index 0000000..d230c55 --- /dev/null +++ b/views/controls/throbber.cc @@ -0,0 +1,170 @@ +// Copyright (c) 2006-2008 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/controls/throbber.h" + +#include "app/gfx/chrome_canvas.h" +#include "app/resource_bundle.h" +#include "base/time.h" +#include "grit/theme_resources.h" +#include "skia/include/SkBitmap.h" + +using base::Time; +using base::TimeDelta; + +namespace views { + +Throbber::Throbber(int frame_time_ms, + bool paint_while_stopped) + : running_(false), + paint_while_stopped_(paint_while_stopped), + frames_(NULL), + frame_time_(TimeDelta::FromMilliseconds(frame_time_ms)) { + ResourceBundle &rb = ResourceBundle::GetSharedInstance(); + frames_ = rb.GetBitmapNamed(IDR_THROBBER); + DCHECK(frames_->width() > 0 && frames_->height() > 0); + DCHECK(frames_->width() % frames_->height() == 0); + frame_count_ = frames_->width() / frames_->height(); +} + +Throbber::~Throbber() { + Stop(); +} + +void Throbber::Start() { + if (running_) + return; + + start_time_ = Time::Now(); + + timer_.Start(frame_time_ - TimeDelta::FromMilliseconds(10), + this, &Throbber::Run); + + running_ = true; + + SchedulePaint(); // paint right away +} + +void Throbber::Stop() { + if (!running_) + return; + + timer_.Stop(); + + running_ = false; + SchedulePaint(); // Important if we're not painting while stopped +} + +void Throbber::Run() { + DCHECK(running_); + + SchedulePaint(); +} + +gfx::Size Throbber::GetPreferredSize() { + return gfx::Size(frames_->height(), frames_->height()); +} + +void Throbber::Paint(ChromeCanvas* canvas) { + if (!running_ && !paint_while_stopped_) + return; + + const TimeDelta elapsed_time = Time::Now() - start_time_; + const int current_frame = + static_cast<int>(elapsed_time / frame_time_) % frame_count_; + + int image_size = frames_->height(); + int image_offset = current_frame * image_size; + canvas->DrawBitmapInt(*frames_, + image_offset, 0, image_size, image_size, + 0, 0, image_size, image_size, + false); +} + + + +// Smoothed throbber --------------------------------------------------------- + + +// Delay after work starts before starting throbber, in milliseconds. +static const int kStartDelay = 200; + +// Delay after work stops before stopping, in milliseconds. +static const int kStopDelay = 50; + + +SmoothedThrobber::SmoothedThrobber(int frame_time_ms) + : Throbber(frame_time_ms, /* paint_while_stopped= */ false) { +} + +void SmoothedThrobber::Start() { + stop_timer_.Stop(); + + if (!running_ && !start_timer_.IsRunning()) { + start_timer_.Start(TimeDelta::FromMilliseconds(kStartDelay), this, + &SmoothedThrobber::StartDelayOver); + } +} + +void SmoothedThrobber::StartDelayOver() { + Throbber::Start(); +} + +void SmoothedThrobber::Stop() { + if (!running_) + start_timer_.Stop(); + + stop_timer_.Stop(); + stop_timer_.Start(TimeDelta::FromMilliseconds(kStopDelay), this, + &SmoothedThrobber::StopDelayOver); +} + +void SmoothedThrobber::StopDelayOver() { + Throbber::Stop(); +} + +// Checkmark throbber --------------------------------------------------------- + +CheckmarkThrobber::CheckmarkThrobber() + : Throbber(kFrameTimeMs, false), + checked_(false) { + InitClass(); +} + +void CheckmarkThrobber::SetChecked(bool checked) { + bool changed = checked != checked_; + if (changed) { + checked_ = checked; + SchedulePaint(); + } +} + +void CheckmarkThrobber::Paint(ChromeCanvas* canvas) { + if (running_) { + // Let the throbber throb... + Throbber::Paint(canvas); + return; + } + // Otherwise we paint our tick mark or nothing depending on our state. + if (checked_) { + int checkmark_x = (width() - checkmark_->width()) / 2; + int checkmark_y = (height() - checkmark_->height()) / 2; + canvas->DrawBitmapInt(*checkmark_, checkmark_x, checkmark_y); + } +} + +// static +void CheckmarkThrobber::InitClass() { + static bool initialized = false; + if (!initialized) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + checkmark_ = rb.GetBitmapNamed(IDR_INPUT_GOOD); + initialized = true; + } +} + +// static +SkBitmap* CheckmarkThrobber::checkmark_ = NULL; + +} // namespace views diff --git a/views/controls/throbber.h b/views/controls/throbber.h new file mode 100644 index 0000000..bd0e67a --- /dev/null +++ b/views/controls/throbber.h @@ -0,0 +1,111 @@ +// Copyright (c) 2006-2008 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. + +// Throbbers display an animation, usually used as a status indicator. + +#ifndef VIEWS_CONTROLS_THROBBER_H_ +#define VIEWS_CONTROLS_THROBBER_H_ + +#include "base/basictypes.h" +#include "base/time.h" +#include "base/timer.h" +#include "views/view.h" + +class SkBitmap; + +namespace views { + +class Throbber : public View { + public: + // |frame_time_ms| is the amount of time that should elapse between frames + // (in milliseconds) + // If |paint_while_stopped| is false, this view will be invisible when not + // running. + Throbber(int frame_time_ms, bool paint_while_stopped); + virtual ~Throbber(); + + // Start and stop the throbber animation + virtual void Start(); + virtual void Stop(); + + // overridden from View + virtual gfx::Size GetPreferredSize(); + virtual void Paint(ChromeCanvas* canvas); + + protected: + // Specifies whether the throbber is currently animating or not + bool running_; + + private: + void Run(); + + bool paint_while_stopped_; + int frame_count_; // How many frames we have. + base::Time start_time_; // Time when Start was called. + SkBitmap* frames_; // Frames bitmaps. + base::TimeDelta frame_time_; // How long one frame is displayed. + base::RepeatingTimer<Throbber> timer_; // Used to schedule Run calls. + + DISALLOW_COPY_AND_ASSIGN(Throbber); +}; + +// A SmoothedThrobber is a throbber that is representing potentially short +// and nonoverlapping bursts of work. SmoothedThrobber ignores small +// pauses in the work stops and starts, and only starts its throbber after +// a small amount of work time has passed. +class SmoothedThrobber : public Throbber { + public: + SmoothedThrobber(int frame_delay_ms); + + virtual void Start(); + virtual void Stop(); + + private: + // Called when the startup-delay timer fires + // This function starts the actual throbbing. + void StartDelayOver(); + + // Called when the shutdown-delay timer fires. + // This function stops the actual throbbing. + void StopDelayOver(); + + base::OneShotTimer<SmoothedThrobber> start_timer_; + base::OneShotTimer<SmoothedThrobber> stop_timer_; + + DISALLOW_COPY_AND_ASSIGN(SmoothedThrobber); +}; + +// A CheckmarkThrobber is a special variant of throbber that has three states: +// 1. not yet completed (which paints nothing) +// 2. working (which paints the throbber animation) +// 3. completed (which paints a checkmark) +// +class CheckmarkThrobber : public Throbber { + public: + CheckmarkThrobber(); + + // If checked is true, the throbber stops spinning and displays a checkmark. + // If checked is false, the throbber stops spinning and displays nothing. + void SetChecked(bool checked); + + // Overridden from Throbber: + virtual void Paint(ChromeCanvas* canvas); + + private: + static const int kFrameTimeMs = 30; + + static void InitClass(); + + // Whether or not we should display a checkmark. + bool checked_; + + // The checkmark image. + static SkBitmap* checkmark_; + + DISALLOW_COPY_AND_ASSIGN(CheckmarkThrobber); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_THROBBER_H_ diff --git a/views/controls/tree/tree_model.h b/views/controls/tree/tree_model.h new file mode 100644 index 0000000..7fec5a8 --- /dev/null +++ b/views/controls/tree/tree_model.h @@ -0,0 +1,91 @@ +// Copyright (c) 2008 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 VIEWS_CONTROLS_TREE_TREE_MODEL_H_ +#define VIEWS_CONTROLS_TREE_TREE_MODEL_H_ + +#include <string> + +#include "base/logging.h" + +class SkBitmap; + +namespace views { + +class TreeModel; + +// TreeModelNode -------------------------------------------------------------- + +// Type of class returned from the model. +class TreeModelNode { + public: + // Returns the title for the node. + virtual std::wstring GetTitle() = 0; +}; + +// Observer for the TreeModel. Notified of significant events to the model. +class TreeModelObserver { + public: + // Notification that nodes were added to the specified parent. + virtual void TreeNodesAdded(TreeModel* model, + TreeModelNode* parent, + int start, + int count) = 0; + + // Notification that nodes were removed from the specified parent. + virtual void TreeNodesRemoved(TreeModel* model, + TreeModelNode* parent, + int start, + int count) = 0; + + // Notification the children of |parent| have been reordered. Note, only + // the direct children of |parent| have been reordered, not descendants. + virtual void TreeNodeChildrenReordered(TreeModel* model, + TreeModelNode* parent) = 0; + + // Notification that the contents of a node has changed. + virtual void TreeNodeChanged(TreeModel* model, TreeModelNode* node) = 0; +}; + +// TreeModel ------------------------------------------------------------------ + +// The model for TreeView. +class TreeModel { + public: + // Returns the root of the tree. This may or may not be shown in the tree, + // see SetRootShown for details. + virtual TreeModelNode* GetRoot() = 0; + + // Returns the number of children in the specified node. + virtual int GetChildCount(TreeModelNode* parent) = 0; + + // Returns the child node at the specified index. + virtual TreeModelNode* GetChild(TreeModelNode* parent, int index) = 0; + + // Returns the parent of a node, or NULL if node is the root. + virtual TreeModelNode* GetParent(TreeModelNode* node) = 0; + + // Sets the observer of the model. + virtual void SetObserver(TreeModelObserver* observer) = 0; + + // Sets the title of the specified node. + // This is only invoked if the node is editable and the user edits a node. + virtual void SetTitle(TreeModelNode* node, + const std::wstring& title) { + NOTREACHED(); + } + + // Returns the set of icons for the nodes in the tree. You only need override + // this if you don't want to use the default folder icons. + virtual void GetIcons(std::vector<SkBitmap>* icons) {} + + // Returns the index of the icon to use for |node|. Return -1 to use the + // default icon. The index is relative to the list of icons returned from + // GetIcons. + virtual int GetIconIndex(TreeModelNode* node) { return -1; } +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TREE_TREE_MODEL_H_ diff --git a/views/controls/tree/tree_node_iterator.h b/views/controls/tree/tree_node_iterator.h new file mode 100644 index 0000000..76618e7 --- /dev/null +++ b/views/controls/tree/tree_node_iterator.h @@ -0,0 +1,74 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_TREE_TREE_NODE_ITERATOR_H_ +#define VIEWS_CONTROLS_TREE_TREE_NODE_ITERATOR_H_ + +#include <stack> + +#include "base/basictypes.h" +#include "base/logging.h" + +namespace views { + +// Iterator that iterates over the descendants of a node. The iteration does +// not include the node itself, only the descendants. The following illustrates +// typical usage: +// while (iterator.has_next()) { +// Node* node = iterator.Next(); +// // do something with node. +// } +template <class NodeType> +class TreeNodeIterator { + public: + explicit TreeNodeIterator(NodeType* node) { + if (node->GetChildCount() > 0) + positions_.push(Position<NodeType>(node, 0)); + } + + // Returns true if there are more descendants. + bool has_next() const { return !positions_.empty(); } + + // Returns the next descendant. + NodeType* Next() { + if (!has_next()) { + NOTREACHED(); + return NULL; + } + + NodeType* result = positions_.top().node->GetChild(positions_.top().index); + + // Make sure we don't attempt to visit result again. + positions_.top().index++; + + // Iterate over result's children. + positions_.push(Position<NodeType>(result, 0)); + + // Advance to next position. + while (!positions_.empty() && positions_.top().index >= + positions_.top().node->GetChildCount()) { + positions_.pop(); + } + + return result; + } + + private: + template <class PositionNodeType> + struct Position { + Position(PositionNodeType* node, int index) : node(node), index(index) {} + Position() : node(NULL), index(-1) {} + + PositionNodeType* node; + int index; + }; + + std::stack<Position<NodeType> > positions_; + + DISALLOW_COPY_AND_ASSIGN(TreeNodeIterator); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TREE_TREE_NODE_ITERATOR_H_ diff --git a/views/controls/tree/tree_node_iterator_unittest.cc b/views/controls/tree/tree_node_iterator_unittest.cc new file mode 100644 index 0000000..e5353fc --- /dev/null +++ b/views/controls/tree/tree_node_iterator_unittest.cc @@ -0,0 +1,41 @@ +// Copyright (c) 2006-2008 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/controls/tree/tree_node_iterator.h" +#include "views/controls/tree/tree_node_model.h" + +typedef testing::Test TreeNodeIteratorTest; + +using views::TreeNodeWithValue; + +TEST_F(TreeNodeIteratorTest, Test) { + TreeNodeWithValue<int> root; + root.Add(0, new TreeNodeWithValue<int>(1)); + root.Add(1, new TreeNodeWithValue<int>(2)); + TreeNodeWithValue<int>* f3 = new TreeNodeWithValue<int>(3); + root.Add(2, f3); + TreeNodeWithValue<int>* f4 = new TreeNodeWithValue<int>(4); + f3->Add(0, f4); + f4->Add(0, new TreeNodeWithValue<int>(5)); + + views::TreeNodeIterator<TreeNodeWithValue<int> > iterator(&root); + ASSERT_TRUE(iterator.has_next()); + ASSERT_EQ(root.GetChild(0), iterator.Next()); + + ASSERT_TRUE(iterator.has_next()); + ASSERT_EQ(root.GetChild(1), iterator.Next()); + + ASSERT_TRUE(iterator.has_next()); + ASSERT_EQ(root.GetChild(2), iterator.Next()); + + ASSERT_TRUE(iterator.has_next()); + ASSERT_EQ(f4, iterator.Next()); + + ASSERT_TRUE(iterator.has_next()); + ASSERT_EQ(f4->GetChild(0), iterator.Next()); + + ASSERT_FALSE(iterator.has_next()); +} diff --git a/views/controls/tree/tree_node_model.h b/views/controls/tree/tree_node_model.h new file mode 100644 index 0000000..f1e1c16 --- /dev/null +++ b/views/controls/tree/tree_node_model.h @@ -0,0 +1,283 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_TREE_TREE_NODE_MODEL_H_ +#define VIEWS_CONTROLS_TREE_TREE_NODE_MODEL_H_ + +#include <algorithm> +#include <vector> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "base/scoped_vector.h" +#include "base/stl_util-inl.h" +#include "views/controls/tree/tree_model.h" + +namespace views { + +// TreeNodeModel and TreeNodes provide an implementation of TreeModel around +// TreeNodes. TreeNodes form a directed acyclic graph. +// +// TreeNodes own their children, so that deleting a node deletes all +// descendants. +// +// TreeNodes do NOT maintain a pointer back to the model. As such, if you +// are using TreeNodes with a TreeNodeModel you will need to notify the observer +// yourself any time you make any change directly to the TreeNodes. For example, +// if you directly invoke SetTitle on a node it does not notify the +// observer, you will need to do it yourself. This includes the following +// methods: SetTitle, Remove and Add. TreeNodeModel provides cover +// methods that mutate the TreeNodes and notify the observer. If you are using +// TreeNodes with a TreeNodeModel use the cover methods to save yourself the +// headache. +// +// The following example creates a TreeNode with two children and then +// creates a TreeNodeModel from it: +// +// TreeNodeWithValue<int> root = new TreeNodeWithValue<int>(0, L"root"); +// root.add(new TreeNodeWithValue<int>(1, L"child 1")); +// root.add(new TreeNodeWithValue<int>(1, L"child 2")); +// TreeNodeModel<TreeNodeWithValue<int>>* model = +// new TreeNodeModel<TreeNodeWithValue<int>>(root); +// +// Two variants of TreeNode are provided here: +// +// . TreeNode itself is intended for subclassing. It has one type parameter +// that corresponds to the type of the node. When subclassing use your class +// name as the type parameter, eg: +// class MyTreeNode : public TreeNode<MyTreeNode> . +// . TreeNodeWithValue is a trivial subclass of TreeNode that has one type +// type parameter: a value type that is associated with the node. +// +// Which you use depends upon the situation. If you want to subclass and add +// methods, then use TreeNode. If you don't need any extra methods and just +// want to associate a value with each node, then use TreeNodeWithValue. +// +// Regardless of which TreeNode you use, if you are using the nodes with a +// TreeView take care to notify the observer when mutating the nodes. + +template <class NodeType> +class TreeNodeModel; + +// TreeNode ------------------------------------------------------------------- + +template <class NodeType> +class TreeNode : public TreeModelNode { + public: + TreeNode() : parent_(NULL) { } + + explicit TreeNode(const std::wstring& title) + : title_(title), parent_(NULL) {} + + virtual ~TreeNode() { + } + + // Adds the specified child node. + virtual void Add(int index, NodeType* child) { + DCHECK(child && index >= 0 && index <= GetChildCount()); + // If the node has a parent, remove it from its parent. + NodeType* node_parent = child->GetParent(); + if (node_parent) + node_parent->Remove(node_parent->IndexOfChild(child)); + child->parent_ = static_cast<NodeType*>(this); + children_->insert(children_->begin() + index, child); + } + + // Removes the node by index. This does NOT delete the specified node, it is + // up to the caller to delete it when done. + virtual NodeType* Remove(int index) { + DCHECK(index >= 0 && index < GetChildCount()); + NodeType* node = GetChild(index); + node->parent_ = NULL; + children_->erase(index + children_->begin()); + return node; + } + + // Removes all the children from this node. This does NOT delete the nodes. + void RemoveAll() { + for (size_t i = 0; i < children_->size(); ++i) + children_[i]->parent_ = NULL; + children_->clear(); + } + + // Returns the number of children. + int GetChildCount() { + return static_cast<int>(children_->size()); + } + + // Returns a child by index. + NodeType* GetChild(int index) { + DCHECK(index >= 0 && index < GetChildCount()); + return children_[index]; + } + + // Returns the parent. + NodeType* GetParent() { + return parent_; + } + + // Returns the index of the specified child, or -1 if node is a not a child. + int IndexOfChild(const NodeType* node) { + DCHECK(node); + typename std::vector<NodeType*>::iterator i = + std::find(children_->begin(), children_->end(), node); + if (i != children_->end()) + return static_cast<int>(i - children_->begin()); + return -1; + } + + // Sets the title of the node. + void SetTitle(const std::wstring& string) { + title_ = string; + } + + // Returns the title of the node. + std::wstring GetTitle() { + return title_; + } + + // Returns true if this is the root. + bool IsRoot() { return (parent_ == NULL); } + + // Returns true if this == ancestor, or one of this nodes parents is + // ancestor. + bool HasAncestor(NodeType* ancestor) const { + if (ancestor == this) + return true; + if (!ancestor) + return false; + return parent_ ? parent_->HasAncestor(ancestor) : false; + } + + protected: + std::vector<NodeType*>& children() { return children_.get(); } + + private: + // Title displayed in the tree. + std::wstring title_; + + NodeType* parent_; + + // Children. + ScopedVector<NodeType> children_; + + DISALLOW_COPY_AND_ASSIGN(TreeNode); +}; + +// TreeNodeWithValue ---------------------------------------------------------- + +template <class ValueType> +class TreeNodeWithValue : public TreeNode< TreeNodeWithValue<ValueType> > { + private: + typedef TreeNode< TreeNodeWithValue<ValueType> > ParentType; + + public: + TreeNodeWithValue() { } + + TreeNodeWithValue(const ValueType& value) + : ParentType(std::wstring()), value(value) { } + + TreeNodeWithValue(const std::wstring& title, const ValueType& value) + : ParentType(title), value(value) { } + + ValueType value; + + private: + DISALLOW_COPY_AND_ASSIGN(TreeNodeWithValue); +}; + +// TreeNodeModel -------------------------------------------------------------- + +// TreeModel implementation intended to be used with TreeNodes. +template <class NodeType> +class TreeNodeModel : public TreeModel { + public: + // Creates a TreeNodeModel with the specified root node. The root is owned + // by the TreeNodeModel. + explicit TreeNodeModel(NodeType* root) + : root_(root), + observer_(NULL) { + } + + virtual ~TreeNodeModel() {} + + virtual void SetObserver(TreeModelObserver* observer) { + observer_ = observer; + } + + TreeModelObserver* GetObserver() { + return observer_; + } + + // TreeModel methods, all forward to the nodes. + virtual NodeType* GetRoot() { return root_.get(); } + + virtual int GetChildCount(TreeModelNode* parent) { + DCHECK(parent); + return AsNode(parent)->GetChildCount(); + } + + virtual NodeType* GetChild(TreeModelNode* parent, int index) { + DCHECK(parent); + return AsNode(parent)->GetChild(index); + } + + virtual TreeModelNode* GetParent(TreeModelNode* node) { + DCHECK(node); + return AsNode(node)->GetParent(); + } + + NodeType* AsNode(TreeModelNode* model_node) { + return reinterpret_cast<NodeType*>(model_node); + } + + // Sets the title of the specified node. + virtual void SetTitle(TreeModelNode* node, + const std::wstring& title) { + DCHECK(node); + AsNode(node)->SetTitle(title); + NotifyObserverTreeNodeChanged(node); + } + + void Add(NodeType* parent, int index, NodeType* child) { + DCHECK(parent && child); + parent->Add(index, child); + NotifyObserverTreeNodesAdded(parent, index, 1); + } + + NodeType* Remove(NodeType* parent, int index) { + DCHECK(parent); + NodeType* child = parent->Remove(index); + NotifyObserverTreeNodesRemoved(parent, index, 1); + return child; + } + + void NotifyObserverTreeNodesAdded(NodeType* parent, int start, int count) { + if (observer_) + observer_->TreeNodesAdded(this, parent, start, count); + } + + void NotifyObserverTreeNodesRemoved(NodeType* parent, int start, int count) { + if (observer_) + observer_->TreeNodesRemoved(this, parent, start, count); + } + + virtual void NotifyObserverTreeNodeChanged(TreeModelNode* node) { + if (observer_) + observer_->TreeNodeChanged(this, node); + } + + private: + // The root. + scoped_ptr<NodeType> root_; + + // The observer. + TreeModelObserver* observer_; + + DISALLOW_COPY_AND_ASSIGN(TreeNodeModel); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TREE_TREE_NODE_MODEL_H_ diff --git a/views/controls/tree/tree_view.cc b/views/controls/tree/tree_view.cc new file mode 100644 index 0000000..08f2255 --- /dev/null +++ b/views/controls/tree/tree_view.cc @@ -0,0 +1,745 @@ +// Copyright (c) 2006-2008 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/controls/tree/tree_view.h" + +#include <shellapi.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/icon_util.h" +#include "app/l10n_util.h" +#include "app/l10n_util_win.h" +#include "app/resource_bundle.h" +#include "base/stl_util-inl.h" +#include "base/win_util.h" +#include "grit/theme_resources.h" +#include "views/focus/focus_manager.h" +#include "views/widget/widget.h" + +namespace views { + +TreeView::TreeView() + : tree_view_(NULL), + model_(NULL), + editable_(true), + next_id_(0), + controller_(NULL), + editing_node_(NULL), + root_shown_(true), + process_enter_(false), + show_context_menu_only_when_node_selected_(true), + select_on_right_mouse_down_(true), + wrapper_(this), + original_handler_(NULL), + drag_enabled_(false), + has_custom_icons_(false), + image_list_(NULL) { +} + +TreeView::~TreeView() { + if (model_) + model_->SetObserver(NULL); + // Both param_to_details_map_ and node_to_details_map_ have the same value, + // as such only need to delete from one. + STLDeleteContainerPairSecondPointers(id_to_details_map_.begin(), + id_to_details_map_.end()); + if (image_list_) + ImageList_Destroy(image_list_); +} + +void TreeView::SetModel(TreeModel* model) { + if (model == model_) + return; + if(model_ && tree_view_) + DeleteRootItems(); + if (model_) + model_->SetObserver(NULL); + model_ = model; + if (tree_view_ && model_) { + CreateRootItems(); + model_->SetObserver(this); + HIMAGELIST last_image_list = image_list_; + image_list_ = CreateImageList(); + TreeView_SetImageList(tree_view_, image_list_, TVSIL_NORMAL); + if (last_image_list) + ImageList_Destroy(last_image_list); + } +} + +// Sets whether the user can edit the nodes. The default is true. +void TreeView::SetEditable(bool editable) { + if (editable == editable_) + return; + editable_ = editable; + if (!tree_view_) + return; + LONG_PTR style = GetWindowLongPtr(tree_view_, GWL_STYLE); + style &= ~TVS_EDITLABELS; + SetWindowLongPtr(tree_view_, GWL_STYLE, style); +} + +void TreeView::StartEditing(TreeModelNode* node) { + DCHECK(node && tree_view_); + // Cancel the current edit. + CancelEdit(); + // Make sure all ancestors are expanded. + if (model_->GetParent(node)) + Expand(model_->GetParent(node)); + const NodeDetails* details = GetNodeDetails(node); + // Tree needs focus for editing to work. + SetFocus(tree_view_); + // Select the node, else if the user commits the edit the selection reverts. + SetSelectedNode(node); + TreeView_EditLabel(tree_view_, details->tree_item); +} + +void TreeView::CancelEdit() { + DCHECK(tree_view_); + TreeView_EndEditLabelNow(tree_view_, TRUE); +} + +void TreeView::CommitEdit() { + DCHECK(tree_view_); + TreeView_EndEditLabelNow(tree_view_, FALSE); +} + +TreeModelNode* TreeView::GetEditingNode() { + // I couldn't find a way to dynamically query for this, so it is cached. + return editing_node_; +} + +void TreeView::SetSelectedNode(TreeModelNode* node) { + DCHECK(tree_view_); + if (!node) { + TreeView_SelectItem(tree_view_, NULL); + return; + } + if (node != model_->GetRoot()) + Expand(model_->GetParent(node)); + if (!root_shown_ && node == model_->GetRoot()) { + // If the root isn't shown, we can't select it, clear out the selection + // instead. + TreeView_SelectItem(tree_view_, NULL); + } else { + // Select the node and make sure it is visible. + TreeView_SelectItem(tree_view_, GetNodeDetails(node)->tree_item); + } +} + +TreeModelNode* TreeView::GetSelectedNode() { + if (!tree_view_) + return NULL; + HTREEITEM selected_item = TreeView_GetSelection(tree_view_); + if (!selected_item) + return NULL; + NodeDetails* details = GetNodeDetailsByTreeItem(selected_item); + DCHECK(details); + return details->node; +} + +void TreeView::Expand(TreeModelNode* node) { + DCHECK(model_ && node); + if (!root_shown_ && model_->GetRoot() == node) { + // Can only expand the root if it is showing. + return; + } + TreeModelNode* parent = model_->GetParent(node); + if (parent) { + // Make sure all the parents are expanded. + Expand(parent); + } + // And expand this item. + TreeView_Expand(tree_view_, GetNodeDetails(node)->tree_item, TVE_EXPAND); +} + +void TreeView::ExpandAll() { + DCHECK(model_); + ExpandAll(model_->GetRoot()); +} + +void TreeView::ExpandAll(TreeModelNode* node) { + DCHECK(node); + // Expand the node. + if (node != model_->GetRoot() || root_shown_) + TreeView_Expand(tree_view_, GetNodeDetails(node)->tree_item, TVE_EXPAND); + // And recursively expand all the children. + for (int i = model_->GetChildCount(node) - 1; i >= 0; --i) { + TreeModelNode* child = model_->GetChild(node, i); + ExpandAll(child); + } +} + +bool TreeView::IsExpanded(TreeModelNode* node) { + TreeModelNode* parent = model_->GetParent(node); + if (!parent) + return true; + if (!IsExpanded(parent)) + return false; + NodeDetails* details = GetNodeDetails(node); + return (TreeView_GetItemState(tree_view_, details->tree_item, TVIS_EXPANDED) & + TVIS_EXPANDED) != 0; +} + +void TreeView::SetRootShown(bool root_shown) { + if (root_shown_ == root_shown) + return; + root_shown_ = root_shown; + if (!model_) + return; + // Repopulate the tree. + DeleteRootItems(); + CreateRootItems(); +} + +void TreeView::TreeNodesAdded(TreeModel* model, + TreeModelNode* parent, + int start, + int count) { + DCHECK(parent && start >= 0 && count > 0); + if (node_to_details_map_.find(parent) == node_to_details_map_.end()) { + // User hasn't navigated to this entry yet. Ignore the change. + return; + } + HTREEITEM parent_tree_item = NULL; + if (root_shown_ || parent != model_->GetRoot()) { + const NodeDetails* details = GetNodeDetails(parent); + if (!details->loaded_children) { + if (count == model_->GetChildCount(parent)) { + // Reset the treeviews child count. This triggers the treeview to call + // us back. + TV_ITEM tv_item = {0}; + tv_item.mask = TVIF_CHILDREN; + tv_item.cChildren = count; + tv_item.hItem = details->tree_item; + TreeView_SetItem(tree_view_, &tv_item); + } + + // Ignore the change, we haven't actually created entries in the tree + // for the children. + return; + } + parent_tree_item = details->tree_item; + } + + // The user has expanded this node, add the items to it. + for (int i = 0; i < count; ++i) { + if (i == 0 && start == 0) { + CreateItem(parent_tree_item, TVI_FIRST, model_->GetChild(parent, 0)); + } else { + TreeModelNode* previous_sibling = model_->GetChild(parent, i + start - 1); + CreateItem(parent_tree_item, + GetNodeDetails(previous_sibling)->tree_item, + model_->GetChild(parent, i + start)); + } + } +} + +void TreeView::TreeNodesRemoved(TreeModel* model, + TreeModelNode* parent, + int start, + int count) { + DCHECK(parent && start >= 0 && count > 0); + HTREEITEM parent_tree_item = GetTreeItemForNodeDuringMutation(parent); + if (!parent_tree_item) + return; + + // Find the last item. Windows doesn't offer a convenient way to get the + // TREEITEM at a particular index, so we iterate. + HTREEITEM tree_item = TreeView_GetChild(tree_view_, parent_tree_item); + for (int i = 0; i < (start + count - 1); ++i) { + tree_item = TreeView_GetNextSibling(tree_view_, tree_item); + } + // NOTE: the direction doesn't matter here. I've made it backwards to + // reinforce we're deleting from the end forward. + for (int i = count - 1; i >= 0; --i) { + HTREEITEM previous = (start + i) > 0 ? + TreeView_GetPrevSibling(tree_view_, tree_item) : NULL; + RecursivelyDelete(GetNodeDetailsByTreeItem(tree_item)); + tree_item = previous; + } +} + +namespace { + +// Callback function used to compare two items. The first two args are the +// LPARAMs of the HTREEITEMs being compared. The last arg maps from LPARAM +// to order. This is invoked from TreeNodeChildrenReordered. +int CALLBACK CompareTreeItems(LPARAM item1_lparam, + LPARAM item2_lparam, + LPARAM map_as_lparam) { + std::map<int, int>& mapping = + *reinterpret_cast<std::map<int, int>*>(map_as_lparam); + return mapping[static_cast<int>(item1_lparam)] - + mapping[static_cast<int>(item2_lparam)]; +} + +} // namespace + +void TreeView::TreeNodeChildrenReordered(TreeModel* model, + TreeModelNode* parent) { + DCHECK(parent); + if (model_->GetChildCount(parent) <= 1) + return; + + TVSORTCB sort_details; + sort_details.hParent = GetTreeItemForNodeDuringMutation(parent); + if (!sort_details.hParent) + return; + + std::map<int, int> lparam_to_order_map; + for (int i = 0; i < model_->GetChildCount(parent); ++i) { + TreeModelNode* node = model_->GetChild(parent, i); + lparam_to_order_map[GetNodeDetails(node)->id] = i; + } + + sort_details.lpfnCompare = &CompareTreeItems; + sort_details.lParam = reinterpret_cast<LPARAM>(&lparam_to_order_map); + TreeView_SortChildrenCB(tree_view_, &sort_details, 0); +} + +void TreeView::TreeNodeChanged(TreeModel* model, TreeModelNode* node) { + if (node_to_details_map_.find(node) == node_to_details_map_.end()) { + // User hasn't navigated to this entry yet. Ignore the change. + return; + } + const NodeDetails* details = GetNodeDetails(node); + TV_ITEM tv_item = {0}; + tv_item.mask = TVIF_TEXT; + tv_item.hItem = details->tree_item; + tv_item.pszText = LPSTR_TEXTCALLBACK; + TreeView_SetItem(tree_view_, &tv_item); +} + +gfx::Point TreeView::GetKeyboardContextMenuLocation() { + int y = height() / 2; + if (GetSelectedNode()) { + RECT bounds; + RECT client_rect; + if (TreeView_GetItemRect(tree_view_, + GetNodeDetails(GetSelectedNode())->tree_item, + &bounds, TRUE) && + GetClientRect(tree_view_, &client_rect) && + bounds.bottom >= 0 && bounds.bottom < client_rect.bottom) { + y = bounds.bottom; + } + } + gfx::Point screen_loc(0, y); + if (UILayoutIsRightToLeft()) + screen_loc.set_x(width()); + ConvertPointToScreen(this, &screen_loc); + return screen_loc; +} + +HWND TreeView::CreateNativeControl(HWND parent_container) { + int style = WS_CHILD | TVS_HASBUTTONS | TVS_HASLINES | TVS_SHOWSELALWAYS; + if (!drag_enabled_) + style |= TVS_DISABLEDRAGDROP; + if (editable_) + style |= TVS_EDITLABELS; + tree_view_ = ::CreateWindowEx(WS_EX_CLIENTEDGE | GetAdditionalExStyle(), + WC_TREEVIEW, + L"", + style, + 0, 0, width(), height(), + parent_container, NULL, NULL, NULL); + SetWindowLongPtr(tree_view_, GWLP_USERDATA, + reinterpret_cast<LONG_PTR>(&wrapper_)); + original_handler_ = win_util::SetWindowProc(tree_view_, + &TreeWndProc); + l10n_util::AdjustUIFontForWindow(tree_view_); + + if (model_) { + CreateRootItems(); + model_->SetObserver(this); + image_list_ = CreateImageList(); + TreeView_SetImageList(tree_view_, image_list_, TVSIL_NORMAL); + } + + // Bug 964884: detach the IME attached to this window. + // We should attach IMEs only when we need to input CJK strings. + ::ImmAssociateContextEx(tree_view_, NULL, 0); + return tree_view_; +} + +LRESULT TreeView::OnNotify(int w_param, LPNMHDR l_param) { + switch (l_param->code) { + case TVN_GETDISPINFO: { + // Windows is requesting more information about an item. + // WARNING: At the time this is called the tree_item of the NodeDetails + // in the maps is NULL. + DCHECK(model_); + NMTVDISPINFO* info = reinterpret_cast<NMTVDISPINFO*>(l_param); + const NodeDetails* details = + GetNodeDetailsByID(static_cast<int>(info->item.lParam)); + if (info->item.mask & TVIF_CHILDREN) + info->item.cChildren = model_->GetChildCount(details->node); + if (info->item.mask & TVIF_TEXT) { + std::wstring text = details->node->GetTitle(); + DCHECK(info->item.cchTextMax); + + // Adjust the string direction if such adjustment is required. + std::wstring localized_text; + if (l10n_util::AdjustStringForLocaleDirection(text, &localized_text)) + text.swap(localized_text); + + wcsncpy_s(info->item.pszText, info->item.cchTextMax, text.c_str(), + _TRUNCATE); + } + // Instructs windows to cache the values for this node. + info->item.mask |= TVIF_DI_SETITEM; + // Return value ignored. + return 0; + } + + case TVN_ITEMEXPANDING: { + // Notification that a node is expanding. If we haven't populated the + // tree view with the contents of the model, we do it here. + DCHECK(model_); + NMTREEVIEW* info = reinterpret_cast<NMTREEVIEW*>(l_param); + NodeDetails* details = + GetNodeDetailsByID(static_cast<int>(info->itemNew.lParam)); + if (!details->loaded_children) { + details->loaded_children = true; + for (int i = 0; i < model_->GetChildCount(details->node); ++i) + CreateItem(details->tree_item, TVI_LAST, + model_->GetChild(details->node, i)); + } + // Return FALSE to allow the item to be expanded. + return FALSE; + } + + case TVN_SELCHANGED: + if (controller_) + controller_->OnTreeViewSelectionChanged(this); + break; + + case TVN_BEGINLABELEDIT: { + NMTVDISPINFO* info = reinterpret_cast<NMTVDISPINFO*>(l_param); + NodeDetails* details = + GetNodeDetailsByID(static_cast<int>(info->item.lParam)); + // Return FALSE to allow editing. + if (!controller_ || controller_->CanEdit(this, details->node)) { + editing_node_ = details->node; + return FALSE; + } + return TRUE; + } + + case TVN_ENDLABELEDIT: { + NMTVDISPINFO* info = reinterpret_cast<NMTVDISPINFO*>(l_param); + if (info->item.pszText) { + // User accepted edit. + NodeDetails* details = + GetNodeDetailsByID(static_cast<int>(info->item.lParam)); + model_->SetTitle(details->node, info->item.pszText); + editing_node_ = NULL; + // Return FALSE so that the tree item doesn't change its text (if the + // model changed the value, it should have sent out notification which + // will have updated the value). + return FALSE; + } + editing_node_ = NULL; + // Return value ignored. + return 0; + } + + case TVN_KEYDOWN: + if (controller_) { + NMTVKEYDOWN* key_down_message = + reinterpret_cast<NMTVKEYDOWN*>(l_param); + controller_->OnTreeViewKeyDown(key_down_message->wVKey); + } + break; + + default: + break; + } + return 0; +} + +bool TreeView::OnKeyDown(int virtual_key_code) { + if (virtual_key_code == VK_F2) { + if (!GetEditingNode()) { + TreeModelNode* selected_node = GetSelectedNode(); + if (selected_node) + StartEditing(selected_node); + } + return true; + } else if (virtual_key_code == VK_RETURN && !process_enter_) { + Widget* widget = GetWidget(); + DCHECK(widget); + FocusManager* fm = FocusManager::GetFocusManager(widget->GetNativeView()); + DCHECK(fm); + Accelerator accelerator(Accelerator(static_cast<int>(virtual_key_code), + win_util::IsShiftPressed(), + win_util::IsCtrlPressed(), + win_util::IsAltPressed())); + fm->ProcessAccelerator(accelerator); + return true; + } + return false; +} + +void TreeView::OnContextMenu(const CPoint& location) { + if (!GetContextMenuController()) + return; + + if (location.x == -1 && location.y == -1) { + // Let NativeControl's implementation handle keyboard gesture. + NativeControl::OnContextMenu(location); + return; + } + + if (show_context_menu_only_when_node_selected_) { + if (!GetSelectedNode()) + return; + + // Make sure the mouse is over the selected node. + TVHITTESTINFO hit_info; + gfx::Point local_loc(location); + ConvertPointToView(NULL, this, &local_loc); + hit_info.pt.x = local_loc.x(); + hit_info.pt.y = local_loc.y(); + HTREEITEM hit_item = TreeView_HitTest(tree_view_, &hit_info); + if (!hit_item || + GetNodeDetails(GetSelectedNode())->tree_item != hit_item || + (hit_info.flags & (TVHT_ONITEM | TVHT_ONITEMRIGHT | + TVHT_ONITEMINDENT)) == 0) { + return; + } + } + ShowContextMenu(location.x, location.y, true); +} + +TreeModelNode* TreeView::GetNodeForTreeItem(HTREEITEM tree_item) { + NodeDetails* details = GetNodeDetailsByTreeItem(tree_item); + return details ? details->node : NULL; +} + +HTREEITEM TreeView::GetTreeItemForNode(TreeModelNode* node) { + NodeDetails* details = GetNodeDetails(node); + return details ? details->tree_item : NULL; +} + +void TreeView::DeleteRootItems() { + HTREEITEM root = TreeView_GetRoot(tree_view_); + if (root) { + if (root_shown_) { + RecursivelyDelete(GetNodeDetailsByTreeItem(root)); + } else { + do { + RecursivelyDelete(GetNodeDetailsByTreeItem(root)); + } while ((root = TreeView_GetRoot(tree_view_))); + } + } +} + +void TreeView::CreateRootItems() { + DCHECK(model_); + TreeModelNode* root = model_->GetRoot(); + if (root_shown_) { + CreateItem(NULL, TVI_LAST, root); + } else { + for (int i = 0; i < model_->GetChildCount(root); ++i) + CreateItem(NULL, TVI_LAST, model_->GetChild(root, i)); + } +} + +void TreeView::CreateItem(HTREEITEM parent_item, + HTREEITEM after, + TreeModelNode* node) { + DCHECK(node); + TVINSERTSTRUCT insert_struct = {0}; + insert_struct.hParent = parent_item; + insert_struct.hInsertAfter = after; + insert_struct.itemex.mask = TVIF_PARAM | TVIF_CHILDREN | TVIF_TEXT | + TVIF_SELECTEDIMAGE | TVIF_IMAGE; + // Call us back for the text. + insert_struct.itemex.pszText = LPSTR_TEXTCALLBACK; + // And the number of children. + insert_struct.itemex.cChildren = I_CHILDRENCALLBACK; + // Set the index of the icons to use. These are relative to the imagelist + // created in CreateImageList. + int icon_index = model_->GetIconIndex(node); + if (icon_index == -1) { + insert_struct.itemex.iImage = 0; + insert_struct.itemex.iSelectedImage = 1; + } else { + // The first two images are the default ones. + insert_struct.itemex.iImage = icon_index + 2; + insert_struct.itemex.iSelectedImage = icon_index + 2; + } + int node_id = next_id_++; + insert_struct.itemex.lParam = node_id; + + // Invoking TreeView_InsertItem triggers OnNotify to be called. As such, + // we set the map entries before adding the item. + NodeDetails* node_details = new NodeDetails(node_id, node); + + node_to_details_map_[node] = node_details; + id_to_details_map_[node_id] = node_details; + + node_details->tree_item = TreeView_InsertItem(tree_view_, &insert_struct); +} + +void TreeView::RecursivelyDelete(NodeDetails* node) { + DCHECK(node); + HTREEITEM item = node->tree_item; + DCHECK(item); + + // Recurse through children. + for (HTREEITEM child = TreeView_GetChild(tree_view_, item); + child ; child = TreeView_GetNextSibling(tree_view_, child)) { + RecursivelyDelete(GetNodeDetailsByTreeItem(child)); + } + + TreeView_DeleteItem(tree_view_, item); + + // finally, it is safe to delete the data for this node. + id_to_details_map_.erase(node->id); + node_to_details_map_.erase(node->node); + delete node; +} + +TreeView::NodeDetails* TreeView::GetNodeDetailsByTreeItem(HTREEITEM tree_item) { + DCHECK(tree_view_ && tree_item); + TV_ITEM tv_item = {0}; + tv_item.hItem = tree_item; + tv_item.mask = TVIF_PARAM; + if (TreeView_GetItem(tree_view_, &tv_item)) + return GetNodeDetailsByID(static_cast<int>(tv_item.lParam)); + return NULL; +} + +HIMAGELIST TreeView::CreateImageList() { + std::vector<SkBitmap> model_images; + model_->GetIcons(&model_images); + + bool rtl = UILayoutIsRightToLeft(); + // Creates the default image list used for trees. + SkBitmap* closed_icon = + ResourceBundle::GetSharedInstance().GetBitmapNamed( + (rtl ? IDR_FOLDER_CLOSED_RTL : IDR_FOLDER_CLOSED)); + SkBitmap* opened_icon = + ResourceBundle::GetSharedInstance().GetBitmapNamed( + (rtl ? IDR_FOLDER_OPEN_RTL : IDR_FOLDER_OPEN)); + int width = closed_icon->width(); + int height = closed_icon->height(); + DCHECK(opened_icon->width() == width && opened_icon->height() == height); + HIMAGELIST image_list = + ImageList_Create(width, height, ILC_COLOR32, model_images.size() + 2, + model_images.size() + 2); + if (image_list) { + // NOTE: the order the images are added in effects the selected + // image index when adding items to the tree. If you change the + // order you'll undoubtedly need to update itemex.iSelectedImage + // when the item is added. + HICON h_closed_icon = IconUtil::CreateHICONFromSkBitmap(*closed_icon); + HICON h_opened_icon = IconUtil::CreateHICONFromSkBitmap(*opened_icon); + ImageList_AddIcon(image_list, h_closed_icon); + ImageList_AddIcon(image_list, h_opened_icon); + DestroyIcon(h_closed_icon); + DestroyIcon(h_opened_icon); + for (size_t i = 0; i < model_images.size(); ++i) { + HICON model_icon = IconUtil::CreateHICONFromSkBitmap(model_images[i]); + ImageList_AddIcon(image_list, model_icon); + DestroyIcon(model_icon); + } + } + return image_list; +} + +HTREEITEM TreeView::GetTreeItemForNodeDuringMutation(TreeModelNode* node) { + if (node_to_details_map_.find(node) == node_to_details_map_.end()) { + // User hasn't navigated to this entry yet. Ignore the change. + return NULL; + } + if (!root_shown_ || node != model_->GetRoot()) { + const NodeDetails* details = GetNodeDetails(node); + if (!details->loaded_children) + return NULL; + return details->tree_item; + } + return TreeView_GetRoot(tree_view_); +} + +LRESULT CALLBACK TreeView::TreeWndProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param) { + TreeViewWrapper* wrapper = reinterpret_cast<TreeViewWrapper*>( + GetWindowLongPtr(window, GWLP_USERDATA)); + DCHECK(wrapper); + TreeView* tree = wrapper->tree_view; + + // We handle the messages WM_ERASEBKGND and WM_PAINT such that we paint into + // a DIB first and then perform a BitBlt from the DIB into the underlying + // window's DC. This double buffering code prevents the tree view from + // flickering during resize. + switch (message) { + case WM_ERASEBKGND: + return 1; + + case WM_PAINT: { + ChromeCanvasPaint canvas(window); + if (canvas.isEmpty()) + return 0; + + HDC dc = canvas.beginPlatformPaint(); + if (l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT) { + // ChromeCanvas ends up configuring the DC with a mode of GM_ADVANCED. + // For some reason a graphics mode of ADVANCED triggers all the text + // to be mirrored when RTL. Set the mode back to COMPATIBLE and + // explicitly set the layout. Additionally SetWorldTransform and + // COMPATIBLE don't play nicely together. We need to use + // SetViewportOrgEx when using a mode of COMPATIBLE. + // + // Reset the transform to the identify transform. Even though + // SetWorldTransform and COMPATIBLE don't play nicely, bits of the + // transform still carry over when we set the mode. + XFORM xform = {0}; + xform.eM11 = xform.eM22 = 1; + SetWorldTransform(dc, &xform); + + // Set the mode and layout. + SetGraphicsMode(dc, GM_COMPATIBLE); + SetLayout(dc, LAYOUT_RTL); + + // Transform the viewport such that the origin of the dc is that of + // the dirty region. This way when we invoke WM_PRINTCLIENT tree-view + // draws the dirty region at the origin of the DC so that when we + // copy the bits everything lines up nicely. Without this we end up + // copying the upper-left corner to the redraw region. + SetViewportOrgEx(dc, -canvas.paintStruct().rcPaint.left, + -canvas.paintStruct().rcPaint.top, NULL); + } + SendMessage(window, WM_PRINTCLIENT, reinterpret_cast<WPARAM>(dc), 0); + if (l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT) { + // Reset the origin of the dc back to 0. This way when we copy the bits + // over we copy the right bits. + SetViewportOrgEx(dc, 0, 0, NULL); + } + canvas.endPlatformPaint(); + return 0; + } + + case WM_RBUTTONDOWN: + if (tree->select_on_right_mouse_down_) { + TVHITTESTINFO hit_info; + hit_info.pt.x = GET_X_LPARAM(l_param); + hit_info.pt.y = GET_Y_LPARAM(l_param); + HTREEITEM hit_item = TreeView_HitTest(window, &hit_info); + if (hit_item && (hit_info.flags & (TVHT_ONITEM | TVHT_ONITEMRIGHT | + TVHT_ONITEMINDENT)) != 0) + TreeView_SelectItem(tree->tree_view_, hit_item); + } + // Fall through and let the default handler process as well. + break; + } + WNDPROC handler = tree->original_handler_; + DCHECK(handler); + return CallWindowProc(handler, window, message, w_param, l_param); +} + +} // namespace views diff --git a/views/controls/tree/tree_view.h b/views/controls/tree/tree_view.h new file mode 100644 index 0000000..1ded058 --- /dev/null +++ b/views/controls/tree/tree_view.h @@ -0,0 +1,305 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_TREE_TREE_VIEW_H_ +#define VIEWS_CONTROLS_TREE_TREE_VIEW_H_ + +#include <map> + +#include "base/basictypes.h" +#include "base/logging.h" +#include "views/controls/native_control.h" +#include "views/controls/tree/tree_model.h" + +namespace views { + +class TreeView; + +// TreeViewController --------------------------------------------------------- + +// Controller for the treeview. +class TreeViewController { + public: + // Notification that the selection of the tree view has changed. Use + // GetSelectedNode to find the current selection. + virtual void OnTreeViewSelectionChanged(TreeView* tree_view) = 0; + + // Returns true if the node can be edited. This is only used if the + // TreeView is editable. + virtual bool CanEdit(TreeView* tree_view, TreeModelNode* node) { + return true; + } + + // Invoked when a key is pressed on the tree view. + virtual void OnTreeViewKeyDown(unsigned short virtual_keycode) {} +}; + +// TreeView ------------------------------------------------------------------- + +// TreeView displays hierarchical data as returned from a TreeModel. The user +// can expand, collapse and edit the items. A Controller may be attached to +// receive notification of selection changes and restrict editing. +class TreeView : public NativeControl, TreeModelObserver { + public: + TreeView(); + virtual ~TreeView(); + + // Is dragging enabled? The default is false. + void set_drag_enabled(bool drag_enabled) { drag_enabled_ = drag_enabled; } + bool drag_enabled() const { return drag_enabled_; } + + // Sets the model. TreeView does not take ownership of the model. + void SetModel(TreeModel* model); + TreeModel* model() const { return model_; } + + // Sets whether the user can edit the nodes. The default is true. If true, + // the Controller is queried to determine if a particular node can be edited. + void SetEditable(bool editable); + + // Edits the specified node. This cancels the current edit and expands + // all parents of node. + void StartEditing(TreeModelNode* node); + + // Cancels the current edit. Does nothing if not editing. + void CancelEdit(); + + // Commits the current edit. Does nothing if not editing. + void CommitEdit(); + + // If the user is editing a node, it is returned. If the user is not + // editing a node, NULL is returned. + TreeModelNode* GetEditingNode(); + + // Selects the specified node. This expands all the parents of node. + void SetSelectedNode(TreeModelNode* node); + + // Returns the selected node, or NULL if nothing is selected. + TreeModelNode* GetSelectedNode(); + + // Make sure node and all its parents are expanded. + void Expand(TreeModelNode* node); + + // Convenience to expand ALL nodes in the tree. + void ExpandAll(); + + // Invoked from ExpandAll(). Expands the supplied node and recursively + // invokes itself with all children. + void ExpandAll(TreeModelNode* node); + + // Returns true if the specified node is expanded. + bool IsExpanded(TreeModelNode* node); + + // Sets whether the root is shown. If true, the root node of the tree is + // shown, if false only the children of the root are shown. The default is + // true. + void SetRootShown(bool root_visible); + + // TreeModelObserver methods. Don't call these directly, instead your model + // should notify the observer TreeView adds to it. + virtual void TreeNodesAdded(TreeModel* model, + TreeModelNode* parent, + int start, + int count); + virtual void TreeNodesRemoved(TreeModel* model, + TreeModelNode* parent, + int start, + int count); + virtual void TreeNodeChildrenReordered(TreeModel* model, + TreeModelNode* parent); + virtual void TreeNodeChanged(TreeModel* model, TreeModelNode* node); + + // Sets the controller, which may be null. TreeView does not take ownership + // of the controller. + void SetController(TreeViewController* controller) { + controller_ = controller; + } + + // Sets whether enter is processed when not editing. If true, enter will + // expand/collapse the node. If false, enter is passed to the focus manager + // so that an enter accelerator can be enabled. The default is false. + // + // NOTE: Changing this has no effect after the hwnd has been created. + void SetProcessesEnter(bool process_enter) { + process_enter_ = process_enter; + } + bool GetProcessedEnter() { return process_enter_; } + + // Sets when the ContextMenuController is notified. If true, the + // ContextMenuController is only notified when a node is selected and the + // mouse is over a node. The default is true. + void SetShowContextMenuOnlyWhenNodeSelected(bool value) { + show_context_menu_only_when_node_selected_ = value; + } + bool GetShowContextMenuOnlyWhenNodeSelected() { + return show_context_menu_only_when_node_selected_; + } + + // If true, a right click selects the node under the mouse. The default + // is true. + void SetSelectOnRightMouseDown(bool value) { + select_on_right_mouse_down_ = value; + } + bool GetSelectOnRightMouseDown() { return select_on_right_mouse_down_; } + + protected: + // Overriden to return a location based on the selected node. + virtual gfx::Point GetKeyboardContextMenuLocation(); + + // Creates and configures the tree_view. + virtual HWND CreateNativeControl(HWND parent_container); + + // Invoked when the native control sends a WM_NOTIFY message to its parent. + // Handles a variety of potential TreeView messages. + virtual LRESULT OnNotify(int w_param, LPNMHDR l_param); + + // Yes, we want to be notified of key down for two reasons. To circumvent + // VK_ENTER from toggling the expaned state when processes_enter_ is false, + // and to have F2 start editting. + virtual bool NotifyOnKeyDown() const { return true; } + virtual bool OnKeyDown(int virtual_key_code); + + virtual void OnContextMenu(const CPoint& location); + + // Returns the TreeModelNode for |tree_item|. + TreeModelNode* GetNodeForTreeItem(HTREEITEM tree_item); + + // Returns the tree item for |node|. + HTREEITEM GetTreeItemForNode(TreeModelNode* node); + + private: + // See notes in TableView::TableViewWrapper for why this is needed. + struct TreeViewWrapper { + explicit TreeViewWrapper(TreeView* view) : tree_view(view) { } + TreeView* tree_view; + }; + + // Internally used to track the state of nodes. NodeDetails are lazily created + // as the user expands nodes. + struct NodeDetails { + NodeDetails(int id, TreeModelNode* node) + : id(id), node(node), tree_item(NULL), loaded_children(false) {} + + // Unique identifier for the node. This corresponds to the lParam of + // the tree item. + const int id; + + // The node from the model. + TreeModelNode* node; + + // From the native TreeView. + // + // This should be treated as const, but can't due to timing in creating the + // entry. + HTREEITEM tree_item; + + // Whether the children have been loaded. + bool loaded_children; + }; + + // Deletes the root items from the treeview. This is used when the model + // changes. + void DeleteRootItems(); + + // Creates the root items in the treeview from the model. This is used when + // the model changes. + void CreateRootItems(); + + // Creates and adds an item to the treeview. parent_item identifies the + // parent and is null for root items. after dictates where among the + // children of parent_item the item is to be created. node is the node from + // the model. + void CreateItem(HTREEITEM parent_item, HTREEITEM after, TreeModelNode* node); + + // Removes entries from the map for item. This method will also + // remove the items from the TreeView because the process of + // deleting an item will send an TVN_GETDISPINFO message, consulting + // our internal map data. + void RecursivelyDelete(NodeDetails* node); + + // Returns the NodeDetails by node from the model. + NodeDetails* GetNodeDetails(TreeModelNode* node) { + DCHECK(node && + node_to_details_map_.find(node) != node_to_details_map_.end()); + return node_to_details_map_[node]; + } + + // Returns the NodeDetails by identifier (lparam of the HTREEITEM). + NodeDetails* GetNodeDetailsByID(int id) { + DCHECK(id_to_details_map_.find(id) != id_to_details_map_.end()); + return id_to_details_map_[id]; + } + + // Returns the NodeDetails by HTREEITEM. + NodeDetails* GetNodeDetailsByTreeItem(HTREEITEM tree_item); + + // Creates the image list to use for the tree. + HIMAGELIST CreateImageList(); + + // Returns the HTREEITEM for |node|. This is intended to be called when a + // model mutation event occur with |node| as the parent. This returns null + // if the user has never expanded |node| or all of its parents. + HTREEITEM GetTreeItemForNodeDuringMutation(TreeModelNode* node); + + // The window function installed on the treeview. + static LRESULT CALLBACK TreeWndProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param); + + // Handle to the tree window. + HWND tree_view_; + + // The model, may be null. + TreeModel* model_; + + // Maps from id to NodeDetails. + std::map<int,NodeDetails*> id_to_details_map_; + + // Maps from model entry to NodeDetails. + std::map<TreeModelNode*,NodeDetails*> node_to_details_map_; + + // Whether the user can edit the items. + bool editable_; + + // Next id to create. Any time an item is added this is incremented by one. + int next_id_; + + // The controller. + TreeViewController* controller_; + + // Node being edited. If null, not editing. + TreeModelNode* editing_node_; + + // Whether or not the root is shown in the tree. + bool root_shown_; + + // Whether enter should be processed by the tree when not editing. + bool process_enter_; + + // Whether we notify context menu controller only when mouse is over node + // and node is selected. + bool show_context_menu_only_when_node_selected_; + + // Whether the selection is changed on right mouse down. + bool select_on_right_mouse_down_; + + // A wrapper around 'this', used for subclassing the TreeView control. + TreeViewWrapper wrapper_; + + // Original handler installed on the TreeView. + WNDPROC original_handler_; + + bool drag_enabled_; + + // Did the model return a non-empty set of icons from GetIcons? + bool has_custom_icons_; + + HIMAGELIST image_list_; + + DISALLOW_COPY_AND_ASSIGN(TreeView); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TREE_TREE_VIEW_H_ diff --git a/views/event.cc b/views/event.cc new file mode 100644 index 0000000..8e6a0f1 --- /dev/null +++ b/views/event.cc @@ -0,0 +1,42 @@ +// Copyright (c) 2006-2008 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/event.h" + +#include "views/view.h" + +namespace views { + +Event::Event(EventType type, int flags) + : type_(type), +#if defined(OS_WIN) + time_stamp_(GetTickCount()), +#else + time_stamp_(0), +#endif + flags_(flags) { +} + +LocatedEvent::LocatedEvent(const LocatedEvent& model, View* from, View* to) + : Event(model), + location_(model.location_) { + if (to) + View::ConvertPointToView(from, to, &location_); +} + +MouseEvent::MouseEvent(EventType type, + View* from, + View* to, + const gfx::Point &l, + int flags) + : LocatedEvent(LocatedEvent(type, gfx::Point(l.x(), l.y()), flags), + from, + to) { +}; + +MouseEvent::MouseEvent(const MouseEvent& model, View* from, View* to) + : LocatedEvent(model, from, to) { +} + +} // namespace views diff --git a/views/event.h b/views/event.h new file mode 100644 index 0000000..f0ab24a --- /dev/null +++ b/views/event.h @@ -0,0 +1,324 @@ +// Copyright (c) 2006-2008 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 VIEWS_EVENT_H_ +#define VIEWS_EVENT_H_ + +#include "base/basictypes.h" + +#if defined(OS_LINUX) +#include <gdk/gdk.h> +#endif + +#include "base/gfx/point.h" + +class OSExchangeData; + +namespace views { + +class View; + +//////////////////////////////////////////////////////////////////////////////// +// +// 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 Event { + public: + // Event types. (prefixed because of a conflict with windows headers) + enum EventType { ET_UNKNOWN = 0, + ET_MOUSE_PRESSED, + ET_MOUSE_DRAGGED, + ET_MOUSE_RELEASED, + ET_MOUSE_MOVED, + ET_MOUSE_ENTERED, + ET_MOUSE_EXITED, + ET_KEY_PRESSED, + ET_KEY_RELEASED, + ET_MOUSEWHEEL, + ET_DROP_TARGET_EVENT }; + + // Event flags currently supported + enum EventFlags { EF_SHIFT_DOWN = 1 << 0, + EF_CONTROL_DOWN = 1 << 1, + EF_ALT_DOWN = 1 << 2, + EF_LEFT_BUTTON_DOWN = 1 << 3, + EF_MIDDLE_BUTTON_DOWN = 1 << 4, + EF_RIGHT_BUTTON_DOWN = 1 << 5 }; + + // Return the event type + EventType GetType() const { + return type_; + } + + // Return the event time stamp in ticks + int GetTimeStamp() const { + return time_stamp_; + } + + // Return the flags + int GetFlags() const { + return flags_; + } + + void set_flags(int flags) { + flags_ = flags; + } + + // Return whether the shift modifier is down + bool IsShiftDown() const { + return (flags_ & EF_SHIFT_DOWN) != 0; + } + + // Return whether the control modifier is down + bool IsControlDown() const { + return (flags_ & EF_CONTROL_DOWN) != 0; + } + + // Return whether the alt modifier is down + bool IsAltDown() const { + return (flags_ & EF_ALT_DOWN) != 0; + } + +#if defined(OS_WIN) + // Returns the EventFlags in terms of windows flags. + int GetWindowsFlags() const; + + // Convert windows flags to views::Event flags + static int ConvertWindowsFlags(uint32 win_flags); +#elif defined(OS_LINUX) + // Convert the state member on a GdkEvent to views::Event flags + static int GetFlagsFromGdkState(int state); +#endif + + protected: + Event(EventType type, int flags); + + Event(const Event& model) + : type_(model.GetType()), + time_stamp_(model.GetTimeStamp()), + flags_(model.GetFlags()) { + } + + private: + void operator=(const Event&); + + EventType type_; + int 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 LocatedEvent : public Event { + public: + LocatedEvent(EventType type, const gfx::Point& location, int flags) + : Event(type, flags), + location_(location) { + } + + // Create a new LocatedEvent which is identical to the provided model. + // If from / to views are provided, the model location will be converted + // from 'from' coordinate system to 'to' coordinate system + LocatedEvent(const LocatedEvent& model, View* from, View* to); + + // Returns the X location. + int x() const { + return location_.x(); + } + + // Returns the Y location. + int y() const { + return location_.y(); + } + + // Returns the location. + const gfx::Point& location() const { + return location_; + } + + private: + gfx::Point location_; +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// MouseEvent class +// +// A mouse event is used for any input event related to the mouse. +// +//////////////////////////////////////////////////////////////////////////////// +class MouseEvent : public LocatedEvent { + public: + // Flags specific to mouse events + enum MouseEventFlags { + EF_IS_DOUBLE_CLICK = 1 << 16, + EF_IS_NON_CLIENT = 1 << 17 + }; + + // Create a new mouse event + MouseEvent(EventType type, int x, int y, int flags) + : LocatedEvent(type, gfx::Point(x, y), flags) { + } + + // Create a new mouse event from a type and a point. If from / to views + // are provided, the point will be converted from 'from' coordinate system to + // 'to' coordinate system. + MouseEvent(EventType type, + View* from, + View* to, + const gfx::Point &l, + int flags); + + // Create a new MouseEvent which is identical to the provided model. + // If from / to views are provided, the model location will be converted + // from 'from' coordinate system to 'to' coordinate system + MouseEvent(const MouseEvent& model, View* from, View* to); + + // Conveniences to quickly test what button is down + bool IsOnlyLeftMouseButton() const { + return (GetFlags() & EF_LEFT_BUTTON_DOWN) && + !(GetFlags() & (EF_MIDDLE_BUTTON_DOWN | EF_RIGHT_BUTTON_DOWN)); + } + + bool IsLeftMouseButton() const { + return (GetFlags() & EF_LEFT_BUTTON_DOWN) != 0; + } + + bool IsOnlyMiddleMouseButton() const { + return (GetFlags() & EF_MIDDLE_BUTTON_DOWN) && + !(GetFlags() & (EF_LEFT_BUTTON_DOWN | EF_RIGHT_BUTTON_DOWN)); + } + + bool IsMiddleMouseButton() const { + return (GetFlags() & EF_MIDDLE_BUTTON_DOWN) != 0; + } + + bool IsOnlyRightMouseButton() const { + return (GetFlags() & EF_RIGHT_BUTTON_DOWN) && + !(GetFlags() & (EF_LEFT_BUTTON_DOWN | EF_MIDDLE_BUTTON_DOWN)); + } + + bool IsRightMouseButton() const { + return (GetFlags() & EF_RIGHT_BUTTON_DOWN) != 0; + } + + private: + DISALLOW_EVIL_CONSTRUCTORS(MouseEvent); +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// KeyEvent class +// +// A key event is used for any input event related to the keyboard. +// Note: this event is about key pressed, not typed characters. +// +//////////////////////////////////////////////////////////////////////////////// +class KeyEvent : public Event { + public: +#if defined(OS_WIN) + // Create a new key event + KeyEvent(EventType type, int ch, int repeat_count, int message_flags); +#elif defined(OS_LINUX) + KeyEvent(GdkEventKey* event); +#endif + + int GetCharacter() const { + return character_; + } + +#if defined(OS_WIN) + bool IsExtendedKey() const; +#endif + + int GetRepeatCount() const { + return repeat_count_; + } + + private: +#if defined(OS_WIN) + int GetKeyStateFlags() const; +#endif + + int character_; + int repeat_count_; + int message_flags_; + + DISALLOW_EVIL_CONSTRUCTORS(KeyEvent); +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// MouseWheelEvent class +// +// A MouseWheelEvent is used to propagate mouse wheel user events +// +//////////////////////////////////////////////////////////////////////////////// +class MouseWheelEvent : public LocatedEvent { + public: + // Create a new key event + MouseWheelEvent(int offset, int x, int y, int flags) + : LocatedEvent(ET_MOUSEWHEEL, gfx::Point(x, y), flags), + offset_(offset) { + } + + int GetOffset() const { + return offset_; + } + + private: + int offset_; + + DISALLOW_EVIL_CONSTRUCTORS(MouseWheelEvent); +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// DropTargetEvent class +// +// A DropTargetEvent is sent to the view the mouse is over during a drag and +// drop operation. +// +//////////////////////////////////////////////////////////////////////////////// +class DropTargetEvent : public LocatedEvent { + public: + DropTargetEvent(const OSExchangeData& data, + int x, + int y, + int source_operations) + : LocatedEvent(ET_DROP_TARGET_EVENT, gfx::Point(x, y), 0), + data_(data), + source_operations_(source_operations) { + } + + // Data associated with the drag/drop session. + const OSExchangeData& GetData() const { return data_; } + + // Bitmask of supported DragDropTypes::DragOperation by the source. + int GetSourceOperations() const { return source_operations_; } + + private: + const OSExchangeData& data_; + int source_operations_; + + DISALLOW_EVIL_CONSTRUCTORS(DropTargetEvent); +}; + +} // namespace views + +#endif // VIEWS_EVENT_H_ diff --git a/views/event_gtk.cc b/views/event_gtk.cc new file mode 100644 index 0000000..ac5a2ce --- /dev/null +++ b/views/event_gtk.cc @@ -0,0 +1,36 @@ +// Copyright (c) 2009 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/event.h" + +namespace views { + +KeyEvent::KeyEvent(GdkEventKey* event) + : Event(event->type == GDK_KEY_PRESS ? + Event::ET_KEY_PRESSED : Event::ET_KEY_RELEASED, + GetFlagsFromGdkState(event->state)), + // TODO(erg): All these values are iffy. + character_(event->keyval), + repeat_count_(0), + message_flags_(0) { +} + +int Event::GetFlagsFromGdkState(int state) { + int flags = 0; + if (state & GDK_CONTROL_MASK) + flags |= Event::EF_CONTROL_DOWN; + if (state & GDK_SHIFT_MASK) + flags |= Event::EF_SHIFT_DOWN; + if (state & GDK_MOD1_MASK) + flags |= Event::EF_ALT_DOWN; + if (state & GDK_BUTTON1_MASK) + flags |= Event::EF_LEFT_BUTTON_DOWN; + if (state & GDK_BUTTON2_MASK) + flags |= Event::EF_MIDDLE_BUTTON_DOWN; + if (state & GDK_BUTTON3_MASK) + flags |= Event::EF_RIGHT_BUTTON_DOWN; + return flags; +} + +} // namespace views diff --git a/views/event_win.cc b/views/event_win.cc new file mode 100644 index 0000000..28d1330 --- /dev/null +++ b/views/event_win.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2009 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/event.h" + +#include <windows.h> + +namespace views { + +int Event::GetWindowsFlags() const { + // TODO: need support for x1/x2. + int result = 0; + result |= (flags_ & EF_SHIFT_DOWN) ? MK_SHIFT : 0; + result |= (flags_ & EF_CONTROL_DOWN) ? MK_CONTROL : 0; + result |= (flags_ & EF_LEFT_BUTTON_DOWN) ? MK_LBUTTON : 0; + result |= (flags_ & EF_MIDDLE_BUTTON_DOWN) ? MK_MBUTTON : 0; + result |= (flags_ & EF_RIGHT_BUTTON_DOWN) ? MK_RBUTTON : 0; + return result; +} + +//static +int Event::ConvertWindowsFlags(UINT win_flags) { + int r = 0; + if (win_flags & MK_CONTROL) + r |= EF_CONTROL_DOWN; + if (win_flags & MK_SHIFT) + r |= EF_SHIFT_DOWN; + if (GetKeyState(VK_MENU) < 0) + r |= EF_ALT_DOWN; + if (win_flags & MK_LBUTTON) + r |= EF_LEFT_BUTTON_DOWN; + if (win_flags & MK_MBUTTON) + r |= EF_MIDDLE_BUTTON_DOWN; + if (win_flags & MK_RBUTTON) + r |= EF_RIGHT_BUTTON_DOWN; + return r; +} + +KeyEvent::KeyEvent(EventType type, int ch, int repeat_count, int message_flags) + : Event(type, GetKeyStateFlags()), + character_(ch), + repeat_count_(repeat_count), + message_flags_(message_flags) { + } + +int KeyEvent::GetKeyStateFlags() const { + // Windows Keyboard messages don't come with control key state as parameters + // like mouse messages do, so we need to explicitly probe for these key + // states. + int flags = 0; + if (GetKeyState(VK_MENU) & 0x80) + flags |= Event::EF_ALT_DOWN; + if (GetKeyState(VK_SHIFT) & 0x80) + flags |= Event::EF_SHIFT_DOWN; + if (GetKeyState(VK_CONTROL) & 0x80) + flags |= Event::EF_CONTROL_DOWN; + return flags; +} + +bool KeyEvent::IsExtendedKey() const { + return (message_flags_ & KF_EXTENDED) == KF_EXTENDED; +} + +} // namespace views diff --git a/views/fill_layout.cc b/views/fill_layout.cc new file mode 100644 index 0000000..7bd1430a --- /dev/null +++ b/views/fill_layout.cc @@ -0,0 +1,33 @@ +// Copyright (c) 2009 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/fill_layout.h" + +#include "base/logging.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// FillLayout + +FillLayout::FillLayout() { +} + +FillLayout::~FillLayout() { +} + +void FillLayout::Layout(View* host) { + if (host->GetChildViewCount() == 0) + return; + + View* frame_view = host->GetChildViewAt(0); + frame_view->SetBounds(0, 0, host->width(), host->height()); +} + +gfx::Size FillLayout::GetPreferredSize(View* host) { + DCHECK(host->GetChildViewCount() == 1); + return host->GetChildViewAt(0)->GetPreferredSize(); +} + +} // namespace views diff --git a/views/fill_layout.h b/views/fill_layout.h new file mode 100644 index 0000000..e47a639 --- /dev/null +++ b/views/fill_layout.h @@ -0,0 +1,35 @@ +// Copyright (c) 2009 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 VIEWS_FILL_LAYOUT_H_ +#define VIEWS_FILL_LAYOUT_H_ + +#include "views/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 FillLayout : public LayoutManager { + public: + FillLayout(); + virtual ~FillLayout(); + + // Overridden from LayoutManager: + virtual void Layout(View* host); + virtual gfx::Size GetPreferredSize(View* host); + + private: + DISALLOW_EVIL_CONSTRUCTORS(FillLayout); +}; + +} // namespace views + +#endif // VIEWS_FILL_LAYOUT_H_ diff --git a/views/focus/external_focus_tracker.cc b/views/focus/external_focus_tracker.cc new file mode 100644 index 0000000..8f8bfdf --- /dev/null +++ b/views/focus/external_focus_tracker.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2006-2008 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 "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) { + view_storage_ = ViewStorage::GetSharedInstance(); + 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::FocusWillChange(View* focused_before, + View* focused_now) { + if (focused_now && !parent_view_->IsParentOf(focused_now) && + parent_view_ != focused_now) { + // Store the newly focused view. + StoreLastFocusedView(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/views/focus/external_focus_tracker.h b/views/focus/external_focus_tracker.h new file mode 100644 index 0000000..22e9b7e --- /dev/null +++ b/views/focus/external_focus_tracker.h @@ -0,0 +1,76 @@ +// Copyright (c) 2006-2008 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 VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ +#define VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ + +#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 ExternalFocusTracker : public FocusChangeListener { + public: + ExternalFocusTracker(View* parent_view, FocusManager* focus_manager); + + virtual ~ExternalFocusTracker(); + // FocusChangeListener implementation. + virtual void FocusWillChange(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_EVIL_CONSTRUCTORS(ExternalFocusTracker); +}; + +} // namespace views + +#endif // VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ diff --git a/views/focus/focus_manager.cc b/views/focus/focus_manager.cc new file mode 100644 index 0000000..5653e7a --- /dev/null +++ b/views/focus/focus_manager.cc @@ -0,0 +1,716 @@ +// Copyright (c) 2006-2008 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 <algorithm> + +#include "base/histogram.h" +#include "base/logging.h" +#include "base/win_util.h" +#include "chrome/browser/renderer_host/render_widget_host_view_win.h" +#include "views/accelerator.h" +#include "views/focus/focus_manager.h" +#include "views/focus/view_storage.h" +#include "views/view.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" + +// The following keys are used in SetProp/GetProp to associate additional +// information needed for focus tracking with a window. + +// Maps to the FocusManager instance for a top level window. See +// CreateFocusManager/DestoryFocusManager for usage. +static const wchar_t* const kFocusManagerKey = L"__VIEW_CONTAINER__"; + +// Maps to the View associated with a window. +// We register views with window so we can: +// - keep in sync the native focus with the view focus (when the native +// component gets the focus, we get the WM_SETFOCUS event and we can focus the +// associated view). +// - prevent tab key events from being sent to views. +static const wchar_t* const kViewKey = L"__CHROME_VIEW__"; + +// A property set to 1 to indicate whether the focus manager has subclassed that +// window. We are doing this to ensure we are not subclassing several times. +// Subclassing twice is not a problem if no one is subclassing the HWND between +// the 2 subclassings (the 2nd subclassing is ignored since the WinProc is the +// same as the current one). However if some other app goes and subclasses the +// HWND between the 2 subclassings, we will end up subclassing twice. +// This flag lets us test that whether we have or not subclassed yet. +static const wchar_t* const kFocusSubclassInstalled = + L"__FOCUS_SUBCLASS_INSTALLED__"; + +namespace views { + +// Callback installed via InstallFocusSubclass. +static LRESULT CALLBACK FocusWindowCallback(HWND window, UINT message, + WPARAM wParam, LPARAM lParam) { + if (!::IsWindow(window)) { + // QEMU has reported crashes when calling GetProp (this seems to happen for + // some weird messages, not sure what they are). + // Here we are just trying to avoid the crasher. + NOTREACHED(); + return 0; + } + + WNDPROC original_handler = win_util::GetSuperclassWNDPROC(window); + DCHECK(original_handler); + FocusManager* focus_manager = FocusManager::GetFocusManager(window); + // There are cases when we have no FocusManager for the window. This happens + // because we subclass certain windows (such as the TabContents window) + // but that window may not have an associated FocusManager. + if (focus_manager) { + switch (message) { + case WM_SETFOCUS: + if (!focus_manager->OnSetFocus(window)) + return 0; + break; + case WM_NCDESTROY: + if (!focus_manager->OnNCDestroy(window)) + return 0; + break; + case WM_ACTIVATE: { + // We call the DefWindowProc before calling OnActivate as some of our + // windows need the OnActivate notifications. The default activation on + // the window causes it to focus the main window, and since + // FocusManager::OnActivate attempts to restore the focused view, it + // needs to be called last so the focus it is setting does not get + // overridden. + LRESULT result = CallWindowProc(original_handler, window, WM_ACTIVATE, + wParam, lParam); + if (!focus_manager->OnPostActivate(window, + LOWORD(wParam), HIWORD(wParam))) + return 0; + return result; + } + default: + break; + } + } + return CallWindowProc(original_handler, window, message, wParam, lParam); +} + +// FocusManager ----------------------------------------------------- + +// static +FocusManager* FocusManager::CreateFocusManager(HWND window, + RootView* root_view) { + DCHECK(window); + DCHECK(root_view); + InstallFocusSubclass(window, NULL); + FocusManager* focus_manager = new FocusManager(window, root_view); + SetProp(window, kFocusManagerKey, focus_manager); + + return focus_manager; +} + +// static +void FocusManager::InstallFocusSubclass(HWND window, View* view) { + DCHECK(window); + + bool already_subclassed = + reinterpret_cast<bool>(GetProp(window, + kFocusSubclassInstalled)); + if (already_subclassed && + !win_util::IsSubclassed(window, &FocusWindowCallback)) { + NOTREACHED() << "window sub-classed by someone other than the FocusManager"; + // Track in UMA so we know if this case happens. + UMA_HISTOGRAM_COUNTS("FocusManager.MultipleSubclass", 1); + } else { + win_util::Subclass(window, &FocusWindowCallback); + SetProp(window, kFocusSubclassInstalled, reinterpret_cast<HANDLE>(true)); + } + if (view) + SetProp(window, kViewKey, view); +} + +void FocusManager::UninstallFocusSubclass(HWND window) { + DCHECK(window); + if (win_util::Unsubclass(window, &FocusWindowCallback)) { + RemoveProp(window, kViewKey); + RemoveProp(window, kFocusSubclassInstalled); + } +} + +// static +FocusManager* FocusManager::GetFocusManager(HWND window) { + DCHECK(window); + + // In case parent windows belong to a different process, yet + // have the kFocusManagerKey property set, we have to be careful + // to also check the process id of the window we're checking. + DWORD window_pid = 0, current_pid = GetCurrentProcessId(); + FocusManager* focus_manager; + for (focus_manager = NULL; focus_manager == NULL && IsWindow(window); + window = GetParent(window)) { + GetWindowThreadProcessId(window, &window_pid); + if (current_pid != window_pid) + break; + focus_manager = reinterpret_cast<FocusManager*>( + GetProp(window, kFocusManagerKey)); + } + return focus_manager; +} + +// static +View* FocusManager::GetViewForWindow(HWND window, bool look_in_parents) { + DCHECK(window); + do { + View* v = reinterpret_cast<View*>(GetProp(window, kViewKey)); + if (v) + return v; + } while (look_in_parents && (window = ::GetParent(window))); + + return NULL; +} + +FocusManager::FocusManager(HWND root, RootView* root_view) + : root_(root), + top_root_view_(root_view), + focused_view_(NULL), + ignore_set_focus_msg_(false) { + stored_focused_view_storage_id_ = + ViewStorage::GetSharedInstance()->CreateStorageID(); + DCHECK(root_); +} + +FocusManager::~FocusManager() { + // If there are still registered FocusChange listeners, chances are they were + // leaked so warn about them. + DCHECK(focus_change_listeners_.empty()); +} + +// Message handlers. +bool FocusManager::OnSetFocus(HWND window) { + if (ignore_set_focus_msg_) + return true; + + // Focus the view associated with that window. + View* v = static_cast<View*>(GetProp(window, kViewKey)); + if (v && v->IsFocusable()) { + v->GetRootView()->FocusView(v); + } else { + SetFocusedView(NULL); + } + + return true; +} + +bool FocusManager::OnNCDestroy(HWND window) { + // Window is being destroyed, undo the subclassing. + FocusManager::UninstallFocusSubclass(window); + + if (window == root_) { + // We are the top window. + + DCHECK(GetProp(window, kFocusManagerKey)); + + // Make sure this is called on the window that was set with the + // FocusManager. + RemoveProp(window, kFocusManagerKey); + + delete this; + } + return true; +} + +bool FocusManager::OnKeyDown(HWND window, UINT message, WPARAM wparam, + LPARAM lparam) { + DCHECK((message == WM_KEYDOWN) || (message == WM_SYSKEYDOWN)); + + if (!IsWindowVisible(root_)) { + // We got a message for a hidden window. Because WidgetWin::Close hides the + // window, then destroys it, it it possible to get a message after we've + // hidden the window. If we allow the message to be dispatched chances are + // we'll crash in some weird place. By returning false we make sure the + // message isn't dispatched. + return false; + } + + // First give the registered keystroke handlers a chance a processing + // the message + // Do some basic checking to try to catch evil listeners that change the list + // from under us. + KeystrokeListenerList::size_type original_count = + keystroke_listeners_.size(); + for (int i = 0; i < static_cast<int>(keystroke_listeners_.size()); i++) { + if (keystroke_listeners_[i]->ProcessKeyStroke(window, message, wparam, + lparam)) { + return false; + } + } + DCHECK_EQ(original_count, keystroke_listeners_.size()) + << "KeystrokeListener list modified during notification"; + + int virtual_key_code = static_cast<int>(wparam); + // 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. + HWND active_window = ::GetActiveWindow(); + if ((active_window == root_ || ::IsChild(active_window, root_)) && + (virtual_key_code == VK_TAB) && !win_util::IsCtrlPressed()) { + if (!focused_view_ || !focused_view_->CanProcessTabKeyEvents()) { + AdvanceFocus(win_util::IsShiftPressed()); + return false; + } + } + + // Intercept arrow key messages to switch between grouped views. + if (focused_view_ && focused_view_->GetGroup() != -1 && + (virtual_key_code == VK_UP || virtual_key_code == VK_DOWN || + virtual_key_code == VK_LEFT || virtual_key_code == VK_RIGHT)) { + bool next = (virtual_key_code == VK_RIGHT || virtual_key_code == VK_DOWN); + std::vector<View*> views; + focused_view_->GetParent()->GetViewsWithGroup(focused_view_->GetGroup(), + &views); + std::vector<View*>::const_iterator iter = std::find(views.begin(), + views.end(), + focused_view_); + DCHECK(iter != views.end()); + int index = static_cast<int>(iter - 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; + } + views[index]->RequestFocus(); + return false; + } + + int repeat_count = LOWORD(lparam); + int flags = HIWORD(lparam); + if (focused_view_ && + !focused_view_->ShouldLookupAccelerators(KeyEvent( + Event::ET_KEY_PRESSED, virtual_key_code, + repeat_count, flags))) { + // This should not be processed as an accelerator. + return true; + } + + // Process keyboard accelerators. + // We process accelerators here as we have no way of knowing if a HWND has + // really processed a key event. If the key combination matches an + // accelerator, the accelerator is triggered, otherwise we forward the + // event to the HWND. + Accelerator accelerator(Accelerator(static_cast<int>(virtual_key_code), + win_util::IsShiftPressed(), + win_util::IsCtrlPressed(), + win_util::IsAltPressed())); + if (ProcessAccelerator(accelerator)) { + // If a shortcut was activated for this keydown message, do not + // propagate the message further. + return false; + } + return true; +} + +bool FocusManager::OnKeyUp(HWND window, UINT message, WPARAM wparam, + LPARAM lparam) { + for (int i = 0; i < static_cast<int>(keystroke_listeners_.size()); ++i) { + if (keystroke_listeners_[i]->ProcessKeyStroke(window, message, wparam, + lparam)) { + return false; + } + } + + return true; +} + +bool FocusManager::OnPostActivate(HWND window, + int activation_state, + int minimized_state) { + if (WA_INACTIVE == LOWORD(activation_state)) { + StoreFocusedView(); + } else { + RestoreFocusedView(); + } + return false; +} + +void FocusManager::ValidateFocusedView() { + if (focused_view_) { + if (!ContainsView(focused_view_)) { + focused_view_ = NULL; + } + } +} + +// Tests whether a view is valid, whether it still belongs to the window +// hierarchy of the FocusManager. +bool FocusManager::ContainsView(View* view) { + DCHECK(view); + RootView* root_view = view->GetRootView(); + if (!root_view) + return false; + + Widget* widget = root_view->GetWidget(); + if (!widget) + return false; + + HWND window = widget->GetNativeView(); + while (window) { + if (window == root_) + return true; + window = ::GetParent(window); + } + return 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); + v->RequestFocus(); + } +} + +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) { + // If the starting view has a focus traversable, use it. + // This is the case with WidgetWins for example. + focus_traversable = original_starting_view->GetFocusTraversable(); + + // Otherwise default to the root view. + if (!focus_traversable) { + focus_traversable = original_starting_view->GetRootView(); + starting_view = original_starting_view; + } + } else { + focus_traversable = top_root_view_; + } + + // Traverse the FocusTraversable tree down to find the focusable view. + View* v = FindFocusableView(focus_traversable, starting_view, + reverse, dont_loop); + 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; + v = parent_focus_traversable ->FindNextFocusableView( + starting_view, reverse, FocusTraversable::UP, dont_loop, + &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, dont_loop); + } + + if (v) + return v; + + starting_view = focus_traversable->GetFocusTraversableParentView(); + parent_focus_traversable = + parent_focus_traversable->GetFocusTraversableParent(); + } + + if (!dont_loop) { + // If we get here, we have reached the end of the focus hierarchy, let's + // loop. + if (reverse) { + // When reversing from the top, the next focusable view is at the end + // of the focus hierarchy. + return FindLastFocusableView(); + } else { + // Easy, just clear the selection and press tab again. + if (original_starting_view) { // Make sure there was at least a view to + // start with, to prevent infinitely + // looping in empty windows. + // By calling with NULL as the starting view, we'll start from the + // top_root_view. + return GetNextFocusableView(NULL, false, true); + } + } + } + } + return NULL; +} + +View* FocusManager::FindLastFocusableView() { + // Just walk the entire focus loop from where we're at until we reach the end. + View* new_focused = NULL; + View* last_focused = focused_view_; + while (new_focused = GetNextFocusableView(last_focused, false, true)) + last_focused = new_focused; + return last_focused; +} + +void FocusManager::SetFocusedView(View* view) { + if (focused_view_ != view) { + View* prev_focused_view = focused_view_; + if (focused_view_) + focused_view_->WillLoseFocus(); + + if (view) + view->WillGainFocus(); + + // Notified listeners that the focus changed. + FocusChangeListenerList::const_iterator iter; + for (iter = focus_change_listeners_.begin(); + iter != focus_change_listeners_.end(); ++iter) { + (*iter)->FocusWillChange(prev_focused_view, view); + } + + focused_view_ = view; + + if (prev_focused_view) + prev_focused_view->SchedulePaint(); // Remove focus artifacts. + + if (view) { + view->SchedulePaint(); + view->Focus(); + view->DidGainFocus(); + } + } +} + +void FocusManager::ClearFocus() { + SetFocusedView(NULL); + ClearHWNDFocus(); +} + +void FocusManager::ClearHWNDFocus() { + // Keep the top root window focused so we get keyboard events. + ignore_set_focus_msg_ = true; + ::SetFocus(root_); + ignore_set_focus_msg_ = false; +} + +void FocusManager::FocusHWND(HWND hwnd) { + ignore_set_focus_msg_ = true; + // Only reset focus if hwnd is not already focused. + if (hwnd && ::GetFocus() != hwnd) + ::SetFocus(hwnd); + ignore_set_focus_msg_ = false; +} + +void FocusManager::StoreFocusedView() { + ViewStorage* view_storage = ViewStorage::GetSharedInstance(); + 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_; + focused_view_ = NULL; + + if (v) + v->SchedulePaint(); // Remove focus border. +} + +void FocusManager::RestoreFocusedView() { + ViewStorage* view_storage = ViewStorage::GetSharedInstance(); + 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)) + view->RequestFocus(); + } else { + // Clearing the focus will focus the root window, so we still get key + // events. + ClearFocus(); + } +} + +void FocusManager::ClearStoredFocusedView() { + ViewStorage* view_storage = ViewStorage::GetSharedInstance(); + 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_); +} + +FocusManager* FocusManager::GetParentFocusManager() const { + HWND parent = ::GetParent(root_); + // If we are a top window, we don't have a parent FocusManager. + if (!parent) + return NULL; + + return GetFocusManager(parent); +} + +// 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, + bool dont_loop) { + FocusTraversable* new_focus_traversable = NULL; + View* new_starting_view = NULL; + View* v = focus_traversable->FindNextFocusableView(starting_view, + reverse, + FocusTraversable::DOWN, + dont_loop, + &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->FindNextFocusableView(starting_view, + reverse, + FocusTraversable::DOWN, + dont_loop, + &new_focus_traversable, + &new_starting_view); + } + return v; +} + +AcceleratorTarget* FocusManager::RegisterAccelerator( + const Accelerator& accelerator, + AcceleratorTarget* target) { + AcceleratorMap::const_iterator iter = accelerators_.find(accelerator); + AcceleratorTarget* previous_target = NULL; + if (iter != accelerators_.end()) + previous_target = iter->second; + + accelerators_[accelerator] = target; + + return previous_target; +} + +void FocusManager::UnregisterAccelerator(const Accelerator& accelerator, + AcceleratorTarget* target) { + AcceleratorMap::iterator iter = accelerators_.find(accelerator); + if (iter == accelerators_.end()) { + NOTREACHED() << "Unregistering non-existing accelerator"; + return; + } + + if (iter->second != target) { + NOTREACHED() << "Unregistering accelerator for wrong target"; + return; + } + + accelerators_.erase(iter); +} + +void FocusManager::UnregisterAccelerators(AcceleratorTarget* target) { + for (AcceleratorMap::iterator iter = accelerators_.begin(); + iter != accelerators_.end();) { + if (iter->second == target) + accelerators_.erase(iter++); + else + ++iter; + } +} + +bool FocusManager::ProcessAccelerator(const Accelerator& accelerator) { + FocusManager* focus_manager = this; + do { + AcceleratorTarget* target = + focus_manager->GetTargetForAccelerator(accelerator); + if (target) { + // If there is focused view, we give it a chance to process that + // accelerator. + if (!focused_view_ || + !focused_view_->OverrideAccelerator(accelerator)) { + if (target->AcceleratorPressed(accelerator)) + return true; + } + } + + // When dealing with child windows that have their own FocusManager (such + // as ConstrainedWindow), we still want the parent FocusManager to process + // the accelerator if the child window did not process it. + focus_manager = focus_manager->GetParentFocusManager(); + } while (focus_manager); + return false; +} + +AcceleratorTarget* FocusManager::GetTargetForAccelerator( + const Accelerator& accelerator) const { + AcceleratorMap::const_iterator iter = accelerators_.find(accelerator); + if (iter != accelerators_.end()) + return iter->second; + return NULL; +} + +void FocusManager::ViewRemoved(View* parent, View* removed) { + if (focused_view_ && focused_view_ == removed) + focused_view_ = NULL; +} + +void FocusManager::AddKeystrokeListener(KeystrokeListener* listener) { + DCHECK(std::find(keystroke_listeners_.begin(), keystroke_listeners_.end(), + listener) == keystroke_listeners_.end()) + << "Adding a listener twice."; + keystroke_listeners_.push_back(listener); +} + +void FocusManager::RemoveKeystrokeListener(KeystrokeListener* listener) { + KeystrokeListenerList::iterator place = + std::find(keystroke_listeners_.begin(), keystroke_listeners_.end(), + listener); + if (place == keystroke_listeners_.end()) { + NOTREACHED() << "Removing a listener that isn't registered."; + return; + } + keystroke_listeners_.erase(place); +} + +void FocusManager::AddFocusChangeListener(FocusChangeListener* listener) { + DCHECK(std::find(focus_change_listeners_.begin(), + focus_change_listeners_.end(), listener) == + focus_change_listeners_.end()) << "Adding a listener twice."; + focus_change_listeners_.push_back(listener); +} + +void FocusManager::RemoveFocusChangeListener(FocusChangeListener* listener) { + FocusChangeListenerList::iterator place = + std::find(focus_change_listeners_.begin(), focus_change_listeners_.end(), + listener); + if (place == focus_change_listeners_.end()) { + NOTREACHED() << "Removing a listener that isn't registered."; + return; + } + focus_change_listeners_.erase(place); +} + +} // namespace views diff --git a/views/focus/focus_manager.h b/views/focus/focus_manager.h new file mode 100644 index 0000000..8c33036 --- /dev/null +++ b/views/focus/focus_manager.h @@ -0,0 +1,343 @@ +// Copyright (c) 2006-2008 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 VIEWS_FOCUS_FOCUS_MANAGER_H_ +#define VIEWS_FOCUS_FOCUS_MANAGER_H_ + +#include "base/basictypes.h" + +#if defined(OS_WIN) +#include <windows.h> +#endif +#include <vector> +#include <map> + +#include "views/accelerator.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 HWND 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 HWNDView class. +// +// When creating a top window, if it derives from WidgetWin, the +// |has_own_focus_manager| of the Init method lets you specify whether that +// window should have its own focus manager (so focus traversal stays confined +// in that window). If you are not deriving from WidgetWin or one of its +// derived classes (Window, FramelessWindow, ConstrainedWindow), you must +// create a FocusManager when the window is created (it is automatically deleted +// when the window is destroyed). +// +// 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 WidgetWin as its native +// component), then you need to: +// - override the View::GetFocusTraversable() method in your outter 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 RootView::SetFocusTraversableParent() on the nested RootView and point +// it to the outter RootView. This is used when the focus goes out of the +// nested RootView. In the example: +// hwnd_view_container_->GetRootView()->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_->GetRootView()->SetFocusTraversableParent( +// native_control); +// +// Note that FocusTraversable do not have to be RootViews: TabContents is +// FocusTraversable. + +namespace views { + +class View; +class RootView; + +// The FocusTraversable interface is used by components that want to process +// focus traversal events (due to Tab/Shift-Tab key events). +class FocusTraversable { + 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 + }; + + // Should find the next view that should be focused and return it. If a + // FocusTraversable is found while searching for the focusable view, NULL + // should be returned, focus_traversable should be set to the FocusTraversable + // and focus_traversable_view should be set to the view associated with the + // FocusTraversable. + // This call should return NULL if the end of the focus loop is reached. + // - |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). + // - |dont_loop| if true specifies that if there is a loop in the focus + // hierarchy, we should keep traversing after the last view of the loop. + // - |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 dont_loop, + FocusTraversable** focus_traversable, + View** focus_traversable_view) = 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; +}; + +// The KeystrokeListener interface is used by components (such as the +// ExternalTabContainer class) which need a crack at handling all +// keystrokes. +class KeystrokeListener { + public: + // If this returns true, then the component handled the keystroke and ate + // it. +#if defined(OS_WIN) + virtual bool ProcessKeyStroke(HWND window, UINT message, WPARAM wparam, + LPARAM lparam) = 0; +#endif +}; + +// 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 FocusChangeListener { + public: + virtual void FocusWillChange(View* focused_before, View* focused_now) = 0; +}; + +class FocusManager { + public: +#if defined(OS_WIN) + // Creates a FocusManager for the specified window. Top level windows + // must invoked this when created. + // The RootView specified should be the top RootView of the window. + // This also invokes InstallFocusSubclass. + static FocusManager* CreateFocusManager(HWND window, RootView* root_view); + + // Subclasses the specified window. The subclassed window procedure listens + // for WM_SETFOCUS notification and keeps the FocusManager's focus owner + // property in sync. + // It's not necessary to explicitly invoke Uninstall, it's automatically done + // when the window is destroyed and Uninstall wasn't invoked. + static void InstallFocusSubclass(HWND window, View* view); + + // Uninstalls the window subclass installed by InstallFocusSubclass. + static void UninstallFocusSubclass(HWND window); + + static FocusManager* GetFocusManager(HWND window); + + // Message handlers (for messages received from registered windows). + // Should return true if the message should be forwarded to the window + // original proc function, false otherwise. + bool OnSetFocus(HWND window); + bool OnNCDestroy(HWND window); + // OnKeyDown covers WM_KEYDOWN and WM_SYSKEYDOWN. + bool OnKeyDown(HWND window, + UINT message, + WPARAM wparam, + LPARAM lparam); + bool OnKeyUp(HWND window, + UINT message, + WPARAM wparam, + LPARAM lparam); + // OnPostActivate is called after WM_ACTIVATE has been propagated to the + // DefWindowProc. + bool OnPostActivate(HWND window, int activation_state, int minimized_state); +#endif + + // 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 is handling the selected view for the RootView. + View* GetFocusedView() const { return focused_view_; } + void SetFocusedView(View* view); + + // Clears the focused view. The window associated with the top root view gets + // the native focus (so we still get keyboard events). + void ClearFocus(); + + // Clears the HWND that has the focus by focusing the HWND from the top + // RootView (so we still get keyboard events). + // Note that this does not change the currently focused view. + void ClearHWNDFocus(); + +#if defined(OS_WIN) + // Focus the specified |hwnd| without changing the focused view. + void FocusHWND(HWND hwnd); +#endif + + // Validates the focused view, clearing it if the window it belongs too is not + // attached to the window hierarchy anymore. + void ValidateFocusedView(); + +#if defined(OS_WIN) + // Returns the view associated with the specified window if any. + // If |look_in_parents| is true, it goes up the window parents until it find + // a view. + static View* GetViewForWindow(HWND window, bool look_in_parents); +#endif + + // 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 the FocusManager of the parent window of the window that is the + // root of this FocusManager. This is useful with ConstrainedWindows that have + // their own FocusManager and need to return focus to the browser when closed. + FocusManager* GetParentFocusManager() const; + + // Register a keyboard accelerator for the specified target. If an + // AcceleratorTarget is already registered for that accelerator, it is + // returned. + // 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) + AcceleratorTarget* RegisterAccelerator(const Accelerator& accelerator, + AcceleratorTarget* target); + + // Unregister the specified keyboard accelerator for the specified target. + void UnregisterAccelerator(const Accelerator& accelerator, + AcceleratorTarget* target); + + // Unregister all keyboard accelerator for the specified target. + void UnregisterAccelerators(AcceleratorTarget* target); + + // Activate the target associated with the specified accelerator if any. + // Returns true if an accelerator was activated. + bool ProcessAccelerator(const 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* parent, View* removed); + + void AddKeystrokeListener(KeystrokeListener* listener); + void RemoveKeystrokeListener(KeystrokeListener* listener); + + // 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. + // TODO(finnur): http://b/1307173 Make this private once the bug is fixed. + AcceleratorTarget* GetTargetForAccelerator( + const Accelerator& accelerator) const; + + private: +#if defined(OS_WIN) + explicit FocusManager(HWND root, RootView* root_view); +#endif + ~FocusManager(); + + // Returns the next focusable view. + View* GetNextFocusableView(View* starting_view, bool reverse, bool dont_loop); + + // Returns the last view of the focus traversal hierarchy. + View* FindLastFocusableView(); + + // 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, + bool dont_loop); + + // The RootView of the window associated with this FocusManager. + RootView* top_root_view_; + + // The view that currently is focused. + View* focused_view_; + + // The storage id used in the ViewStorage to store/restore the view that last + // had focus. + int stored_focused_view_storage_id_; + +#if defined(OS_WIN) + // The window associated with this focus manager. + HWND root_; +#endif + + // Used to allow setting the focus on an HWND without changing the currently + // focused view. + bool ignore_set_focus_msg_; + + // The accelerators and associated targets. + typedef std::map<Accelerator, AcceleratorTarget*> AcceleratorMap; + AcceleratorMap accelerators_; + + // The list of registered keystroke listeners + typedef std::vector<KeystrokeListener*> KeystrokeListenerList; + KeystrokeListenerList keystroke_listeners_; + + // The list of registered FocusChange listeners. + typedef std::vector<FocusChangeListener*> FocusChangeListenerList; + FocusChangeListenerList focus_change_listeners_; + + DISALLOW_COPY_AND_ASSIGN(FocusManager); +}; + +} // namespace views + +#endif // VIEWS_FOCUS_FOCUS_MANAGER_H_ diff --git a/views/focus/focus_manager_unittest.cc b/views/focus/focus_manager_unittest.cc new file mode 100644 index 0000000..234db1d --- /dev/null +++ b/views/focus/focus_manager_unittest.cc @@ -0,0 +1,666 @@ +// Copyright (c) 2006-2008 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. + +// Disabled right now as this won't work on BuildBots right now as this test +// require the box it runs on to be unlocked (and no screen-savers). +// The test actually simulates mouse and key events, so if the screen is locked, +// the events don't go to the Chrome window. +#include "testing/gtest/include/gtest/gtest.h" + +#include "app/resource_bundle.h" +#include "base/gfx/rect.h" +#include "skia/include/SkColor.h" +#include "views/background.h" +#include "views/border.h" +#include "views/controls/button/checkbox.h" +#include "views/controls/button/native_button.h" +#include "views/controls/button/radio_button.h" +#include "views/controls/label.h" +#include "views/controls/link.h" +#include "views/controls/scroll_view.h" +#include "views/controls/tabbed_pane.h" +#include "views/controls/text_field.h" +#include "views/widget/accelerator_handler.h" +#include "views/widget/root_view.h" +#include "views/widget/widget_win.h" +#include "views/window/window.h" +#include "views/window/window_delegate.h" + + + +namespace { + +static const int kWindowWidth = 600; +static const int kWindowHeight = 500; + +static int count = 1; + +static const int kTopCheckBoxID = count++; // 1 +static const int kLeftContainerID = count++; +static const int kAppleLabelID = count++; +static const int kAppleTextFieldID = count++; +static const int kOrangeLabelID = count++; // 5 +static const int kOrangeTextFieldID = count++; +static const int kBananaLabelID = count++; +static const int kBananaTextFieldID = count++; +static const int kKiwiLabelID = count++; +static const int kKiwiTextFieldID = count++; // 10 +static const int kFruitButtonID = count++; +static const int kFruitCheckBoxID = count++; + +static const int kRightContainerID = count++; +static const int kAsparagusButtonID = count++; +static const int kBroccoliButtonID = count++; // 15 +static const int kCauliflowerButtonID = count++; + +static const int kInnerContainerID = count++; +static const int kScrollViewID = count++; +static const int kScrollContentViewID = count++; +static const int kRosettaLinkID = count++; // 20 +static const int kStupeurEtTremblementLinkID = count++; +static const int kDinerGameLinkID = count++; +static const int kRidiculeLinkID = count++; +static const int kClosetLinkID = count++; +static const int kVisitingLinkID = count++; // 25 +static const int kAmelieLinkID = count++; +static const int kJoyeuxNoelLinkID = count++; +static const int kCampingLinkID = count++; +static const int kBriceDeNiceLinkID = count++; +static const int kTaxiLinkID = count++; // 30 +static const int kAsterixLinkID = count++; + +static const int kOKButtonID = count++; +static const int kCancelButtonID = count++; +static const int kHelpButtonID = count++; + +static const int kStyleContainerID = count++; // 35 +static const int kBoldCheckBoxID = count++; +static const int kItalicCheckBoxID = count++; +static const int kUnderlinedCheckBoxID = count++; + +static const int kSearchContainerID = count++; +static const int kSearchTextFieldID = count++; // 40 +static const int kSearchButtonID = count++; +static const int kHelpLinkID = count++; + +static const int kThumbnailContainerID = count++; +static const int kThumbnailStarID = count++; +static const int kThumbnailSuperStarID = count++; + +class FocusManagerTest; + +// BorderView is a NativeControl that creates a tab control as its child and +// takes a View to add as the child of the tab control. The tab control is used +// to give a nice background for the view. At some point we'll have a real +// wrapper for TabControl, and this can be nuked in favor of it. +// Taken from keyword_editor_view.cc. +// It is interesting in our test as it is a native control containing another +// RootView. +class BorderView : public views::NativeControl { + public: + explicit BorderView(View* child) : child_(child) { + DCHECK(child); + SetFocusable(false); + } + + virtual ~BorderView() {} + + virtual HWND CreateNativeControl(HWND parent_container) { + // Create the tab control. + HWND tab_control = ::CreateWindowEx(GetAdditionalExStyle(), + WC_TABCONTROL, + L"", + WS_CHILD, + 0, 0, width(), height(), + parent_container, NULL, NULL, NULL); + // Create the view container which is a child of the TabControl. + widget_ = new views::WidgetWin(); + widget_->Init(tab_control, gfx::Rect(), false); + widget_->SetContentsView(child_); + widget_->SetFocusTraversableParentView(this); + ResizeContents(tab_control); + return tab_control; + } + + virtual LRESULT OnNotify(int w_param, LPNMHDR l_param) { + return 0; + } + + virtual void Layout() { + NativeControl::Layout(); + ResizeContents(GetNativeControlHWND()); + } + + virtual views::RootView* GetContentsRootView() { + return widget_->GetRootView(); + } + + virtual views::FocusTraversable* GetFocusTraversable() { + return widget_; + } + + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child) { + NativeControl::ViewHierarchyChanged(is_add, parent, child); + + if (child == this && is_add) { + // We have been added to a view hierarchy, update the FocusTraversable + // parent. + widget_->SetFocusTraversableParent(GetRootView()); + } + } + +private: + void ResizeContents(HWND tab_control) { + DCHECK(tab_control); + CRect content_bounds; + if (!GetClientRect(tab_control, &content_bounds)) + return; + TabCtrl_AdjustRect(tab_control, FALSE, &content_bounds); + widget_->MoveWindow(content_bounds.left, content_bounds.top, + content_bounds.Width(), content_bounds.Height(), + TRUE); + } + + View* child_; + views::WidgetWin* widget_; + + DISALLOW_EVIL_CONSTRUCTORS(BorderView); +}; + +class TestViewWindow : public views::WidgetWin { + public: + explicit TestViewWindow(FocusManagerTest* test); + ~TestViewWindow() { } + + void Init(); + + views::View* contents() const { return contents_; } + + + // Return the ID of the component that currently has the focus. + int GetFocusedComponentID(); + + // Simulate pressing the tab button in the window. + void PressTab(bool shift_pressed, bool ctrl_pressed); + + views::RootView* GetContentsRootView() const { + return contents_->GetRootView(); + } + + views::RootView* GetStyleRootView() const { + return style_tab_->GetContentsRootView(); + } + + views::RootView* GetSearchRootView() const { + return search_border_view_->GetContentsRootView(); + } + + private: + views::View* contents_; + + views::TabbedPane* style_tab_; + BorderView* search_border_view_; + + FocusManagerTest* test_; + + DISALLOW_EVIL_CONSTRUCTORS(TestViewWindow); +}; + +class FocusManagerTest : public testing::Test { + public: + TestViewWindow* GetWindow(); + ~FocusManagerTest(); + + protected: + FocusManagerTest(); + + virtual void SetUp(); + virtual void TearDown(); + + MessageLoopForUI message_loop_; + TestViewWindow* test_window_; +}; + +//////////////////////////////////////////////////////////////////////////////// +// TestViewWindow +//////////////////////////////////////////////////////////////////////////////// + +TestViewWindow::TestViewWindow(FocusManagerTest* test) + : test_(test), + contents_(NULL), + style_tab_(NULL), + search_border_view_(NULL) { +} + +// Initializes and shows the window with the contents view. +void TestViewWindow::Init() { + gfx::Rect bounds(0, 0, 600, 460); + contents_ = new views::View(); + contents_->set_background( + views::Background::CreateSolidBackground(255, 255, 255)); + + WidgetWin::Init(NULL, bounds, true); + SetContentsView(contents_); + + views::Checkbox* cb = + new views::Checkbox(L"This is a checkbox"); + contents_->AddChildView(cb); + // In this fast paced world, who really has time for non hard-coded layout? + cb->SetBounds(10, 10, 200, 20); + cb->SetID(kTopCheckBoxID); + + views::View* left_container = new views::View(); + left_container->set_border( + views::Border::CreateSolidBorder(1, SK_ColorBLACK)); + left_container->set_background( + views::Background::CreateSolidBackground(240, 240, 240)); + left_container->SetID(kLeftContainerID); + contents_->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; + + views::Label* label = new views::Label(L"Apple:"); + label->SetID(kAppleLabelID); + left_container->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + views::TextField* text_field = new views::TextField(); + text_field->SetID(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 views::Label(L"Orange:"); + label->SetID(kOrangeLabelID); + left_container->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new views::TextField(); + text_field->SetID(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 views::Label(L"Banana:"); + label->SetID(kBananaLabelID); + left_container->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new views::TextField(); + text_field->SetID(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 views::Label(L"Kiwi:"); + label->SetID(kKiwiLabelID); + left_container->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new views::TextField(); + text_field->SetID(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; + + views::NativeButton* button = new views::NativeButton(NULL, L"Click me"); + button->SetBounds(label_x, y + 10, 50, 20); + button->SetID(kFruitButtonID); + left_container->AddChildView(button); + y += 40; + + cb = new views::Checkbox(L"This is another check box"); + cb->SetBounds(label_x + label_width + 5, y, 100, 20); + cb->SetID(kFruitCheckBoxID); + left_container->AddChildView(cb); + + views::View* right_container = new views::View(); + right_container->set_border( + views::Border::CreateSolidBorder(1, SK_ColorBLACK)); + right_container->set_background( + views::Background::CreateSolidBackground(240, 240, 240)); + right_container->SetID(kRightContainerID); + contents_->AddChildView(right_container); + right_container->SetBounds(270, 35, 300, 200); + + y = 10; + int radio_button_height = 15; + int gap_between_radio_buttons = 10; + views::View* radio_button = + new views::RadioButton(L"Asparagus", 1); + radio_button->SetID(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 views::RadioButton(L"Broccoli", 1); + radio_button->SetID(kBroccoliButtonID); + 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 views::RadioButton(L"Cauliflower", 1); + radio_button->SetID(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; + + views::View* inner_container = new views::View(); + inner_container->set_border( + views::Border::CreateSolidBorder(1, SK_ColorBLACK)); + inner_container->set_background( + views::Background::CreateSolidBackground(230, 230, 230)); + inner_container->SetID(kInnerContainerID); + right_container->AddChildView(inner_container); + inner_container->SetBounds(100, 10, 150, 180); + + views::ScrollView* scroll_view = new views::ScrollView(); + scroll_view->SetID(kScrollViewID); + inner_container->AddChildView(scroll_view); + scroll_view->SetBounds(1, 1, 148, 178); + + views::View* scroll_content = new views::View(); + scroll_content->SetBounds(0, 0, 200, 200); + scroll_content->set_background( + views::Background::CreateSolidBackground(200, 200, 200)); + scroll_view->SetContents(scroll_content); + + static const wchar_t* const kTitles[] = { + L"Rosetta", L"Stupeur et tremblement", L"The diner game", + L"Ridicule", L"Le placard", L"Les Visiteurs", L"Amelie", + L"Joyeux Noel", L"Camping", L"Brice de Nice", + L"Taxi", L"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 (int i = 0; i < arraysize(kTitles); ++i) { + views::Link* link = new views::Link(kTitles[i]); + link->SetHorizontalAlignment(views::Label::ALIGN_LEFT); + link->SetID(kIDs[i]); + scroll_content->AddChildView(link); + link->SetBounds(5, y, 300, 15); + y += 15; + } + + y = 250; + int width = 50; + button = new views::NativeButton(NULL, L"OK"); + button->SetID(kOKButtonID); + + contents_->AddChildView(button); + button->SetBounds(150, y, width, 20); + + button = new views::NativeButton(NULL, L"Cancel"); + button->SetID(kCancelButtonID); + contents_->AddChildView(button); + button->SetBounds(250, y, width, 20); + + button = new views::NativeButton(NULL, L"Help"); + button->SetID(kHelpButtonID); + contents_->AddChildView(button); + button->SetBounds(350, y, width, 20); + + y += 40; + + // Left bottom box with style checkboxes. + views::View* contents = new views::View(); + contents->set_background( + views::Background::CreateSolidBackground(SK_ColorWHITE)); + cb = new views::Checkbox(L"Bold"); + contents->AddChildView(cb); + cb->SetBounds(10, 10, 50, 20); + cb->SetID(kBoldCheckBoxID); + + cb = new views::Checkbox(L"Italic"); + contents->AddChildView(cb); + cb->SetBounds(70, 10, 50, 20); + cb->SetID(kItalicCheckBoxID); + + cb = new views::Checkbox(L"Underlined"); + contents->AddChildView(cb); + cb->SetBounds(130, 10, 70, 20); + cb->SetID(kUnderlinedCheckBoxID); + + style_tab_ = new views::TabbedPane(); + style_tab_->SetID(kStyleContainerID); + contents_->AddChildView(style_tab_); + style_tab_->SetBounds(10, y, 210, 50); + style_tab_->AddTab(L"Style", contents); + style_tab_->AddTab(L"Other", new views::View()); + + // Right bottom box with search. + contents = new views::View(); + contents->set_background( + views::Background::CreateSolidBackground(SK_ColorWHITE)); + text_field = new views::TextField(); + contents->AddChildView(text_field); + text_field->SetBounds(10, 10, 100, 20); + text_field->SetID(kSearchTextFieldID); + + button = new views::NativeButton(NULL, L"Search"); + contents->AddChildView(button); + button->SetBounds(115, 10, 50, 20); + button->SetID(kSearchButtonID); + + views::Link* link = new views::Link(L"Help"); + link->SetHorizontalAlignment(views::Label::ALIGN_LEFT); + link->SetID(kHelpLinkID); + contents->AddChildView(link); + link->SetBounds(170, 10, 30, 15); + + search_border_view_ = new BorderView(contents); + search_border_view_->SetID(kSearchContainerID); + + contents_->AddChildView(search_border_view_); + search_border_view_->SetBounds(300, y, 200, 50); + + y += 60; + + contents = new views::View(); + contents->SetFocusable(true); + contents->set_background( + views::Background::CreateSolidBackground(SK_ColorBLUE)); + contents->SetID(kThumbnailContainerID); + button = new views::NativeButton(NULL, L"Star"); + contents->AddChildView(button); + button->SetBounds(5, 5, 50, 20); + button->SetID(kThumbnailStarID); + button = new views::NativeButton(NULL, L"SuperStar"); + contents->AddChildView(button); + button->SetBounds(60, 5, 100, 20); + button->SetID(kThumbnailSuperStarID); + + contents_->AddChildView(contents); + contents->SetBounds(200, y, 200, 50); +} + +//////////////////////////////////////////////////////////////////////////////// +// FocusManagerTest +//////////////////////////////////////////////////////////////////////////////// + +FocusManagerTest::FocusManagerTest() { +} + +FocusManagerTest::~FocusManagerTest() { +} + +TestViewWindow* FocusManagerTest::GetWindow() { + return test_window_; +} + +void FocusManagerTest::SetUp() { + OleInitialize(NULL); + test_window_ = new TestViewWindow(this); + test_window_->Init(); + ShowWindow(test_window_->GetNativeView(), SW_SHOW); +} + +void FocusManagerTest::TearDown() { + test_window_->CloseNow(); + + // Flush the message loop to make Purify happy. + message_loop_.RunAllPending(); + OleUninitialize(); +} + +//////////////////////////////////////////////////////////////////////////////// +// The tests +//////////////////////////////////////////////////////////////////////////////// + + +TEST_F(FocusManagerTest, NormalTraversal) { + const int kTraversalIDs[] = { kTopCheckBoxID, kAppleTextFieldID, + kOrangeTextFieldID, kBananaTextFieldID, kKiwiTextFieldID, + kFruitButtonID, kFruitCheckBoxID, kAsparagusButtonID, kRosettaLinkID, + kStupeurEtTremblementLinkID, + kDinerGameLinkID, kRidiculeLinkID, kClosetLinkID, kVisitingLinkID, + kAmelieLinkID, kJoyeuxNoelLinkID, kCampingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID, kOKButtonID, kCancelButtonID, kHelpButtonID, + kStyleContainerID, kBoldCheckBoxID, kItalicCheckBoxID, + kUnderlinedCheckBoxID, kSearchTextFieldID, kSearchButtonID, kHelpLinkID, + kThumbnailContainerID, kThumbnailStarID, kThumbnailSuperStarID }; + + // Uncomment the following line if you want to test manually the UI of this + // test. + // MessageLoop::current()->Run(new views::AcceleratorHandler()); + + views::FocusManager* focus_manager = + views::FocusManager::GetFocusManager(test_window_->GetNativeView()); + // Let's traverse the whole focus hierarchy (several times, to make sure it + // loops OK). + focus_manager->SetFocusedView(NULL); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < arraysize(kTraversalIDs); j++) { + focus_manager->AdvanceFocus(false); + views::View* focused_view = focus_manager->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->GetID()); + } + } + + // Focus the 1st item. + views::RootView* root_view = test_window_->GetContentsRootView(); + focus_manager->SetFocusedView(root_view->GetViewByID(kTraversalIDs[0])); + + /* BROKEN because of bug #1153276. The reverse order of traversal in Tabbed + Panes is broken (we go to the tab before going to the content + // Let's traverse in reverse order. + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + focus_manager->AdvanceFocus(true); + views::View* focused_view = focus_manager->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->GetID()); + } + } + */ +} + +TEST_F(FocusManagerTest, TraversalWithNonEnabledViews) { + const int kMainContentsDisabledIDs[] = { + kBananaTextFieldID, kFruitCheckBoxID, kAsparagusButtonID, + kCauliflowerButtonID, kClosetLinkID, kVisitingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID, kHelpButtonID }; + + const int kStyleContentsDisabledIDs[] = { kBoldCheckBoxID }; + + const int kSearchContentsDisabledIDs[] = { kSearchTextFieldID, kHelpLinkID }; + + const int kTraversalIDs[] = { kTopCheckBoxID, kAppleTextFieldID, + kOrangeTextFieldID, kKiwiTextFieldID, kFruitButtonID, kBroccoliButtonID, + kRosettaLinkID, kStupeurEtTremblementLinkID, kDinerGameLinkID, + kRidiculeLinkID, kAmelieLinkID, kJoyeuxNoelLinkID, kCampingLinkID, + kOKButtonID, kCancelButtonID, kStyleContainerID, + kItalicCheckBoxID, kUnderlinedCheckBoxID, kSearchButtonID, + kThumbnailContainerID, kThumbnailStarID, kThumbnailSuperStarID }; + + // Let's disable some views. + views::RootView* root_view = test_window_->GetContentsRootView(); + for (int i = 0; i < arraysize(kMainContentsDisabledIDs); i++) { + views::View* v = root_view->GetViewByID(kMainContentsDisabledIDs[i]); + ASSERT_TRUE(v != NULL); + if (v) + v->SetEnabled(false); + } + root_view = test_window_->GetStyleRootView(); + for (int i = 0; i < arraysize(kStyleContentsDisabledIDs); i++) { + views::View* v = root_view->GetViewByID(kStyleContentsDisabledIDs[i]); + ASSERT_TRUE(v != NULL); + if (v) + v->SetEnabled(false); + } + root_view = test_window_->GetSearchRootView(); + for (int i = 0; i < arraysize(kSearchContentsDisabledIDs); i++) { + views::View* v = + root_view->GetViewByID(kSearchContentsDisabledIDs[i]); + ASSERT_TRUE(v != NULL); + if (v) + v->SetEnabled(false); + } + + + views::FocusManager* focus_manager = + views::FocusManager::GetFocusManager(test_window_->GetNativeView()); + views::View* focused_view; + // Let's do one traversal (several times, to make sure it loops ok). + for (int i = 0; i < 3;++i) { + for (int j = 0; j < arraysize(kTraversalIDs); j++) { + focus_manager->AdvanceFocus(false); + focused_view = focus_manager->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->GetID()); + } + } + + // Focus the 1st item. + focus_manager->AdvanceFocus(false); + focused_view = focus_manager->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[0], focused_view->GetID()); + + // Same thing in reverse. + /* BROKEN because of bug #1153276. The reverse order of traversal in Tabbed + Panes is broken (we go to the tab before going to the content + + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + focus_manager->AdvanceFocus(true); + focused_view = focus_manager->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->GetID()); + } + } + */ +} + +} diff --git a/views/focus/focus_util_win.cc b/views/focus/focus_util_win.cc new file mode 100644 index 0000000..046df70 --- /dev/null +++ b/views/focus/focus_util_win.cc @@ -0,0 +1,118 @@ +// Copyright (c) 2009 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_util_win.h" + +#include <windowsx.h> + +#include "base/win_util.h" + +namespace views { + +// Property used to indicate the HWND supports having mouse wheel messages +// rerouted to it. +static const wchar_t* const kHWNDSupportMouseWheelRerouting = + L"__HWND_MW_REROUTE_OK"; + +static bool WindowSupportsRerouteMouseWheel(HWND window) { + while (GetWindowLong(window, GWL_STYLE) & WS_CHILD) { + if (!IsWindow(window)) + break; + + if (reinterpret_cast<bool>(GetProp(window, + kHWNDSupportMouseWheelRerouting))) { + return true; + } + window = GetParent(window); + } + return false; +} + +static bool IsCompatibleWithMouseWheelRedirection(HWND window) { + std::wstring class_name = win_util::GetClassName(window); + // Mousewheel redirection to comboboxes is a surprising and + // undesireable user behavior. + return !(class_name == L"ComboBox" || + class_name == L"ComboBoxEx32"); +} + +static bool CanRedirectMouseWheelFrom(HWND window) { + std::wstring class_name = win_util::GetClassName(window); + + // Older Thinkpad mouse wheel drivers create a window under mouse wheel + // pointer. Detect if we are dealing with this window. In this case we + // don't need to do anything as the Thinkpad mouse driver will send + // mouse wheel messages to the right window. + if ((class_name == L"Syn Visual Class") || + (class_name == L"SynTrackCursorWindowClass")) + return false; + + return true; +} + +void SetWindowSupportsRerouteMouseWheel(HWND hwnd) { + SetProp(hwnd, kHWNDSupportMouseWheelRerouting, + reinterpret_cast<HANDLE>(true)); +} + +bool RerouteMouseWheel(HWND window, WPARAM w_param, LPARAM l_param) { + // Since this is called from a subclass for every window, we can get + // here recursively. This will happen if, for example, a control + // reflects wheel scroll messages to its parent. Bail out if we got + // here recursively. + static bool recursion_break = false; + if (recursion_break) + return false; + // Check if this window's class has a bad interaction with rerouting. + if (!IsCompatibleWithMouseWheelRedirection(window)) + return false; + + DWORD current_process = GetCurrentProcessId(); + POINT wheel_location = { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) }; + HWND window_under_wheel = WindowFromPoint(wheel_location); + + if (!CanRedirectMouseWheelFrom(window_under_wheel)) + return false; + + // Find the lowest Chrome window in the hierarchy that can be the + // target of mouse wheel redirection. + while (window != window_under_wheel) { + // If window_under_wheel is not a valid Chrome window, then return true to + // suppress further processing of the message. + if (!::IsWindow(window_under_wheel)) + return true; + DWORD wheel_window_process = 0; + GetWindowThreadProcessId(window_under_wheel, &wheel_window_process); + if (current_process != wheel_window_process) { + if (IsChild(window, window_under_wheel)) { + // If this message is reflected from a child window in a different + // process (happens with out of process windowed plugins) then + // we don't want to reroute the wheel message. + return false; + } else { + // The wheel is scrolling over an unrelated window. Make sure that we + // have marked that window as supporting mouse wheel rerouting. + // Otherwise, we cannot send random WM_MOUSEWHEEL messages to arbitrary + // windows. So just drop the message. + if (!WindowSupportsRerouteMouseWheel(window_under_wheel)) + return true; + } + } + + // window_under_wheel is a Chrome window. If allowed, redirect. + if (IsCompatibleWithMouseWheelRedirection(window_under_wheel)) { + recursion_break = true; + SendMessage(window_under_wheel, WM_MOUSEWHEEL, w_param, l_param); + recursion_break = false; + return true; + } + // If redirection is disallowed, try the parent. + window_under_wheel = GetAncestor(window_under_wheel, GA_PARENT); + } + // If we traversed back to the starting point, we should process + // this message normally; return false. + return false; +} + +} // namespace views diff --git a/views/focus/focus_util_win.h b/views/focus/focus_util_win.h new file mode 100644 index 0000000..832a6df --- /dev/null +++ b/views/focus/focus_util_win.h @@ -0,0 +1,28 @@ +// Copyright (c) 2009 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 VIEWS_FOCUS_FOCUS_UTIL_WIN_H_ +#define VIEWS_FOCUS_FOCUS_UTIL_WIN_H_ + +#include <windows.h> + +namespace views { + +// Marks the passed |hwnd| as supporting mouse-wheel message rerouting. +// We reroute the mouse wheel messages to such HWND when they are under the +// mouse pointer (but are not the active window) +void SetWindowSupportsRerouteMouseWheel(HWND hwnd); + +// Forwards mouse wheel messages to the window under it. +// Windows sends mouse wheel messages to the currently active window. +// This causes a window to scroll even if it is not currently under the mouse +// wheel. The following code gives mouse wheel messages to the window under the +// mouse wheel in order to scroll that window. This is arguably a better user +// experience. The returns value says whether the mouse wheel message was +// successfully redirected. +bool RerouteMouseWheel(HWND window, WPARAM w_param, LPARAM l_param); + +} // namespace views + +#endif // VIEWS_FOCUS_FOCUS_UTIL_WIN_H_ diff --git a/views/focus/view_storage.cc b/views/focus/view_storage.cc new file mode 100644 index 0000000..a108530 --- /dev/null +++ b/views/focus/view_storage.cc @@ -0,0 +1,182 @@ +// Copyright (c) 2006-2008 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-inl.h" + +namespace views { + +// This struct contains the information to locate a specific view. +// Locating a view is not always straight-forward as a view can be a floating +// view, or the child of a floating view. Floating views are frequently deleted +// and recreated (typically when their container is laid out). +struct ViewLocationInfo { + // True if the view is floating or the child of a floating view. False + // otherwise. + bool is_floating_view; + + // If non floating, this is the stored view. If floating, the parent of the + // floating view. + View* view; + + // The id of the floating view. + int floating_view_id; + + // The path from the floating view to the stored view. The path is composed + // of the indexes of the views in the hierarchy. + std::vector<int> floating_view_to_view_path; +}; + +// static +ViewStorage* ViewStorage::GetSharedInstance() { + return Singleton<ViewStorage>::get(); +} + +ViewStorage::ViewStorage() : view_storage_next_id_(0) { +} + +ViewStorage::~ViewStorage() { + STLDeleteContainerPairSecondPointers(id_to_view_location_.begin(), + id_to_view_location_.end()); + + 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, ViewLocationInfo*>::iterator iter = + id_to_view_location_.find(storage_id); + DCHECK(iter == id_to_view_location_.end()); + + if (iter != id_to_view_location_.end()) + RemoveView(storage_id); + + ViewLocationInfo* view_location_info = new ViewLocationInfo(); + View* floating_view_parent = view->RetrieveFloatingViewParent(); + if (floating_view_parent) { + // The view is a floating view or is a child of a floating view. + view_location_info->is_floating_view = true; + view_location_info->view = floating_view_parent->GetParent(); + view_location_info->floating_view_id = + floating_view_parent->GetFloatingViewID(); + // Ley's store the path from the floating view to the actual view so we can + // locate it when restoring. + View::GetViewPath(floating_view_parent, view, + &(view_location_info->floating_view_to_view_path)); + } else { + // It is a non floating view, it can be stored as is. + view_location_info->is_floating_view = false; + view_location_info->view = view; + } + id_to_view_location_[storage_id] = view_location_info; + + std::vector<int>* ids = NULL; + std::map<View*, std::vector<int>*>::iterator id_iter = + view_to_ids_.find(view_location_info->view); + if (id_iter == view_to_ids_.end()) { + ids = new std::vector<int>(); + view_to_ids_[view_location_info->view] = ids; + } else { + ids = id_iter->second; + } + ids->push_back(storage_id); +} + +View* ViewStorage::RetrieveView(int storage_id) { + std::map<int, ViewLocationInfo*>::iterator iter = + id_to_view_location_.find(storage_id); + if (iter == id_to_view_location_.end()) + return NULL; + + ViewLocationInfo* view_location_info = iter->second; + if (view_location_info->is_floating_view) { + View* floating_view = view_location_info->view-> + RetrieveFloatingViewForID(view_location_info->floating_view_id); + View* v = NULL; + if (floating_view) { + v = View::GetViewForPath(floating_view, + view_location_info->floating_view_to_view_path); + } + if (!v) { + // If we have not found the view, it means either the floating view with + // the id we have is gone, or it has changed and the actual child view + // we have the path for is not accessible. In that case, let's make sure + // we don't leak the ViewLocationInfo. + RemoveView(storage_id); + } + return v; + } else { + return view_location_info->view; + } +} + +void ViewStorage::RemoveView(int storage_id) { + EraseView(storage_id, false); +} + +void ViewStorage::ViewRemoved(View* parent, 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, ViewLocationInfo*>::iterator location_iter = + id_to_view_location_.find(storage_id); + if (location_iter == id_to_view_location_.end()) + return; + + ViewLocationInfo* view_location = location_iter->second; + View* view = view_location->view; + delete view_location; + id_to_view_location_.erase(location_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) { + location_iter = id_to_view_location_.find((*ids)[i]); + if (location_iter != id_to_view_location_.end()) { + delete location_iter->second; + id_to_view_location_.erase(location_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/views/focus/view_storage.h b/views/focus/view_storage.h new file mode 100644 index 0000000..9182429 --- /dev/null +++ b/views/focus/view_storage.h @@ -0,0 +1,78 @@ +// Copyright (c) 2006-2008 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 VIEWS_FOCUS_VIEW_STORAGE_H_ +#define VIEWS_FOCUS_VIEW_STORAGE_H_ + +#include "base/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 supports floating views, meaning +// that when you store a view, it can be retrieved even if it is a floating +// view or the child of a floating view that has been detached since the view +// was stored (in which case the floating view is recreated and reattached). +// It also automatically removes a view from the storage if the view is removed +// from the tree hierarchy (or in the case of a floating view, if the view +// containing the floating view is removed). +// +// To use it, you first need to create a view storage id that can then be used +// to store/retrieve views. + +namespace views { + +struct ViewLocationInfo; + +class ViewStorage { + public: + // Returns the global ViewStorage instance. + // It is guaranted to be non NULL. + static ViewStorage* GetSharedInstance(); + + // 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* parent, View* removed); + +#ifdef UNIT_TEST + size_t view_count() const { return view_to_ids_.size(); } +#endif + + 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. The ViewStorage owns + // the ViewLocationInfo. + std::map<int, ViewLocationInfo*> id_to_view_location_; + + // 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 // #ifndef VIEWS_FOCUS_VIEW_STORAGE_H_ diff --git a/views/grid_layout.cc b/views/grid_layout.cc new file mode 100644 index 0000000..7eb07d1 --- /dev/null +++ b/views/grid_layout.cc @@ -0,0 +1,1013 @@ +// Copyright (c) 2006-2008 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/grid_layout.h" + +#include <algorithm> + +#include "base/logging.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_EVIL_CONSTRUCTORS(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_EVIL_CONSTRUCTORS(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) { + } + + virtual ~Row() {} + + virtual void ResetSize() { + SetSize(height_); + } + + ColumnSet* column_set() { + return column_set_; + } + + private: + const bool fixed_height_; + const int height_; + // The column set used for this row; null for padding rows. + ColumnSet* column_set_; + + DISALLOW_EVIL_CONSTRUCTORS(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) { + 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; +}; + +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() { + for (std::vector<Column*>::iterator i = columns_.begin(); + i != columns_.end(); ++i) { + delete *i; + } +} + +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; + for (int i = start_col; i < max_col; ++i) { + if (columns_[i]->IsResizable()) { + resizable_columns++; + } else if (columns_[i]->size_type_ == GridLayout::USE_PREF) { + pref_size_columns++; + } + } + + if (resizable_columns > 0) { + // There are resizable columns, give the remaining width to them. + int to_distribute = width / resizable_columns; + for (int i = start_col; i < max_col; ++i) { + if (columns_[i]->IsResizable()) { + width -= to_distribute; + if (width < to_distribute) { + // Give all slop to the last column. + to_distribute += width; + } + columns_[i]->SetSize(columns_[i]->Size() + to_distribute); + } + } + } 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() { + for (std::vector<ColumnSet*>::iterator i = column_sets_.begin(); + i != column_sets_.end(); ++i) { + delete *i; + } + for (std::vector<ViewState*>::iterator i = view_states_.begin(); + i != view_states_.end(); ++i) { + delete *i; + } + for (std::vector<Row*>::iterator i = rows_.begin(); + i != rows_.end(); ++i) { + delete *i; + } +} + +void GridLayout::SetInsets(int top, int left, int bottom, int right) { + top_inset_ = top; + bottom_inset_ = bottom; + left_inset_ = left; + right_inset_ = 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()); + 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::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_); + 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 size 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(); + if (layout || width > 0) { + // 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(); + } + pref->set_width(std::max(pref->width(), (*i)->LayoutWidth())); + } + pref->set_width(pref->width() + left_inset_ + right_inset_); + + // Reset the height of each row. + LayoutElement::ResetSizes(&rows_); + + // Do two things: + // . 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->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 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); + 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->GetParent() == NULL || + view_state->view->GetParent() == host_)); + if (!view_state->view->GetParent()) { + 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/views/grid_layout.h b/views/grid_layout.h new file mode 100644 index 0000000..e3e7a62 --- /dev/null +++ b/views/grid_layout.h @@ -0,0 +1,354 @@ +// Copyright (c) 2006-2008 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 VIEWS_GRID_LAYOUT_H_ +#define VIEWS_GRID_LAYOUT_H_ + +#include <string> +#include <vector> + +#include "views/layout_manager.h" +#include "views/view.h" + +// 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 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 + }; + + // 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(); + + // Sets the insets. All views are placed relative to these offsets. + void SetInsets(int top, int left, int bottom, int right); + + // 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_EVIL_CONSTRUCTORS(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 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. + 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_EVIL_CONSTRUCTORS(ColumnSet); +}; + +} // namespace views + +#endif // VIEWS_GRID_LAYOUT_H_ diff --git a/views/grid_layout_unittest.cc b/views/grid_layout_unittest.cc new file mode 100644 index 0000000..57bfa83 --- /dev/null +++ b/views/grid_layout_unittest.cc @@ -0,0 +1,514 @@ +// Copyright (c) 2006-2008 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/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_; +}; + +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.GetChildViewCount() - 1; i >= 0; i--) { + host.RemoveChildView(host.GetChildViewAt(i)); + } + } + + void GetPreferredSize() { + pref = layout->GetPreferredSize(&host); + } + + gfx::Size pref; + CRect 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.GetChildViewCount() - 1; i >= 0; i--) { + host.RemoveChildView(host.GetChildViewAt(i)); + } + } + + void TestAlignment(GridLayout::Alignment alignment, CRect* 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_TRUE(gfx::Size(10, 20) == pref); + host.SetBounds(0, 0, 100, 100); + layout->Layout(&host); + *bounds = v1.bounds().ToRECT(); + RemoveAll(); + } + + View host; + SettableSizeView v1; + GridLayout* layout; +}; + +TEST_F(GridLayoutAlignmentTest, Fill) { + CRect bounds; + TestAlignment(GridLayout::FILL, &bounds); + EXPECT_TRUE(CRect(0, 0, 100, 100) == bounds); +} + +TEST_F(GridLayoutAlignmentTest, Leading) { + CRect bounds; + TestAlignment(GridLayout::LEADING, &bounds); + EXPECT_TRUE(CRect(0, 0, 10, 20) == bounds); +} + +TEST_F(GridLayoutAlignmentTest, Center) { + CRect bounds; + TestAlignment(GridLayout::CENTER, &bounds); + EXPECT_TRUE(CRect(45, 40, 55, 60) == bounds); +} + +TEST_F(GridLayoutAlignmentTest, Trailing) { + CRect bounds; + TestAlignment(GridLayout::TRAILING, &bounds); + EXPECT_TRUE(CRect(90, 80, 100, 100) == 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_TRUE(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_TRUE(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_TRUE(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_TRUE(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_TRUE(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_TRUE(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_TRUE(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_TRUE(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.GetChildViewAt(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_TRUE(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_TRUE(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_TRUE(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); +} diff --git a/views/layout_manager.cc b/views/layout_manager.cc new file mode 100644 index 0000000..800dbc2 --- /dev/null +++ b/views/layout_manager.cc @@ -0,0 +1,15 @@ +// Copyright (c) 2006-2008 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_manager.h" + +#include "views/view.h" + +namespace views { + +int LayoutManager::GetPreferredHeightForWidth(View* host, int width) { + return GetPreferredSize(host).height(); +} + +} // namespace views diff --git a/views/layout_manager.h b/views/layout_manager.h new file mode 100644 index 0000000..48a9c96 --- /dev/null +++ b/views/layout_manager.h @@ -0,0 +1,60 @@ +// Copyright (c) 2006-2008 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 VIEWS_LAYOUT_MANAGER_H_ +#define VIEWS_LAYOUT_MANAGER_H_ + +#include "views/view.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 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 // VIEWS_LAYOUT_MANAGER_H_ diff --git a/views/painter.cc b/views/painter.cc new file mode 100644 index 0000000..a2ad4ee --- /dev/null +++ b/views/painter.cc @@ -0,0 +1,165 @@ +// Copyright (c) 2006-2008 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/painter.h" + +#include "app/gfx/chrome_canvas.h" +#include "app/resource_bundle.h" +#include "base/logging.h" +#include "skia/include/SkBitmap.h" +#include "skia/include/SkGradientShader.h" + +namespace views { + +namespace { + +class GradientPainter : public Painter { + public: + GradientPainter(bool horizontal, const SkColor& top, const SkColor& bottom) + : horizontal_(horizontal) { + colors_[0] = top; + colors_[1] = bottom; + } + + virtual ~GradientPainter() { + } + + void Paint(int w, int h, ChromeCanvas* canvas) { + SkPaint paint; + SkPoint p[2]; + p[0].set(SkIntToScalar(0), SkIntToScalar(0)); + if (horizontal_) + p[1].set(SkIntToScalar(w), SkIntToScalar(0)); + else + p[1].set(SkIntToScalar(0), SkIntToScalar(h)); + + SkShader* s = + SkGradientShader::CreateLinear(p, colors_, NULL, 2, + SkShader::kClamp_TileMode, NULL); + paint.setStyle(SkPaint::kFill_Style); + paint.setShader(s); + // Need to unref shader, otherwise never deleted. + s->unref(); + + canvas->drawRectCoords(SkIntToScalar(0), SkIntToScalar(0), + SkIntToScalar(w), SkIntToScalar(h), paint); + } + + private: + bool horizontal_; + SkColor colors_[2]; + + DISALLOW_EVIL_CONSTRUCTORS(GradientPainter); +}; + + +} + +// static +void Painter::PaintPainterAt(int x, int y, int w, int h, + ChromeCanvas* canvas, Painter* painter) { + DCHECK(canvas && painter); + if (w < 0 || h < 0) + return; + canvas->save(); + canvas->TranslateInt(x, y); + painter->Paint(w, h, canvas); + canvas->restore(); +} + +ImagePainter::ImagePainter(const int image_resource_names[], + bool draw_center) + : draw_center_(draw_center) { + DCHECK(image_resource_names); + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + for (int i = 0, max = draw_center ? 9 : 8; i < max; ++i) + images_.push_back(rb.GetBitmapNamed(image_resource_names[i])); +} + +void ImagePainter::Paint(int w, int h, ChromeCanvas* canvas) { + canvas->DrawBitmapInt(*images_[BORDER_TOP_LEFT], 0, 0); + canvas->TileImageInt(*images_[BORDER_TOP], + images_[BORDER_TOP_LEFT]->width(), + 0, + w - images_[BORDER_TOP_LEFT]->width() - + images_[BORDER_TOP_RIGHT]->width(), + images_[BORDER_TOP_LEFT]->height()); + canvas->DrawBitmapInt(*images_[BORDER_TOP_RIGHT], + w - images_[BORDER_TOP_RIGHT]->width(), + 0); + canvas->TileImageInt(*images_[BORDER_RIGHT], + w - images_[BORDER_RIGHT]->width(), + images_[BORDER_TOP_RIGHT]->height(), + images_[BORDER_RIGHT]->width(), + h - images_[BORDER_TOP_RIGHT]->height() - + images_[BORDER_BOTTOM_RIGHT]->height()); + canvas->DrawBitmapInt(*images_[BORDER_BOTTOM_RIGHT], + w - images_[BORDER_BOTTOM_RIGHT]->width(), + h - images_[BORDER_BOTTOM_RIGHT]->height()); + canvas->TileImageInt(*images_[BORDER_BOTTOM], + images_[BORDER_BOTTOM_LEFT]->width(), + h - images_[BORDER_BOTTOM]->height(), + w - images_[BORDER_BOTTOM_LEFT]->width() - + images_[BORDER_BOTTOM_RIGHT]->width(), + images_[BORDER_BOTTOM]->height()); + canvas->DrawBitmapInt(*images_[BORDER_BOTTOM_LEFT], + 0, + h - images_[BORDER_BOTTOM_LEFT]->height()); + canvas->TileImageInt(*images_[BORDER_LEFT], + 0, + images_[BORDER_TOP_LEFT]->height(), + images_[BORDER_LEFT]->width(), + h - images_[BORDER_TOP_LEFT]->height() - + images_[BORDER_BOTTOM_LEFT]->height()); + if (draw_center_) { + canvas->DrawBitmapInt(*images_[BORDER_CENTER], + 0, + 0, + images_[BORDER_CENTER]->width(), + images_[BORDER_CENTER]->height(), + images_[BORDER_TOP_LEFT]->width(), + images_[BORDER_TOP_LEFT]->height(), + w - images_[BORDER_TOP_LEFT]->width() - + images_[BORDER_TOP_RIGHT]->width(), + h - images_[BORDER_TOP_LEFT]->height() - + images_[BORDER_TOP_RIGHT]->height(), + false); + } +} + +// static +Painter* Painter::CreateHorizontalGradient(SkColor c1, SkColor c2) { + return new GradientPainter(true, c1, c2); +} + +// static +Painter* Painter::CreateVerticalGradient(SkColor c1, SkColor c2) { + return new GradientPainter(false, c1, c2); +} + +HorizontalPainter::HorizontalPainter(const int image_resource_names[]) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + for (int i = 0; i < 3; ++i) + images_[i] = rb.GetBitmapNamed(image_resource_names[i]); + height_ = images_[LEFT]->height(); + DCHECK(images_[LEFT]->height() == images_[RIGHT]->height() && + images_[LEFT]->height() == images_[CENTER]->height()); +} + +void HorizontalPainter::Paint(int w, int h, ChromeCanvas* canvas) { + if (w < (images_[LEFT]->width() + images_[CENTER]->width() + + images_[RIGHT]->width())) { + // No room to paint. + return; + } + canvas->DrawBitmapInt(*images_[LEFT], 0, 0); + canvas->DrawBitmapInt(*images_[RIGHT], w - images_[RIGHT]->width(), 0); + canvas->TileImageInt(*images_[CENTER], + images_[LEFT]->width(), + 0, + w - images_[LEFT]->width() - images_[RIGHT]->width(), + height_); +} + +} // namespace views diff --git a/views/painter.h b/views/painter.h new file mode 100644 index 0000000..83610c3 --- /dev/null +++ b/views/painter.h @@ -0,0 +1,120 @@ +// Copyright (c) 2006-2008 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 VIEWS_PAINTER_H_ +#define VIEWS_PAINTER_H_ + +#include <vector> + +#include "base/basictypes.h" +#include "skia/include/SkColor.h" + +class ChromeCanvas; +class SkBitmap; + +namespace views { + +// Painter, as the name implies, is responsible for painting in a particular +// region. Think of Painter as a Border or Background that can be painted +// in any region of a View. +class Painter { + public: + // A convenience method for painting a Painter in a particular region. + // This translates the canvas to x/y and paints the painter. + static void PaintPainterAt(int x, int y, int w, int h, + ChromeCanvas* canvas, Painter* painter); + + // Creates a painter that draws a gradient between the two colors. + static Painter* CreateHorizontalGradient(SkColor c1, SkColor c2); + static Painter* CreateVerticalGradient(SkColor c1, SkColor c2); + + virtual ~Painter() {} + + // Paints the painter in the specified region. + virtual void Paint(int w, int h, ChromeCanvas* canvas) = 0; +}; + +// ImagePainter paints 8 (or 9) images into a box. The four corner +// images are drawn at the size of the image, the top/left/bottom/right +// images are tiled to fit the area, and the center (if rendered) is +// stretched. +class ImagePainter : public Painter { + public: + enum BorderElements { + BORDER_TOP_LEFT = 0, + BORDER_TOP, + BORDER_TOP_RIGHT, + BORDER_RIGHT, + BORDER_BOTTOM_RIGHT, + BORDER_BOTTOM, + BORDER_BOTTOM_LEFT, + BORDER_LEFT, + BORDER_CENTER + }; + + // Constructs a new ImagePainter loading the specified image names. + // The images must be in the order defined by the BorderElements. + // If draw_center is false, there must be 8 image names, if draw_center + // is true, there must be 9 image names with the last giving the name + // of the center image. + ImagePainter(const int image_resource_names[], + bool draw_center); + + virtual ~ImagePainter() {} + + // Paints the images. + virtual void Paint(int w, int h, ChromeCanvas* canvas); + + // Returns the specified image. The returned image should NOT be deleted. + SkBitmap* GetImage(BorderElements element) { + return images_[element]; + } + + private: + bool tile_; + bool draw_center_; + bool tile_center_; + // NOTE: the images are owned by ResourceBundle. Don't free them. + std::vector<SkBitmap*> images_; + + DISALLOW_EVIL_CONSTRUCTORS(ImagePainter); +}; + +// HorizontalPainter paints 3 images into a box: left, center and right. The +// left and right images are drawn to size at the left/right edges of the +// region. The center is tiled in the remaining space. All images must have the +// same height. +class HorizontalPainter : public Painter { + public: + // Constructs a new HorizontalPainter loading the specified image names. + // The images must be in the order left, right and center. + explicit HorizontalPainter(const int image_resource_names[]); + + virtual ~HorizontalPainter() {} + + // Paints the images. + virtual void Paint(int w, int h, ChromeCanvas* canvas); + + // Height of the images. + int height() const { return height_; } + + private: + // The image chunks. + enum BorderElements { + LEFT, + CENTER, + RIGHT + }; + + // The height. + int height_; + // NOTE: the images are owned by ResourceBundle. Don't free them. + SkBitmap* images_[3]; + + DISALLOW_EVIL_CONSTRUCTORS(HorizontalPainter); +}; + +} // namespace views + +#endif // VIEWS_PAINTER_H_ diff --git a/views/repeat_controller.cc b/views/repeat_controller.cc new file mode 100644 index 0000000..bb70852 --- /dev/null +++ b/views/repeat_controller.cc @@ -0,0 +1,45 @@ +// Copyright (c) 2006-2008 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/repeat_controller.h" + +using base::TimeDelta; + +namespace views { + +// The delay before the first and then subsequent repeats. Values taken from +// XUL code: http://mxr.mozilla.org/seamonkey/source/layout/xul/base/src/nsRepeatService.cpp#52 +static const int kInitialRepeatDelay = 250; +static const int kRepeatDelay = 50; + +/////////////////////////////////////////////////////////////////////////////// +// RepeatController, public: + +RepeatController::RepeatController(RepeatCallback* callback) + : callback_(callback) { +} + +RepeatController::~RepeatController() { +} + +void RepeatController::Start() { + // The first timer is slightly longer than subsequent repeats. + timer_.Start(TimeDelta::FromMilliseconds(kInitialRepeatDelay), this, + &RepeatController::Run); +} + +void RepeatController::Stop() { + timer_.Stop(); +} + +/////////////////////////////////////////////////////////////////////////////// +// RepeatController, private: + +void RepeatController::Run() { + timer_.Start(TimeDelta::FromMilliseconds(kRepeatDelay), this, + &RepeatController::Run); + callback_->Run(); +} + +} // namespace views diff --git a/views/repeat_controller.h b/views/repeat_controller.h new file mode 100644 index 0000000..9e01dd1 --- /dev/null +++ b/views/repeat_controller.h @@ -0,0 +1,53 @@ +// Copyright (c) 2006-2008 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 VIEWS_REPEAT_CONTROLLER_H_ +#define VIEWS_REPEAT_CONTROLLER_H_ + +#include "base/scoped_ptr.h" +#include "base/timer.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// +// RepeatController +// +// An object that handles auto-repeating UI actions. There is a longer initial +// delay after which point repeats become constant. Users provide a callback +// that is notified when each repeat occurs so that they can perform the +// associated action. +// +/////////////////////////////////////////////////////////////////////////////// +class RepeatController { + public: + typedef Callback0::Type RepeatCallback; + + // The RepeatController takes ownership of this callback object. + explicit RepeatController(RepeatCallback* callback); + virtual ~RepeatController(); + + // Start repeating. + void Start(); + + // Stop repeating. + void Stop(); + + private: + RepeatController(); + + // Called when the timer expires. + void Run(); + + // The current timer. + base::OneShotTimer<RepeatController> timer_; + + scoped_ptr<RepeatCallback> callback_; + + DISALLOW_COPY_AND_ASSIGN(RepeatController); +}; + +} // namespace views + +#endif // #ifndef VIEWS_REPEAT_CONTROLLER_H_ diff --git a/views/view.cc b/views/view.cc new file mode 100644 index 0000000..cb73d74 --- /dev/null +++ b/views/view.cc @@ -0,0 +1,1643 @@ +// Copyright (c) 2006-2008 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/view.h" + +#include <algorithm> +#ifndef NDEBUG +#include <iostream> +#endif + +#include "app/drag_drop_types.h" +#include "app/gfx/chrome_canvas.h" +#include "app/l10n_util.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/scoped_handle.h" +#include "base/string_util.h" +#include "skia/include/SkShader.h" +#include "views/background.h" +#include "views/layout_manager.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" +#include "views/window/window.h" +#if defined(OS_WIN) +#include "views/widget/tooltip_manager.h" +#include "views/accessibility/view_accessibility_wrapper.h" +#endif + +namespace views { + +// static +char View::kViewClassName[] = "views/View"; + +//////////////////////////////////////////////////////////////////////////////// +// +// A task used to automatically restore focus on the last focused floating view +// +//////////////////////////////////////////////////////////////////////////////// + +class RestoreFocusTask : public Task { + public: + explicit RestoreFocusTask(View* target) : view_(target) { + } + + ~RestoreFocusTask() {} + + void Cancel() { + view_ = NULL; + } + + void Run() { + if (view_) + view_->RestoreFloatingViewFocus(); + } + private: + // The target view. + View* view_; + + DISALLOW_COPY_AND_ASSIGN(RestoreFocusTask); +}; + +///////////////////////////////////////////////////////////////////////////// +// +// View - constructors, destructors, initialization +// +///////////////////////////////////////////////////////////////////////////// + +View::View() + : id_(0), + group_(-1), + enabled_(true), + focusable_(false), + bounds_(0, 0, 0, 0), + parent_(NULL), + should_restore_focus_(false), + is_visible_(true), + is_parent_owned_(true), + notify_when_visible_bounds_in_root_changes_(false), + registered_for_visible_bounds_notification_(false), + next_focusable_view_(NULL), + previous_focusable_view_(NULL), + restore_focus_view_task_(NULL), + context_menu_controller_(NULL), +#if defined(OS_WIN) + accessibility_(NULL), +#endif + drag_controller_(NULL), + ui_mirroring_is_enabled_for_rtl_languages_(true), + flip_canvas_on_paint_for_rtl_ui_(false) { +} + +View::~View() { + if (restore_focus_view_task_) + restore_focus_view_task_->Cancel(); + + int c = static_cast<int>(child_views_.size()); + while (--c >= 0) { + if (child_views_[c]->IsParentOwned()) + delete child_views_[c]; + else + child_views_[c]->SetParent(NULL); + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// View - sizing +// +///////////////////////////////////////////////////////////////////////////// + +gfx::Rect View::GetBounds(PositionMirroringSettings settings) const { + gfx::Rect bounds(bounds_); + + // If the parent uses an RTL UI layout and if we are asked to transform the + // bounds to their mirrored position if necessary, then we should shift the + // rectangle appropriately. + if (settings == APPLY_MIRRORING_TRANSFORMATION) + bounds.set_x(MirroredX()); + + return bounds; +} + +// y(), width() and height() are agnostic to the RTL UI layout of the +// parent view. x(), on the other hand, is not. +int View::GetX(PositionMirroringSettings settings) const { + return settings == IGNORE_MIRRORING_TRANSFORMATION ? x() : MirroredX(); +} + +void View::SetBounds(const gfx::Rect& bounds) { + if (bounds == bounds_) + return; + + gfx::Rect prev = bounds_; + bounds_ = bounds; + DidChangeBounds(prev, bounds_); + + RootView* root = GetRootView(); + if (root) { + bool size_changed = prev.size() != bounds_.size(); + bool position_changed = prev.origin() != bounds_.origin(); + if (size_changed || position_changed) + root->ViewBoundsChanged(this, size_changed, position_changed); + } +} + +gfx::Rect View::GetLocalBounds(bool include_border) const { + if (include_border || !border_.get()) + return gfx::Rect(0, 0, width(), height()); + + gfx::Insets insets; + border_->GetInsets(&insets); + return gfx::Rect(insets.left(), insets.top(), + std::max(0, width() - insets.width()), + std::max(0, height() - insets.height())); +} + +gfx::Point View::GetPosition() const { + return gfx::Point(GetX(APPLY_MIRRORING_TRANSFORMATION), y()); +} + +gfx::Size View::GetPreferredSize() { + if (layout_manager_.get()) + return layout_manager_->GetPreferredSize(this); + return gfx::Size(); +} + +void View::SizeToPreferredSize() { + gfx::Size prefsize = GetPreferredSize(); + if ((prefsize.width() != width()) || (prefsize.height() != height())) + SetBounds(x(), y(), prefsize.width(), prefsize.height()); +} + +gfx::Size View::GetMinimumSize() { + return GetPreferredSize(); +} + +int View::GetHeightForWidth(int w) { + if (layout_manager_.get()) + return layout_manager_->GetPreferredHeightForWidth(this, w); + return GetPreferredSize().height(); +} + +void View::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + Layout(); +} + +void View::ScrollRectToVisible(int x, int y, int width, int height) { + View* parent = GetParent(); + + // We must take RTL UI mirroring into account when adjusting the position of + // the region. + if (parent) + parent->ScrollRectToVisible( + GetX(APPLY_MIRRORING_TRANSFORMATION) + x, View::y() + y, width, height); +} + +///////////////////////////////////////////////////////////////////////////// +// +// View - layout +// +///////////////////////////////////////////////////////////////////////////// + +void View::Layout() { + // Layout child Views + if (layout_manager_.get()) { + layout_manager_->Layout(this); + SchedulePaint(); + // TODO(beng): We believe the right thing to do here is return since the + // layout manager should be handling things, but it causes + // regressions (missing options from Options dialog and a hang + // in interactive_ui_tests). + } + + // Lay out contents of child Views + int child_count = GetChildViewCount(); + for (int i = 0; i < child_count; ++i) { + View* child = GetChildViewAt(i); + child->Layout(); + } +} + +LayoutManager* View::GetLayoutManager() const { + return layout_manager_.get(); +} + +void View::SetLayoutManager(LayoutManager* layout_manager) { + if (layout_manager_.get()) { + layout_manager_->Uninstalled(this); + } + layout_manager_.reset(layout_manager); + if (layout_manager_.get()) { + layout_manager_->Installed(this); + } +} + +bool View::UILayoutIsRightToLeft() const { + return (ui_mirroring_is_enabled_for_rtl_languages_ && + l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// View - Right-to-left UI layout +// +//////////////////////////////////////////////////////////////////////////////// + +inline int View::MirroredX() const { + // TODO(beng): reimplement in terms of MirroredLeftPointForRect. + View* parent = GetParent(); + if (parent && parent->UILayoutIsRightToLeft()) + return parent->width() - x() - width(); + return x(); +} + +int View::MirroredLeftPointForRect(const gfx::Rect& bounds) const { + if (!UILayoutIsRightToLeft()) { + return bounds.x(); + } + return width() - bounds.x() - bounds.width(); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// View - states +// +//////////////////////////////////////////////////////////////////////////////// + +bool View::IsEnabled() const { + return enabled_; +} + +void View::SetEnabled(bool state) { + if (enabled_ != state) { + enabled_ = state; + SchedulePaint(); + } +} + +bool View::IsFocusable() const { + return focusable_ && enabled_ && is_visible_; +} + +void View::SetFocusable(bool focusable) { + focusable_ = focusable; +} + +bool View::HasFocus() { + FocusManager* focus_manager = GetFocusManager(); + if (focus_manager) + return focus_manager->GetFocusedView() == this; + return false; +} + +void View::SetHotTracked(bool flag) { +} + +///////////////////////////////////////////////////////////////////////////// +// +// View - painting +// +///////////////////////////////////////////////////////////////////////////// + +void View::SchedulePaint(const gfx::Rect& r, bool urgent) { + if (!IsVisible()) + return; + + if (parent_) { + // Translate the requested paint rect to the parent's coordinate system + // then pass this notification up to the parent. + gfx::Rect paint_rect = r; + paint_rect.Offset(GetPosition()); + parent_->SchedulePaint(paint_rect, urgent); + } +} + +void View::SchedulePaint() { + SchedulePaint(GetLocalBounds(true), false); +} + +void View::SchedulePaint(int x, int y, int w, int h) { + SchedulePaint(gfx::Rect(x, y, w, h), false); +} + +void View::Paint(ChromeCanvas* canvas) { + PaintBackground(canvas); + PaintFocusBorder(canvas); + PaintBorder(canvas); +} + +void View::PaintBackground(ChromeCanvas* canvas) { + if (background_.get()) + background_->Paint(canvas, this); +} + +void View::PaintBorder(ChromeCanvas* canvas) { + if (border_.get()) + border_->Paint(*this, canvas); +} + +void View::PaintFocusBorder(ChromeCanvas* canvas) { + if (HasFocus() && IsFocusable()) + canvas->DrawFocusRect(0, 0, width(), height()); +} + +void View::PaintChildren(ChromeCanvas* canvas) { + int i, c; + for (i = 0, c = GetChildViewCount(); i < c; ++i) { + View* child = GetChildViewAt(i); + if (!child) { + NOTREACHED() << "Should not have a NULL child View for index in bounds"; + continue; + } + child->ProcessPaint(canvas); + } +} + +void View::ProcessPaint(ChromeCanvas* canvas) { + if (!IsVisible()) { + return; + } + + // We're going to modify the canvas, save it's state first. + canvas->save(); + + // Paint this View and its children, setting the clip rect to the bounds + // of this View and translating the origin to the local bounds' top left + // point. + // + // Note that the X (or left) position we pass to ClipRectInt takes into + // consideration whether or not the view uses a right-to-left layout so that + // we paint our view in its mirrored position if need be. + if (canvas->ClipRectInt(MirroredX(), y(), width(), height())) { + // Non-empty clip, translate the graphics such that 0,0 corresponds to + // where this view is located (related to its parent). + canvas->TranslateInt(MirroredX(), y()); + + // Save the state again, so that any changes don't effect PaintChildren. + canvas->save(); + + // If the View we are about to paint requested the canvas to be flipped, we + // should change the transform appropriately. + bool flip_canvas = FlipCanvasOnPaintForRTLUI(); + if (flip_canvas) { + canvas->TranslateInt(width(), 0); + canvas->ScaleInt(-1, 1); + canvas->save(); + } + + Paint(canvas); + + // We must undo the canvas mirroring once the View is done painting so that + // we don't pass the canvas with the mirrored transform to Views that + // didn't request the canvas to be flipped. + if (flip_canvas) { + canvas->restore(); + } + canvas->restore(); + PaintChildren(canvas); + } + + // Restore the canvas's original transform. + canvas->restore(); +} + +void View::PaintNow() { + if (!IsVisible()) { + return; + } + + View* view = GetParent(); + if (view) + view->PaintNow(); +} + +void View::PaintFloatingView(ChromeCanvas* canvas, View* view, + int x, int y, int w, int h) { + if (should_restore_focus_ && ShouldRestoreFloatingViewFocus()) { + // We are painting again a floating view, this is a good time to restore the + // focus to the last focused floating view if any. + should_restore_focus_ = false; + restore_focus_view_task_ = new RestoreFocusTask(this); + MessageLoop::current()->PostTask(FROM_HERE, restore_focus_view_task_); + } + View* saved_parent = view->GetParent(); + view->SetParent(this); + view->SetBounds(x, y, w, h); + view->Layout(); + view->ProcessPaint(canvas); + view->SetParent(saved_parent); +} + +gfx::Insets View::GetInsets() const { + gfx::Insets insets; + if (border_.get()) + border_->GetInsets(&insets); + return insets; +} + +void View::SetContextMenuController(ContextMenuController* menu_controller) { + context_menu_controller_ = menu_controller; +} + +void View::ShowContextMenu(int x, int y, bool is_mouse_gesture) { + if (!context_menu_controller_) + return; + + context_menu_controller_->ShowContextMenu(this, x, y, is_mouse_gesture); +} + +///////////////////////////////////////////////////////////////////////////// +// +// View - tree +// +///////////////////////////////////////////////////////////////////////////// + +bool View::ProcessMousePressed(const MouseEvent& e, DragInfo* drag_info) { + const bool enabled = enabled_; + int drag_operations; + if (enabled && e.IsOnlyLeftMouseButton() && HitTest(e.location())) + drag_operations = GetDragOperations(e.x(), e.y()); + else + drag_operations = 0; + ContextMenuController* context_menu_controller = + e.IsRightMouseButton() ? context_menu_controller_ : 0; + + const bool result = OnMousePressed(e); + // WARNING: we may have been deleted, don't use any View variables; + + if (!enabled) + return result; + + if (drag_operations != DragDropTypes::DRAG_NONE) { + drag_info->PossibleDrag(e.x(), e.y()); + return true; + } + return !!context_menu_controller || result; +} + +bool View::ProcessMouseDragged(const MouseEvent& e, DragInfo* drag_info) { + // Copy the field, that way if we're deleted after drag and drop no harm is + // done. + ContextMenuController* context_menu_controller = context_menu_controller_; + const bool possible_drag = drag_info->possible_drag; + if (possible_drag && ExceededDragThreshold(drag_info->start_x - e.x(), + drag_info->start_y - e.y())) { + DoDrag(e, drag_info->start_x, drag_info->start_y); + } else { + if (OnMouseDragged(e)) + return true; + // Fall through to return value based on context menu controller. + } + // WARNING: we may have been deleted. + return (context_menu_controller != NULL) || possible_drag; +} + +void View::ProcessMouseReleased(const MouseEvent& e, bool canceled) { + if (!canceled && context_menu_controller_ && e.IsOnlyRightMouseButton()) { + // Assume that if there is a context menu controller we won't be deleted + // from mouse released. + gfx::Point location(e.location()); + OnMouseReleased(e, canceled); + if (HitTest(location)) { + ConvertPointToScreen(this, &location); + ShowContextMenu(location.x(), location.y(), true); + } + } else { + OnMouseReleased(e, canceled); + } + // WARNING: we may have been deleted. +} + +void View::AddChildView(View* v) { + AddChildView(static_cast<int>(child_views_.size()), v, false); +} + +void View::AddChildView(int index, View* v) { + AddChildView(index, v, false); +} + +void View::AddChildView(int index, View* v, bool floating_view) { + // Remove the view from its current parent if any. + if (v->GetParent()) + v->GetParent()->RemoveChildView(v); + + if (!floating_view) { + // Sets the prev/next focus views. + InitFocusSiblings(v, index); + } + + // Let's insert the view. + child_views_.insert(child_views_.begin() + index, v); + v->SetParent(this); + + for (View* p = this; p; p = p->GetParent()) { + p->ViewHierarchyChangedImpl(false, true, this, v); + } + v->PropagateAddNotifications(this, v); + UpdateTooltip(); + RootView* root = GetRootView(); + if (root) + RegisterChildrenForVisibleBoundsNotification(root, v); + + if (layout_manager_.get()) + layout_manager_->ViewAdded(this, v); +} + +View* View::GetChildViewAt(int index) const { + return index < GetChildViewCount() ? child_views_[index] : NULL; +} + +int View::GetChildViewCount() const { + return static_cast<int>(child_views_.size()); +} + +void View::RemoveChildView(View* a_view) { + DoRemoveChildView(a_view, true, true, false); +} + +void View::RemoveAllChildViews(bool delete_views) { + ViewList::iterator iter; + while ((iter = child_views_.begin()) != child_views_.end()) { + DoRemoveChildView(*iter, false, false, delete_views); + } + UpdateTooltip(); +} + +void View::DoRemoveChildView(View* a_view, + bool update_focus_cycle, + bool update_tool_tip, + bool delete_removed_view) { +#ifndef NDEBUG + DCHECK(!IsProcessingPaint()) << "Should not be removing a child view " << + "during a paint, this will seriously " << + "mess things up!"; +#endif + DCHECK(a_view); + const ViewList::iterator i = find(child_views_.begin(), + child_views_.end(), + a_view); + if (i != child_views_.end()) { + if (update_focus_cycle && !a_view->IsFloatingView()) { + // Let's remove the view from the focus traversal. + View* next_focusable = a_view->next_focusable_view_; + View* prev_focusable = a_view->previous_focusable_view_; + if (prev_focusable) + prev_focusable->next_focusable_view_ = next_focusable; + if (next_focusable) + next_focusable->previous_focusable_view_ = prev_focusable; + } + + RootView* root = GetRootView(); + if (root) + UnregisterChildrenForVisibleBoundsNotification(root, a_view); + a_view->PropagateRemoveNotifications(this); + a_view->SetParent(NULL); + + if (delete_removed_view && a_view->IsParentOwned()) + delete a_view; + + child_views_.erase(i); + } + + if (update_tool_tip) + UpdateTooltip(); + + if (layout_manager_.get()) + layout_manager_->ViewRemoved(this, a_view); +} + +void View::PropagateRemoveNotifications(View* parent) { + int i, c; + for (i = 0, c = GetChildViewCount(); i < c; ++i) { + GetChildViewAt(i)->PropagateRemoveNotifications(parent); + } + + View *t; + for (t = this; t; t = t->GetParent()) { + t->ViewHierarchyChangedImpl(true, false, parent, this); + } +} + +void View::PropagateAddNotifications(View* parent, View* child) { + int i, c; + for (i = 0, c = GetChildViewCount(); i < c; ++i) { + GetChildViewAt(i)->PropagateAddNotifications(parent, child); + } + ViewHierarchyChangedImpl(true, true, parent, child); +} + +void View::ThemeChanged() { + int c = GetChildViewCount(); + for (int i = c - 1; i >= 0; --i) + GetChildViewAt(i)->ThemeChanged(); +} + +#ifndef NDEBUG +bool View::IsProcessingPaint() const { + return GetParent() && GetParent()->IsProcessingPaint(); +} +#endif + +gfx::Point View::GetKeyboardContextMenuLocation() { + gfx::Rect vis_bounds = GetVisibleBounds(); + gfx::Point screen_point(vis_bounds.x() + vis_bounds.width() / 2, + vis_bounds.y() + vis_bounds.height() / 2); + ConvertPointToScreen(this, &screen_point); + return screen_point; +} + +bool View::HasHitTestMask() const { + return false; +} + +void View::GetHitTestMask(gfx::Path* mask) const { + DCHECK(mask); +} + +void View::ViewHierarchyChanged(bool is_add, + View* parent, + View* child) { +} + +void View::ViewHierarchyChangedImpl(bool register_accelerators, + bool is_add, + View* parent, + View* child) { + if (register_accelerators) { + if (is_add) { + // If you get this registration, you are part of a subtree that has been + // added to the view hierarchy. + RegisterAccelerators(); + } else { + if (child == this) + UnregisterAccelerators(); + } + } + + ViewHierarchyChanged(is_add, parent, child); +} + +void View::PropagateVisibilityNotifications(View* start, bool is_visible) { + int i, c; + for (i = 0, c = GetChildViewCount(); i < c; ++i) { + GetChildViewAt(i)->PropagateVisibilityNotifications(start, is_visible); + } + VisibilityChanged(start, is_visible); +} + +void View::VisibilityChanged(View* starting_from, bool is_visible) { +} + +View* View::GetViewForPoint(const gfx::Point& point) { + return GetViewForPoint(point, true); +} + +void View::SetNotifyWhenVisibleBoundsInRootChanges(bool value) { + if (notify_when_visible_bounds_in_root_changes_ == value) + return; + notify_when_visible_bounds_in_root_changes_ = value; + RootView* root = GetRootView(); + if (root) { + if (value) + root->RegisterViewForVisibleBoundsNotification(this); + else + root->UnregisterViewForVisibleBoundsNotification(this); + } +} + +bool View::GetNotifyWhenVisibleBoundsInRootChanges() { + return notify_when_visible_bounds_in_root_changes_; +} + +View* View::GetViewForPoint(const gfx::Point& point, + bool can_create_floating) { + // Walk the child Views recursively looking for the View that most + // tightly encloses the specified point. + for (int i = GetChildViewCount() - 1 ; i >= 0 ; --i) { + View* child = GetChildViewAt(i); + if (!child->IsVisible()) + continue; + + gfx::Point point_in_child_coords(point); + View::ConvertPointToView(this, child, &point_in_child_coords); + if (child->HitTest(point_in_child_coords)) + return child->GetViewForPoint(point_in_child_coords, true); + } + + // We haven't found a view for the point. Try to create floating views + // and try again if one was created. + // can_create_floating makes sure we don't try forever even if + // GetFloatingViewIDForPoint lies or if RetrieveFloatingViewForID creates a + // view which doesn't contain the provided point + int id; + if (can_create_floating && + GetFloatingViewIDForPoint(point.x(), point.y(), &id)) { + RetrieveFloatingViewForID(id); // This creates the floating view. + return GetViewForPoint(point, false); + } + return this; +} + +Widget* View::GetWidget() const { + // The root view holds a reference to this view hierarchy's Widget. + return parent_ ? parent_->GetWidget() : NULL; +} + +Window* View::GetWindow() const { + Widget* widget = GetWidget(); + return widget ? widget->GetWindow() : NULL; +} + +// Get the containing RootView +RootView* View::GetRootView() { + Widget* widget = GetWidget(); + return widget ? widget->GetRootView() : NULL; +} + +View* View::GetViewByID(int id) const { + if (id == id_) + return const_cast<View*>(this); + + int view_count = GetChildViewCount(); + for (int i = 0; i < view_count; ++i) { + View* child = GetChildViewAt(i); + View* view = child->GetViewByID(id); + if (view) + return view; + } + return NULL; +} + +void View::GetViewsWithGroup(int group_id, std::vector<View*>* out) { + if (group_ == group_id) + out->push_back(this); + + int view_count = GetChildViewCount(); + for (int i = 0; i < view_count; ++i) + GetChildViewAt(i)->GetViewsWithGroup(group_id, out); +} + +View* View::GetSelectedViewForGroup(int group_id) { + std::vector<View*> views; + GetRootView()->GetViewsWithGroup(group_id, &views); + if (views.size() > 0) + return views[0]; + else + return NULL; +} + +void View::SetID(int id) { + id_ = id; +} + +int View::GetID() const { + return id_; +} + +void View::SetGroup(int gid) { + group_ = gid; +} + +int View::GetGroup() const { + return group_; +} + +void View::SetParent(View* parent) { + if (parent != parent_) { + parent_ = parent; + } +} + +bool View::IsParentOf(View* v) const { + DCHECK(v); + View* parent = v->GetParent(); + while (parent) { + if (this == parent) + return true; + parent = parent->GetParent(); + } + return false; +} + +int View::GetChildIndex(View* v) const { + for (int i = 0; i < GetChildViewCount(); i++) { + if (v == GetChildViewAt(i)) + return i; + } + return -1; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// View - focus +// +/////////////////////////////////////////////////////////////////////////////// + +View* View::GetNextFocusableView() { + return next_focusable_view_; +} + +View* View::GetPreviousFocusableView() { + return previous_focusable_view_; +} + +void View::SetNextFocusableView(View* view) { + view->previous_focusable_view_ = this; + next_focusable_view_ = view; +} + +void View::InitFocusSiblings(View* v, int index) { + int child_count = static_cast<int>(child_views_.size()); + + if (child_count == 0) { + v->next_focusable_view_ = NULL; + v->previous_focusable_view_ = NULL; + } else { + if (index == child_count) { + // We are inserting at the end, but the end of the child list may not be + // the last focusable element. Let's try to find an element with no next + // focusable element to link to. + View* last_focusable_view = NULL; + for (std::vector<View*>::iterator iter = child_views_.begin(); + iter != child_views_.end(); ++iter) { + if (!(*iter)->next_focusable_view_) { + last_focusable_view = *iter; + break; + } + } + if (last_focusable_view == NULL) { + // Hum... there is a cycle in the focus list. Let's just insert ourself + // after the last child. + View* prev = child_views_[index - 1]; + v->previous_focusable_view_ = prev; + v->next_focusable_view_ = prev->next_focusable_view_; + prev->next_focusable_view_->previous_focusable_view_ = v; + prev->next_focusable_view_ = v; + } else { + last_focusable_view->next_focusable_view_ = v; + v->next_focusable_view_ = NULL; + v->previous_focusable_view_ = last_focusable_view; + } + } else { + View* prev = child_views_[index]->GetPreviousFocusableView(); + v->previous_focusable_view_ = prev; + v->next_focusable_view_ = child_views_[index]; + if (prev) + prev->next_focusable_view_ = v; + child_views_[index]->previous_focusable_view_ = v; + } + } +} + +#ifndef NDEBUG +void View::PrintViewHierarchy() { + PrintViewHierarchyImp(0); +} + +void View::PrintViewHierarchyImp(int indent) { + std::wostringstream buf; + int ind = indent; + while (ind-- > 0) + buf << L' '; + buf << UTF8ToWide(GetClassName()); + buf << L' '; + buf << GetID(); + buf << L' '; + buf << bounds_.x() << L"," << bounds_.y() << L","; + buf << bounds_.right() << L"," << bounds_.bottom(); + buf << L' '; + buf << this; + + LOG(INFO) << buf.str(); + std::cout << buf.str() << std::endl; + + for (int i = 0; i < GetChildViewCount(); ++i) { + GetChildViewAt(i)->PrintViewHierarchyImp(indent + 2); + } +} + + +void View::PrintFocusHierarchy() { + PrintFocusHierarchyImp(0); +} + +void View::PrintFocusHierarchyImp(int indent) { + std::wostringstream buf; + int ind = indent; + while (ind-- > 0) + buf << L' '; + buf << UTF8ToWide(GetClassName()); + buf << L' '; + buf << GetID(); + buf << L' '; + buf << GetClassName().c_str(); + buf << L' '; + buf << this; + + LOG(INFO) << buf.str(); + std::cout << buf.str() << std::endl; + + if (GetChildViewCount() > 0) + GetChildViewAt(0)->PrintFocusHierarchyImp(indent + 2); + + View* v = GetNextFocusableView(); + if (v) + v->PrintFocusHierarchyImp(indent); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// +// View - accelerators +// +//////////////////////////////////////////////////////////////////////////////// + +void View::AddAccelerator(const Accelerator& accelerator) { + if (!accelerators_.get()) + accelerators_.reset(new std::vector<Accelerator>()); + accelerators_->push_back(accelerator); + RegisterAccelerators(); +} + +void View::RemoveAccelerator(const Accelerator& accelerator) { + std::vector<Accelerator>::iterator iter; + if (!accelerators_.get() || + ((iter = std::find(accelerators_->begin(), accelerators_->end(), + accelerator)) == accelerators_->end())) { + NOTREACHED() << "Removing non-existing accelerator"; + return; + } + + accelerators_->erase(iter); + RootView* root_view = GetRootView(); + if (!root_view) { + // We are not part of a view hierarchy, so there is nothing to do as we + // removed ourselves from accelerators_, we won't be registered when added + // to one. + return; + } + + // TODO(port): Fix this once we have a FocusManger for Linux. +#if defined(OS_WIN) + FocusManager* focus_manager = GetFocusManager(); + if (focus_manager) { + // We may not have a FocusManager if the window containing us is being + // closed, in which case the FocusManager is being deleted so there is + // nothing to unregister. + focus_manager->UnregisterAccelerator(accelerator, this); + } +#endif +} + +void View::ResetAccelerators() { + if (accelerators_.get()) { + UnregisterAccelerators(); + accelerators_->clear(); + accelerators_.reset(); + } +} + +void View::RegisterAccelerators() { + if (!accelerators_.get()) + return; + + RootView* root_view = GetRootView(); + if (!root_view) { + // We are not yet part of a view hierarchy, we'll register ourselves once + // added to one. + return; + } + + // TODO(port): Fix this once we have a FocusManger for Linux. +#if defined(OS_WIN) + FocusManager* focus_manager = GetFocusManager(); + if (!focus_manager) { + // Some crash reports seem to show that we may get cases where we have no + // focus manager (see bug #1291225). This should never be the case, just + // making sure we don't crash. + NOTREACHED(); + return; + } + for (std::vector<Accelerator>::const_iterator iter = accelerators_->begin(); + iter != accelerators_->end(); ++iter) { + focus_manager->RegisterAccelerator(*iter, this); + } +#endif +} + +void View::UnregisterAccelerators() { + if (!accelerators_.get()) + return; + + RootView* root_view = GetRootView(); + if (root_view) { + // TODO(port): Fix this once we have a FocusManger for Linux. +#if defined(OS_WIN) + FocusManager* focus_manager = GetFocusManager(); + if (focus_manager) { + // We may not have a FocusManager if the window containing us is being + // closed, in which case the FocusManager is being deleted so there is + // nothing to unregister. + focus_manager->UnregisterAccelerators(this); + } +#endif + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// View - floating views +// +///////////////////////////////////////////////////////////////////////////// + +bool View::IsFloatingView() { + if (!parent_) + return false; + + return parent_->floating_views_ids_.find(this) != + parent_->floating_views_ids_.end(); +} + +// default implementation does nothing +bool View::GetFloatingViewIDForPoint(int x, int y, int* id) { + return false; +} + +int View::GetFloatingViewCount() const { + return static_cast<int>(floating_views_.size()); +} + +View* View::RetrieveFloatingViewParent() { + View* v = this; + while (v) { + if (v->IsFloatingView()) + return v; + v = v->GetParent(); + } + return NULL; +} + +bool View::EnumerateFloatingViews(FloatingViewPosition position, + int starting_id, int* id) { + return false; +} + +int View::GetDragOperations(int press_x, int press_y) { + if (!drag_controller_) + return DragDropTypes::DRAG_NONE; + return drag_controller_->GetDragOperations(this, press_x, press_y); +} + +void View::WriteDragData(int press_x, int press_y, OSExchangeData* data) { + DCHECK(drag_controller_); + drag_controller_->WriteDragData(this, press_x, press_y, data); +} + +void View::OnDragDone() { +} + +bool View::InDrag() { + RootView* root_view = GetRootView(); + return root_view ? (root_view->GetDragView() == this) : false; +} + +View* View::ValidateFloatingViewForID(int id) { + return NULL; +} + +bool View::ShouldRestoreFloatingViewFocus() { + return true; +} + +void View::AttachFloatingView(View* v, int id) { + floating_views_.push_back(v); + floating_views_ids_[v] = id; + AddChildView(static_cast<int>(child_views_.size()), v, true); +} + +bool View::HasFloatingViewForPoint(int x, int y) { + int i, c; + View* v; + gfx::Rect r; + + for (i = 0, c = static_cast<int>(floating_views_.size()); i < c; ++i) { + v = floating_views_[i]; + r.SetRect(v->GetX(APPLY_MIRRORING_TRANSFORMATION), v->y(), + v->width(), v->height()); + if (r.Contains(x, y)) + return true; + } + return false; +} + +void View::DetachAllFloatingViews() { + RootView* root_view = GetRootView(); + View* focused_view = NULL; + FocusManager* focus_manager = NULL; + if (root_view) { + // We may be called when we are not attached to a root view in which case + // there is nothing to do for focus. + focus_manager = GetFocusManager(); + if (focus_manager) { + // We may not have a focus manager (if we are detached from a top window). + focused_view = focus_manager->GetFocusedView(); + } + } + + int c = static_cast<int>(floating_views_.size()); + while (--c >= 0) { + // If the focused view is a floating view or a floating view's children, + // use the focus manager to store it. + int tmp_id; + if (focused_view && + ((focused_view == floating_views_[c]) || + floating_views_[c]->IsParentOf(focused_view))) { + // We call EnumerateFloatingView to make sure the floating view is still + // valid: the model may have changed and could not know anything about + // that floating view anymore. + if (EnumerateFloatingViews(CURRENT, + floating_views_[c]->GetFloatingViewID(), + &tmp_id)) { + // TODO(port): Fix this once we have a FocusManger for Linux. +#if defined(OS_WIN) + focus_manager->StoreFocusedView(); +#endif + should_restore_focus_ = true; + } + focused_view = NULL; + } + + RemoveChildView(floating_views_[c]); + delete floating_views_[c]; + } + floating_views_.clear(); + floating_views_ids_.clear(); +} + +int View::GetFloatingViewID() { + DCHECK(IsFloatingView()); + std::map<View*, int>::iterator iter = parent_->floating_views_ids_.find(this); + DCHECK(iter != parent_->floating_views_ids_.end()); + return iter->second; +} + +View* View::RetrieveFloatingViewForID(int id) { + for (ViewList::const_iterator iter = floating_views_.begin(); + iter != floating_views_.end(); ++iter) { + if ((*iter)->GetFloatingViewID() == id) + return *iter; + } + return ValidateFloatingViewForID(id); +} + +void View::RestoreFloatingViewFocus() { + // Clear the reference to the task as if we have been triggered by it, it will + // soon be invalid. + restore_focus_view_task_ = NULL; + should_restore_focus_ = false; + + // TODO(port): Fix this once we have a FocusManger for Linux. +#if defined(OS_WIN) + FocusManager* focus_manager = GetFocusManager(); + DCHECK(focus_manager); + if (focus_manager) + focus_manager->RestoreFocusedView(); +#endif +} + +// static +bool View::EnumerateFloatingViewsForInterval(int low_bound, int high_bound, + bool ascending_order, + FloatingViewPosition position, + int starting_id, + int* id) { + DCHECK(low_bound <= high_bound); + if (low_bound >= high_bound) + return false; + + switch (position) { + case CURRENT: + if ((starting_id >= low_bound) && (starting_id < high_bound)) { + *id = starting_id; + return true; + } + return false; + case FIRST: + *id = ascending_order ? low_bound : high_bound - 1; + return true; + case LAST: + *id = ascending_order ? high_bound - 1 : low_bound; + return true; + case NEXT: + case PREVIOUS: + if (((position == NEXT) && ascending_order) || + ((position == PREVIOUS) && !ascending_order)) { + starting_id++; + if (starting_id < high_bound) { + *id = starting_id; + return true; + } + return false; + } + DCHECK(((position == NEXT) && !ascending_order) || + ((position == PREVIOUS) && ascending_order)); + starting_id--; + if (starting_id >= low_bound) { + *id = starting_id; + return true; + } + return false; + default: + NOTREACHED(); + } + return false; +} + +// static +void View::ConvertPointToView(const View* src, + const View* dst, + gfx::Point* point) { + ConvertPointToView(src, dst, point, true); +} + +// static +void View::ConvertPointToView(const View* src, + const View* dst, + gfx::Point* point, + bool try_other_direction) { + // src can be NULL + DCHECK(dst); + DCHECK(point); + + const View* v; + gfx::Point offset; + + for (v = dst; v && v != src; v = v->GetParent()) { + offset.SetPoint(offset.x() + v->GetX(APPLY_MIRRORING_TRANSFORMATION), + offset.y() + v->y()); + } + + // The source was not found. The caller wants a conversion + // from a view to a transitive parent. + if (src && v == NULL && try_other_direction) { + gfx::Point p; + // note: try_other_direction is force to FALSE so we don't + // end up in an infinite recursion should both src and dst + // are not parented. + ConvertPointToView(dst, src, &p, false); + // since the src and dst are inverted, p should also be negated + point->SetPoint(point->x() - p.x(), point->y() - p.y()); + } else { + point->SetPoint(point->x() - offset.x(), point->y() - offset.y()); + + // If src is NULL, sp is in the screen coordinate system + if (src == NULL) { + Widget* widget = dst->GetWidget(); + if (widget) { + gfx::Rect b; + widget->GetBounds(&b, false); + point->SetPoint(point->x() - b.x(), point->y() - b.y()); + } + } + } +} + +// static +void View::ConvertPointToWidget(const View* src, gfx::Point* p) { + DCHECK(src); + DCHECK(p); + + gfx::Point offset; + for (const View* v = src; v; v = v->GetParent()) { + offset.set_x(offset.x() + v->GetX(APPLY_MIRRORING_TRANSFORMATION)); + offset.set_y(offset.y() + v->y()); + } + p->SetPoint(p->x() + offset.x(), p->y() + offset.y()); +} + +// static +void View::ConvertPointFromWidget(const View* dest, gfx::Point* p) { + gfx::Point t; + ConvertPointToWidget(dest, &t); + p->SetPoint(p->x() - t.x(), p->y() - t.y()); +} + +// static +void View::ConvertPointToScreen(const View* src, gfx::Point* p) { + DCHECK(src); + DCHECK(p); + + // If the view is not connected to a tree, there's nothing we can do. + Widget* widget = src->GetWidget(); + if (widget) { + ConvertPointToWidget(src, p); + gfx::Rect r; + widget->GetBounds(&r, false); + p->SetPoint(p->x() + r.x(), p->y() + r.y()); + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// View - event handlers +// +///////////////////////////////////////////////////////////////////////////// + +bool View::OnMousePressed(const MouseEvent& e) { + return false; +} + +bool View::OnMouseDragged(const MouseEvent& e) { + return false; +} + +void View::OnMouseReleased(const MouseEvent& e, bool canceled) { +} + +void View::OnMouseMoved(const MouseEvent& e) { +} + +void View::OnMouseEntered(const MouseEvent& e) { +} + +void View::OnMouseExited(const MouseEvent& e) { +} + +void View::SetMouseHandler(View *new_mouse_handler) { + // It is valid for new_mouse_handler to be NULL + if (parent_) { + parent_->SetMouseHandler(new_mouse_handler); + } +} + +void View::SetVisible(bool flag) { + if (flag != is_visible_) { + // If the tab is currently visible, schedule paint to + // refresh parent + if (IsVisible()) { + SchedulePaint(); + } + + is_visible_ = flag; + + // This notifies all subviews recursively. + PropagateVisibilityNotifications(this, flag); + + // If we are newly visible, schedule paint. + if (IsVisible()) { + SchedulePaint(); + } + } +} + +bool View::IsVisibleInRootView() const { + View* parent = GetParent(); + if (IsVisible() && parent) + return parent->IsVisibleInRootView(); + else + return false; +} + +///////////////////////////////////////////////////////////////////////////// +// +// View - keyboard and focus +// +///////////////////////////////////////////////////////////////////////////// + +void View::RequestFocus() { + RootView* rv = GetRootView(); + if (rv && IsFocusable()) + rv->FocusView(this); +} + +void View::WillGainFocus() { +} + +void View::DidGainFocus() { +} + +void View::WillLoseFocus() { +} + +bool View::OnKeyPressed(const KeyEvent& e) { + return false; +} + +bool View::OnKeyReleased(const KeyEvent& e) { + return false; +} + +bool View::OnMouseWheel(const MouseWheelEvent& e) { + return false; +} + +void View::SetDragController(DragController* drag_controller) { + drag_controller_ = drag_controller; +} + +DragController* View::GetDragController() { + return drag_controller_; +} + +bool View::CanDrop(const OSExchangeData& data) { + return false; +} + +void View::OnDragEntered(const DropTargetEvent& event) { +} + +int View::OnDragUpdated(const DropTargetEvent& event) { + return DragDropTypes::DRAG_NONE; +} + +void View::OnDragExited() { +} + +int View::OnPerformDrop(const DropTargetEvent& event) { + return DragDropTypes::DRAG_NONE; +} + +// static +bool View::ExceededDragThreshold(int delta_x, int delta_y) { + return (abs(delta_x) > GetHorizontalDragThreshold() || + abs(delta_y) > GetVerticalDragThreshold()); +} + +bool View::CanProcessTabKeyEvents() { + return false; +} + +// Tooltips ----------------------------------------------------------------- +bool View::GetTooltipText(int x, int y, std::wstring* tooltip) { + return false; +} + +bool View::GetTooltipTextOrigin(int x, int y, gfx::Point* loc) { + return false; +} + +void View::TooltipTextChanged() { +#if defined(OS_WIN) + Widget* widget = GetWidget(); + if (widget && widget->GetTooltipManager()) + widget->GetTooltipManager()->TooltipTextChanged(this); +#else + // TODO(port): Not actually windows specific; I just haven't ported this part + // yet. + NOTIMPLEMENTED(); +#endif +} + +void View::UpdateTooltip() { +#if defined(OS_WIN) + Widget* widget = GetWidget(); + if (widget && widget->GetTooltipManager()) + widget->GetTooltipManager()->UpdateTooltip(); +#else + // TODO(port): Not actually windows specific; I just haven't ported this part + // yet. + NOTIMPLEMENTED(); +#endif +} + +void View::SetParentOwned(bool f) { + is_parent_owned_ = f; +} + +bool View::IsParentOwned() const { + return is_parent_owned_; +} + +std::string View::GetClassName() const { + return kViewClassName; +} + +View* View::GetAncestorWithClassName(const std::string& name) { + for (View* view = this; view; view = view->GetParent()) { + if (view->GetClassName() == name) + return view; + } + return NULL; +} + +gfx::Rect View::GetVisibleBounds() { + if (!IsVisibleInRootView()) + return gfx::Rect(); + gfx::Rect vis_bounds(0, 0, width(), height()); + gfx::Rect ancestor_bounds; + View* view = this; + int root_x = 0; + int root_y = 0; + while (view != NULL && !vis_bounds.IsEmpty()) { + root_x += view->GetX(APPLY_MIRRORING_TRANSFORMATION); + root_y += view->y(); + vis_bounds.Offset(view->GetX(APPLY_MIRRORING_TRANSFORMATION), view->y()); + View* ancestor = view->GetParent(); + if (ancestor != NULL) { + ancestor_bounds.SetRect(0, 0, ancestor->width(), + ancestor->height()); + vis_bounds = vis_bounds.Intersect(ancestor_bounds); + } else if (!view->GetWidget()) { + // If the view has no Widget, we're not visible. Return an empty rect. + return gfx::Rect(); + } + view = ancestor; + } + if (vis_bounds.IsEmpty()) + return vis_bounds; + // Convert back to this views coordinate system. + vis_bounds.Offset(-root_x, -root_y); + return vis_bounds; +} + +int View::GetPageScrollIncrement(ScrollView* scroll_view, + bool is_horizontal, bool is_positive) { + return 0; +} + +int View::GetLineScrollIncrement(ScrollView* scroll_view, + bool is_horizontal, bool is_positive) { + return 0; +} + +// static +void View::RegisterChildrenForVisibleBoundsNotification( + RootView* root, View* view) { + DCHECK(root && view); + if (view->GetNotifyWhenVisibleBoundsInRootChanges()) + root->RegisterViewForVisibleBoundsNotification(view); + for (int i = 0; i < view->GetChildViewCount(); ++i) + RegisterChildrenForVisibleBoundsNotification(root, view->GetChildViewAt(i)); +} + +// static +void View::UnregisterChildrenForVisibleBoundsNotification( + RootView* root, View* view) { + DCHECK(root && view); + if (view->GetNotifyWhenVisibleBoundsInRootChanges()) + root->UnregisterViewForVisibleBoundsNotification(view); + for (int i = 0; i < view->GetChildViewCount(); ++i) + UnregisterChildrenForVisibleBoundsNotification(root, + view->GetChildViewAt(i)); +} + +void View::AddDescendantToNotify(View* view) { + DCHECK(view); + if (!descendants_to_notify_.get()) + descendants_to_notify_.reset(new ViewList()); + descendants_to_notify_->push_back(view); +} + +void View::RemoveDescendantToNotify(View* view) { + DCHECK(view && descendants_to_notify_.get()); + ViewList::iterator i = find(descendants_to_notify_->begin(), + descendants_to_notify_->end(), + view); + DCHECK(i != descendants_to_notify_->end()); + descendants_to_notify_->erase(i); + if (descendants_to_notify_->empty()) + descendants_to_notify_.reset(); +} + +// static +bool View::GetViewPath(View* start, View* end, std::vector<int>* path) { + while (end && (end != start)) { + View* parent = end->GetParent(); + if (!parent) + return false; + path->insert(path->begin(), parent->GetChildIndex(end)); + end = parent; + } + return end == start; +} + +// static +View* View::GetViewForPath(View* start, const std::vector<int>& path) { + View* v = start; + for (std::vector<int>::const_iterator iter = path.begin(); + iter != path.end(); ++iter) { + int index = *iter; + if (index >= v->GetChildViewCount()) + return NULL; + v = v->GetChildViewAt(index); + } + return v; +} + +// DropInfo -------------------------------------------------------------------- + +void View::DragInfo::Reset() { + possible_drag = false; + start_x = start_y = 0; +} + +void View::DragInfo::PossibleDrag(int x, int y) { + possible_drag = true; + start_x = x; + start_y = y; +} + +} // namespace diff --git a/views/view.h b/views/view.h new file mode 100644 index 0000000..d2d8e52 --- /dev/null +++ b/views/view.h @@ -0,0 +1,1351 @@ +// Copyright (c) 2006-2008 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 VIEWS_VIEW_H_ +#define VIEWS_VIEW_H_ + +#include "build/build_config.h" + +#if defined(OS_WIN) +#include <atlbase.h> +#include <atlapp.h> +#include <atlmisc.h> +#endif // defined(OS_WIN) + +#include <algorithm> +#include <map> +#include <string> +#include <vector> + +#include "base/gfx/rect.h" +#include "base/scoped_ptr.h" +#include "chrome/common/accessibility_types.h" +#include "views/accelerator.h" +#include "views/background.h" +#include "views/border.h" + +namespace gfx { +class Insets; +class Path; +} + +class ChromeCanvas; +class OSExchangeData; +class ViewAccessibilityWrapper; + +namespace views { + +class Background; +class Border; +class FocusManager; +class FocusTraversable; +class LayoutManager; +class RestoreFocusTask; +class RootView; +class ScrollView; +class Widget; +class Window; + +// ContextMenuController is responsible for showing the context menu for a +// View. To use a ContextMenuController invoke SetContextMenuController on a +// View. When the appropriate user gesture occurs ShowContextMenu is invoked +// on the ContextMenuController. +// +// Setting a ContextMenuController on a view makes the view process mouse +// events. +// +// It is up to subclasses that do their own mouse processing to invoke +// the appropriate ContextMenuController method, typically by invoking super's +// implementation for mouse processing. +// +class ContextMenuController { + public: + // Invoked to show the context menu for the source view. If is_mouse_gesture + // is true, the x/y coordinate are the location of the mouse. If + // is_mouse_gesture is false, this method was not invoked by a mouse gesture + // and x/y is the recommended location to show the menu at. + // + // x/y is in screen coordinates. + virtual void ShowContextMenu(View* source, + int x, + int y, + bool is_mouse_gesture) = 0; +}; + +// DragController is responsible for writing drag data for a view, as well as +// supplying the supported drag operations. Use DragController if you don't +// want to subclass. + +class DragController { + public: + // Writes the data for the drag. + virtual void WriteDragData(View* sender, + int press_x, + int press_y, + OSExchangeData* data) = 0; + + // Returns the supported drag operations (see DragDropTypes for possible + // values). A drag is only started if this returns a non-zero value. + virtual int GetDragOperations(View* sender, int x, int y) = 0; +}; + + +///////////////////////////////////////////////////////////////////////////// +// +// View class +// +// A View is a rectangle within the views View hierarchy. It is the base +// class for all Views. +// +// A View is a container of other Views (there is no such thing as a Leaf +// View - makes code simpler, reduces type conversion headaches, design +// mistakes etc) +// +// The View contains basic properties for sizing (bounds), layout (flex, +// orientation, etc), painting of children and event dispatch. +// +// The View also uses a simple Box Layout Manager similar to XUL's +// SprocketLayout system. Alternative Layout Managers implementing the +// LayoutManager interface can be used to lay out children if required. +// +// It is up to the subclass to implement Painting and storage of subclass - +// specific properties and functionality. +// +///////////////////////////////////////////////////////////////////////////// +class View : public AcceleratorTarget { + public: + + // Used in EnumerateFloatingViews() to specify which floating view to + // retrieve. + enum FloatingViewPosition { + FIRST = 0, + NEXT, + PREVIOUS, + LAST, + CURRENT + }; + + // Used in the versions of GetBounds() and x() that take a transformation + // parameter in order to determine whether or not to take into account the + // mirroring setting of the View when returning bounds positions. + enum PositionMirroringSettings { + IGNORE_MIRRORING_TRANSFORMATION = 0, + APPLY_MIRRORING_TRANSFORMATION + }; + + // The view class name. + static char kViewClassName[]; + + View(); + virtual ~View(); + + // Sizing functions + + // Get the bounds of the View, relative to the parent. Essentially, this + // function returns the bounds_ rectangle. + // + // This is the function subclasses should use whenever they need to obtain + // the bounds of one of their child views (for example, when implementing + // View::Layout()). + // TODO(beng): Convert |bounds_| to a gfx::Rect. + gfx::Rect bounds() const { return bounds_; } + + // Get the size of the View. + gfx::Size size() const { return bounds_.size(); } + + // Return the bounds of the View, relative to the parent. If + // |settings| is IGNORE_MIRRORING_TRANSFORMATION, the function returns the + // bounds_ rectangle. If |settings| is APPLY_MIRRORING_SETTINGS AND the + // parent View is using a right-to-left UI layout, then the function returns + // a shifted version of the bounds_ rectangle that represents the mirrored + // View bounds. + // + // NOTE: in the vast majority of the cases, the mirroring implementation is + // transparent to the View subclasses and therefore you should use the + // version of GetBounds() which does not take a transformation settings + // parameter. + gfx::Rect GetBounds(PositionMirroringSettings settings) const; + + // Set the bounds in the parent's coordinate system. + void SetBounds(const gfx::Rect& bounds); + void SetBounds(int x, int y, int width, int height) { + SetBounds(gfx::Rect(x, y, std::max(0, width), std::max(0, height))); + } + void SetX(int x) { SetBounds(x, y(), width(), height()); } + void SetY(int y) { SetBounds(x(), y, width(), height()); } + + // Returns the left coordinate of the View, relative to the parent View, + // which is the value of bounds_.x(). + // + // This is the function subclasses should use whenever they need to obtain + // the left position of one of their child views (for example, when + // implementing View::Layout()). + // This is equivalent to GetX(IGNORE_MIRRORING_TRANSFORMATION), but + // inlinable. + int x() const { return bounds_.x(); } + int y() const { return bounds_.y(); } + int width() const { return bounds_.width(); } + int height() const { return bounds_.height(); } + + // Return the left coordinate of the View, relative to the parent. If + // |settings| is IGNORE_MIRRORING_SETTINGS, the function returns the value of + // bounds_.x(). If |settings| is APPLY_MIRRORING_SETTINGS AND the parent + // View is using a right-to-left UI layout, then the function returns the + // mirrored value of bounds_.x(). + // + // NOTE: in the vast majority of the cases, the mirroring implementation is + // transparent to the View subclasses and therefore you should use the + // paremeterless version of x() when you need to get the X + // coordinate of a child View. + int GetX(PositionMirroringSettings settings) const; + + // Return this control local bounds. If include_border is true, local bounds + // is the rectangle {0, 0, width(), height()}, otherwise, it does not + // include the area where the border (if any) is painted. + gfx::Rect GetLocalBounds(bool include_border) const; + + // Get the position of the View, relative to the parent. + // + // Note that if the parent uses right-to-left UI layout, then the mirrored + // position of this View is returned. Use x()/y() if you want to ignore + // mirroring. + gfx::Point GetPosition() const; + + // Get the size the View would like to be, if enough space were available. + virtual gfx::Size GetPreferredSize(); + + // Convenience method that sizes this view to its preferred size. + void SizeToPreferredSize(); + + // Gets the minimum size of the view. View's implementation invokes + // GetPreferredSize. + virtual gfx::Size GetMinimumSize(); + + // Return the height necessary to display this view with the provided width. + // View's implementation returns the value from getPreferredSize.cy. + // Override if your View's preferred height depends upon the width (such + // as with Labels). + virtual int GetHeightForWidth(int w); + + // This method is invoked when this object size or position changes. + // The default implementation does nothing. + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + + // Set whether the receiving view is visible. Painting is scheduled as needed + virtual void SetVisible(bool flag); + + // Return whether a view is visible + virtual bool IsVisible() const { return is_visible_; } + + // Return whether a view and its ancestors are visible. Returns true if the + // path from this view to the root view is visible. + virtual bool IsVisibleInRootView() const; + + // Set whether this view is enabled. A disabled view does not receive keyboard + // or mouse inputs. If flag differs from the current value, SchedulePaint is + // invoked. + virtual void SetEnabled(bool flag); + + // Returns whether the view is enabled. + virtual bool IsEnabled() const; + + // Set whether this view is hottracked. A disabled view cannot be hottracked. + // If flag differs from the current value, SchedulePaint is invoked. + virtual void SetHotTracked(bool flag); + + // Returns whether the view is hot-tracked. + virtual bool IsHotTracked() const { return false; } + + // Returns whether the view is pushed. + virtual bool IsPushed() const { return false; } + + // Scrolls the specified region, in this View's coordinate system, to be + // visible. View's implementation passes the call onto the parent View (after + // adjusting the coordinates). It is up to views that only show a portion of + // the child view, such as Viewport, to override appropriately. + virtual void ScrollRectToVisible(int x, int y, int width, int height); + + // Layout functions + + // Lay out the child Views (set their bounds based on sizing heuristics + // specific to the current Layout Manager) + virtual void Layout(); + + // Gets/Sets the Layout Manager used by this view to size and place its + // children. + // The LayoutManager is owned by the View and is deleted when the view is + // deleted, or when a new LayoutManager is installed. + LayoutManager* GetLayoutManager() const; + void SetLayoutManager(LayoutManager* layout); + + // Right-to-left UI layout functions + + // Indicates whether the UI layout for this view is right-to-left. The view + // has an RTL UI layout if RTL hasn't been disabled for the view and if the + // locale's language is an RTL language. + bool UILayoutIsRightToLeft() const; + + // Enables or disables the right-to-left layout for the view. If |enable| is + // true, the layout will become right-to-left only if the locale's language + // is right-to-left. + // + // By default, right-to-left UI layout is enabled for the view and therefore + // this function must be called (with false as the |enable| parameter) in + // order to disable the right-to-left layout property for a specific instance + // of the view. Disabling the right-to-left UI layout is necessary in case a + // UI element will not appear correctly when mirrored. + void EnableUIMirroringForRTLLanguages(bool enable) { + ui_mirroring_is_enabled_for_rtl_languages_ = enable; + } + + // This method determines whether the ChromeCanvas object passed to + // View::Paint() needs to be transformed such that anything drawn on the + // canvas object during View::Paint() is flipped horizontally. + // + // By default, this function returns false (which is the initial value of + // |flip_canvas_on_paint_for_rtl_ui_|). View subclasses that need to paint on + // a flipped ChromeCanvas when the UI layout is right-to-left need to call + // EnableCanvasFlippingForRTLUI(). + bool FlipCanvasOnPaintForRTLUI() const { + return flip_canvas_on_paint_for_rtl_ui_ ? UILayoutIsRightToLeft() : false; + } + + // Enables or disables flipping of the ChromeCanvas during View::Paint(). + // Note that if canvas flipping is enabled, the canvas will be flipped only + // if the UI layout is right-to-left; that is, the canvas will be flipped + // only if UILayoutIsRightToLeft() returns true. + // + // Enabling canvas flipping is useful for leaf views that draw a bitmap that + // needs to be flipped horizontally when the UI layout is right-to-left + // (views::Button, for example). This method is helpful for such classes + // because their drawing logic stays the same and they can become agnostic to + // the UI directionality. + void EnableCanvasFlippingForRTLUI(bool enable) { + flip_canvas_on_paint_for_rtl_ui_ = enable; + } + + // Returns the mirrored X position for the view, relative to the parent. If + // the parent view is not mirrored, this function returns bound_.left. + // + // UI mirroring is transparent to most View subclasses and therefore there is + // no need to call this routine from anywhere within your subclass + // implementation. + int MirroredX() const; + + // Given a rectangle specified in this View's coordinate system, the function + // computes the 'left' value for the mirrored rectangle within this View. If + // the View's UI layout is not right-to-left, then bounds.x() is returned. + // + // UI mirroring is transparent to most View subclasses and therefore there is + // no need to call this routine from anywhere within your subclass + // implementation. + int MirroredLeftPointForRect(const gfx::Rect& rect) const; + + // Given the X coordinate of a point inside the View, this function returns + // the mirrored X coordinate of the point if the View's UI layout is + // right-to-left. If the layout is left-to-right, the same X coordinate is + // returned. + // + // Following are a few examples of the values returned by this function for + // a View with the bounds {0, 0, 100, 100} and a right-to-left layout: + // + // MirroredXCoordinateInsideView(0) -> 100 + // MirroredXCoordinateInsideView(20) -> 80 + // MirroredXCoordinateInsideView(99) -> 1 + int MirroredXCoordinateInsideView(int x) const { + return UILayoutIsRightToLeft() ? width() - x : x; + } + + // Painting functions + + // Mark the specified rectangle as dirty (needing repaint). If |urgent| is + // true, the view will be repainted when the current event processing is + // done. Otherwise, painting will take place as soon as possible. + virtual void SchedulePaint(const gfx::Rect& r, bool urgent); + + // Mark the entire View's bounds as dirty. Painting will occur as soon as + // possible. + virtual void SchedulePaint(); + + // Convenience to schedule a paint given some ints. Painting will occur as + // soon as possible. + virtual void SchedulePaint(int x, int y, int w, int h); + + // Paint the receiving view. g is prepared such as it is in + // receiver's coordinate system. g's state is restored after this + // call so your implementation can change the graphics configuration + // + // Default implementation paints the background if it is defined + // + // Override this method when implementing a new control. + virtual void Paint(ChromeCanvas* canvas); + + // Paint the background if any. This method is called by Paint() and + // should rarely be invoked directly. + virtual void PaintBackground(ChromeCanvas* canvas); + + // Paint the border if any. This method is called by Paint() and + // should rarely be invoked directly. + virtual void PaintBorder(ChromeCanvas* canvas); + + // Paints the focus border (only if the view has the focus). + // This method is called by Paint() and should rarely be invoked directly. + // The default implementation paints a gray border around the view. Override + // it for custom focus effects. + virtual void PaintFocusBorder(ChromeCanvas* canvas); + + // Paint this View immediately. + virtual void PaintNow(); + + // Paint a view without attaching it to this view hierarchy. + // Any view can be painted that way. + // This method set bounds, calls layout and handles clipping properly. The + // provided view can be attached to a parent. The parent will be saved and + // restored. (x, y, width, height) define the floating view bounds + void PaintFloatingView(ChromeCanvas* canvas, View* view, + int x, int y, int w, int h); + + // Tree functions + + // Add a child View. + void AddChildView(View* v); + + // Adds a child View at the specified position. + void AddChildView(int index, View* v); + + // Get the child View at the specified index. + View* GetChildViewAt(int index) const; + + // Remove a child view from this view. v's parent will change to NULL + void RemoveChildView(View *v); + + // Remove all child view from this view. If |delete_views| is true, the views + // are deleted, unless marked as not parent owned. + void RemoveAllChildViews(bool delete_views); + + // Get the number of child Views. + int GetChildViewCount() const; + + // Get the child View at the specified point. + virtual View* GetViewForPoint(const gfx::Point& point); + + // Get the Widget that hosts this View, if any. + virtual Widget* GetWidget() const; + + // Gets the Widget that most closely contains this View, if any. + virtual Window* GetWindow() const; + + // Get the containing RootView + virtual RootView* GetRootView(); + + // Get the parent View + View* GetParent() const { return parent_; } + + // Returns the index of the specified |view| in this view's children, or -1 + // if the specified view is not a child of this view. + int GetChildIndex(View* v) const; + + // Returns true if the specified view is a direct or indirect child of this + // view. + bool IsParentOf(View* v) const; + + // Recursively descends the view tree starting at this view, and returns + // the first child that it encounters that has the given ID. + // Returns NULL if no matching child view is found. + virtual View* GetViewByID(int id) const; + + // Sets and gets the ID for this view. ID should be unique within the subtree + // that you intend to search for it. 0 is the default ID for views. + void SetID(int id); + int GetID() const; + + // A group id is used to tag views which are part of the same logical group. + // Focus can be moved between views with the same group using the arrow keys. + // Groups are currently used to implement radio button mutual exclusion. + void SetGroup(int gid); + int GetGroup() const; + + // If this returns true, the views from the same group can each be focused + // when moving focus with the Tab/Shift-Tab key. If this returns false, + // only the selected view from the group (obtained with + // GetSelectedViewForGroup()) is focused. + virtual bool IsGroupFocusTraversable() const { return true; } + + // Fills the provided vector with all the available views which belong to the + // provided group. + void GetViewsWithGroup(int group_id, std::vector<View*>* out); + + // Return the View that is currently selected in the specified group. + // The default implementation simply returns the first View found for that + // group. + virtual View* GetSelectedViewForGroup(int group_id); + + // Focus support + // + // Returns the view that should be selected next when pressing Tab. + View* GetNextFocusableView(); + + // Returns the view that should be selected next when pressing Shift-Tab. + View* GetPreviousFocusableView(); + + // Sets the component that should be selected next when pressing Tab, and + // makes the current view the precedent view of the specified one. + // Note that by default views are linked in the order they have been added to + // their container. Use this method if you want to modify the order. + // IMPORTANT NOTE: loops in the focus hierarchy are not supported. + void SetNextFocusableView(View* view); + + // Return whether this view can accept the focus. + virtual bool IsFocusable() const; + + // Sets whether this view can accept the focus. + // Note that this is false by default so that a view used as a container does + // not get the focus. + virtual void SetFocusable(bool focusable); + + // Convenience method to retrieve the FocusManager associated with the + // Widget that contains this view. This can return NULL if this view is not + // part of a view hierarchy with a Widget. + virtual FocusManager* GetFocusManager(); + + // Sets a keyboard accelerator for that view. When the user presses the + // accelerator key combination, the AcceleratorPressed method is invoked. + // Note that you can set multiple accelerators for a view by invoking this + // method several times. + virtual void AddAccelerator(const Accelerator& accelerator); + + // Removes the specified accelerator for this view. + virtual void RemoveAccelerator(const Accelerator& accelerator); + + // Removes all the keyboard accelerators for this view. + virtual void ResetAccelerators(); + + // Called when a keyboard accelerator is pressed. + // Derived classes should implement desired behavior and return true if they + // handled the accelerator. + virtual bool AcceleratorPressed(const Accelerator& accelerator) { + return false; + } + + // Called on a view (if it is has focus) before an Accelerator is processed. + // Views that want to override an accelerator should override this method to + // perform the required action and return true, to indicate that the + // accelerator should not be processed any further. + virtual bool OverrideAccelerator(const Accelerator& accelerator) { + return false; + } + + // Returns whether this view currently has the focus. + virtual bool HasFocus(); + + // Accessibility support + // TODO(klink): Move all this out to a AccessibleInfo wrapper class. + // + // Returns the MSAA default action of the current view. The string returned + // describes the default action that will occur when executing + // IAccessible::DoDefaultAction. For instance, default action of a button is + // 'Press'. Sets the input string appropriately, and returns true if + // successful. + virtual bool GetAccessibleDefaultAction(std::wstring* action) { + return false; + } + + // Returns a string containing the mnemonic, or the keyboard shortcut, for a + // given control. Sets the input string appropriately, and returns true if + // successful. + virtual bool GetAccessibleKeyboardShortcut(std::wstring* shortcut) { + return false; + } + + // Returns a brief, identifying string, containing a unique, readable name of + // a given control. Sets the input string appropriately, and returns true if + // successful. + virtual bool GetAccessibleName(std::wstring* name) { return false; } + + // Returns the accessibility role of the current view. The role is what + // assistive technologies (ATs) use to determine what behavior to expect from + // a given control. Sets the input Role appropriately, and returns true if + // successful. + virtual bool GetAccessibleRole(AccessibilityTypes::Role* role) { + return false; + } + + // Returns the accessibility state of the current view. Sets the input State + // appropriately, and returns true if successful. + virtual bool GetAccessibleState(AccessibilityTypes::State* state) { + return false; + } + + // Assigns a keyboard shortcut string description to the given control. Needed + // as a View does not know which shortcut will be associated with it until it + // is created to be a certain type. + virtual void SetAccessibleKeyboardShortcut(const std::wstring& shortcut) {} + + // Assigns a string name to the given control. Needed as a View does not know + // which name will be associated with it until it is created to be a + // certain type. + virtual void SetAccessibleName(const std::wstring& name) {} + + // Returns an instance of a wrapper class implementing the (platform-specific) + // accessibility interface for a given View. If one exists, it will be + // re-used, otherwise a new instance will be created. + ViewAccessibilityWrapper* GetViewAccessibilityWrapper(); + + // Accessor used to determine if a child view (leaf) has accessibility focus. + // Returns NULL if there are no children, or if none of the children has + // accessibility focus. + virtual View* GetAccFocusedChildView() { return NULL; } + + // Floating views + // + // A floating view is a view that is used to paint a cell within a parent view + // Floating Views are painted using PaintFloatingView() above. + // + // Floating views can also be lazily created and attached to the view + // hierarchy to process events. To make this possible, each view is given an + // opportunity to create and attach a floating view right before an mouse + // event is processed. + + // Retrieves the id for the floating view at the specified coordinates if any. + // Derived classes that use floating views should implement this method and + // return true if a view has been found and its id set in |id|. + virtual bool GetFloatingViewIDForPoint(int x, int y, int* id); + + // Retrieves the ID of the floating view at the specified |position| and sets + // it in |id|. + // For positions NEXT and PREVIOUS, the specified |starting_id| is used as + // the origin, it is ignored for FIRST and LAST. + // Returns true if an ID was found, false otherwise. + // For CURRENT, the |starting_id| should be set in |id| and true returned if + // the |starting_id| is a valid floating view id. + // Derived classes that use floating views should implement this method and + // return a unique ID for each floating view. + // The default implementation always returns false. + virtual bool EnumerateFloatingViews(FloatingViewPosition position, + int starting_id, + int* id); + + // Creates and attaches the floating view with the specified |id| to this view + // hierarchy and returns it. + // Derived classes that use floating views should implement this method. + // + // NOTE: subclasses implementing this should return NULL if passed an invalid + // id. An invalid ID may be passed in by the focus manager when attempting + // to restore focus. + virtual View* ValidateFloatingViewForID(int id); + + // Whether the focus should automatically be restored to the last focused + // view. Default implementation returns true. + // Derived classes that want to restore focus themselves should override this + // method and return false. + virtual bool ShouldRestoreFloatingViewFocus(); + + // Attach a floating view to the receiving view. The view is inserted + // in the child view list and will behave like a normal view. |id| is the + // floating view id for that view. + void AttachFloatingView(View* v, int id); + + // Return whether a view already has a floating view which bounds intersects + // the provided point. + // + // If the View uses right-to-left UI layout, then the given point is checked + // against the mirrored position of each floating View. + bool HasFloatingViewForPoint(int x, int y); + + // Detach and delete all floating views. Call this method when your model + // or layout changes. + void DetachAllFloatingViews(); + + // Returns the view with the specified |id|, by calling + // ValidateFloatingViewForID if that view has not yet been attached. + virtual View* RetrieveFloatingViewForID(int id); + + // Restores the focus to the previously selected floating view. + virtual void RestoreFloatingViewFocus(); + + // Goes up the parent hierarchy of this view and returns the first floating + // view found. Returns NULL if none were found. + View* RetrieveFloatingViewParent(); + + // Utility functions + + // Note that the utility coordinate conversions functions always operate on + // the mirrored position of the child Views if the parent View uses a + // right-to-left UI layout. + + // Convert a point from source coordinate system to dst coordinate system. + // + // source is a parent or a child of dst, directly or transitively. + // If source and dst are not in the same View hierarchy, the result is + // undefined. + // Source can be NULL in which case it means the screen coordinate system + static void ConvertPointToView(const View* src, + const View* dst, + gfx::Point* point); + + // Convert a point from the coordinate system of a View to that of the + // Widget. This is useful for example when sizing HWND children of the + // Widget that don't know about the View hierarchy and need to be placed + // relative to the Widget that is their parent. + static void ConvertPointToWidget(const View* src, gfx::Point* point); + + // Convert a point from a view Widget to a View dest + static void ConvertPointFromWidget(const View* dest, gfx::Point* p); + + // Convert a point from the coordinate system of a View to that of the + // screen. This is useful for example when placing popup windows. + static void ConvertPointToScreen(const View* src, gfx::Point* point); + + // Event Handlers + + // This method is invoked when the user clicks on this view. + // The provided event is in the receiver's coordinate system. + // + // Return true if you processed the event and want to receive subsequent + // MouseDraggged and MouseReleased events. This also stops the event from + // bubbling. If you return false, the event will bubble through parent + // views. + // + // If you remove yourself from the tree while processing this, event bubbling + // stops as if you returned true, but you will not receive future events. + // The return value is ignored in this case. + // + // Default implementation returns true if a ContextMenuController has been + // set, false otherwise. Override as needed. + // + virtual bool OnMousePressed(const MouseEvent& event); + + // This method is invoked when the user clicked on this control. + // and is still moving the mouse with a button pressed. + // The provided event is in the receiver's coordinate system. + // + // Return true if you processed the event and want to receive + // subsequent MouseDragged and MouseReleased events. + // + // Default implementation returns true if a ContextMenuController has been + // set, false otherwise. Override as needed. + // + virtual bool OnMouseDragged(const MouseEvent& event); + + // This method is invoked when the user releases the mouse + // button. The event is in the receiver's coordinate system. + // + // If canceled is true it indicates the mouse press/drag was canceled by a + // system/user gesture. + // + // Default implementation notifies the ContextMenuController is appropriate. + // Subclasses that wish to honor the ContextMenuController should invoke + // super. + virtual void OnMouseReleased(const MouseEvent& event, bool canceled); + + // This method is invoked when the mouse is above this control + // The event is in the receiver's coordinate system. + // + // Default implementation does nothing. Override as needed. + virtual void OnMouseMoved(const MouseEvent& e); + + // This method is invoked when the mouse enters this control. + // + // Default implementation does nothing. Override as needed. + virtual void OnMouseEntered(const MouseEvent& event); + + // This method is invoked when the mouse exits this control + // The provided event location is always (0, 0) + // Default implementation does nothing. Override as needed. + virtual void OnMouseExited(const MouseEvent& event); + + // Set the MouseHandler for a drag session. + // + // A drag session is a stream of mouse events starting + // with a MousePressed event, followed by several MouseDragged + // events and finishing with a MouseReleased event. + // + // This method should be only invoked while processing a + // MouseDragged or MouseReleased event. + // + // All further mouse dragged and mouse up events will be sent + // the MouseHandler, even if it is reparented to another window. + // + // The MouseHandler is automatically cleared when the control + // comes back from processing the MouseReleased event. + // + // Note: if the mouse handler is no longer connected to a + // view hierarchy, events won't be sent. + // + virtual void SetMouseHandler(View* new_mouse_handler); + + // Request the keyboard focus. The receiving view will become the + // focused view. + virtual void RequestFocus(); + + // Invoked when a view is about to gain focus + virtual void WillGainFocus(); + + // Invoked when a view just gained focus. + virtual void DidGainFocus(); + + // Invoked when a view is about lose focus + virtual void WillLoseFocus(); + + // Invoked when a view is about to be requested for focus due to the focus + // traversal. Reverse is this request was generated going backward + // (Shift-Tab). + virtual void AboutToRequestFocusFromTabTraversal(bool reverse) { } + + // Invoked when a key is pressed or released. + // Subclasser should return true if the event has been processed and false + // otherwise. If the event has not been processed, the parent will be given a + // chance. + virtual bool OnKeyPressed(const KeyEvent& e); + virtual bool OnKeyReleased(const KeyEvent& e); + + // Whether the view wants to receive Tab and Shift-Tab key events. + // If false, Tab and Shift-Tabs key events are used for focus traversal and + // are not sent to the view. If true, the events are sent to the view and not + // used for focus traversal. + // This implementation returns false (so that by default views handle nicely + // the keyboard focus traversal). + virtual bool CanProcessTabKeyEvents(); + + // Invoked when the user uses the mousewheel. Implementors should return true + // if the event has been processed and false otherwise. This message is sent + // if the view is focused. If the event has not been processed, the parent + // will be given a chance. + virtual bool OnMouseWheel(const MouseWheelEvent& e); + + // Drag and drop functions. + + // Set/get the DragController. See description of DragController for more + // information. + void SetDragController(DragController* drag_controller); + DragController* GetDragController(); + + // During a drag and drop session when the mouse moves the view under the + // mouse is queried to see if it should be a target for the drag and drop + // session. A view indicates it is a valid target by returning true from + // CanDrop. If a view returns true from CanDrop, + // OnDragEntered is sent to the view when the mouse first enters the view, + // as the mouse moves around within the view OnDragUpdated is invoked. + // If the user releases the mouse over the view and OnDragUpdated returns a + // valid drop, then OnPerformDrop is invoked. If the mouse moves outside the + // view or over another view that wants the drag, OnDragExited is invoked. + // + // Similar to mouse events, the deepest view under the mouse is first checked + // if it supports the drop (Drop). If the deepest view under + // the mouse does not support the drop, the ancestors are walked until one + // is found that supports the drop. + + // A view that supports drag and drop must override this and return true if + // data contains a type that may be dropped on this view. + virtual bool CanDrop(const OSExchangeData& data); + + // OnDragEntered is invoked when the mouse enters this view during a drag and + // drop session and CanDrop returns true. This is immediately + // followed by an invocation of OnDragUpdated, and eventually one of + // OnDragExited or OnPerformDrop. + virtual void OnDragEntered(const DropTargetEvent& event); + + // Invoked during a drag and drop session while the mouse is over the view. + // This should return a bitmask of the DragDropTypes::DragOperation supported + // based on the location of the event. Return 0 to indicate the drop should + // not be accepted. + virtual int OnDragUpdated(const DropTargetEvent& event); + + // Invoked during a drag and drop session when the mouse exits the views, or + // when the drag session was canceled and the mouse was over the view. + virtual void OnDragExited(); + + // Invoked during a drag and drop session when OnDragUpdated returns a valid + // operation and the user release the mouse. + virtual int OnPerformDrop(const DropTargetEvent& event); + + // Returns true if the mouse was dragged enough to start a drag operation. + // delta_x and y are the distance the mouse was dragged. + static bool ExceededDragThreshold(int delta_x, int delta_y); + + // This method is the main entry point to process paint for this + // view and its children. This method is called by the painting + // system. You should call this only if you want to draw a sub tree + // inside a custom graphics. + // To customize painting override either the Paint or PaintChildren method, + // not this one. + virtual void ProcessPaint(ChromeCanvas* canvas); + + // Paint the View's child Views, in reverse order. + virtual void PaintChildren(ChromeCanvas* canvas); + + // Sets the ContextMenuController. Setting this to non-null makes the View + // process mouse events. + void SetContextMenuController(ContextMenuController* menu_controller); + ContextMenuController* GetContextMenuController() { + return context_menu_controller_; + } + + // Provides default implementation for context menu handling. The default + // implementation calls the ShowContextMenu of the current + // ContextMenuController (if it is not NULL). Overridden in subclassed views + // to provide right-click menu display triggerd by the keyboard (i.e. for the + // Chrome toolbar Back and Forward buttons). No source needs to be specified, + // as it is always equal to the current View. + virtual void ShowContextMenu(int x, + int y, + bool is_mouse_gesture); + + // The background object is owned by this object and may be NULL. + void set_background(Background* b) { background_.reset(b); } + const Background* background() const { return background_.get(); } + + // The border object is owned by this object and may be NULL. + void set_border(Border* b) { border_.reset(b); } + const Border* border() const { return border_.get(); } + + // Returns the insets of the current border. If there is no border an empty + // insets is returned. + virtual gfx::Insets GetInsets() const; + +#if defined(OS_WIN) + // TODO(port): Make GetCursorForPoint portable. + + // Return the cursor that should be used for this view or NULL if + // the default cursor should be used. The provided point is in the + // receiver's coordinate system. + virtual HCURSOR GetCursorForPoint(Event::EventType event_type, int x, int y); +#endif // defined(OS_WIN) + + // Convenience to test whether a point is within this view's bounds + virtual bool HitTest(const gfx::Point& l) const; + + // Gets the tooltip for this View. If the View does not have a tooltip, + // return false. If the View does have a tooltip, copy the tooltip into + // the supplied string and return true. + // Any time the tooltip text that a View is displaying changes, it must + // invoke TooltipTextChanged. + // The x/y provide the coordinates of the mouse (relative to this view). + virtual bool GetTooltipText(int x, int y, std::wstring* tooltip); + + // Returns the location (relative to this View) for the text on the tooltip + // to display. If false is returned (the default), the tooltip is placed at + // a default position. + virtual bool GetTooltipTextOrigin(int x, int y, gfx::Point* loc); + + // Set whether this view is owned by its parent. A view that is owned by its + // parent is automatically deleted when the parent is deleted. The default is + // true. Set to false if the view is owned by another object and should not + // be deleted by its parent. + void SetParentOwned(bool f); + + // Return whether a view is owned by its parent. See SetParentOwned() + bool IsParentOwned() const; + + // Return the receiving view's class name. A view class is a string which + // uniquely identifies the view class. It is intended to be used as a way to + // find out during run time if a view can be safely casted to a specific view + // subclass. The default implementation returns kViewClassName. + virtual std::string GetClassName() const; + + // Returns the first ancestor, starting at this, whose class name is |name|. + // Returns null if no ancestor has the class name |name|. + View* GetAncestorWithClassName(const std::string& name); + + // Returns the visible bounds of the receiver in the receivers coordinate + // system. + // + // When traversing the View hierarchy in order to compute the bounds, the + // function takes into account the mirroring setting for each View and + // therefore it will return the mirrored version of the visible bounds if + // need be. + gfx::Rect GetVisibleBounds(); + + // Subclasses that contain traversable children that are not directly + // accessible through the children hierarchy should return the associated + // FocusTraversable for the focus traversal to work properly. + virtual FocusTraversable* GetFocusTraversable() { return NULL; } + +#ifndef NDEBUG + // Debug method that logs the view hierarchy to the output. + void PrintViewHierarchy(); + + // Debug method that logs the focus traversal hierarchy to the output. + void PrintFocusHierarchy(); +#endif + + // The following methods are used by ScrollView to determine the amount + // to scroll relative to the visible bounds of the view. For example, a + // return value of 10 indicates the scrollview should scroll 10 pixels in + // the appropriate direction. + // + // Each method takes the following parameters: + // + // is_horizontal: if true, scrolling is along the horizontal axis, otherwise + // the vertical axis. + // is_positive: if true, scrolling is by a positive amount. Along the + // vertical axis scrolling by a positive amount equates to + // scrolling down. + // + // The return value should always be positive and gives the number of pixels + // to scroll. ScrollView interprets a return value of 0 (or negative) + // to scroll by a default amount. + // + // See VariableRowHeightScrollHelper and FixedRowHeightScrollHelper for + // implementations of common cases. + virtual int GetPageScrollIncrement(ScrollView* scroll_view, + bool is_horizontal, bool is_positive); + virtual int GetLineScrollIncrement(ScrollView* scroll_view, + bool is_horizontal, bool is_positive); + + protected: + // The id of this View. Used to find this View. + int id_; + + // The group of this view. Some view subclasses use this id to find other + // views of the same group. For example radio button uses this information + // to find other radio buttons. + int group_; + + // Called when the UI theme has changed, overriding allows individual Views to + // do special cleanup and processing (such as dropping resource caches). + // Subclasses that override this method must call the base class + // implementation to ensure child views are processed. + // Can only be called by subclasses. To dispatch a theme changed notification, + // call this method on the RootView. + virtual void ThemeChanged(); + +#ifndef NDEBUG + // Returns true if the View is currently processing a paint. + virtual bool IsProcessingPaint() const; +#endif + + // Returns the location, in screen coordinates, to show the context menu at + // when the context menu is shown from the keyboard. This implementation + // returns the middle of the visible region of this view. + // + // This method is invoked when the context menu is shown by way of the + // keyboard. + virtual gfx::Point GetKeyboardContextMenuLocation(); + + // Called by HitTest to see if this View has a custom hit test mask. If the + // return value is true, GetHitTestMask will be called to obtain the mask. + // Default value is false, in which case the View will hit-test against its + // bounds. + virtual bool HasHitTestMask() const; + + // Called by HitTest to retrieve a mask for hit-testing against. Subclasses + // override to provide custom shaped hit test regions. + virtual void GetHitTestMask(gfx::Path* mask) const; + + // This method is invoked when the tree changes. + // + // When a view is removed, it is invoked for all children and grand + // children. For each of these views, a notification is sent to the + // view and all parents. + // + // When a view is added, a notification is sent to the view, all its + // parents, and all its children (and grand children) + // + // Default implementation does nothing. Override to perform operations + // required when a view is added or removed from a view hierarchy + // + // parent is the new or old parent. Child is the view being added or + // removed. + // + virtual void ViewHierarchyChanged(bool is_add, View* parent, View* child); + + // When SetVisible() changes the visibility of a view, this method is + // invoked for that view as well as all the children recursively. + virtual void VisibilityChanged(View* starting_from, bool is_visible); + + // Views must invoke this when the tooltip text they are to display changes. + void TooltipTextChanged(); + + // Actual implementation of GetViewForPoint. + virtual View* GetViewForPoint(const gfx::Point& point, + bool can_create_floating); + + // Sets whether this view wants notification when its visible bounds relative + // to the root view changes. If true, this view is notified any time the + // origin of one its ancestors changes, or the portion of the bounds not + // obscured by ancestors changes. The default is false. + void SetNotifyWhenVisibleBoundsInRootChanges(bool value); + bool GetNotifyWhenVisibleBoundsInRootChanges(); + + // Notification that this views visible bounds, relative to the RootView + // has changed. The visible bounds corresponds to the region of the + // view not obscured by other ancestors. + virtual void VisibleBoundsInRootChanged() {} + + // Sets the keyboard focus to this View. The correct way to set the focus is + // to call RequestFocus() on the view. This method is called when the focus is + // set and gives an opportunity to subclasses to perform any extra focus steps + // (for example native component set the native focus on their native + // component). The default behavior is to set the native focus on the root + // Widget, which is what is appropriate for views that have no native window + // associated with them (so the root view gets the keyboard messages). + virtual void Focus(); + + // Invoked when a key is pressed before the key event is processed by the + // focus manager for accelerators. This gives a chance to the view to + // override an accelerator. Subclasser should return false if they want to + // process the key event and not have it translated to an accelerator (if + // any). In that case, OnKeyPressed will subsequently be invoked for that + // event. + virtual bool ShouldLookupAccelerators(const KeyEvent& e) { return true; } + + // A convenience method for derived classes which have floating views with IDs + // that are consecutive numbers in an interval [|low_bound|, |high_bound|[. + // They can call this method in their EnumerateFloatingViews implementation. + // If |ascending_order| is true, the first id is |low_bound|, the next after + // id n is n + 1, and so on. If |ascending_order| is false, the order is + // reversed, first id is |high_bound|, the next id after id n is n -1... + static bool EnumerateFloatingViewsForInterval(int low_bound, int high_bound, + bool ascending_order, + FloatingViewPosition position, + int starting_id, + int* id); + + // These are cover methods that invoke the method of the same name on + // the DragController. Subclasses may wish to override rather than install + // a DragController. + // See DragController for a description of these methods. + virtual int GetDragOperations(int press_x, int press_y); + virtual void WriteDragData(int press_x, int press_y, OSExchangeData* data); + + // Invoked from DoDrag after the drag completes. This implementation does + // nothing, and is intended for subclasses to do cleanup. + virtual void OnDragDone(); + + // Returns whether we're in the middle of a drag session that was initiated + // by us. + bool InDrag(); + + // Whether this view is enabled. + bool enabled_; + + // Whether the view can be focused. + bool focusable_; + + private: + friend class RootView; + friend class FocusManager; + friend class ViewStorage; + + // Used to track a drag. RootView passes this into + // ProcessMousePressed/Dragged. + struct DragInfo { + // Sets possible_drag to false and start_x/y to 0. This is invoked by + // RootView prior to invoke ProcessMousePressed. + void Reset(); + + // Sets possible_drag to true and start_x/y to the specified coordinates. + // This is invoked by the target view if it detects the press may generate + // a drag. + void PossibleDrag(int x, int y); + + // Whether the press may generate a drag. + bool possible_drag; + + // Coordinates of the mouse press. + int start_x; + int start_y; + }; + + // Returns how much the mouse needs to move in one direction to start a + // drag. These methods cache in a platform-appropriate way. These values are + // used by the public static method ExceededDragThreshold(). + static int GetHorizontalDragThreshold(); + static int GetVerticalDragThreshold(); + + // RootView invokes these. These in turn invoke the appropriate OnMouseXXX + // method. If a drag is detected, DoDrag is invoked. + bool ProcessMousePressed(const MouseEvent& e, DragInfo* drop_info); + bool ProcessMouseDragged(const MouseEvent& e, DragInfo* drop_info); + void ProcessMouseReleased(const MouseEvent& e, bool canceled); + + // Starts a drag and drop operation originating from this view. This invokes + // WriteDragData to write the data and GetDragOperations to determine the + // supported drag operations. When done, OnDragDone is invoked. + void DoDrag(const MouseEvent& e, int press_x, int press_y); + + // Adds a child View at the specified position. |floating_view| should be true + // if the |v| is a floating view. + void AddChildView(int index, View* v, bool floating_view); + + // Removes |view| from the hierarchy tree. If |update_focus_cycle| is true, + // the next and previous focusable views of views pointing to this view are + // updated. If |update_tool_tip| is true, the tooltip is updated. If + // |delete_removed_view| is true, the view is also deleted (if it is parent + // owned). + void DoRemoveChildView(View* view, + bool update_focus_cycle, + bool update_tool_tip, + bool delete_removed_view); + + // Sets the parent View. This is called automatically by AddChild and is + // thus private. + void SetParent(View *parent); + + // Call ViewHierarchyChanged for all child views on all parents + void PropagateRemoveNotifications(View* parent); + + // Call ViewHierarchyChanged for all children + void PropagateAddNotifications(View* parent, View* child); + + // Call VisibilityChanged() recursively for all children. + void PropagateVisibilityNotifications(View* from, bool is_visible); + + // Takes care of registering/unregistering accelerators if + // |register_accelerators| true and calls ViewHierarchyChanged(). + void ViewHierarchyChangedImpl(bool register_accelerators, + bool is_add, + View* parent, + View* child); + + // This is the actual implementation for ConvertPointToView() + // Attempts a parent -> child conversion and then a + // child -> parent conversion if try_other_direction is true + static void ConvertPointToView(const View* src, + const View* dst, + gfx::Point* point, + bool try_other_direction); + + // Propagates UpdateTooltip() to the TooltipManager for the Widget. + // This must be invoked any time the View hierarchy changes in such a way + // the view under the mouse differs. For example, if the bounds of a View is + // changed, this is invoked. Similarly, as Views are added/removed, this + // is invoked. + void UpdateTooltip(); + + // Recursively descends through all descendant views, + // registering/unregistering all views that want visible bounds in root + // view notification. + static void RegisterChildrenForVisibleBoundsNotification(RootView* root, + View* view); + static void UnregisterChildrenForVisibleBoundsNotification(RootView* root, + View* view); + + // Adds/removes view to the list of descendants that are notified any time + // this views location and possibly size are changed. + void AddDescendantToNotify(View* view); + void RemoveDescendantToNotify(View* view); + + // Initialize the previous/next focusable views of the specified view relative + // to the view at the specified index. + void InitFocusSiblings(View* view, int index); + + // Actual implementation of PrintFocusHierarchy. + void PrintViewHierarchyImp(int indent); + void PrintFocusHierarchyImp(int indent); + + // Registers/unregister this view's keyboard accelerators with the + // FocusManager. + void RegisterAccelerators(); + void UnregisterAccelerators(); + + // Returns the number of children that are actually attached floating views. + int GetFloatingViewCount() const; + + // Returns the id for this floating view. + int GetFloatingViewID(); + + // Returns whether this view is a floating view. + bool IsFloatingView(); + + // Sets in |path| the path in the view hierarchy from |start| to |end| (the + // path is the list of indexes in each view's children to get from |start| + // to |end|). + // Returns true if |start| and |view| are connected and the |path| has been + // retrieved succesfully, false otherwise. + static bool GetViewPath(View* start, View* end, std::vector<int>* path); + + // Returns the view at the end of the specified |path|, starting at the + // |start| view. + static View* GetViewForPath(View* start, const std::vector<int>& path); + + // This View's bounds in the parent coordinate system. + gfx::Rect bounds_; + + // This view's parent + View *parent_; + + // This view's children. + typedef std::vector<View*> ViewList; + ViewList child_views_; + + // List of floating children. A floating view is always referenced by + // child_views_ and will be deleted on destruction just like any other + // child view. + ViewList floating_views_; + + // Maps a floating view to its floating view id. + std::map<View*, int> floating_views_ids_; + + // Whether we want the focus to be restored. This is used to store/restore + // focus for floating views. + bool should_restore_focus_; + + // The View's LayoutManager defines the sizing heuristics applied to child + // Views. The default is absolute positioning according to bounds_. + scoped_ptr<LayoutManager> layout_manager_; + + // Visible state + bool is_visible_; + + // Background + scoped_ptr<Background> background_; + + // Border. + scoped_ptr<Border> border_; + + // Whether this view is owned by its parent. + bool is_parent_owned_; + + // See SetNotifyWhenVisibleBoundsInRootChanges. + bool notify_when_visible_bounds_in_root_changes_; + + // Whether or not RegisterViewForVisibleBoundsNotification on the RootView + // has been invoked. + bool registered_for_visible_bounds_notification_; + + // List of descendants wanting notification when their visible bounds change. + scoped_ptr<ViewList> descendants_to_notify_; + + // Next view to be focused when the Tab key is pressed. + View* next_focusable_view_; + + // Next view to be focused when the Shift-Tab key combination is pressed. + View* previous_focusable_view_; + + // The list of accelerators. + scoped_ptr<std::vector<Accelerator> > accelerators_; + + // The task used to restore automatically the focus to the last focused + // floating view. + RestoreFocusTask* restore_focus_view_task_; + + // The menu controller. + ContextMenuController* context_menu_controller_; + +#if defined(OS_WIN) + // The accessibility implementation for this View. + scoped_ptr<ViewAccessibilityWrapper> accessibility_; +#endif + + DragController* drag_controller_; + + // Indicates whether or not the view is going to be mirrored (that is, use a + // right-to-left UI layout) if the locale's language is a right-to-left + // language like Arabic or Hebrew. + bool ui_mirroring_is_enabled_for_rtl_languages_; + + // Indicates whether or not the ChromeCanvas object passed to View::Paint() + // is going to be flipped horizontally (using the appropriate transform) on + // right-to-left locales for this View. + bool flip_canvas_on_paint_for_rtl_ui_; + + DISALLOW_COPY_AND_ASSIGN(View); +}; + +} // namespace views + +#endif // VIEWS_VIEW_H_ diff --git a/views/view_constants.cc b/views/view_constants.cc new file mode 100644 index 0000000..0e2ed17 --- /dev/null +++ b/views/view_constants.cc @@ -0,0 +1,13 @@ +// Copyright (c) 2006-2008 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/view_constants.h" + +namespace views { + +const int kAutoscrollSize = 10; +const int kAutoscrollRowTimerMS = 200; +const int kDropBetweenPixels = 5; + +} // namespace views diff --git a/views/view_constants.h b/views/view_constants.h new file mode 100644 index 0000000..d36dbb4 --- /dev/null +++ b/views/view_constants.h @@ -0,0 +1,25 @@ +// Copyright (c) 2006-2008 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 VIEWS_VIEW_CONSTANTS_H_ +#define VIEWS_VIEW_CONSTANTS_H_ + +namespace views { + +// Size (width or height) within which the user can hold the mouse and the +// view should scroll. +extern const int kAutoscrollSize; + +// Time in milliseconds to autoscroll by a row. This is used during drag and +// drop. +extern const int kAutoscrollRowTimerMS; + +// Used to determine whether a drop is on an item or before/after it. If a drop +// occurs kDropBetweenPixels from the top/bottom it is considered before/after +// the item, otherwise it is on the item. +extern const int kDropBetweenPixels; + +} // namespace views + +#endif // VIEWS_VIEW_CONSTANTS_H_ diff --git a/views/view_gtk.cc b/views/view_gtk.cc new file mode 100644 index 0000000..61ca6ca --- /dev/null +++ b/views/view_gtk.cc @@ -0,0 +1,56 @@ +// Copyright (c) 2009 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/view.h" + +#include "base/logging.h" + +namespace views { + +FocusManager* View::GetFocusManager() { + NOTIMPLEMENTED(); + return NULL; +} + +void View::DoDrag(const MouseEvent& e, int press_x, int press_y) { + NOTIMPLEMENTED(); +} + +ViewAccessibilityWrapper* View::GetViewAccessibilityWrapper() { + NOTIMPLEMENTED(); + return NULL; +} + +bool View::HitTest(const gfx::Point& l) const { + if (l.x() >= 0 && l.x() < static_cast<int>(width()) && + l.y() >= 0 && l.y() < static_cast<int>(height())) { + if (HasHitTestMask()) { + // TODO(port): port the windows hit test code here. Once that's factored + // out, we can probably move View::HitTest back into views.cc. + NOTIMPLEMENTED(); + } + // No mask, but inside our bounds. + return true; + } + // Outside our bounds. + return false; +} + +void View::Focus() { + NOTIMPLEMENTED(); +} + +int View::GetHorizontalDragThreshold() { + static int threshold = -1; + NOTIMPLEMENTED(); + return threshold; +} + +int View::GetVerticalDragThreshold() { + static int threshold = -1; + NOTIMPLEMENTED(); + return threshold; +} + +} // namespace views diff --git a/views/view_unittest.cc b/views/view_unittest.cc new file mode 100644 index 0000000..d509940e --- /dev/null +++ b/views/view_unittest.cc @@ -0,0 +1,1009 @@ +// Copyright (c) 2006-2008 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 "app/gfx/chrome_canvas.h" +#include "app/gfx/path.h" +#include "base/clipboard.h" +#include "base/message_loop.h" +#include "chrome/browser/browser_process.h" +#include "chrome/common/notification_service.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "views/background.h" +#include "views/controls/button/checkbox.h" +#if defined(OS_WIN) +#include "views/controls/button/native_button_win.h" +#endif +#include "views/controls/scroll_view.h" +#include "views/controls/text_field.h" +#include "views/event.h" +#include "views/focus/view_storage.h" +#include "views/view.h" +#include "views/widget/root_view.h" +#include "views/widget/widget_win.h" +#include "views/window/dialog_delegate.h" +#include "views/window/window.h" + +using namespace views; + +namespace { + +class ViewTest : public testing::Test { + public: + ViewTest() { + OleInitialize(NULL); + } + + ~ViewTest() { + OleUninitialize(); + } + + private: + MessageLoopForUI message_loop_; +}; + +// Paints the RootView. +void PaintRootView(views::RootView* root, bool empty_paint) { + if (!empty_paint) { + root->PaintNow(); + } else { + // User isn't logged in, so that PaintNow will generate an empty rectangle. + // Invoke paint directly. + gfx::Rect paint_rect = root->GetScheduledPaintRect(); + ChromeCanvas canvas(paint_rect.width(), paint_rect.height(), true); + canvas.TranslateInt(-paint_rect.x(), -paint_rect.y()); + canvas.ClipRectInt(0, 0, paint_rect.width(), paint_rect.height()); + root->ProcessPaint(&canvas); + } +} + +/* +typedef CWinTraits<WS_VISIBLE|WS_CLIPCHILDREN|WS_CLIPSIBLINGS> CVTWTraits; + +// A trivial window implementation that tracks whether or not it has been +// painted. This is used by the painting test to determine if paint will result +// in an empty region. +class EmptyWindow : public CWindowImpl<EmptyWindow, + CWindow, + CVTWTraits> { + public: + DECLARE_FRAME_WND_CLASS(L"Chrome_ChromeViewsEmptyWindow", 0) + + BEGIN_MSG_MAP_EX(EmptyWindow) + MSG_WM_PAINT(OnPaint) + END_MSG_MAP() + + EmptyWindow::EmptyWindow(const CRect& bounds) : empty_paint_(false) { + Create(NULL, static_cast<RECT>(bounds)); + ShowWindow(SW_SHOW); + } + + EmptyWindow::~EmptyWindow() { + ShowWindow(SW_HIDE); + DestroyWindow(); + } + + void EmptyWindow::OnPaint(HDC dc) { + PAINTSTRUCT ps; + HDC paint_dc = BeginPaint(&ps); + if (!empty_paint_ && (ps.rcPaint.top - ps.rcPaint.bottom) == 0 && + (ps.rcPaint.right - ps.rcPaint.left) == 0) { + empty_paint_ = true; + } + EndPaint(&ps); + } + + bool empty_paint() { + return empty_paint_; + } + + private: + bool empty_paint_; + + DISALLOW_EVIL_CONSTRUCTORS(EmptyWindow); +}; +*/ + +//////////////////////////////////////////////////////////////////////////////// +// +// A view subclass for testing purpose +// +//////////////////////////////////////////////////////////////////////////////// +class TestView : public View { + public: + TestView() : View(){ + } + + virtual ~TestView() {} + + // Reset all test state + void Reset() { + did_change_bounds_ = false; + child_added_ = false; + child_removed_ = false; + last_mouse_event_type_ = 0; + location_.x = 0; + location_.y = 0; + last_clip_.setEmpty(); + } + + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + virtual bool OnMousePressed(const MouseEvent& event); + virtual bool OnMouseDragged(const MouseEvent& event); + virtual void OnMouseReleased(const MouseEvent& event, bool canceled); + virtual void Paint(ChromeCanvas* canvas); + + // DidChangeBounds test + bool did_change_bounds_; + gfx::Rect previous_bounds_; + gfx::Rect new_bounds_; + + // AddRemoveNotifications test + bool child_added_; + bool child_removed_; + View* parent_; + View* child_; + + // MouseEvent + int last_mouse_event_type_; + CPoint location_; + + // Painting + SkRect last_clip_; +}; + +//////////////////////////////////////////////////////////////////////////////// +// DidChangeBounds +//////////////////////////////////////////////////////////////////////////////// + +void TestView::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + did_change_bounds_ = true; + previous_bounds_ = previous; + new_bounds_ = current; +} + +TEST_F(ViewTest, DidChangeBounds) { + TestView* v = new TestView(); + + gfx::Rect prev_rect(0, 0, 200, 200); + gfx::Rect new_rect(100, 100, 250, 250); + + v->SetBounds(prev_rect); + v->Reset(); + + v->SetBounds(new_rect); + EXPECT_EQ(v->did_change_bounds_, true); + EXPECT_EQ(v->previous_bounds_, prev_rect); + EXPECT_EQ(v->new_bounds_, new_rect); + + EXPECT_EQ(v->bounds(), gfx::Rect(new_rect)); + delete v; +} + + +//////////////////////////////////////////////////////////////////////////////// +// AddRemoveNotifications +//////////////////////////////////////////////////////////////////////////////// + +void TestView::ViewHierarchyChanged(bool is_add, View *parent, View *child) { + if (is_add) { + child_added_ = true; + } else { + child_removed_ = true; + } + parent_ = parent; + child_ = child; +} + +} + +TEST_F(ViewTest, AddRemoveNotifications) { + TestView* v1 = new TestView(); + v1->SetBounds(0, 0, 300, 300); + + TestView* v2 = new TestView(); + v2->SetBounds(0, 0, 300, 300); + + TestView* v3 = new TestView(); + v3->SetBounds(0, 0, 300, 300); + + // Add a child. Make sure both v2 and v3 receive the right + // notification + v2->Reset(); + v3->Reset(); + v2->AddChildView(v3); + EXPECT_EQ(v2->child_added_, true); + EXPECT_EQ(v2->parent_, v2); + EXPECT_EQ(v2->child_, v3); + + EXPECT_EQ(v3->child_added_, true); + EXPECT_EQ(v3->parent_, v2); + EXPECT_EQ(v3->child_, v3); + + // Add v2 and transitively v3 to v1. Make sure that all views + // received the right notification + + v1->Reset(); + v2->Reset(); + v3->Reset(); + v1->AddChildView(v2); + + EXPECT_EQ(v1->child_added_, true); + EXPECT_EQ(v1->child_, v2); + EXPECT_EQ(v1->parent_, v1); + + EXPECT_EQ(v2->child_added_, true); + EXPECT_EQ(v2->child_, v2); + EXPECT_EQ(v2->parent_, v1); + + EXPECT_EQ(v3->child_added_, true); + EXPECT_EQ(v3->child_, v2); + EXPECT_EQ(v3->parent_, v1); + + // Remove v2. Make sure all views received the right notification + v1->Reset(); + v2->Reset(); + v3->Reset(); + v1->RemoveChildView(v2); + + EXPECT_EQ(v1->child_removed_, true); + EXPECT_EQ(v1->parent_, v1); + EXPECT_EQ(v1->child_, v2); + + EXPECT_EQ(v2->child_removed_, true); + EXPECT_EQ(v2->parent_, v1); + EXPECT_EQ(v2->child_, v2); + + EXPECT_EQ(v3->child_removed_, true); + EXPECT_EQ(v3->parent_, v1); + EXPECT_EQ(v3->child_, v3); + + // Clean-up + delete v1; + delete v2; // This also deletes v3 (child of v2). +} + +//////////////////////////////////////////////////////////////////////////////// +// MouseEvent +//////////////////////////////////////////////////////////////////////////////// + +bool TestView::OnMousePressed(const MouseEvent& event) { + last_mouse_event_type_ = event.GetType(); + location_.x = event.x(); + location_.y = event.y(); + return true; +} + +bool TestView::OnMouseDragged(const MouseEvent& event) { + last_mouse_event_type_ = event.GetType(); + location_.x = event.x(); + location_.y = event.y(); + return true; +} + +void TestView::OnMouseReleased(const MouseEvent& event, bool canceled) { + last_mouse_event_type_ = event.GetType(); + location_.x = event.x(); + location_.y = event.y(); +} + +TEST_F(ViewTest, MouseEvent) { + TestView* v1 = new TestView(); + v1->SetBounds(0, 0, 300, 300); + + TestView* v2 = new TestView(); + v2->SetBounds (100, 100, 100, 100); + + views::WidgetWin window; + window.set_delete_on_destroy(false); + window.set_window_style(WS_OVERLAPPEDWINDOW); + window.Init(NULL, gfx::Rect(50, 50, 650, 650), false); + RootView* root = window.GetRootView(); + + root->AddChildView(v1); + v1->AddChildView(v2); + + v1->Reset(); + v2->Reset(); + + MouseEvent pressed(Event::ET_MOUSE_PRESSED, + 110, + 120, + Event::EF_LEFT_BUTTON_DOWN); + root->OnMousePressed(pressed); + EXPECT_EQ(v2->last_mouse_event_type_, Event::ET_MOUSE_PRESSED); + EXPECT_EQ(v2->location_.x, 10); + EXPECT_EQ(v2->location_.y, 20); + // Make sure v1 did not receive the event + EXPECT_EQ(v1->last_mouse_event_type_, 0); + + // Drag event out of bounds. Should still go to v2 + v1->Reset(); + v2->Reset(); + MouseEvent dragged(Event::ET_MOUSE_DRAGGED, + 50, + 40, + Event::EF_LEFT_BUTTON_DOWN); + root->OnMouseDragged(dragged); + EXPECT_EQ(v2->last_mouse_event_type_, Event::ET_MOUSE_DRAGGED); + EXPECT_EQ(v2->location_.x, -50); + EXPECT_EQ(v2->location_.y, -60); + // Make sure v1 did not receive the event + EXPECT_EQ(v1->last_mouse_event_type_, 0); + + // Releasted event out of bounds. Should still go to v2 + v1->Reset(); + v2->Reset(); + MouseEvent released(Event::ET_MOUSE_RELEASED, 0, 0, 0); + root->OnMouseDragged(released); + EXPECT_EQ(v2->last_mouse_event_type_, Event::ET_MOUSE_RELEASED); + EXPECT_EQ(v2->location_.x, -100); + EXPECT_EQ(v2->location_.y, -100); + // Make sure v1 did not receive the event + EXPECT_EQ(v1->last_mouse_event_type_, 0); + + window.CloseNow(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Painting +//////////////////////////////////////////////////////////////////////////////// + +void TestView::Paint(ChromeCanvas* canvas) { + canvas->getClipBounds(&last_clip_); +} + +void CheckRect(const SkRect& check_rect, const SkRect& target_rect) { + EXPECT_EQ(target_rect.fLeft, check_rect.fLeft); + EXPECT_EQ(target_rect.fRight, check_rect.fRight); + EXPECT_EQ(target_rect.fTop, check_rect.fTop); + EXPECT_EQ(target_rect.fBottom, check_rect.fBottom); +} + +/* This test is disabled because it is flakey on some systems. +TEST_F(ViewTest, DISABLED_Painting) { + // Determine if InvalidateRect generates an empty paint rectangle. + EmptyWindow paint_window(CRect(50, 50, 650, 650)); + paint_window.RedrawWindow(CRect(0, 0, 600, 600), NULL, + RDW_UPDATENOW | RDW_INVALIDATE | RDW_ALLCHILDREN); + bool empty_paint = paint_window.empty_paint(); + + views::WidgetWin window; + window.set_delete_on_destroy(false); + window.set_window_style(WS_OVERLAPPEDWINDOW); + window.Init(NULL, gfx::Rect(50, 50, 650, 650), NULL); + RootView* root = window.GetRootView(); + + TestView* v1 = new TestView(); + v1->SetBounds(0, 0, 650, 650); + root->AddChildView(v1); + + TestView* v2 = new TestView(); + v2->SetBounds(10, 10, 80, 80); + v1->AddChildView(v2); + + TestView* v3 = new TestView(); + v3->SetBounds(10, 10, 60, 60); + v2->AddChildView(v3); + + TestView* v4 = new TestView(); + v4->SetBounds(10, 200, 100, 100); + v1->AddChildView(v4); + + // Make sure to paint current rects + PaintRootView(root, empty_paint); + + + v1->Reset(); + v2->Reset(); + v3->Reset(); + v4->Reset(); + v3->SchedulePaint(10, 10, 10, 10); + PaintRootView(root, empty_paint); + + SkRect tmp_rect; + + tmp_rect.set(SkIntToScalar(10), + SkIntToScalar(10), + SkIntToScalar(20), + SkIntToScalar(20)); + CheckRect(v3->last_clip_, tmp_rect); + + tmp_rect.set(SkIntToScalar(20), + SkIntToScalar(20), + SkIntToScalar(30), + SkIntToScalar(30)); + CheckRect(v2->last_clip_, tmp_rect); + + tmp_rect.set(SkIntToScalar(30), + SkIntToScalar(30), + SkIntToScalar(40), + SkIntToScalar(40)); + CheckRect(v1->last_clip_, tmp_rect); + + // Make sure v4 was not painted + tmp_rect.setEmpty(); + CheckRect(v4->last_clip_, tmp_rect); + + window.DestroyWindow(); +} +*/ + +TEST_F(ViewTest, RemoveNotification) { + views::ViewStorage* vs = views::ViewStorage::GetSharedInstance(); + views::WidgetWin* window = new views::WidgetWin; + views::RootView* root_view = window->GetRootView(); + + View* v1 = new View; + int s1 = vs->CreateStorageID(); + vs->StoreView(s1, v1); + root_view->AddChildView(v1); + View* v11 = new View; + int s11 = vs->CreateStorageID(); + vs->StoreView(s11, v11); + v1->AddChildView(v11); + View* v111 = new View; + int s111 = vs->CreateStorageID(); + vs->StoreView(s111, v111); + v11->AddChildView(v111); + View* v112 = new View; + int s112 = vs->CreateStorageID(); + vs->StoreView(s112, v112); + v11->AddChildView(v112); + View* v113 = new View; + int s113 = vs->CreateStorageID(); + vs->StoreView(s113, v113); + v11->AddChildView(v113); + View* v1131 = new View; + int s1131 = vs->CreateStorageID(); + vs->StoreView(s1131, v1131); + v113->AddChildView(v1131); + View* v12 = new View; + int s12 = vs->CreateStorageID(); + vs->StoreView(s12, v12); + v1->AddChildView(v12); + + View* v2 = new View; + int s2 = vs->CreateStorageID(); + vs->StoreView(s2, v2); + root_view->AddChildView(v2); + View* v21 = new View; + int s21 = vs->CreateStorageID(); + vs->StoreView(s21, v21); + v2->AddChildView(v21); + View* v211 = new View; + int s211 = vs->CreateStorageID(); + vs->StoreView(s211, v211); + v21->AddChildView(v211); + + size_t stored_views = vs->view_count(); + + // Try removing a leaf view. + v21->RemoveChildView(v211); + EXPECT_EQ(stored_views - 1, vs->view_count()); + EXPECT_EQ(NULL, vs->RetrieveView(s211)); + delete v211; // We won't use this one anymore. + + // Now try removing a view with a hierarchy of depth 1. + v11->RemoveChildView(v113); + EXPECT_EQ(stored_views - 3, vs->view_count()); + EXPECT_EQ(NULL, vs->RetrieveView(s113)); + EXPECT_EQ(NULL, vs->RetrieveView(s1131)); + delete v113; // We won't use this one anymore. + + // Now remove even more. + root_view->RemoveChildView(v1); + EXPECT_EQ(stored_views - 8, vs->view_count()); + EXPECT_EQ(NULL, vs->RetrieveView(s1)); + EXPECT_EQ(NULL, vs->RetrieveView(s11)); + EXPECT_EQ(NULL, vs->RetrieveView(s12)); + EXPECT_EQ(NULL, vs->RetrieveView(s111)); + EXPECT_EQ(NULL, vs->RetrieveView(s112)); + + // Put v1 back for more tests. + root_view->AddChildView(v1); + vs->StoreView(s1, v1); + + // Now delete the root view (deleting the window will trigger a delete of the + // RootView) and make sure we are notified that the views were removed. + delete window; + EXPECT_EQ(stored_views - 10, vs->view_count()); + EXPECT_EQ(NULL, vs->RetrieveView(s1)); + EXPECT_EQ(NULL, vs->RetrieveView(s12)); + EXPECT_EQ(NULL, vs->RetrieveView(s11)); + EXPECT_EQ(NULL, vs->RetrieveView(s12)); + EXPECT_EQ(NULL, vs->RetrieveView(s21)); + EXPECT_EQ(NULL, vs->RetrieveView(s111)); + EXPECT_EQ(NULL, vs->RetrieveView(s112)); +} + +namespace { +class HitTestView : public views::View { + public: + explicit HitTestView(bool has_hittest_mask) + : has_hittest_mask_(has_hittest_mask) { + } + virtual ~HitTestView() {} + + protected: + // Overridden from views::View: + virtual bool HasHitTestMask() const { + return has_hittest_mask_; + } + virtual void GetHitTestMask(gfx::Path* mask) const { + DCHECK(has_hittest_mask_); + DCHECK(mask); + + SkScalar w = SkIntToScalar(width()); + SkScalar h = SkIntToScalar(height()); + + // Create a triangular mask within the bounds of this View. + mask->moveTo(w / 2, 0); + mask->lineTo(w, h); + mask->lineTo(0, h); + mask->close(); + } + + private: + bool has_hittest_mask_; + + DISALLOW_COPY_AND_ASSIGN(HitTestView); +}; + +gfx::Point ConvertPointToView(views::View* view, const gfx::Point& p) { + gfx::Point tmp(p); + views::View::ConvertPointToView(view->GetRootView(), view, &tmp); + return tmp; +} +} + +TEST_F(ViewTest, HitTestMasks) { + views::WidgetWin window; + views::RootView* root_view = window.GetRootView(); + root_view->SetBounds(0, 0, 500, 500); + + gfx::Rect v1_bounds = gfx::Rect(0, 0, 100, 100); + HitTestView* v1 = new HitTestView(false); + v1->SetBounds(v1_bounds); + root_view->AddChildView(v1); + + gfx::Rect v2_bounds = gfx::Rect(105, 0, 100, 100); + HitTestView* v2 = new HitTestView(true); + v2->SetBounds(v2_bounds); + root_view->AddChildView(v2); + + gfx::Point v1_centerpoint = v1_bounds.CenterPoint(); + gfx::Point v2_centerpoint = v2_bounds.CenterPoint(); + gfx::Point v1_origin = v1_bounds.origin(); + gfx::Point v2_origin = v2_bounds.origin(); + + // Test HitTest + EXPECT_EQ(true, v1->HitTest(ConvertPointToView(v1, v1_centerpoint))); + EXPECT_EQ(true, v2->HitTest(ConvertPointToView(v2, v2_centerpoint))); + + EXPECT_EQ(true, v1->HitTest(ConvertPointToView(v1, v1_origin))); + EXPECT_EQ(false, v2->HitTest(ConvertPointToView(v2, v2_origin))); + + // Test GetViewForPoint + EXPECT_EQ(v1, root_view->GetViewForPoint(v1_centerpoint)); + EXPECT_EQ(v2, root_view->GetViewForPoint(v2_centerpoint)); + EXPECT_EQ(v1, root_view->GetViewForPoint(v1_origin)); + EXPECT_EQ(root_view, root_view->GetViewForPoint(v2_origin)); +} + +#if defined(OS_WIN) +// Tests that the TextField view respond appropiately to cut/copy/paste. +TEST_F(ViewTest, TextFieldCutCopyPaste) { + const std::wstring kNormalText = L"Normal"; + const std::wstring kReadOnlyText = L"Read only"; + const std::wstring kPasswordText = L"Password! ** Secret stuff **"; + + Clipboard* clipboard = g_browser_process->clipboard(); + + WidgetWin* window = new WidgetWin; + window->Init(NULL, gfx::Rect(0, 0, 100, 100), true); + RootView* root_view = window->GetRootView(); + + TextField* normal = new TextField(); + TextField* read_only = new TextField(); + read_only->SetReadOnly(true); + TextField* password = new TextField(TextField::STYLE_PASSWORD); + + root_view->AddChildView(normal); + root_view->AddChildView(read_only); + root_view->AddChildView(password); + + normal->SetText(kNormalText); + read_only->SetText(kReadOnlyText); + password->SetText(kPasswordText); + + // + // Test cut. + // + ASSERT_TRUE(normal->GetNativeComponent()); + normal->SelectAll(); + ::SendMessage(normal->GetNativeComponent(), WM_CUT, 0, 0); + + string16 result; + clipboard->ReadText(&result); + EXPECT_EQ(kNormalText, result); + normal->SetText(kNormalText); // Let's revert to the original content. + + ASSERT_TRUE(read_only->GetNativeComponent()); + read_only->SelectAll(); + ::SendMessage(read_only->GetNativeComponent(), WM_CUT, 0, 0); + result.clear(); + clipboard->ReadText(&result); + // Cut should have failed, so the clipboard content should not have changed. + EXPECT_EQ(kNormalText, result); + + ASSERT_TRUE(password->GetNativeComponent()); + password->SelectAll(); + ::SendMessage(password->GetNativeComponent(), WM_CUT, 0, 0); + result.clear(); + clipboard->ReadText(&result); + // Cut should have failed, so the clipboard content should not have changed. + EXPECT_EQ(kNormalText, result); + + // + // Test copy. + // + + // Let's start with read_only as the clipboard already contains the content + // of normal. + read_only->SelectAll(); + ::SendMessage(read_only->GetNativeComponent(), WM_COPY, 0, 0); + result.clear(); + clipboard->ReadText(&result); + EXPECT_EQ(kReadOnlyText, result); + + normal->SelectAll(); + ::SendMessage(normal->GetNativeComponent(), WM_COPY, 0, 0); + result.clear(); + clipboard->ReadText(&result); + EXPECT_EQ(kNormalText, result); + + password->SelectAll(); + ::SendMessage(password->GetNativeComponent(), WM_COPY, 0, 0); + result.clear(); + clipboard->ReadText(&result); + // We don't let you copy from a password field, clipboard should not have + // changed. + EXPECT_EQ(kNormalText, result); + + // + // Test Paste. + // + // Note that we use GetWindowText instead of TextField::GetText below as the + // text in the TextField class is synced to the text of the HWND on + // WM_KEYDOWN messages that we are not simulating here. + + // Attempting to copy kNormalText in a read-only text-field should fail. + read_only->SelectAll(); + ::SendMessage(read_only->GetNativeComponent(), WM_KEYDOWN, 0, 0); + wchar_t buffer[1024] = { 0 }; + ::GetWindowText(read_only->GetNativeComponent(), buffer, 1024); + EXPECT_EQ(kReadOnlyText, std::wstring(buffer)); + + password->SelectAll(); + ::SendMessage(password->GetNativeComponent(), WM_PASTE, 0, 0); + ::GetWindowText(password->GetNativeComponent(), buffer, 1024); + EXPECT_EQ(kNormalText, std::wstring(buffer)); + + // Copy from read_only so the string we are pasting is not the same as the + // current one. + read_only->SelectAll(); + ::SendMessage(read_only->GetNativeComponent(), WM_COPY, 0, 0); + normal->SelectAll(); + ::SendMessage(normal->GetNativeComponent(), WM_PASTE, 0, 0); + ::GetWindowText(normal->GetNativeComponent(), buffer, 1024); + EXPECT_EQ(kReadOnlyText, std::wstring(buffer)); +} +#endif + +#if defined(OS_WIN) +//////////////////////////////////////////////////////////////////////////////// +// Mouse-wheel message rerouting +//////////////////////////////////////////////////////////////////////////////// +class ButtonTest : public NativeButton { + public: + ButtonTest(ButtonListener* listener, const std::wstring& label) + : NativeButton(listener, label) { + } + + HWND GetHWND() { + return static_cast<NativeButtonWin*>(native_wrapper_)->GetHWND(); + } +}; + +class CheckboxTest : public Checkbox { + public: + explicit CheckboxTest(const std::wstring& label) : Checkbox(label) { + } + + HWND GetHWND() { + return static_cast<NativeCheckboxWin*>(native_wrapper_)->GetHWND(); + } +}; + +class ScrollableTestView : public View { + public: + ScrollableTestView() { } + + virtual gfx::Size GetPreferredSize() { + return gfx::Size(100, 10000); + } + + virtual void Layout() { + SizeToPreferredSize(); + } +}; + +class TestViewWithControls : public View { + public: + TestViewWithControls() { + button_ = new ButtonTest(NULL, L"Button"); + checkbox_ = new CheckboxTest(L"My checkbox"); + text_field_ = new TextField(); + AddChildView(button_); + AddChildView(checkbox_); + AddChildView(text_field_); + } + + ButtonTest* button_; + CheckboxTest* checkbox_; + TextField* text_field_; +}; + +class SimpleWindowDelegate : public WindowDelegate { + public: + SimpleWindowDelegate(View* contents) : contents_(contents) { } + + virtual void DeleteDelegate() { delete this; } + + virtual View* GetContentsView() { return contents_; } + + private: + View* contents_; +}; + +// Tests that the mouse-wheel messages are correctly rerouted to the window +// under the mouse. +// TODO(jcampan): http://crbug.com/10572 Disabled as it fails on the Vista build +// bot. +TEST_F(ViewTest, DISABLED_RerouteMouseWheelTest) { + TestViewWithControls* view_with_controls = new TestViewWithControls(); + views::Window* window1 = + views::Window::CreateChromeWindow( + NULL, gfx::Rect(0, 0, 100, 100), + new SimpleWindowDelegate(view_with_controls)); + window1->Show(); + ScrollView* scroll_view = new ScrollView(); + scroll_view->SetContents(new ScrollableTestView()); + views::Window* window2 = + views::Window::CreateChromeWindow(NULL, gfx::Rect(200, 200, 100, 100), + new SimpleWindowDelegate(scroll_view)); + window2->Show(); + EXPECT_EQ(0, scroll_view->GetVisibleRect().y()); + + // Make the window1 active, as this is what it would be in real-world. + window1->Activate(); + + // Let's send a mouse-wheel message to the different controls and check that + // it is rerouted to the window under the mouse (effectively scrolling the + // scroll-view). + + // First to the Window's HWND. + ::SendMessage(view_with_controls->GetWidget()->GetNativeView(), + WM_MOUSEWHEEL, MAKEWPARAM(0, -20), MAKELPARAM(250, 250)); + EXPECT_EQ(20, scroll_view->GetVisibleRect().y()); + + // Then the button. + ::SendMessage(view_with_controls->button_->GetHWND(), + WM_MOUSEWHEEL, MAKEWPARAM(0, -20), MAKELPARAM(250, 250)); + EXPECT_EQ(40, scroll_view->GetVisibleRect().y()); + + // Then the check-box. + ::SendMessage(view_with_controls->checkbox_->GetHWND(), + WM_MOUSEWHEEL, MAKEWPARAM(0, -20), MAKELPARAM(250, 250)); + EXPECT_EQ(60, scroll_view->GetVisibleRect().y()); + + // Then the text-field. + ::SendMessage(view_with_controls->text_field_->GetNativeComponent(), + WM_MOUSEWHEEL, MAKEWPARAM(0, -20), MAKELPARAM(250, 250)); + EXPECT_EQ(80, scroll_view->GetVisibleRect().y()); + + // Ensure we don't scroll when the mouse is not over that window. + ::SendMessage(view_with_controls->text_field_->GetNativeComponent(), + WM_MOUSEWHEEL, MAKEWPARAM(0, -20), MAKELPARAM(50, 50)); + EXPECT_EQ(80, scroll_view->GetVisibleRect().y()); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Dialogs' default button +//////////////////////////////////////////////////////////////////////////////// + +class TestDialogView : public views::View, + public views::DialogDelegate, + public views::ButtonListener { + public: + TestDialogView() + : last_pressed_button_(NULL), + button1_(NULL), + button2_(NULL), + checkbox_(NULL), + canceled_(false), + oked_(false) { + } + + // views::DialogDelegate implementation: + virtual int GetDefaultDialogButton() const { + return MessageBoxFlags::DIALOGBUTTON_OK; + } + + virtual View* GetContentsView() { + views::View* container = new views::View(); + button1_ = new views::NativeButton(this, L"Button1"); + button2_ = new views::NativeButton(this, L"Button2"); + checkbox_ = new views::Checkbox(L"My checkbox"); + container->AddChildView(button1_); + container->AddChildView(button2_); + container->AddChildView(checkbox_); + return container; + } + + // Prevent the dialog from really closing (so we can click the OK/Cancel + // buttons to our heart's content). + virtual bool Cancel() { + canceled_ = true; + return false; + } + virtual bool Accept() { + oked_ = true; + return false; + } + + // views::ButtonListener implementation. + virtual void ButtonPressed(Button* sender) { + last_pressed_button_ = sender; + } + + void ResetStates() { + oked_ = false; + canceled_ = false; + last_pressed_button_ = NULL; + } + + views::NativeButton* button1_; + views::NativeButton* button2_; + views::NativeButton* checkbox_; + views::Button* last_pressed_button_; + + bool canceled_; + bool oked_; +}; + + +class DefaultButtonTest : public ViewTest { + public: + enum ButtonID { + OK, + CANCEL, + BUTTON1, + BUTTON2 + }; + + DefaultButtonTest() + : native_window_(NULL), + focus_manager_(NULL), + client_view_(NULL), + ok_button_(NULL), + cancel_button_(NULL) { + } + + virtual void SetUp() { + dialog_view_ = new TestDialogView(); + views::Window* window = + views::Window::CreateChromeWindow(NULL, gfx::Rect(0, 0, 100, 100), + dialog_view_); + window->Show(); + native_window_ = window->GetNativeWindow(); + focus_manager_ = FocusManager::GetFocusManager(native_window_); + client_view_ = + static_cast<views::DialogClientView*>(window->GetClientView()); + ok_button_ = client_view_->ok_button(); + cancel_button_ = client_view_->cancel_button(); + } + + void SimularePressingEnterAndCheckDefaultButton(ButtonID button_id) { +#if defined(OS_WIN) + focus_manager_->OnKeyDown(native_window_, WM_KEYDOWN, VK_RETURN, 0); +#else + // TODO(platform) + return; +#endif + switch (button_id) { + case OK: + EXPECT_TRUE(dialog_view_->oked_); + EXPECT_FALSE(dialog_view_->canceled_); + EXPECT_FALSE(dialog_view_->last_pressed_button_); + break; + case CANCEL: + EXPECT_FALSE(dialog_view_->oked_); + EXPECT_TRUE(dialog_view_->canceled_); + EXPECT_FALSE(dialog_view_->last_pressed_button_); + break; + case BUTTON1: + EXPECT_FALSE(dialog_view_->oked_); + EXPECT_FALSE(dialog_view_->canceled_); + EXPECT_TRUE(dialog_view_->last_pressed_button_ == + dialog_view_->button1_); + break; + case BUTTON2: + EXPECT_FALSE(dialog_view_->oked_); + EXPECT_FALSE(dialog_view_->canceled_); + EXPECT_TRUE(dialog_view_->last_pressed_button_ == + dialog_view_->button2_); + break; + } + dialog_view_->ResetStates(); + } + + gfx::NativeWindow native_window_; + views::FocusManager* focus_manager_; + TestDialogView* dialog_view_; + DialogClientView* client_view_; + views::NativeButton* ok_button_; + views::NativeButton* cancel_button_; +}; + +TEST_F(DefaultButtonTest, DialogDefaultButtonTest) { + // Window has just been shown, we expect the default button specified in the + // DialogDelegate. + EXPECT_TRUE(ok_button_->is_default()); + + // Simulate pressing enter, that should trigger the OK button. + SimularePressingEnterAndCheckDefaultButton(OK); + + // Simulate focusing another button, it should become the default button. + client_view_->FocusWillChange(ok_button_, dialog_view_->button1_); + EXPECT_FALSE(ok_button_->is_default()); + EXPECT_TRUE(dialog_view_->button1_->is_default()); + // Simulate pressing enter, that should trigger button1. + SimularePressingEnterAndCheckDefaultButton(BUTTON1); + + // Now select something that is not a button, the OK should become the default + // button again. + client_view_->FocusWillChange(dialog_view_->button1_, + dialog_view_->checkbox_); + EXPECT_TRUE(ok_button_->is_default()); + EXPECT_FALSE(dialog_view_->button1_->is_default()); + SimularePressingEnterAndCheckDefaultButton(OK); + + // Select yet another button. + client_view_->FocusWillChange(dialog_view_->checkbox_, + dialog_view_->button2_); + EXPECT_FALSE(ok_button_->is_default()); + EXPECT_FALSE(dialog_view_->button1_->is_default()); + EXPECT_TRUE(dialog_view_->button2_->is_default()); + SimularePressingEnterAndCheckDefaultButton(BUTTON2); + + // Focus nothing. + client_view_->FocusWillChange(dialog_view_->button2_, NULL); + EXPECT_TRUE(ok_button_->is_default()); + EXPECT_FALSE(dialog_view_->button1_->is_default()); + EXPECT_FALSE(dialog_view_->button2_->is_default()); + SimularePressingEnterAndCheckDefaultButton(OK); + + // Focus the cancel button. + client_view_->FocusWillChange(NULL, cancel_button_); + EXPECT_FALSE(ok_button_->is_default()); + EXPECT_TRUE(cancel_button_->is_default()); + EXPECT_FALSE(dialog_view_->button1_->is_default()); + EXPECT_FALSE(dialog_view_->button2_->is_default()); + SimularePressingEnterAndCheckDefaultButton(CANCEL); +} diff --git a/views/view_win.cc b/views/view_win.cc new file mode 100644 index 0000000..6dfe183 --- /dev/null +++ b/views/view_win.cc @@ -0,0 +1,95 @@ +// Copyright (c) 2009 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/view.h" + +#include "app/drag_drop_types.h" +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/path.h" +#include "app/os_exchange_data.h" +#include "base/scoped_handle.h" +#include "base/string_util.h" +#include "views/accessibility/view_accessibility_wrapper.h" +#include "views/border.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" + +namespace views { + +FocusManager* View::GetFocusManager() { + Widget* widget = GetWidget(); + if (!widget) + return NULL; + + HWND hwnd = widget->GetNativeView(); + if (!hwnd) + return NULL; + + return FocusManager::GetFocusManager(hwnd); +} + +void View::DoDrag(const MouseEvent& e, int press_x, int press_y) { + int drag_operations = GetDragOperations(press_x, press_y); + if (drag_operations == DragDropTypes::DRAG_NONE) + return; + + scoped_refptr<OSExchangeData> data = new OSExchangeData; + WriteDragData(press_x, press_y, data.get()); + + // Message the RootView to do the drag and drop. That way if we're removed + // the RootView can detect it and avoid calling us back. + RootView* root_view = GetRootView(); + root_view->StartDragForViewFromMouseEvent(this, data, drag_operations); +} + +ViewAccessibilityWrapper* View::GetViewAccessibilityWrapper() { + if (accessibility_.get() == NULL) { + accessibility_.reset(new ViewAccessibilityWrapper(this)); + } + return accessibility_.get(); +} + +bool View::HitTest(const gfx::Point& l) const { + if (l.x() >= 0 && l.x() < static_cast<int>(width()) && + l.y() >= 0 && l.y() < static_cast<int>(height())) { + if (HasHitTestMask()) { + gfx::Path mask; + GetHitTestMask(&mask); + ScopedHRGN rgn(mask.CreateHRGN()); + return !!PtInRegion(rgn, l.x(), l.y()); + } + // No mask, but inside our bounds. + return true; + } + // Outside our bounds. + return false; +} + +HCURSOR View::GetCursorForPoint(Event::EventType event_type, int x, int y) { + return NULL; +} + +void View::Focus() { + // Set the native focus to the root view window so it receives the keyboard + // messages. + FocusManager* focus_manager = GetFocusManager(); + if (focus_manager) + focus_manager->FocusHWND(GetRootView()->GetWidget()->GetNativeView()); +} + +int View::GetHorizontalDragThreshold() { + static int threshold = -1; + if (threshold == -1) + threshold = GetSystemMetrics(SM_CXDRAG) / 2; + return threshold; +} + +int View::GetVerticalDragThreshold() { + static int threshold = -1; + if (threshold == -1) + threshold = GetSystemMetrics(SM_CYDRAG) / 2; + return threshold; +} + +} // namespace views diff --git a/views/views.vcproj b/views/views.vcproj new file mode 100644 index 0000000..c10e68d --- /dev/null +++ b/views/views.vcproj @@ -0,0 +1,869 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<VisualStudioProject + ProjectType="Visual C++" + Version="8.00" + Name="views" + ProjectGUID="{6F9258E5-294F-47B2-919D-17FFE7A8B751}" + RootNamespace="Views" + Keyword="Win32Proj" + > + <Platforms> + <Platform + Name="Win32" + /> + </Platforms> + <ToolFiles> + </ToolFiles> + <Configurations> + <Configuration + Name="Debug|Win32" + ConfigurationType="4" + InheritedPropertySheets=".\views.vsprops;$(SolutionDir)..\build\debug.vsprops;$(SolutionDir)\tools\build\win\precompiled_wtl.vsprops;..\third_party\icu38\build\using_icu.vsprops" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLibrarianTool" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + <Configuration + Name="Release|Win32" + ConfigurationType="4" + InheritedPropertySheets=".\views.vsprops;$(SolutionDir)..\build\release.vsprops;..\third_party\icu38\build\using_icu.vsprops" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLibrarianTool" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + </Configurations> + <References> + </References> + <Files> + <Filter + Name="accessibility" + > + <File + RelativePath=".\accessibility\view_accessibility.cc" + > + </File> + <File + RelativePath=".\accessibility\view_accessibility.h" + > + </File> + <File + RelativePath=".\accessibility\view_accessibility_wrapper.cc" + > + </File> + <File + RelativePath=".\accessibility\view_accessibility_wrapper.h" + > + </File> + </Filter> + <Filter + Name="widget" + > + <File + RelativePath=".\widget\accelerator_handler.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\widget\accelerator_handler.h" + > + </File> + <File + RelativePath=".\widget\aero_tooltip_manager.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\widget\aero_tooltip_manager.h" + > + </File> + <File + RelativePath=".\widget\root_view.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\widget\root_view.h" + > + </File> + <File + RelativePath=".\widget\root_view_drop_target.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\widget\root_view_drop_target.h" + > + </File> + <File + RelativePath=".\widget\root_view_win.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\widget\tooltip_manager.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\widget\tooltip_manager.h" + > + </File> + <File + RelativePath=".\widget\widget.h" + > + </File> + <File + RelativePath=".\widget\widget_win.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + ObjectFile="$(IntDir)\$(InputName)1.obj" + XMLDocumentationFileName="$(IntDir)\$(InputName)1.xdc" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\widget\widget_win.h" + > + </File> + </Filter> + <Filter + Name="window" + > + <File + RelativePath=".\window\client_view.cc" + > + </File> + <File + RelativePath=".\window\client_view.h" + > + </File> + <File + RelativePath=".\window\custom_frame_view.cc" + > + </File> + <File + RelativePath=".\window\custom_frame_view.h" + > + </File> + <File + RelativePath=".\window\dialog_client_view.cc" + > + </File> + <File + RelativePath=".\window\dialog_client_view.h" + > + </File> + <File + RelativePath=".\window\dialog_delegate.cc" + > + </File> + <File + RelativePath=".\window\dialog_delegate.h" + > + </File> + <File + RelativePath=".\window\native_frame_view.cc" + > + </File> + <File + RelativePath=".\window\native_frame_view.h" + > + </File> + <File + RelativePath=".\window\non_client_view.cc" + > + </File> + <File + RelativePath=".\window\non_client_view.h" + > + </File> + <File + RelativePath=".\window\window.h" + > + </File> + <File + RelativePath=".\window\window_delegate.cc" + > + </File> + <File + RelativePath=".\window\window_delegate.h" + > + </File> + <File + RelativePath=".\window\window_resources.h" + > + </File> + <File + RelativePath=".\window\window_win.cc" + > + </File> + <File + RelativePath=".\window\window_win.h" + > + </File> + </Filter> + <Filter + Name="focus" + > + <File + RelativePath=".\focus\external_focus_tracker.cc" + > + </File> + <File + RelativePath=".\focus\external_focus_tracker.h" + > + </File> + <File + RelativePath=".\focus\focus_manager.cc" + > + </File> + <File + RelativePath=".\focus\focus_manager.h" + > + </File> + <File + RelativePath=".\focus\focus_util_win.cc" + > + </File> + <File + RelativePath=".\focus\focus_util_win.h" + > + </File> + <File + RelativePath=".\focus\view_storage.cc" + > + </File> + <File + RelativePath=".\focus\view_storage.h" + > + </File> + </Filter> + <Filter + Name="controls" + > + <File + RelativePath=".\controls\combo_box.cc" + > + </File> + <File + RelativePath=".\controls\combo_box.h" + > + </File> + <File + RelativePath=".\controls\hwnd_view.cc" + > + </File> + <File + RelativePath=".\controls\hwnd_view.h" + > + </File> + <File + RelativePath=".\controls\image_view.cc" + > + </File> + <File + RelativePath=".\controls\image_view.h" + > + </File> + <File + RelativePath=".\controls\label.cc" + > + </File> + <File + RelativePath=".\controls\label.h" + > + </File> + <File + RelativePath=".\controls\link.cc" + > + </File> + <File + RelativePath=".\controls\link.h" + > + </File> + <File + RelativePath=".\controls\message_box_view.cc" + > + </File> + <File + RelativePath=".\controls\message_box_view.h" + > + </File> + <File + RelativePath=".\controls\native_control.cc" + > + </File> + <File + RelativePath=".\controls\native_control.h" + > + </File> + <File + RelativePath=".\controls\native_control_win.cc" + > + </File> + <File + RelativePath=".\controls\native_control_win.h" + > + </File> + <File + RelativePath=".\controls\native_view_host.cc" + > + </File> + <File + RelativePath=".\controls\native_view_host.h" + > + </File> + <File + RelativePath=".\controls\scroll_view.cc" + > + </File> + <File + RelativePath=".\controls\scroll_view.h" + > + </File> + <File + RelativePath=".\controls\separator.cc" + > + </File> + <File + RelativePath=".\controls\separator.h" + > + </File> + <File + RelativePath=".\controls\single_split_view.cc" + > + </File> + <File + RelativePath=".\controls\single_split_view.h" + > + </File> + <File + RelativePath=".\controls\tabbed_pane.cc" + > + </File> + <File + RelativePath=".\controls\tabbed_pane.h" + > + </File> + <File + RelativePath=".\controls\text_field.cc" + > + </File> + <File + RelativePath=".\controls\text_field.h" + > + </File> + <File + RelativePath=".\controls\throbber.cc" + > + </File> + <File + RelativePath=".\controls\throbber.h" + > + </File> + <Filter + Name="button" + > + <File + RelativePath=".\controls\button\button.cc" + > + </File> + <File + RelativePath=".\controls\button\button.h" + > + </File> + <File + RelativePath=".\controls\button\button_dropdown.cc" + > + </File> + <File + RelativePath=".\controls\button\button_dropdown.h" + > + </File> + <File + RelativePath=".\controls\button\checkbox.cc" + > + </File> + <File + RelativePath=".\controls\button\checkbox.h" + > + </File> + <File + RelativePath=".\controls\button\custom_button.cc" + > + </File> + <File + RelativePath=".\controls\button\custom_button.h" + > + </File> + <File + RelativePath=".\controls\button\image_button.cc" + > + </File> + <File + RelativePath=".\controls\button\image_button.h" + > + </File> + <File + RelativePath=".\controls\button\menu_button.cc" + > + </File> + <File + RelativePath=".\controls\button\menu_button.h" + > + </File> + <File + RelativePath=".\controls\button\native_button.cc" + > + </File> + <File + RelativePath=".\controls\button\native_button.h" + > + </File> + <File + RelativePath=".\controls\button\native_button_win.cc" + > + </File> + <File + RelativePath=".\controls\button\native_button_win.h" + > + </File> + <File + RelativePath=".\controls\button\native_button_wrapper.h" + > + </File> + <File + RelativePath=".\controls\button\radio_button.cc" + > + </File> + <File + RelativePath=".\controls\button\radio_button.h" + > + </File> + <File + RelativePath=".\controls\button\text_button.cc" + > + </File> + <File + RelativePath=".\controls\button\text_button.h" + > + </File> + </Filter> + <Filter + Name="scrollbar" + > + <File + RelativePath=".\controls\scrollbar\bitmap_scroll_bar.cc" + > + </File> + <File + RelativePath=".\controls\scrollbar\bitmap_scroll_bar.h" + > + </File> + <File + RelativePath=".\controls\scrollbar\native_scroll_bar.cc" + > + </File> + <File + RelativePath=".\controls\scrollbar\native_scroll_bar.h" + > + </File> + <File + RelativePath=".\controls\scrollbar\scroll_bar.cc" + > + </File> + <File + RelativePath=".\controls\scrollbar\scroll_bar.h" + > + </File> + </Filter> + <Filter + Name="tree" + > + <File + RelativePath=".\controls\tree\tree_model.h" + > + </File> + <File + RelativePath=".\controls\tree\tree_node_iterator.h" + > + </File> + <File + RelativePath=".\controls\tree\tree_node_model.h" + > + </File> + <File + RelativePath=".\controls\tree\tree_view.cc" + > + </File> + <File + RelativePath=".\controls\tree\tree_view.h" + > + </File> + </Filter> + <Filter + Name="table" + > + <File + RelativePath=".\controls\table\group_table_view.cc" + > + </File> + <File + RelativePath=".\controls\table\group_table_view.h" + > + </File> + <File + RelativePath=".\controls\table\table_view.cc" + > + </File> + <File + RelativePath=".\controls\table\table_view.h" + > + </File> + </Filter> + <Filter + Name="menu" + > + <File + RelativePath=".\controls\menu\chrome_menu.cc" + > + </File> + <File + RelativePath=".\controls\menu\chrome_menu.h" + > + </File> + <File + RelativePath=".\controls\menu\controller.h" + > + </File> + <File + RelativePath=".\controls\menu\menu.cc" + > + </File> + <File + RelativePath=".\controls\menu\menu.h" + > + </File> + <File + RelativePath=".\controls\menu\view_menu_delegate.h" + > + </File> + </Filter> + </Filter> + <File + RelativePath=".\accelerator.cc" + > + </File> + <File + RelativePath=".\accelerator.h" + > + </File> + <File + RelativePath=".\background.cc" + > + </File> + <File + RelativePath=".\background.h" + > + </File> + <File + RelativePath=".\border.cc" + > + </File> + <File + RelativePath=".\border.h" + > + </File> + <File + RelativePath=".\event.cc" + > + </File> + <File + RelativePath=".\event.h" + > + </File> + <File + RelativePath=".\event_win.cc" + > + </File> + <File + RelativePath=".\fill_layout.cc" + > + </File> + <File + RelativePath=".\fill_layout.h" + > + </File> + <File + RelativePath=".\grid_layout.cc" + > + </File> + <File + RelativePath=".\grid_layout.h" + > + </File> + <File + RelativePath=".\layout_manager.cc" + > + </File> + <File + RelativePath=".\layout_manager.h" + > + </File> + <File + RelativePath=".\painter.cc" + > + </File> + <File + RelativePath=".\painter.h" + > + </File> + <File + RelativePath="$(SolutionDir)\tools\build\win\precompiled_wtl.cc" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + UsePrecompiledHeader="1" + /> + </FileConfiguration> + </File> + <File + RelativePath="$(SolutionDir)\tools\build\win\precompiled_wtl.h" + > + </File> + <File + RelativePath=".\repeat_controller.cc" + > + </File> + <File + RelativePath=".\repeat_controller.h" + > + </File> + <File + RelativePath=".\view.cc" + > + </File> + <File + RelativePath=".\view.h" + > + </File> + <File + RelativePath=".\view_constants.cc" + > + </File> + <File + RelativePath=".\view_constants.h" + > + </File> + <File + RelativePath=".\view_win.cc" + > + </File> + </Files> + <Globals> + </Globals> +</VisualStudioProject> diff --git a/views/views.vsprops b/views/views.vsprops new file mode 100644 index 0000000..72a6e0d --- /dev/null +++ b/views/views.vsprops @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<VisualStudioPropertySheet + ProjectType="Visual C++" + Version="8.00" + Name="views" + InheritedPropertySheets="$(SolutionDir)..\build\common.vsprops;$(SolutionDir)third_party\wtl\using_wtl.vsprops;$(SolutionDir)..\skia\using_skia.vsprops;$(SolutionDir)..\tools\grit\build\using_generated_resources.vsprops" + > + <Tool + Name="VCCLCompilerTool" + AdditionalIncludeDirectories="..\..\;"$(IntDir)\..\generated_resources\"" + /> +</VisualStudioPropertySheet> diff --git a/views/widget/accelerator_handler.cc b/views/widget/accelerator_handler.cc new file mode 100644 index 0000000..386357f --- /dev/null +++ b/views/widget/accelerator_handler.cc @@ -0,0 +1,46 @@ +// Copyright (c) 2006-2008 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/widget/accelerator_handler.h" + +#include "views/focus/focus_manager.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() { +} + +bool AcceleratorHandler::Dispatch(const MSG& msg) { + bool process_message = true; + + if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST) { + FocusManager* focus_manager = FocusManager::GetFocusManager(msg.hwnd); + if (focus_manager) { + // FocusManager::OnKeyDown and OnKeyUp return false if this message has + // been consumed and should not be propagated further. + switch (msg.message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + process_message = focus_manager->OnKeyDown(msg.hwnd, msg.message, + msg.wParam, msg.lParam); + break; + + case WM_KEYUP: + case WM_SYSKEYUP: + process_message = focus_manager->OnKeyUp(msg.hwnd, msg.message, + msg.wParam, msg.lParam); + break; + } + } + } + + if (process_message) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return true; +} + +} // namespace views diff --git a/views/widget/accelerator_handler.h b/views/widget/accelerator_handler.h new file mode 100644 index 0000000..5ee896c --- /dev/null +++ b/views/widget/accelerator_handler.h @@ -0,0 +1,30 @@ +// Copyright (c) 2006-2008 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 VIEWS_WIDGET_ACCELERATOR_HANDLER_H_ +#define VIEWS_WIDGET_ACCELERATOR_HANDLER_H_ + +#include "base/message_loop.h" + +namespace views { + +// This class delegates WM_KEYDOWN and WM_SYSKEYDOWN messages to +// the associated FocusManager class for the window that is receiving +// these messages for accelerator processing. The BrowserProcess object +// holds a singleton instance of this class which can be used by other +// custom message loop dispatcher objects to implement default accelerator +// handling. +class AcceleratorHandler : public MessageLoopForUI::Dispatcher { + public: + AcceleratorHandler(); + // Dispatcher method. This returns true if an accelerator was + // processed by the focus manager + virtual bool Dispatch(const MSG& msg); + private: + DISALLOW_EVIL_CONSTRUCTORS(AcceleratorHandler); +}; + +} // namespace views + +#endif // VIEWS_WIDGET_ACCELERATOR_HANDLER_H_ diff --git a/views/widget/aero_tooltip_manager.cc b/views/widget/aero_tooltip_manager.cc new file mode 100644 index 0000000..ca58c24 --- /dev/null +++ b/views/widget/aero_tooltip_manager.cc @@ -0,0 +1,128 @@ +// Copyright (c) 2006-2008 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/widget/aero_tooltip_manager.h" + +#include <windows.h> +#include <atlbase.h> +#include <atlapp.h> // for GET_X/Y_LPARAM +#include <commctrl.h> +#include <shlobj.h> + +#include "app/l10n_util_win.h" +#include "base/message_loop.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// AeroTooltipManager, public: + +AeroTooltipManager::AeroTooltipManager(Widget* widget, HWND parent) + : TooltipManager(widget, parent), + initial_delay_(0) { +} + +AeroTooltipManager::~AeroTooltipManager() { + if (initial_timer_) + initial_timer_->Disown(); +} + +void AeroTooltipManager::OnMouse(UINT u_msg, WPARAM w_param, LPARAM l_param) { + if (initial_timer_) + initial_timer_->Disown(); + + if (u_msg == WM_MOUSEMOVE || u_msg == WM_NCMOUSEMOVE) { + int x = GET_X_LPARAM(l_param); + int y = GET_Y_LPARAM(l_param); + if (last_mouse_x_ != x || last_mouse_y_ != y) { + last_mouse_x_ = x; + last_mouse_y_ = y; + HideKeyboardTooltip(); + UpdateTooltip(x, y); + } + + // Delay opening of the tooltip just in case the user moves their + // mouse to another control. We defer this from Init because we get + // zero if we query it too soon. + if (!initial_delay_) { + initial_delay_ = static_cast<int>( + ::SendMessage(tooltip_hwnd_, TTM_GETDELAYTIME, TTDT_INITIAL, 0)); + } + initial_timer_ = new InitialTimer(this, initial_delay_); + } else { + // Hide the tooltip and cancel any timers. + ::SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); + ::SendMessage(tooltip_hwnd_, TTM_TRACKACTIVATE, false, (LPARAM)&toolinfo_); + return; + } +} + +void AeroTooltipManager::OnMouseLeave() { + last_mouse_x_ = last_mouse_y_ = -1; + UpdateTooltip(); +} + +/////////////////////////////////////////////////////////////////////////////// +// AeroTooltipManager, private: + +void AeroTooltipManager::Init() { + // Create the tooltip control. + tooltip_hwnd_ = CreateWindowEx( + WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), + TOOLTIPS_CLASS, NULL, TTS_NOPREFIX, 0, 0, 0, 0, + parent_, NULL, NULL, NULL); + + l10n_util::AdjustUIFontForWindow(tooltip_hwnd_); + + // Add one tool that is used for all tooltips. + toolinfo_.cbSize = sizeof(toolinfo_); + + // We use tracking tooltips on Vista to allow us to manually control the + // visibility of the tooltip. + toolinfo_.uFlags = TTF_TRANSPARENT | TTF_IDISHWND | TTF_TRACK | TTF_ABSOLUTE; + toolinfo_.hwnd = parent_; + toolinfo_.uId = (UINT_PTR)parent_; + + // Setting this tells windows to call parent_ back (using a WM_NOTIFY + // message) for the actual tooltip contents. + toolinfo_.lpszText = LPSTR_TEXTCALLBACK; + SetRectEmpty(&toolinfo_.rect); + ::SendMessage(tooltip_hwnd_, TTM_ADDTOOL, 0, (LPARAM)&toolinfo_); +} + +void AeroTooltipManager::OnTimer() { + initial_timer_ = NULL; + + POINT pt; + pt.x = last_mouse_x_; + pt.y = last_mouse_y_; + ::ClientToScreen(parent_, &pt); + + // Set the position and visibility. + if (!tooltip_showing_) { + ::SendMessage(tooltip_hwnd_, TTM_POPUP, 0, 0); + ::SendMessage(tooltip_hwnd_, TTM_TRACKPOSITION, 0, MAKELPARAM(pt.x, pt.y)); + ::SendMessage(tooltip_hwnd_, TTM_TRACKACTIVATE, true, (LPARAM)&toolinfo_); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// AeroTooltipManager::InitialTimer + +AeroTooltipManager::InitialTimer::InitialTimer(AeroTooltipManager* manager, + int time) : manager_(manager) { + MessageLoop::current()->PostDelayedTask(FROM_HERE, NewRunnableMethod( + this, &InitialTimer::Execute), time); +} + +void AeroTooltipManager::InitialTimer::Disown() { + manager_ = NULL; +} + +void AeroTooltipManager::InitialTimer::Execute() { + if (manager_) + manager_->OnTimer(); +} + +} // namespace views diff --git a/views/widget/aero_tooltip_manager.h b/views/widget/aero_tooltip_manager.h new file mode 100644 index 0000000..fe5856d --- /dev/null +++ b/views/widget/aero_tooltip_manager.h @@ -0,0 +1,57 @@ +// Copyright (c) 2006-2008 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 VIEWS_WIDGET_AERO_TOOLTIP_MANAGER_H_ +#define VIEWS_WIDGET_AERO_TOOLTIP_MANAGER_H_ + +#include "base/ref_counted.h" +#include "base/task.h" +#include "views/widget/tooltip_manager.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// AeroTooltipManager +// +// Default Windows tooltips are broken when using our custom window frame +// - as soon as the tooltip receives a WM_MOUSEMOVE event, it starts spewing +// NCHITTEST messages at its parent window (us). These messages have random +// x/y coordinates and can't be ignored, as the DwmDefWindowProc uses +// NCHITTEST messages to determine how to highlight the caption buttons +// (the buttons then flicker as the hit tests sent by the user's mouse +// trigger different effects to those sent by the tooltip). +// +// So instead, we have to partially implement tooltips ourselves using +// TTF_TRACKed tooltips. +// +// TODO(glen): Resolve this with Microsoft. +class AeroTooltipManager : public TooltipManager { + public: + AeroTooltipManager(Widget* widget, HWND parent); + virtual ~AeroTooltipManager(); + + virtual void OnMouse(UINT u_msg, WPARAM w_param, LPARAM l_param); + virtual void OnMouseLeave(); + + private: + void Init(); + void OnTimer(); + + class InitialTimer : public base::RefCounted<InitialTimer> { + public: + InitialTimer(AeroTooltipManager* manager, int time); + void Disown(); + void Execute(); + + private: + AeroTooltipManager* manager_; + }; + + int initial_delay_; + scoped_refptr<InitialTimer> initial_timer_; +}; + +} // namespace views + +#endif // #ifndef VIEWS_WIDGET_AERO_TOOLTIP_MANAGER_H_ diff --git a/views/widget/root_view.cc b/views/widget/root_view.cc new file mode 100644 index 0000000..fd9f1c0 --- /dev/null +++ b/views/widget/root_view.cc @@ -0,0 +1,1001 @@ +// Copyright (c) 2006-2008 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/widget/root_view.h" + +#include <algorithm> + +#include "app/drag_drop_types.h" +#include "app/gfx/chrome_canvas.h" +#if defined(OS_WIN) +#include "base/base_drag_source.h" +#endif +#include "base/logging.h" +#include "base/message_loop.h" +#if defined(OS_WIN) +#include "views/focus/view_storage.h" +#include "views/widget/root_view_drop_target.h" +#endif +#include "views/widget/widget.h" +#include "views/window/window.h" + +namespace views { + +///////////////////////////////////////////////////////////////////////////// +// +// A Task to trigger non urgent painting. +// +///////////////////////////////////////////////////////////////////////////// +class PaintTask : public Task { + public: + explicit PaintTask(RootView* target) : root_view_(target) { + } + + ~PaintTask() {} + + void Cancel() { + root_view_ = NULL; + } + + void Run() { + if (root_view_) + root_view_->PaintNow(); + } + private: + // The target root view. + RootView* root_view_; + + DISALLOW_EVIL_CONSTRUCTORS(PaintTask); +}; + +const char RootView::kViewClassName[] = "views/RootView"; + +///////////////////////////////////////////////////////////////////////////// +// +// RootView - constructors, destructors, initialization +// +///////////////////////////////////////////////////////////////////////////// + +RootView::RootView(Widget* widget) + : mouse_pressed_handler_(NULL), + mouse_move_handler_(NULL), + last_click_handler_(NULL), + widget_(widget), + invalid_rect_urgent_(false), + pending_paint_task_(NULL), + paint_task_needed_(false), + explicit_mouse_handler_(false), +#if defined(OS_WIN) + previous_cursor_(NULL), +#endif + default_keyboard_handler_(NULL), + focus_listener_(NULL), + focus_on_mouse_pressed_(false), + ignore_set_focus_calls_(false), + focus_traversable_parent_(NULL), + focus_traversable_parent_view_(NULL), + drag_view_(NULL) +#ifndef NDEBUG + , + is_processing_paint_(false) +#endif +{ +} + +RootView::~RootView() { + // If we have children remove them explicitly so to make sure a remove + // notification is sent for each one of them. + if (!child_views_.empty()) + RemoveAllChildViews(true); + + if (pending_paint_task_) + pending_paint_task_->Cancel(); // Ensure we're not called any more. +} + +///////////////////////////////////////////////////////////////////////////// +// +// RootView - layout, painting +// +///////////////////////////////////////////////////////////////////////////// + +void RootView::SchedulePaint(const gfx::Rect& r, bool urgent) { + // If there is an existing invalid rect, add the union of the scheduled + // rect with the invalid rect. This could be optimized further if + // necessary. + if (invalid_rect_.IsEmpty()) + invalid_rect_ = r; + else + invalid_rect_ = invalid_rect_.Union(r); + + if (urgent || invalid_rect_urgent_) { + invalid_rect_urgent_ = true; + } else { + if (!pending_paint_task_) { + pending_paint_task_ = new PaintTask(this); + MessageLoop::current()->PostTask(FROM_HERE, pending_paint_task_); + } + paint_task_needed_ = true; + } +} + +void RootView::SchedulePaint() { + View::SchedulePaint(); +} + +void RootView::SchedulePaint(int x, int y, int w, int h) { + View::SchedulePaint(); +} + +#ifndef NDEBUG +// Sets the value of RootView's |is_processing_paint_| member to true as long +// as ProcessPaint is being called. Sets it to |false| when it returns. +class ScopedProcessingPaint { + public: + explicit ScopedProcessingPaint(bool* is_processing_paint) + : is_processing_paint_(is_processing_paint) { + *is_processing_paint_ = true; + } + + ~ScopedProcessingPaint() { + *is_processing_paint_ = false; + } + private: + bool* is_processing_paint_; + + DISALLOW_EVIL_CONSTRUCTORS(ScopedProcessingPaint); +}; +#endif + +void RootView::ProcessPaint(ChromeCanvas* canvas) { +#ifndef NDEBUG + ScopedProcessingPaint processing_paint(&is_processing_paint_); +#endif + + // Clip the invalid rect to our bounds. If a view is in a scrollview + // it could be a lot larger + invalid_rect_ = GetScheduledPaintRectConstrainedToSize(); + + if (invalid_rect_.IsEmpty()) + return; + + // Clear the background. + canvas->drawColor(SK_ColorBLACK, SkPorterDuff::kClear_Mode); + + // Save the current transforms. + canvas->save(); + + // Set the clip rect according to the invalid rect. + int clip_x = invalid_rect_.x() + x(); + int clip_y = invalid_rect_.y() + y(); + canvas->ClipRectInt(clip_x, clip_y, invalid_rect_.width(), + invalid_rect_.height()); + + // Paint the tree + View::ProcessPaint(canvas); + + // Restore the previous transform + canvas->restore(); + + ClearPaintRect(); +} + +void RootView::PaintNow() { + if (pending_paint_task_) { + pending_paint_task_->Cancel(); + pending_paint_task_ = NULL; + } + if (!paint_task_needed_) + return; + Widget* widget = GetWidget(); + if (widget) + widget->PaintNow(invalid_rect_); +} + +bool RootView::NeedsPainting(bool urgent) { + bool has_invalid_rect = !invalid_rect_.IsEmpty(); + if (urgent) { + if (invalid_rect_urgent_) + return has_invalid_rect; + else + return false; + } else { + return has_invalid_rect; + } +} + +const gfx::Rect& RootView::GetScheduledPaintRect() { + return invalid_rect_; +} + +gfx::Rect RootView::GetScheduledPaintRectConstrainedToSize() { + if (invalid_rect_.IsEmpty()) + return invalid_rect_; + + return invalid_rect_.Intersect(GetLocalBounds(true)); +} + +///////////////////////////////////////////////////////////////////////////// +// +// RootView - tree +// +///////////////////////////////////////////////////////////////////////////// + +Widget* RootView::GetWidget() const { + return widget_; +} + +void RootView::ThemeChanged() { + View::ThemeChanged(); +} + +///////////////////////////////////////////////////////////////////////////// +// +// RootView - event dispatch and propagation +// +///////////////////////////////////////////////////////////////////////////// + +void RootView::ViewHierarchyChanged(bool is_add, View* parent, View* child) { + if (!is_add) { + if (!explicit_mouse_handler_ && mouse_pressed_handler_ == child) { + mouse_pressed_handler_ = NULL; + } + +#if defined(OS_WIN) + if (drop_target_.get()) + drop_target_->ResetTargetViewIfEquals(child); +#else + NOTIMPLEMENTED(); +#endif + + if (mouse_move_handler_ == child) { + mouse_move_handler_ = NULL; + } + + if (GetFocusedView() == child) { + FocusView(NULL); + } + + if (child == drag_view_) + drag_view_ = NULL; + + if (default_keyboard_handler_ == child) { + default_keyboard_handler_ = NULL; + } + + // For a given widget hierarchy, focus is tracked by a FocusManager attached + // to our nearest enclosing Window. <-- Important Assumption! + // We may not have access to our window if this function is called as a + // result of teardown during the deletion of the RootView and its hierarchy, + // so we don't bother notifying the FocusManager in that case because it + // will have already been destroyed (the Widget that contains us is + // NCDESTROY'ed which in turn destroys the focus manager _before_ the + // RootView is deleted.) +#if defined(OS_WIN) + Window* window = GetWindow(); + if (window) { + FocusManager* focus_manager = + FocusManager::GetFocusManager(window->GetNativeWindow()); + focus_manager->ViewRemoved(parent, child); + } + ViewStorage::GetSharedInstance()->ViewRemoved(parent, child); +#endif + } +} + +void RootView::SetFocusOnMousePressed(bool f) { + focus_on_mouse_pressed_ = f; +} + +bool RootView::OnMousePressed(const MouseEvent& e) { + // This function does not normally handle non-client messages except for + // non-client double-clicks. Actually, all double-clicks are special as the + // are formed from a single-click followed by a double-click event. When the + // double-click event lands on a different view than its single-click part, + // we transform it into a single-click which prevents odd things. + if ((e.GetFlags() & MouseEvent::EF_IS_NON_CLIENT) && + !(e.GetFlags() & MouseEvent::EF_IS_DOUBLE_CLICK)) { + last_click_handler_ = NULL; + return false; + } + + UpdateCursor(e); + SetMouseLocationAndFlags(e); + + // If mouse_pressed_handler_ is non null, we are currently processing + // a pressed -> drag -> released session. In that case we send the + // event to mouse_pressed_handler_ + if (mouse_pressed_handler_) { + MouseEvent mouse_pressed_event(e, this, mouse_pressed_handler_); + drag_info.Reset(); + mouse_pressed_handler_->ProcessMousePressed(mouse_pressed_event, + &drag_info); + return true; + } + DCHECK(!explicit_mouse_handler_); + + bool hit_disabled_view = false; + // Walk up the tree until we find a view that wants the mouse event. + for (mouse_pressed_handler_ = GetViewForPoint(e.location()); + mouse_pressed_handler_ && (mouse_pressed_handler_ != this); + mouse_pressed_handler_ = mouse_pressed_handler_->GetParent()) { + if (!mouse_pressed_handler_->IsEnabled()) { + // Disabled views should eat events instead of propagating them upwards. + hit_disabled_view = true; + break; + } + + // See if this view wants to handle the mouse press. + MouseEvent mouse_pressed_event(e, this, mouse_pressed_handler_); + + // Remove the double-click flag if the handler is different than the + // one which got the first click part of the double-click. + if (mouse_pressed_handler_ != last_click_handler_) + mouse_pressed_event.set_flags(e.GetFlags() & + ~MouseEvent::EF_IS_DOUBLE_CLICK); + + drag_info.Reset(); + bool handled = mouse_pressed_handler_->ProcessMousePressed( + mouse_pressed_event, &drag_info); + + // The view could have removed itself from the tree when handling + // OnMousePressed(). In this case, the removal notification will have + // reset mouse_pressed_handler_ to NULL out from under us. Detect this + // case and stop. (See comments in view.h.) + // + // NOTE: Don't return true here, because we don't want the frame to + // forward future events to us when there's no handler. + if (!mouse_pressed_handler_) + break; + + // If the view handled the event, leave mouse_pressed_handler_ set and + // return true, which will cause subsequent drag/release events to get + // forwarded to that view. + if (handled) { + last_click_handler_ = mouse_pressed_handler_; + return true; + } + } + + // Reset mouse_pressed_handler_ to indicate that no processing is occurring. + mouse_pressed_handler_ = NULL; + + if (focus_on_mouse_pressed_) { +#if defined(OS_WIN) + HWND hwnd = GetWidget()->GetNativeView(); + if (::GetFocus() != hwnd) { + ::SetFocus(hwnd); + } +#else + NOTIMPLEMENTED(); +#endif + } + + // In the event that a double-click is not handled after traversing the + // entire hierarchy (even as a single-click when sent to a different view), + // it must be marked as handled to avoid anything happening from default + // processing if it the first click-part was handled by us. + if (last_click_handler_ && e.GetFlags() & MouseEvent::EF_IS_DOUBLE_CLICK) + hit_disabled_view = true; + + last_click_handler_ = NULL; + return hit_disabled_view; +} + +bool RootView::ConvertPointToMouseHandler(const gfx::Point& l, + gfx::Point* p) { + // + // If the mouse_handler was set explicitly, we need to keep + // sending events even if it was reparented in a different + // window. (a non explicit mouse handler is automatically + // cleared when the control is removed from the hierarchy) + if (explicit_mouse_handler_) { + if (mouse_pressed_handler_->GetWidget()) { + *p = l; + ConvertPointToScreen(this, p); + ConvertPointToView(NULL, mouse_pressed_handler_, p); + } else { + // If the mouse_pressed_handler_ is not connected, we send the + // event in screen coordinate system + *p = l; + ConvertPointToScreen(this, p); + return true; + } + } else { + *p = l; + ConvertPointToView(this, mouse_pressed_handler_, p); + } + return true; +} + +bool RootView::OnMouseDragged(const MouseEvent& e) { + UpdateCursor(e); + + if (mouse_pressed_handler_) { + SetMouseLocationAndFlags(e); + + gfx::Point p; + ConvertPointToMouseHandler(e.location(), &p); + MouseEvent mouse_event(e.GetType(), p.x(), p.y(), e.GetFlags()); + return mouse_pressed_handler_->ProcessMouseDragged(mouse_event, &drag_info); + } + return false; +} + +void RootView::OnMouseReleased(const MouseEvent& e, bool canceled) { + UpdateCursor(e); + + if (mouse_pressed_handler_) { + gfx::Point p; + ConvertPointToMouseHandler(e.location(), &p); + MouseEvent mouse_released(e.GetType(), p.x(), p.y(), e.GetFlags()); + // We allow the view to delete us from ProcessMouseReleased. As such, + // configure state such that we're done first, then call View. + View* mouse_pressed_handler = mouse_pressed_handler_; + mouse_pressed_handler_ = NULL; + explicit_mouse_handler_ = false; + mouse_pressed_handler->ProcessMouseReleased(mouse_released, canceled); + // WARNING: we may have been deleted. + } +} + +void RootView::OnMouseMoved(const MouseEvent& e) { + View* v = GetViewForPoint(e.location()); + // Find the first enabled view. + while (v && !v->IsEnabled()) + v = v->GetParent(); + if (v && v != this) { + if (v != mouse_move_handler_) { + if (mouse_move_handler_ != NULL) { + MouseEvent exited_event(Event::ET_MOUSE_EXITED, 0, 0, 0); + mouse_move_handler_->OnMouseExited(exited_event); + } + + mouse_move_handler_ = v; + + MouseEvent entered_event(Event::ET_MOUSE_ENTERED, + this, + mouse_move_handler_, + e.location(), + 0); + mouse_move_handler_->OnMouseEntered(entered_event); + } + MouseEvent moved_event(Event::ET_MOUSE_MOVED, + this, + mouse_move_handler_, + e.location(), + 0); + mouse_move_handler_->OnMouseMoved(moved_event); + +#if defined(OS_WIN) + HCURSOR cursor = mouse_move_handler_->GetCursorForPoint( + moved_event.GetType(), moved_event.x(), moved_event.y()); + if (cursor) { + previous_cursor_ = ::SetCursor(cursor); + } else if (previous_cursor_) { + ::SetCursor(previous_cursor_); + previous_cursor_ = NULL; + } +#else + NOTIMPLEMENTED(); +#endif + } else if (mouse_move_handler_ != NULL) { + MouseEvent exited_event(Event::ET_MOUSE_EXITED, 0, 0, 0); + mouse_move_handler_->OnMouseExited(exited_event); +#if defined(OS_WIN) + if (previous_cursor_) { + ::SetCursor(previous_cursor_); + previous_cursor_ = NULL; + } +#else + NOTIMPLEMENTED(); +#endif + } +} + +void RootView::ProcessOnMouseExited() { + if (mouse_move_handler_ != NULL) { + MouseEvent exited_event(Event::ET_MOUSE_EXITED, 0, 0, 0); + mouse_move_handler_->OnMouseExited(exited_event); + mouse_move_handler_ = NULL; + } +} + +void RootView::SetMouseHandler(View *new_mh) { + // If we're clearing the mouse handler, clear explicit_mouse_handler as well. + explicit_mouse_handler_ = (new_mh != NULL); + mouse_pressed_handler_ = new_mh; +} + +void RootView::OnWidgetCreated() { +#if defined(OS_WIN) + DCHECK(!drop_target_.get()); + drop_target_ = new RootViewDropTarget(this); +#else + // TODO(port): Port RootViewDropTarget and this goes away. + NOTIMPLEMENTED(); +#endif +} + +void RootView::OnWidgetDestroyed() { +#if defined(OS_WIN) + if (drop_target_.get()) { + RevokeDragDrop(GetWidget()->GetNativeView()); + drop_target_ = NULL; + } +#else + // TODO(port): Port RootViewDropTarget and this goes away. + NOTIMPLEMENTED(); +#endif + widget_ = NULL; +} + +void RootView::ProcessMouseDragCanceled() { + if (mouse_pressed_handler_) { + // Synthesize a release event. + MouseEvent release_event(Event::ET_MOUSE_RELEASED, last_mouse_event_x_, + last_mouse_event_y_, last_mouse_event_flags_); + OnMouseReleased(release_event, true); + } +} + +void RootView::SetFocusListener(FocusListener* listener) { + focus_listener_ = listener; +} + +void RootView::FocusView(View* view) { + if (view != GetFocusedView()) { +#if defined(OS_WIN) + FocusManager* focus_manager = GetFocusManager(); + DCHECK(focus_manager) << "No Focus Manager for Window " << + (GetWidget() ? GetWidget()->GetNativeView() : 0); + if (!focus_manager) + return; + + View* prev_focused_view = focus_manager->GetFocusedView(); + focus_manager->SetFocusedView(view); + + if (focus_listener_) + focus_listener_->FocusChanged(prev_focused_view, view); +#else + // TODO(port): Port the focus manager and this goes away. + NOTIMPLEMENTED(); +#endif + } +} + +View* RootView::GetFocusedView() { + FocusManager* focus_manager = GetFocusManager(); + if (!focus_manager) { + // We may not have a FocusManager when the window that contains us is being + // deleted. Sadly we cannot wait for the window to be destroyed before we + // remove the FocusManager (see xp_frame.cc for more info). + return NULL; + } + + // Make sure the focused view belongs to this RootView's view hierarchy. + View* view = focus_manager->GetFocusedView(); + if (view && (view->GetRootView() == this)) + return view; + return NULL; +} + +View* RootView::FindNextFocusableView(View* starting_view, + bool reverse, + Direction direction, + bool dont_loop, + FocusTraversable** focus_traversable, + View** focus_traversable_view) { + *focus_traversable = NULL; + *focus_traversable_view = NULL; + + if (GetChildViewCount() == 0) { + NOTREACHED(); + // Nothing to focus on here. + return NULL; + } + + bool skip_starting_view = true; + if (!starting_view) { + // Default to the first/last child + starting_view = reverse ? GetChildViewAt(GetChildViewCount() - 1) : + GetChildViewAt(0) ; + // If there was no starting view, then the one we select is a potential + // focus candidate. + skip_starting_view = false; + } else { + // The starting view should be part of this RootView. + DCHECK(IsParentOf(starting_view)); + } + + View* v = NULL; + if (!reverse) { + v = FindNextFocusableViewImpl(starting_view, skip_starting_view, + true, + (direction == DOWN) ? true : false, + starting_view->GetGroup()); + } 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) && !starting_view->IsFocusable(); + v = FindPreviousFocusableViewImpl(starting_view, true, + true, + can_go_down, + starting_view->GetGroup()); + } + if (v) { + if (v->IsFocusable()) + return v; + *focus_traversable = v->GetFocusTraversable(); + DCHECK(*focus_traversable); + *focus_traversable_view = v; + return NULL; + } + // Nothing found. + return 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* RootView::FindNextFocusableViewImpl(View* starting_view, + bool skip_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id) { + if (!skip_starting_view) { + if (IsViewFocusableCandidate(starting_view, skip_group_id)) + return FindSelectedViewForGroup(starting_view); + if (starting_view->GetFocusTraversable()) + return starting_view; + } + + // First let's try the left child. + if (can_go_down) { + View* v = NULL; + if (starting_view->GetChildViewCount() > 0) { + // We are only interested in non floating-views, as attached floating + // views order is variable (depending on mouse moves). + for (int i = 0; i < starting_view->GetChildViewCount(); i++) { + View* child = starting_view->GetChildViewAt(i); + if (!child->IsFloatingView()) { + v = FindNextFocusableViewImpl(child, false, false, true, + skip_group_id); + break; + } + } + } + if (v == NULL) { + // Try the floating views. + int id = 0; + if (starting_view->EnumerateFloatingViews(View::FIRST, 0, &id)) { + View* child = starting_view->RetrieveFloatingViewForID(id); + DCHECK(child); + v = FindNextFocusableViewImpl(child, false, false, true, skip_group_id); + } + } + if (v) + return v; + } + + // Then try the right sibling. + View* sibling = NULL; + if (starting_view->IsFloatingView()) { + int id = 0; + if (starting_view->GetParent()->EnumerateFloatingViews( + View::NEXT, starting_view->GetFloatingViewID(), &id)) { + sibling = starting_view->GetParent()->RetrieveFloatingViewForID(id); + DCHECK(sibling); + } + } else { + sibling = starting_view->GetNextFocusableView(); + if (!sibling) { + // Let's try floating views. + int id = 0; + if (starting_view->GetParent()->EnumerateFloatingViews(View::FIRST, + 0, &id)) { + sibling = starting_view->GetParent()->RetrieveFloatingViewForID(id); + DCHECK(sibling); + } + } + } + if (sibling) { + View* v = + FindNextFocusableViewImpl(sibling, false, false, true, skip_group_id); + if (v) + return v; + } + + // Then go up to the parent sibling. + if (can_go_up) { + View* parent = starting_view->GetParent(); + while (parent) { + int id = 0; + if (parent->IsFloatingView() && + parent->GetParent()->EnumerateFloatingViews( + View::NEXT, parent->GetFloatingViewID(), &id)) { + sibling = parent->GetParent()->RetrieveFloatingViewForID(id); + DCHECK(sibling); + } else { + sibling = parent->GetNextFocusableView(); + } + if (sibling) { + return FindNextFocusableViewImpl(sibling, + false, true, true, + skip_group_id); + } + parent = parent->GetParent(); + } + } + + // 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* RootView::FindPreviousFocusableViewImpl(View* starting_view, + bool skip_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id) { + // Let's go down and right as much as we can. + if (can_go_down) { + View* v = NULL; + if (starting_view->GetChildViewCount() - + starting_view->GetFloatingViewCount() > 0) { + View* view = + starting_view->GetChildViewAt(starting_view->GetChildViewCount() - 1); + v = FindPreviousFocusableViewImpl(view, false, false, true, + skip_group_id); + } else { + // Let's try floating views. + int id = 0; + if (starting_view->EnumerateFloatingViews(View::LAST, 0, &id)) { + View* child = starting_view->RetrieveFloatingViewForID(id); + DCHECK(child); + v = FindNextFocusableViewImpl(child, false, false, true, skip_group_id); + } + } + if (v) + return v; + } + + if (!skip_starting_view) { + if (IsViewFocusableCandidate(starting_view, skip_group_id)) + return FindSelectedViewForGroup(starting_view); + if (starting_view->GetFocusTraversable()) + return starting_view; + } + + // Then try the left sibling. + View* sibling = NULL; + if (starting_view->IsFloatingView()) { + int id = 0; + if (starting_view->GetParent()->EnumerateFloatingViews( + View::PREVIOUS, starting_view->GetFloatingViewID(), &id)) { + sibling = starting_view->GetParent()->RetrieveFloatingViewForID(id); + DCHECK(sibling); + } + if (!sibling) { + // No more floating views, try regular views, starting at the last one. + View* parent = starting_view->GetParent(); + for (int i = parent->GetChildViewCount() - 1; i >= 0; i--) { + View* v = parent->GetChildViewAt(i); + if (!v->IsFloatingView()) { + sibling = v; + break; + } + } + } + } else { + sibling = starting_view->GetPreviousFocusableView(); + } + if (sibling) { + return FindPreviousFocusableViewImpl(sibling, + false, true, true, + skip_group_id); + } + + // Then go up the parent. + if (can_go_up) { + View* parent = starting_view->GetParent(); + if (parent) + return FindPreviousFocusableViewImpl(parent, + false, true, false, + skip_group_id); + } + + // We found nothing. + return NULL; +} + +FocusTraversable* RootView::GetFocusTraversableParent() { + return focus_traversable_parent_; +} + +void RootView::SetFocusTraversableParent(FocusTraversable* focus_traversable) { + DCHECK(focus_traversable != this); + focus_traversable_parent_ = focus_traversable; +} + +View* RootView::GetFocusTraversableParentView() { + return focus_traversable_parent_view_; +} + +void RootView::SetFocusTraversableParentView(View* view) { + focus_traversable_parent_view_ = view; +} + +// static +View* RootView::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; +} + +// static +bool RootView::IsViewFocusableCandidate(View* v, int skip_group_id) { + return v->IsFocusable() && + (v->IsGroupFocusTraversable() || skip_group_id == -1 || + v->GetGroup() != skip_group_id); +} + +bool RootView::ProcessKeyEvent(const KeyEvent& event) { + bool consumed = false; + + View* v = GetFocusedView(); +#if defined(OS_WIN) + // Special case to handle right-click context menus triggered by the + // keyboard. + if (v && v->IsEnabled() && ((event.GetCharacter() == VK_APPS) || + (event.GetCharacter() == VK_F10 && event.IsShiftDown()))) { + gfx::Point screen_loc = v->GetKeyboardContextMenuLocation(); + v->ShowContextMenu(screen_loc.x(), screen_loc.y(), false); + return true; + } +#else + // TODO(port): The above block needs the VK_* refactored out. + NOTIMPLEMENTED(); +#endif + + for (; v && v != this && !consumed; v = v->GetParent()) { + consumed = (event.GetType() == Event::ET_KEY_PRESSED) ? + v->OnKeyPressed(event) : v->OnKeyReleased(event); + } + + if (!consumed && default_keyboard_handler_) { + consumed = (event.GetType() == Event::ET_KEY_PRESSED) ? + default_keyboard_handler_->OnKeyPressed(event) : + default_keyboard_handler_->OnKeyReleased(event); + } + + return consumed; +} + +bool RootView::ProcessMouseWheelEvent(const MouseWheelEvent& e) { + View* v; + bool consumed = false; + if (GetFocusedView()) { + for (v = GetFocusedView(); + v && v != this && !consumed; v = v->GetParent()) { + consumed = v->OnMouseWheel(e); + } + } + + if (!consumed && default_keyboard_handler_) { + consumed = default_keyboard_handler_->OnMouseWheel(e); + } + return consumed; +} + +void RootView::SetDefaultKeyboardHandler(View* v) { + default_keyboard_handler_ = v; +} + +bool RootView::IsVisibleInRootView() const { + return IsVisible(); +} + +void RootView::ViewBoundsChanged(View* view, bool size_changed, + bool position_changed) { + DCHECK(view && (size_changed || position_changed)); + if (!view->descendants_to_notify_.get()) + return; + + for (std::vector<View*>::iterator i = view->descendants_to_notify_->begin(); + i != view->descendants_to_notify_->end(); ++i) { + (*i)->VisibleBoundsInRootChanged(); + } +} + +void RootView::RegisterViewForVisibleBoundsNotification(View* view) { + DCHECK(view); + if (view->registered_for_visible_bounds_notification_) + return; + view->registered_for_visible_bounds_notification_ = true; + View* ancestor = view->GetParent(); + while (ancestor) { + ancestor->AddDescendantToNotify(view); + ancestor = ancestor->GetParent(); + } +} + +void RootView::UnregisterViewForVisibleBoundsNotification(View* view) { + DCHECK(view); + if (!view->registered_for_visible_bounds_notification_) + return; + view->registered_for_visible_bounds_notification_ = false; + View* ancestor = view->GetParent(); + while (ancestor) { + ancestor->RemoveDescendantToNotify(view); + ancestor = ancestor->GetParent(); + } +} + +void RootView::SetMouseLocationAndFlags(const MouseEvent& e) { + last_mouse_event_flags_ = e.GetFlags(); + last_mouse_event_x_ = e.x(); + last_mouse_event_y_ = e.y(); +} + +std::string RootView::GetClassName() const { + return kViewClassName; +} + +void RootView::ClearPaintRect() { + invalid_rect_.SetRect(0, 0, 0, 0); + + // This painting has been done. Reset the urgent flag. + invalid_rect_urgent_ = false; + + // If a pending_paint_task_ does Run(), we don't need to do anything. + paint_task_needed_ = false; +} + +///////////////////////////////////////////////////////////////////////////// +// +// RootView - accessibility +// +///////////////////////////////////////////////////////////////////////////// + +bool RootView::GetAccessibleRole(AccessibilityTypes::Role* role) { + DCHECK(role); + + *role = AccessibilityTypes::ROLE_APPLICATION; + return true; +} + +bool RootView::GetAccessibleName(std::wstring* name) { + if (!accessible_name_.empty()) { + *name = accessible_name_; + return true; + } + return false; +} + +void RootView::SetAccessibleName(const std::wstring& name) { + accessible_name_.assign(name); +} + +View* RootView::GetDragView() { + return drag_view_; +} + +} // namespace views diff --git a/views/widget/root_view.h b/views/widget/root_view.h new file mode 100644 index 0000000..d775ac1 --- /dev/null +++ b/views/widget/root_view.h @@ -0,0 +1,363 @@ +// Copyright (c) 2009 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 VIEWS_WIDGET_ROOT_VIEW_H_ +#define VIEWS_WIDGET_ROOT_VIEW_H_ + +#include "build/build_config.h" + +#if defined(OS_LINUX) +#include <gtk/gtk.h> +#endif + +#if defined(OS_WIN) +#include "base/ref_counted.h" +#endif + +#include "views/focus/focus_manager.h" +#include "views/view.h" + +namespace views { + +class PaintTask; +class RootViewDropTarget; +class Widget; + +//////////////////////////////////////////////////////////////////////////////// +// +// FocusListener Interface +// +//////////////////////////////////////////////////////////////////////////////// +class FocusListener { + public: + virtual void FocusChanged(View* lost_focus, View* got_focus) = 0; +}; + + +///////////////////////////////////////////////////////////////////////////// +// +// RootView class +// +// The RootView is the root of a View hierarchy. A RootView is always the +// first and only child of a Widget. +// +// The RootView manages the View hierarchy's interface with the Widget +// and also maintains the current invalid rect - the region that needs +// repainting. +// +///////////////////////////////////////////////////////////////////////////// +class RootView : public View, + public FocusTraversable { + public: + static const char kViewClassName[]; + + explicit RootView(Widget* widget); + + virtual ~RootView(); + + // Layout and Painting functions + + // Overridden from View to implement paint scheduling. + virtual void SchedulePaint(const gfx::Rect& r, bool urgent); + + // Convenience to schedule the whole view + virtual void SchedulePaint(); + + // Convenience to schedule a paint given some ints + virtual void SchedulePaint(int x, int y, int w, int h); + + // Paint this RootView and its child Views. + virtual void ProcessPaint(ChromeCanvas* canvas); + + // If the invalid rect is non-empty and there is a pending paint the RootView + // is painted immediately. This is internally invoked as the result of + // invoking SchedulePaint. + virtual void PaintNow(); + + // Whether or not this View needs repainting. If |urgent| is true, this method + // returns whether this root view needs to paint as soon as possible. + virtual bool NeedsPainting(bool urgent); + + // Invoked by the Widget to discover what rectangle should be painted. + const gfx::Rect& GetScheduledPaintRect(); + + // Returns the region scheduled to paint clipped to the RootViews bounds. + gfx::Rect GetScheduledPaintRectConstrainedToSize(); + + // Tree functions + + // Get the Widget that hosts this View. + virtual Widget* GetWidget() const; + + // Public API for broadcasting theme change notifications to this View + // hierarchy. + virtual void ThemeChanged(); + + // The following event methods are overridden to propagate event to the + // control tree + virtual bool OnMousePressed(const MouseEvent& e); + virtual bool OnMouseDragged(const MouseEvent& e); + virtual void OnMouseReleased(const MouseEvent& e, bool canceled); + virtual void OnMouseMoved(const MouseEvent& e); + virtual void SetMouseHandler(View* new_mouse_handler); + + // Invoked when the Widget has been fully initialized. + // At the time the constructor is invoked the Widget may not be completely + // initialized, when this method is invoked, it is. + void OnWidgetCreated(); + + // Invoked prior to the Widget being destroyed. + void OnWidgetDestroyed(); + + // Invoked By the Widget if the mouse drag is interrupted by + // the system. Invokes OnMouseReleased with a value of true for canceled. + void ProcessMouseDragCanceled(); + + // Invoked by the Widget instance when the mouse moves outside of the Widget + // bounds. + virtual void ProcessOnMouseExited(); + + // Make the provided view focused. Also make sure that our Widget is focused. + void FocusView(View* view); + + // Check whether the provided view is in the focus path. The focus path is the + // path between the focused view (included) to the root view. + bool IsInFocusPath(View* view); + + // Returns the View in this RootView hierarchy that has the focus, or NULL if + // no View currently has the focus. + View* GetFocusedView(); + + // Process a key event. Send the event to the focused view and up the focus + // path, and finally to the default keyboard handler, until someone consumes + // it. Returns whether anyone consumed the event. + bool ProcessKeyEvent(const KeyEvent& event); + + // Set the default keyboard handler. The default keyboard handler is + // a view that will get an opportunity to process key events when all + // views in the focus path did not process an event. + // + // Note: this is a single view at this point. We may want to make + // this a list if needed. + void SetDefaultKeyboardHandler(View* v); + + // Set whether this root view should focus the corresponding hwnd + // when an unprocessed mouse event occurs. + void SetFocusOnMousePressed(bool f); + + // Process a mousewheel event. Return true if the event was processed + // and false otherwise. + // MouseWheel events are sent on the focus path. + virtual bool ProcessMouseWheelEvent(const MouseWheelEvent& e); + + // Overridden to handle special root view case. + virtual bool IsVisibleInRootView() const; + + // Sets a listener that receives focus changes events. + void SetFocusListener(FocusListener* listener); + + // FocusTraversable implementation. + virtual View* FindNextFocusableView(View* starting_view, + bool reverse, + Direction direction, + bool dont_loop, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + virtual FocusTraversable* GetFocusTraversableParent(); + virtual View* GetFocusTraversableParentView(); + + // Used to set the FocusTraversable parent after the view has been created + // (typically when the hierarchy changes and this RootView is added/removed). + virtual void SetFocusTraversableParent(FocusTraversable* focus_traversable); + + // Used to set the View parent after the view has been created. + virtual void SetFocusTraversableParentView(View* view); + + // Returns the name of this class: views/RootView + virtual std::string GetClassName() const; + + // Clears the region that is schedule to be painted. You nearly never need + // to invoke this. This is primarily intended for Widgets. + void ClearPaintRect(); + +#if defined(OS_WIN) + // Invoked from the Widget to service a WM_PAINT call. + void OnPaint(HWND hwnd); +#elif defined(OS_LINUX) + void OnPaint(GdkEventExpose* event); +#endif + + // Accessibility accessors/mutators, overridden from View. + virtual bool GetAccessibleRole(AccessibilityTypes::Role* role); + virtual bool GetAccessibleName(std::wstring* name); + virtual void SetAccessibleName(const std::wstring& name); + + protected: + + // Overridden to properly reset our event propagation member + // variables when a child is removed + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child); + +#ifndef NDEBUG + virtual bool IsProcessingPaint() const { return is_processing_paint_; } +#endif + + private: + friend class View; + friend class PaintTask; + + RootView(); + DISALLOW_EVIL_CONSTRUCTORS(RootView); + + // Convert a point to our current mouse handler. Returns false if the + // mouse handler is not connected to a Widget. In that case, the + // conversion cannot take place and *p is unchanged + bool ConvertPointToMouseHandler(const gfx::Point& l, gfx::Point *p); + + // Update the cursor given a mouse event. This is called by non mouse_move + // event handlers to honor the cursor desired by views located under the + // cursor during drag operations. + void UpdateCursor(const MouseEvent& e); + + // Notification that size and/or position of a view has changed. This + // notifies the appropriate views. + void ViewBoundsChanged(View* view, bool size_changed, bool position_changed); + + // Registers a view for notification when the visible bounds relative to the + // root of a view changes. + void RegisterViewForVisibleBoundsNotification(View* view); + void UnregisterViewForVisibleBoundsNotification(View* view); + + // Returns the next focusable view or view containing a FocusTraversable (NULL + // if none was found), starting at the starting_view. + // skip_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 skip_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id); + + // Same as FindNextFocusableViewImpl but returns the previous focusable view. + View* FindPreviousFocusableViewImpl(View* starting_view, + bool skip_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id); + + // 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); + + // 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. + static View* FindSelectedViewForGroup(View* view); + + // Updates the last_mouse_* fields from e. + void SetMouseLocationAndFlags(const MouseEvent& e); + +#if defined(OS_WIN) + // Starts a drag operation for the specified view. This blocks until done. + // If the view has not been deleted during the drag, OnDragDone is invoked + // on the view. + void StartDragForViewFromMouseEvent(View* view, + IDataObject* data, + int operation); +#endif + + // If a view is dragging, this returns it. Otherwise returns NULL. + View* GetDragView(); + + // The view currently handing down - drag - up + View* mouse_pressed_handler_; + + // The view currently handling enter / exit + View* mouse_move_handler_; + + // The last view to handle a mouse click, so that we can determine if + // a double-click lands on the same view as its single-click part. + View* last_click_handler_; + + // The host Widget + Widget* widget_; + + // The rectangle that should be painted + gfx::Rect invalid_rect_; + + // Whether the current invalid rect should be painted urgently. + bool invalid_rect_urgent_; + + // The task that we are using to trigger some non urgent painting or NULL + // if no painting has been scheduled yet. + PaintTask* pending_paint_task_; + + // Indicate if, when the pending_paint_task_ is run, actual painting is still + // required. + bool paint_task_needed_; + + // true if mouse_handler_ has been explicitly set + bool explicit_mouse_handler_; + +#if defined(OS_WIN) + // Previous cursor + HCURSOR previous_cursor_; +#endif + + // Default keyboard handler + View* default_keyboard_handler_; + + // The listener that gets focus change notifications. + FocusListener* focus_listener_; + + // Whether this root view should make our hwnd focused + // when an unprocessed mouse press event occurs + bool focus_on_mouse_pressed_; + + // Flag used to ignore focus events when we focus the native window associated + // with a view. + bool ignore_set_focus_calls_; + + // Whether this root view belongs to the current active window. + // bool activated_; + + // Last position/flag of a mouse press/drag. Used if capture stops and we need + // to synthesize a release. + int last_mouse_event_flags_; + int last_mouse_event_x_; + int last_mouse_event_y_; + + // The parent FocusTraversable, used for focus traversal. + FocusTraversable* focus_traversable_parent_; + + // The View that contains this RootView. This is used when we have RootView + // wrapped inside native components, and is used for the focus traversal. + View* focus_traversable_parent_view_; + +#if defined(OS_WIN) + // Handles dnd for us. + scoped_refptr<RootViewDropTarget> drop_target_; +#endif + + // Storage of strings needed for accessibility. + std::wstring accessible_name_; + + // Tracks drag state for a view. + View::DragInfo drag_info; + + // Valid for the lifetime of StartDragForViewFromMouseEvent, indicates the + // view the drag started from. + View* drag_view_; + +#ifndef NDEBUG + // True if we're currently processing paint. + bool is_processing_paint_; +#endif +}; + +} // namespace views + +#endif // VIEWS_WIDGET_ROOT_VIEW_H_ diff --git a/views/widget/root_view_drop_target.cc b/views/widget/root_view_drop_target.cc new file mode 100644 index 0000000..9c1f087 --- /dev/null +++ b/views/widget/root_view_drop_target.cc @@ -0,0 +1,118 @@ +// Copyright (c) 2006-2008 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/widget/root_view_drop_target.h" + +#include "app/drag_drop_types.h" +#include "base/gfx/point.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" + +namespace views { + +RootViewDropTarget::RootViewDropTarget(RootView* root_view) + : BaseDropTarget(root_view->GetWidget()->GetNativeView()), + root_view_(root_view), + target_view_(NULL), + deepest_view_(NULL) { +} + +RootViewDropTarget::~RootViewDropTarget() { +} + +void RootViewDropTarget::ResetTargetViewIfEquals(View* view) { + if (target_view_ == view) + target_view_ = NULL; + if (deepest_view_ == view) + deepest_view_ = NULL; +} + +DWORD RootViewDropTarget::OnDragOver(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect) { + const OSExchangeData data(data_object); + gfx::Point root_view_location(cursor_position.x, cursor_position.y); + View::ConvertPointToView(NULL, root_view_, &root_view_location); + View* view = CalculateTargetView(root_view_location, data); + + if (view != target_view_) { + // Target changed notify old drag exited, then new drag entered. + if (target_view_) + target_view_->OnDragExited(); + target_view_ = view; + if (target_view_) { + gfx::Point target_view_location(root_view_location); + View::ConvertPointToView(root_view_, target_view_, &target_view_location); + DropTargetEvent enter_event(data, + target_view_location.x(), + target_view_location.y(), + DragDropTypes::DropEffectToDragOperation(effect)); + target_view_->OnDragEntered(enter_event); + } + } + + if (target_view_) { + gfx::Point target_view_location(root_view_location); + View::ConvertPointToView(root_view_, target_view_, &target_view_location); + DropTargetEvent enter_event(data, + target_view_location.x(), + target_view_location.y(), + DragDropTypes::DropEffectToDragOperation(effect)); + int result_operation = target_view_->OnDragUpdated(enter_event); + return DragDropTypes::DragOperationToDropEffect(result_operation); + } else { + return DROPEFFECT_NONE; + } +} + +void RootViewDropTarget::OnDragLeave(IDataObject* data_object) { + if (target_view_) + target_view_->OnDragExited(); + deepest_view_ = target_view_ = NULL; +} + +DWORD RootViewDropTarget::OnDrop(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect) { + const OSExchangeData data(data_object); + DWORD drop_effect = OnDragOver(data_object, key_state, cursor_position, + effect); + View* drop_view = target_view_; + deepest_view_ = target_view_ = NULL; + if (drop_effect != DROPEFFECT_NONE) { + gfx::Point view_location(cursor_position.x, cursor_position.y); + View::ConvertPointToView(NULL, drop_view, &view_location); + DropTargetEvent drop_event(data, view_location.x(), view_location.y(), + DragDropTypes::DropEffectToDragOperation(effect)); + return DragDropTypes::DragOperationToDropEffect( + drop_view->OnPerformDrop(drop_event)); + } else { + if (drop_view) + drop_view->OnDragExited(); + return DROPEFFECT_NONE; + } +} + +View* RootViewDropTarget::CalculateTargetView( + const gfx::Point& root_view_location, + const OSExchangeData& data) { + View* view = root_view_->GetViewForPoint(root_view_location); + if (view == deepest_view_) { + // The view the mouse is over hasn't changed; reuse the target. + return target_view_; + } + // View under mouse changed, which means a new view may want the drop. + // Walk the tree, stopping at target_view_ as we know it'll accept the + // drop. + deepest_view_ = view; + while (view && view != target_view_ && + (!view->IsEnabled() || !view->CanDrop(data))) { + view = view->GetParent(); + } + return view; +} + +} // namespace views diff --git a/views/widget/root_view_drop_target.h b/views/widget/root_view_drop_target.h new file mode 100644 index 0000000..a3c3afd --- /dev/null +++ b/views/widget/root_view_drop_target.h @@ -0,0 +1,75 @@ +// Copyright (c) 2006-2008 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 VIEWS_WIDGET_ROOT_VIEW_DROP_TARGET_H_ +#define VIEWS_WIDGET_ROOT_VIEW_DROP_TARGET_H_ + +#include <atlbase.h> +#include <atlapp.h> +#include <atlmisc.h> + +#include "app/os_exchange_data.h" +#include "base/base_drop_target.h" + +namespace gfx { +class Point; +} + +namespace views { + +class RootView; +class View; + +// RootViewDropTarget takes care of managing drag and drop for the RootView and +// converts Windows OLE drop messages into Views drop messages. +// +// RootViewDropTarget is responsible for determining the appropriate View to +// use during a drag and drop session, and forwarding events to it. +class RootViewDropTarget : public BaseDropTarget { + public: + explicit RootViewDropTarget(RootView* root_view); + virtual ~RootViewDropTarget(); + + // If a drag and drop is underway and view is the current drop target, the + // drop target is set to null. + // This is invoked when a View is removed from the RootView to make sure + // we don't target a view that was removed during dnd. + void ResetTargetViewIfEquals(View* view); + + protected: + virtual DWORD OnDragOver(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect); + + virtual void OnDragLeave(IDataObject* data_object); + + virtual DWORD OnDrop(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect); + + private: + // Calculates the target view for a drop given the specified location in + // the coordinate system of the rootview. This tries to avoid continually + // querying CanDrop by returning target_view_ if the mouse is still over + // target_view_. + View* CalculateTargetView(const gfx::Point& root_view_location, + const OSExchangeData& data); + + // RootView we were created for. + RootView* root_view_; + + // View we're targeting events at. + View* target_view_; + + // The deepest view under the current drop coordinate. + View* deepest_view_; + + DISALLOW_EVIL_CONSTRUCTORS(RootViewDropTarget); +}; + +} // namespace views + +#endif // VIEWS_WIDGET_ROOT_VIEW_DROP_TARGET_H_ diff --git a/views/widget/root_view_gtk.cc b/views/widget/root_view_gtk.cc new file mode 100644 index 0000000..428c695 --- /dev/null +++ b/views/widget/root_view_gtk.cc @@ -0,0 +1,28 @@ +// Copyright (c) 2006-2008 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/widget/root_view.h" + +#include "app/gfx/chrome_canvas.h" +#include "base/logging.h" +#include "skia/include/SkColor.h" + +namespace views { + +void RootView::UpdateCursor(const MouseEvent& e) { + NOTIMPLEMENTED(); +} + +void RootView::OnPaint(GdkEventExpose* event) { + ChromeCanvasPaint canvas(event); + + if (!canvas.isEmpty()) { + SchedulePaint(gfx::Rect(canvas.rectangle()), false); + if (NeedsPainting(false)) { + ProcessPaint(&canvas); + } + } +} + +} diff --git a/views/widget/root_view_win.cc b/views/widget/root_view_win.cc new file mode 100644 index 0000000..ac4a50d --- /dev/null +++ b/views/widget/root_view_win.cc @@ -0,0 +1,70 @@ +// Copyright (c) 2006-2008 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/widget/root_view.h" + +#include "app/drag_drop_types.h" +#include "app/gfx/chrome_canvas.h" +#include "base/base_drag_source.h" +#include "base/logging.h" +#include "views/widget/root_view_drop_target.h" + +namespace views { + +void RootView::UpdateCursor(const MouseEvent& e) { + View *v = GetViewForPoint(e.location()); + + if (v && v != this) { + gfx::Point l(e.location()); + View::ConvertPointToView(this, v, &l); + HCURSOR cursor = v->GetCursorForPoint(e.GetType(), l.x(), l.y()); + if (cursor) { + ::SetCursor(cursor); + return; + } + } + if (previous_cursor_) { + SetCursor(previous_cursor_); + } +} + +void RootView::OnPaint(HWND hwnd) { + gfx::Rect original_dirty_region = GetScheduledPaintRectConstrainedToSize(); + if (!original_dirty_region.IsEmpty()) { + // Invoke InvalidateRect so that the dirty region of the window includes the + // region we need to paint. If we didn't do this and the region didn't + // include the dirty region, ProcessPaint would incorrectly mark everything + // as clean. This can happen if a WM_PAINT is generated by the system before + // the InvokeLater schedule by RootView is processed. + RECT win_version = original_dirty_region.ToRECT(); + InvalidateRect(hwnd, &win_version, FALSE); + } + ChromeCanvasPaint canvas(hwnd); + if (!canvas.isEmpty()) { + const PAINTSTRUCT& ps = canvas.paintStruct(); + SchedulePaint(gfx::Rect(ps.rcPaint), false); + if (NeedsPainting(false)) + ProcessPaint(&canvas); + } +} + +void RootView::StartDragForViewFromMouseEvent( + View* view, + IDataObject* data, + int operation) { + drag_view_ = view; + scoped_refptr<BaseDragSource> drag_source(new BaseDragSource); + DWORD effects; + DoDragDrop(data, drag_source, + DragDropTypes::DragOperationToDropEffect(operation), &effects); + // If the view is removed during the drag operation, drag_view_ is set to + // NULL. + if (drag_view_ == view) { + View* drag_view = drag_view_; + drag_view_ = NULL; + drag_view->OnDragDone(); + } +} + +} diff --git a/views/widget/tooltip_manager.cc b/views/widget/tooltip_manager.cc new file mode 100644 index 0000000..18faf4a --- /dev/null +++ b/views/widget/tooltip_manager.cc @@ -0,0 +1,447 @@ +// Copyright (c) 2006-2008 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/widget/tooltip_manager.h" + +#include <limits> + +#include "app/l10n_util.h" +#include "app/l10n_util_win.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "chrome/common/gfx/text_elider.h" +#include "chrome/common/win_util.h" +#include "views/view.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" + +namespace views { + +//static +int TooltipManager::tooltip_height_ = 0; + +// Default timeout for the tooltip displayed using keyboard. +// Timeout is mentioned in milliseconds. +static const int kDefaultTimeout = 4000; + +// Maximum number of lines we allow in the tooltip. +static const int kMaxLines = 6; + +// Maximum number of characters we allow in a tooltip. +static const int kMaxTooltipLength = 1024; + +// Breaks |text| along line boundaries, placing each line of text into lines. +static void SplitTooltipString(const std::wstring& text, + std::vector<std::wstring>* lines) { + size_t index = 0; + size_t next_index; + while ((next_index = text.find(TooltipManager::GetLineSeparator(), index)) + != std::wstring::npos && lines->size() < kMaxLines) { + lines->push_back(text.substr(index, next_index - index)); + index = next_index + TooltipManager::GetLineSeparator().size(); + } + if (next_index != text.size() && lines->size() < kMaxLines) + lines->push_back(text.substr(index, text.size() - index)); +} + +// static +int TooltipManager::GetTooltipHeight() { + DCHECK(tooltip_height_ > 0); + return tooltip_height_; +} + +static ChromeFont DetermineDefaultFont() { + HWND window = CreateWindowEx( + WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), + TOOLTIPS_CLASS, NULL, 0 , 0, 0, 0, 0, NULL, NULL, NULL, NULL); + HFONT hfont = reinterpret_cast<HFONT>(SendMessage(window, WM_GETFONT, 0, 0)); + ChromeFont font = hfont ? ChromeFont::CreateFont(hfont) : ChromeFont(); + DestroyWindow(window); + return font; +} + +// static +ChromeFont TooltipManager::GetDefaultFont() { + static ChromeFont* font = NULL; + if (!font) + font = new ChromeFont(DetermineDefaultFont()); + return *font; +} + +// static +const std::wstring& TooltipManager::GetLineSeparator() { + static const std::wstring* separator = NULL; + if (!separator) + separator = new std::wstring(L"\r\n"); + return *separator; +} + +TooltipManager::TooltipManager(Widget* widget, HWND parent) + : widget_(widget), + parent_(parent), + last_mouse_x_(-1), + last_mouse_y_(-1), + tooltip_showing_(false), + last_tooltip_view_(NULL), + last_view_out_of_sync_(false), + tooltip_width_(0), + keyboard_tooltip_hwnd_(NULL), +#pragma warning(suppress: 4355) + keyboard_tooltip_factory_(this) { + DCHECK(widget && parent); + Init(); +} + +TooltipManager::~TooltipManager() { + if (tooltip_hwnd_) + DestroyWindow(tooltip_hwnd_); + if (keyboard_tooltip_hwnd_) + DestroyWindow(keyboard_tooltip_hwnd_); +} + +void TooltipManager::Init() { + // Create the tooltip control. + tooltip_hwnd_ = CreateWindowEx( + WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), + TOOLTIPS_CLASS, NULL, TTS_NOPREFIX, 0, 0, 0, 0, + parent_, NULL, NULL, NULL); + + l10n_util::AdjustUIFontForWindow(tooltip_hwnd_); + + // This effectively turns off clipping of tooltips. We need this otherwise + // multi-line text (\r\n) won't work right. The size doesn't really matter + // (just as long as its bigger than the monitor's width) as we clip to the + // screen size before rendering. + SendMessage(tooltip_hwnd_, TTM_SETMAXTIPWIDTH, 0, + std::numeric_limits<short>::max()); + + // Add one tool that is used for all tooltips. + toolinfo_.cbSize = sizeof(toolinfo_); + toolinfo_.uFlags = TTF_TRANSPARENT | TTF_IDISHWND; + toolinfo_.hwnd = parent_; + toolinfo_.uId = reinterpret_cast<UINT_PTR>(parent_); + // Setting this tells windows to call parent_ back (using a WM_NOTIFY + // message) for the actual tooltip contents. + toolinfo_.lpszText = LPSTR_TEXTCALLBACK; + SetRectEmpty(&toolinfo_.rect); + SendMessage(tooltip_hwnd_, TTM_ADDTOOL, 0, (LPARAM)&toolinfo_); +} + +void TooltipManager::UpdateTooltip() { + // Set last_view_out_of_sync_ to indicate the view is currently out of sync. + // This doesn't update the view under the mouse immediately as it may cause + // timing problems. + last_view_out_of_sync_ = true; + last_tooltip_view_ = NULL; + // Hide the tooltip. + SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); +} + +void TooltipManager::TooltipTextChanged(View* view) { + if (view == last_tooltip_view_) + UpdateTooltip(last_mouse_x_, last_mouse_y_); +} + +LRESULT TooltipManager::OnNotify(int w_param, NMHDR* l_param, bool* handled) { + *handled = false; + if (l_param->hwndFrom == tooltip_hwnd_ && keyboard_tooltip_hwnd_ == NULL) { + switch (l_param->code) { + case TTN_GETDISPINFO: { + if (last_view_out_of_sync_) { + // View under the mouse is out of sync, determine it now. + RootView* root_view = widget_->GetRootView(); + last_tooltip_view_ = root_view->GetViewForPoint( + gfx::Point(last_mouse_x_, last_mouse_y_)); + last_view_out_of_sync_ = false; + } + // Tooltip control is asking for the tooltip to display. + NMTTDISPINFOW* tooltip_info = + reinterpret_cast<NMTTDISPINFOW*>(l_param); + // Initialize the string, if we have a valid tooltip the string will + // get reset below. + tooltip_info->szText[0] = TEXT('\0'); + tooltip_text_.clear(); + tooltip_info->lpszText = NULL; + clipped_text_.clear(); + if (last_tooltip_view_ != NULL) { + tooltip_text_.clear(); + // Mouse is over a View, ask the View for it's tooltip. + gfx::Point view_loc(last_mouse_x_, last_mouse_y_); + View::ConvertPointToView(widget_->GetRootView(), + last_tooltip_view_, &view_loc); + if (last_tooltip_view_->GetTooltipText(view_loc.x(), view_loc.y(), + &tooltip_text_) && + !tooltip_text_.empty()) { + // View has a valid tip, copy it into TOOLTIPINFO. + clipped_text_ = tooltip_text_; + TrimTooltipToFit(&clipped_text_, &tooltip_width_, &line_count_, + last_mouse_x_, last_mouse_y_, tooltip_hwnd_); + // Adjust the clipped tooltip text for locale direction. + l10n_util::AdjustStringForLocaleDirection(clipped_text_, + &clipped_text_); + tooltip_info->lpszText = const_cast<WCHAR*>(clipped_text_.c_str()); + } else { + tooltip_text_.clear(); + } + } + *handled = true; + return 0; + } + case TTN_POP: + tooltip_showing_ = false; + *handled = true; + return 0; + case TTN_SHOW: { + *handled = true; + tooltip_showing_ = true; + // The tooltip is about to show, allow the view to position it + gfx::Point text_origin; + if (tooltip_height_ == 0) + tooltip_height_ = CalcTooltipHeight(); + gfx::Point view_loc(last_mouse_x_, last_mouse_y_); + View::ConvertPointToView(widget_->GetRootView(), + last_tooltip_view_, &view_loc); + if (last_tooltip_view_->GetTooltipTextOrigin( + view_loc.x(), view_loc.y(), &text_origin) && + SetTooltipPosition(text_origin.x(), text_origin.y())) { + // Return true, otherwise the rectangle we specified is ignored. + return TRUE; + } + return 0; + } + default: + // Fall through. + break; + } + } + return 0; +} + +bool TooltipManager::SetTooltipPosition(int text_x, int text_y) { + // NOTE: this really only tests that the y location fits on screen, but that + // is good enough for our usage. + + // Calculate the bounds the tooltip will get. + gfx::Point view_loc; + View::ConvertPointToScreen(last_tooltip_view_, &view_loc); + RECT bounds = { view_loc.x() + text_x, + view_loc.y() + text_y, + view_loc.x() + text_x + tooltip_width_, + view_loc.y() + line_count_ * GetTooltipHeight() }; + SendMessage(tooltip_hwnd_, TTM_ADJUSTRECT, TRUE, (LPARAM)&bounds); + + // Make sure the rectangle completely fits on the current monitor. If it + // doesn't, return false so that windows positions the tooltip at the + // default location. + gfx::Rect monitor_bounds = + win_util::GetMonitorBoundsForRect(gfx::Rect(bounds.left,bounds.right, + 0, 0)); + if (!monitor_bounds.Contains(gfx::Rect(bounds))) { + return false; + } + + ::SetWindowPos(tooltip_hwnd_, NULL, bounds.left, bounds.top, 0, 0, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE); + return true; +} + +int TooltipManager::CalcTooltipHeight() { + // Ask the tooltip for it's font. + int height; + HFONT hfont = reinterpret_cast<HFONT>( + SendMessage(tooltip_hwnd_, WM_GETFONT, 0, 0)); + if (hfont != NULL) { + HDC dc = GetDC(tooltip_hwnd_); + HFONT previous_font = static_cast<HFONT>(SelectObject(dc, hfont)); + int last_map_mode = SetMapMode(dc, MM_TEXT); + TEXTMETRIC font_metrics; + GetTextMetrics(dc, &font_metrics); + height = font_metrics.tmHeight; + // To avoid the DC referencing font_handle_, select the previous font. + SelectObject(dc, previous_font); + SetMapMode(dc, last_map_mode); + ReleaseDC(NULL, dc); + } else { + // Tooltip is using the system font. Use ChromeFont, which should pick + // up the system font. + height = ChromeFont().height(); + } + // Get the margins from the tooltip + RECT tooltip_margin; + SendMessage(tooltip_hwnd_, TTM_GETMARGIN, 0, (LPARAM)&tooltip_margin); + return height + tooltip_margin.top + tooltip_margin.bottom; +} + +void TooltipManager::TrimTooltipToFit(std::wstring* text, + int* max_width, + int* line_count, + int position_x, + int position_y, + HWND window) { + *max_width = 0; + *line_count = 0; + + // Clamp the tooltip length to kMaxTooltipLength so that we don't + // accidentally DOS the user with a mega tooltip (since Windows doesn't seem + // to do this itself). + if (text->length() > kMaxTooltipLength) + *text = text->substr(0, kMaxTooltipLength); + + // Determine the available width for the tooltip. + gfx::Point screen_loc(position_x, position_y); + View::ConvertPointToScreen(widget_->GetRootView(), &screen_loc); + gfx::Rect monitor_bounds = + win_util::GetMonitorBoundsForRect(gfx::Rect(screen_loc.x(), + screen_loc.y(), + 0, 0)); + RECT tooltip_margin; + SendMessage(window, TTM_GETMARGIN, 0, (LPARAM)&tooltip_margin); + const int available_width = monitor_bounds.width() - tooltip_margin.left - + tooltip_margin.right; + if (available_width <= 0) + return; + + // Split the string. + std::vector<std::wstring> lines; + SplitTooltipString(*text, &lines); + *line_count = static_cast<int>(lines.size()); + + // Format each line to fit. + ChromeFont font = GetDefaultFont(); + std::wstring result; + for (std::vector<std::wstring>::iterator i = lines.begin(); i != lines.end(); + ++i) { + std::wstring elided_text = gfx::ElideText(*i, font, available_width); + *max_width = std::max(*max_width, font.GetStringWidth(elided_text)); + if (i == lines.begin() && i + 1 == lines.end()) { + *text = elided_text; + return; + } + if (!result.empty()) + result.append(GetLineSeparator()); + result.append(elided_text); + } + *text = result; +} + +void TooltipManager::UpdateTooltip(int x, int y) { + RootView* root_view = widget_->GetRootView(); + View* view = root_view->GetViewForPoint(gfx::Point(x, y)); + if (view != last_tooltip_view_) { + // NOTE: This *must* be sent regardless of the visibility of the tooltip. + // It triggers Windows to ask for the tooltip again. + SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); + last_tooltip_view_ = view; + } else if (last_tooltip_view_ != NULL) { + // Tooltip is showing, and mouse is over the same view. See if the tooltip + // text has changed. + gfx::Point view_point(x, y); + View::ConvertPointToView(root_view, last_tooltip_view_, &view_point); + std::wstring new_tooltip_text; + if (last_tooltip_view_->GetTooltipText(view_point.x(), view_point.y(), + &new_tooltip_text) && + new_tooltip_text != tooltip_text_) { + // The text has changed, hide the popup. + SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); + if (!new_tooltip_text.empty() && tooltip_showing_) { + // New text is valid, show the popup. + SendMessage(tooltip_hwnd_, TTM_POPUP, 0, 0); + } + } + } +} + +void TooltipManager::OnMouse(UINT u_msg, WPARAM w_param, LPARAM l_param) { + int x = GET_X_LPARAM(l_param); + int y = GET_Y_LPARAM(l_param); + + if (u_msg >= WM_NCMOUSEMOVE && u_msg <= WM_NCXBUTTONDBLCLK) { + // NC message coordinates are in screen coordinates. + gfx::Rect frame_bounds; + widget_->GetBounds(&frame_bounds, true); + x -= frame_bounds.x(); + y -= frame_bounds.y(); + } + + if (u_msg != WM_MOUSEMOVE || last_mouse_x_ != x || last_mouse_y_ != y) { + last_mouse_x_ = x; + last_mouse_y_ = y; + HideKeyboardTooltip(); + UpdateTooltip(x, y); + } + // Forward the message onto the tooltip. + MSG msg; + msg.hwnd = parent_; + msg.message = u_msg; + msg.wParam = w_param; + msg.lParam = l_param; + SendMessage(tooltip_hwnd_, TTM_RELAYEVENT, 0, (LPARAM)&msg); +} + +void TooltipManager::ShowKeyboardTooltip(View* focused_view) { + if (tooltip_showing_) { + SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); + tooltip_text_.clear(); + } + HideKeyboardTooltip(); + std::wstring tooltip_text; + if (!focused_view->GetTooltipText(0, 0, &tooltip_text)) + return; + gfx::Rect focused_bounds = focused_view->bounds(); + gfx::Point screen_point; + focused_view->ConvertPointToScreen(focused_view, &screen_point); + gfx::Point relative_point_coordinates; + focused_view->ConvertPointToWidget(focused_view, &relative_point_coordinates); + keyboard_tooltip_hwnd_ = CreateWindowEx( + WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), + TOOLTIPS_CLASS, NULL, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL); + SendMessage(keyboard_tooltip_hwnd_, TTM_SETMAXTIPWIDTH, 0, + std::numeric_limits<short>::max()); + int tooltip_width; + int line_count; + TrimTooltipToFit(&tooltip_text, &tooltip_width, &line_count, + relative_point_coordinates.x(), + relative_point_coordinates.y(), keyboard_tooltip_hwnd_); + TOOLINFO keyboard_toolinfo; + memset(&keyboard_toolinfo, 0, sizeof(keyboard_toolinfo)); + keyboard_toolinfo.cbSize = sizeof(keyboard_toolinfo); + keyboard_toolinfo.hwnd = parent_; + keyboard_toolinfo.uFlags = TTF_TRACK | TTF_TRANSPARENT | TTF_IDISHWND ; + keyboard_toolinfo.lpszText = const_cast<WCHAR*>(tooltip_text.c_str()); + SendMessage(keyboard_tooltip_hwnd_, TTM_ADDTOOL, 0, + reinterpret_cast<LPARAM>(&keyboard_toolinfo)); + SendMessage(keyboard_tooltip_hwnd_, TTM_TRACKACTIVATE, TRUE, + reinterpret_cast<LPARAM>(&keyboard_toolinfo)); + if (!tooltip_height_) + tooltip_height_ = CalcTooltipHeight(); + RECT rect_bounds = {screen_point.x(), + screen_point.y() + focused_bounds.height(), + screen_point.x() + tooltip_width, + screen_point.y() + focused_bounds.height() + + line_count * tooltip_height_ }; + gfx::Rect monitor_bounds = + win_util::GetMonitorBoundsForRect(gfx::Rect(rect_bounds)); + rect_bounds = gfx::Rect(rect_bounds).AdjustToFit(monitor_bounds).ToRECT(); + ::SetWindowPos(keyboard_tooltip_hwnd_, NULL, rect_bounds.left, + rect_bounds.top, 0, 0, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE); + MessageLoop::current()->PostDelayedTask(FROM_HERE, + keyboard_tooltip_factory_.NewRunnableMethod( + &TooltipManager::DestroyKeyboardTooltipWindow, keyboard_tooltip_hwnd_), + kDefaultTimeout); +} + +void TooltipManager::HideKeyboardTooltip() { + if (keyboard_tooltip_hwnd_ != NULL) { + SendMessage(keyboard_tooltip_hwnd_, WM_CLOSE, 0, 0); + keyboard_tooltip_hwnd_ = NULL; + } +} + +void TooltipManager::DestroyKeyboardTooltipWindow(HWND window_to_destroy) { + if (keyboard_tooltip_hwnd_ == window_to_destroy) + HideKeyboardTooltip(); +} + +} // namespace views diff --git a/views/widget/tooltip_manager.h b/views/widget/tooltip_manager.h new file mode 100644 index 0000000..3d5620d --- /dev/null +++ b/views/widget/tooltip_manager.h @@ -0,0 +1,168 @@ +// Copyright (c) 2006-2008 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 VIEWS_WIDGET_TOOLTIP_MANAGER_H_ +#define VIEWS_WIDGET_TOOLTIP_MANAGER_H_ + +#include <windows.h> +#include <commctrl.h> + +#include <string> +#include "base/basictypes.h" +#include "base/task.h" + +class ChromeFont; + +namespace views { + +class View; +class Widget; + +// TooltipManager takes care of the wiring to support tooltips for Views. +// This class is intended to be used by Widgets. To use this, you must +// do the following: +// Add the following to your MSG_MAP: +// +// MESSAGE_RANGE_HANDLER(WM_MOUSEFIRST, WM_MOUSELAST, OnMouseRange) +// MESSAGE_RANGE_HANDLER(WM_NCMOUSEMOVE, WM_NCMOUSEMOVE, OnMouseRange) +// MSG_WM_NOTIFY(OnNotify) +// +// With the following implementations: +// LRESULT XXX::OnMouseRange(UINT u_msg, WPARAM w_param, LPARAM l_param, +// BOOL& handled) { +// tooltip_manager_->OnMouse(u_msg, w_param, l_param); +// handled = FALSE; +// return 0; +// } +// +// LRESULT XXX::OnNotify(int w_param, NMHDR* l_param) { +// bool handled; +// LRESULT result = tooltip_manager_->OnNotify(w_param, l_param, &handled); +// SetMsgHandled(handled); +// return result; +// } +// +// And of course you'll need to create the TooltipManager! +// +// Lastly, you'll need to override GetTooltipManager. +// +// See XPFrame for an example of this in action. +class TooltipManager { + public: + // Returns the height of tooltips. This should only be invoked from within + // GetTooltipTextOrigin. + static int GetTooltipHeight(); + + // Returns the default font used by tooltips. + static ChromeFont GetDefaultFont(); + + // Returns the separator for lines of text in a tooltip. + static const std::wstring& GetLineSeparator(); + + // Creates a TooltipManager for the specified Widget and parent window. + TooltipManager(Widget* widget, HWND parent); + virtual ~TooltipManager(); + + // Notification that the view hierarchy has changed in some way. + void UpdateTooltip(); + + // Invoked when the tooltip text changes for the specified views. + void TooltipTextChanged(View* view); + + // Invoked when toolbar icon gets focus. + void ShowKeyboardTooltip(View* view); + + // Invoked when toolbar loses focus. + void HideKeyboardTooltip(); + + // Message handlers. These forward to the tooltip control. + virtual void OnMouse(UINT u_msg, WPARAM w_param, LPARAM l_param); + LRESULT OnNotify(int w_param, NMHDR* l_param, bool* handled); + // Not used directly by TooltipManager, but provided for AeroTooltipManager. + virtual void OnMouseLeave() {} + + protected: + virtual void Init(); + + // Updates the tooltip for the specified location. + void UpdateTooltip(int x, int y); + + // Parent window the tooltip is added to. + HWND parent_; + + // Tooltip control window. + HWND tooltip_hwnd_; + + // Tooltip information. + TOOLINFO toolinfo_; + + // Last location of the mouse. This is in the coordinates of the rootview. + int last_mouse_x_; + int last_mouse_y_; + + // Whether or not the tooltip is showing. + bool tooltip_showing_; + + private: + // Sets the tooltip position based on the x/y position of the text. If the + // tooltip fits, true is returned. + bool SetTooltipPosition(int text_x, int text_y); + + // Calculates the preferred height for tooltips. This always returns a + // positive value. + int CalcTooltipHeight(); + + // Trims the tooltip to fit, setting text to the clipped result, width to the + // width (in pixels) of the clipped text and line_count to the number of lines + // of text in the tooltip. + void TrimTooltipToFit(std::wstring* text, + int* width, + int* line_count, + int position_x, + int position_y, + HWND window); + + // Invoked when the timer elapses and tooltip has to be destroyed. + void DestroyKeyboardTooltipWindow(HWND window_to_destroy); + + // Hosting Widget. + Widget* widget_; + + // The View the mouse is under. This is null if the mouse isn't under a + // View. + View* last_tooltip_view_; + + // Whether or not the view under the mouse needs to be refreshed. If this + // is true, when the tooltip is asked for the view under the mouse is + // refreshed. + bool last_view_out_of_sync_; + + // Text for tooltip from the view. + std::wstring tooltip_text_; + + // The clipped tooltip. + std::wstring clipped_text_; + + // Number of lines in the tooltip. + int line_count_; + + // Width of the last tooltip. + int tooltip_width_; + + // Height for a tooltip; lazily calculated. + static int tooltip_height_; + + // control window for tooltip displayed using keyboard. + HWND keyboard_tooltip_hwnd_; + + // Used to register DestroyTooltipWindow function with PostDelayedTask + // function. + ScopedRunnableMethodFactory<TooltipManager> keyboard_tooltip_factory_; + + DISALLOW_EVIL_CONSTRUCTORS(TooltipManager); +}; + +} // namespace views + +#endif // VIEWS_WIDGET_TOOLTIP_MANAGER_H_ diff --git a/views/widget/widget.h b/views/widget/widget.h new file mode 100644 index 0000000..8dd3f2e --- /dev/null +++ b/views/widget/widget.h @@ -0,0 +1,81 @@ +// Copyright (c) 2006-2008 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 VIEWS_WIDGET_WIDGET_H_ +#define VIEWS_WIDGET_WIDGET_H_ + +#include "base/gfx/native_widget_types.h" + +namespace gfx { +class Rect; +} + +namespace views { + +class Accelerator; +class RootView; +class TooltipManager; +class Window; + +//////////////////////////////////////////////////////////////////////////////// +// +// Widget interface +// +// Widget is an abstract class that defines the API that should be implemented +// by a native window in order to host a view hierarchy. +// +// Widget wraps a hierarchy of View objects (see view.h) that implement +// painting and flexible layout within the bounds of the Widget's window. +// +// The Widget is responsible for handling various system events and forwarding +// them to the appropriate view. +// +///////////////////////////////////////////////////////////////////////////// + +class Widget { + public: + virtual ~Widget() { } + + // Returns the bounds of this Widget in the screen coordinate system. + // If the receiving Widget is a frame which is larger than its client area, + // this method returns the client area if including_frame is false and the + // frame bounds otherwise. If the receiving Widget is not a frame, + // including_frame is ignored. + virtual void GetBounds(gfx::Rect* out, bool including_frame) const = 0; + + // Returns the gfx::NativeView associated with this Widget. + virtual gfx::NativeView GetNativeView() const = 0; + + // Forces a paint of a specified rectangle immediately. + virtual void PaintNow(const gfx::Rect& update_rect) = 0; + + // Returns the RootView contained by this Widget. + virtual RootView* GetRootView() = 0; + + // Returns whether the Widget is visible to the user. + virtual bool IsVisible() const = 0; + + // Returns whether the Widget is the currently active window. + virtual bool IsActive() const = 0; + + // Returns the TooltipManager for this Widget. If this Widget does not support + // tooltips, NULL is returned. + virtual TooltipManager* GetTooltipManager() { + return NULL; + } + + // Returns the accelerator given a command id. Returns false if there is + // no accelerator associated with a given id, which is a common condition. + virtual bool GetAccelerator(int cmd_id, + Accelerator* accelerator) = 0; + + // Returns the Window containing this Widget, or NULL if not contained in a + // window. + virtual Window* GetWindow() { return NULL; } + virtual const Window* GetWindow() const { return NULL; } +}; + +} // namespace views + +#endif // VIEWS_WIDGET_WIDGET_H_ diff --git a/views/widget/widget_gtk.cc b/views/widget/widget_gtk.cc new file mode 100644 index 0000000..25869f6 --- /dev/null +++ b/views/widget/widget_gtk.cc @@ -0,0 +1,386 @@ +// Copyright (c) 2009 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/widget/widget_gtk.h" + +#include "views/fill_layout.h" +#include "views/widget/root_view.h" + +namespace views { + +WidgetGtk::WidgetGtk() + : widget_(NULL), + is_mouse_down_(false), + last_mouse_event_was_move_(false) { +} + +WidgetGtk::~WidgetGtk() { + gtk_widget_unref(widget_); + + // MessageLoopForUI::current()->RemoveObserver(this); +} + +void WidgetGtk::Init(const gfx::Rect& bounds, + bool has_own_focus_manager) { + + // Force creation of the RootView if it hasn't been created yet. + GetRootView(); + + // Make container here. + widget_ = gtk_drawing_area_new(); + gtk_drawing_area_size(GTK_DRAWING_AREA(widget_), 100, 100); + gtk_widget_show(widget_); + + // Make sure we receive our motion events. + gtk_widget_set_events(widget_, + gtk_widget_get_events(widget_) | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK); + + root_view_->OnWidgetCreated(); + + // TODO(port): if(has_own_focus_manager) block + + SetViewForNative(widget_, this); + SetRootViewForWidget(widget_, root_view_.get()); + + // MessageLoopForUI::current()->AddObserver(this); + + g_signal_connect_after(G_OBJECT(widget_), "size_allocate", + G_CALLBACK(CallSizeAllocate), NULL); + g_signal_connect(G_OBJECT(widget_), "expose_event", + G_CALLBACK(CallPaint), NULL); + g_signal_connect(G_OBJECT(widget_), "enter_notify_event", + G_CALLBACK(CallEnterNotify), NULL); + g_signal_connect(G_OBJECT(widget_), "leave_notify_event", + G_CALLBACK(CallLeaveNotify), NULL); + g_signal_connect(G_OBJECT(widget_), "motion_notify_event", + G_CALLBACK(CallMotionNotify), NULL); + g_signal_connect(G_OBJECT(widget_), "button_press_event", + G_CALLBACK(CallButtonPress), NULL); + g_signal_connect(G_OBJECT(widget_), "button_release_event", + G_CALLBACK(CallButtonRelease), NULL); + g_signal_connect(G_OBJECT(widget_), "focus_in_event", + G_CALLBACK(CallFocusIn), NULL); + g_signal_connect(G_OBJECT(widget_), "focus_out_event", + G_CALLBACK(CallFocusOut), NULL); + g_signal_connect(G_OBJECT(widget_), "key_press_event", + G_CALLBACK(CallKeyPress), NULL); + g_signal_connect(G_OBJECT(widget_), "key_release_event", + G_CALLBACK(CallKeyRelease), NULL); + g_signal_connect(G_OBJECT(widget_), "scroll_event", + G_CALLBACK(CallScroll), NULL); + g_signal_connect(G_OBJECT(widget_), "visibility_notify_event", + G_CALLBACK(CallVisibilityNotify), NULL); + + // TODO(erg): Ignore these signals for now because they're such a drag. + // + // g_signal_connect(G_OBJECT(widget_), "drag_motion", + // G_CALLBACK(drag_motion_event_cb), NULL); + // g_signal_connect(G_OBJECT(widget_), "drag_leave", + // G_CALLBACK(drag_leave_event_cb), NULL); + // g_signal_connect(G_OBJECT(widget_), "drag_drop", + // G_CALLBACK(drag_drop_event_cb), NULL); + // g_signal_connect(G_OBJECT(widget_), "drag_data_received", + // G_CALLBACK(drag_data_received_event_cb), NULL); +} + +void WidgetGtk::SetContentsView(View* view) { + DCHECK(view && widget_) << "Can't be called until after the HWND is created!"; + // The ContentsView must be set up _after_ the window is created so that its + // Widget pointer is valid. + root_view_->SetLayoutManager(new FillLayout); + if (root_view_->GetChildViewCount() != 0) + root_view_->RemoveAllChildViews(true); + root_view_->AddChildView(view); + + // TODO(erg): Terrible hack to work around lack of real sizing mechanics for + // now. + root_view_->SetBounds(0, 0, 100, 100); + root_view_->Layout(); + root_view_->SchedulePaint(); + NOTIMPLEMENTED(); +} + +void WidgetGtk::GetBounds(gfx::Rect* out, bool including_frame) const { + if (including_frame) { + NOTIMPLEMENTED(); + *out = gfx::Rect(); + return; + } + + // TODO(erg): Not sure how to implement this. gtk_widget_size_request() + // returns a widget's requested size--not it's actual size. The system of + // containers and such do auto sizing tricks to make everything work within + // the constraints and requested sizes... + NOTIMPLEMENTED(); +} + +gfx::NativeView WidgetGtk::GetNativeView() const { + return widget_; +} + +void WidgetGtk::PaintNow(const gfx::Rect& update_rect) { + // TODO(erg): This is woefully incomplete and is a straw man implementation. + gtk_widget_queue_draw_area(widget_, update_rect.x(), update_rect.y(), + update_rect.width(), update_rect.height()); +} + +RootView* WidgetGtk::GetRootView() { + if (!root_view_.get()) { + // First time the root view is being asked for, create it now. + root_view_.reset(CreateRootView()); + } + return root_view_.get(); +} + +bool WidgetGtk::IsVisible() const { + return GTK_WIDGET_VISIBLE(widget_); +} + +bool WidgetGtk::IsActive() const { + NOTIMPLEMENTED(); + return false; +} + +TooltipManager* WidgetGtk::GetTooltipManager() { + NOTIMPLEMENTED(); + return NULL; +} + +bool WidgetGtk::GetAccelerator(int cmd_id, Accelerator* accelerator) { + NOTIMPLEMENTED(); + return false; +} + +gboolean WidgetGtk::OnMotionNotify(GtkWidget* widget, GdkEventMotion* event) { + gfx::Point screen_loc(event->x_root, event->y_root); + if (last_mouse_event_was_move_ && last_mouse_move_x_ == screen_loc.x() && + last_mouse_move_y_ == screen_loc.y()) { + // Don't generate a mouse event for the same location as the last. + return false; + } + last_mouse_move_x_ = screen_loc.x(); + last_mouse_move_y_ = screen_loc.y(); + last_mouse_event_was_move_ = true; + MouseEvent mouse_move(Event::ET_MOUSE_MOVED, + event->x, + event->y, + Event::GetFlagsFromGdkState(event->state)); + root_view_->OnMouseMoved(mouse_move); + return true; +} + +gboolean WidgetGtk::OnButtonPress(GtkWidget* widget, GdkEventButton* event) { + return ProcessMousePressed(event); +} + +gboolean WidgetGtk::OnButtonRelease(GtkWidget* widget, GdkEventButton* event) { + ProcessMouseReleased(event); + return true; +} + +gboolean WidgetGtk::OnPaint(GtkWidget* widget, GdkEventExpose* event) { + root_view_->OnPaint(event); + return true; +} + +gboolean WidgetGtk::OnEnterNotify(GtkWidget* widget, GdkEventCrossing* event) { + // TODO(port): We may not actually need this message; it looks like + // OnNotificationNotify() takes care of this case... + return false; +} + +gboolean WidgetGtk::OnLeaveNotify(GtkWidget* widget, GdkEventCrossing* event) { + last_mouse_event_was_move_ = false; + root_view_->ProcessOnMouseExited(); + return true; +} + +gboolean WidgetGtk::OnKeyPress(GtkWidget* widget, GdkEventKey* event) { + KeyEvent key_event(event); + return root_view_->ProcessKeyEvent(key_event); +} + +gboolean WidgetGtk::OnKeyRelease(GtkWidget* widget, GdkEventKey* event) { + KeyEvent key_event(event); + return root_view_->ProcessKeyEvent(key_event); +} + +RootView* WidgetGtk::CreateRootView() { + return new RootView(this); +} + +bool WidgetGtk::ProcessMousePressed(GdkEventButton* event) { + last_mouse_event_was_move_ = false; + MouseEvent mouse_pressed(Event::ET_MOUSE_PRESSED, + event->x, event->y, +// (dbl_click ? MouseEvent::EF_IS_DOUBLE_CLICK : 0) | + Event::GetFlagsFromGdkState(event->state)); + if (root_view_->OnMousePressed(mouse_pressed)) { + is_mouse_down_ = true; + // TODO(port): Enable this once I figure out what capture is. + // if (!has_capture_) { + // SetCapture(); + // has_capture_ = true; + // current_action_ = FA_FORWARDING; + // } + return true; + } + + return false; +} + +void WidgetGtk::ProcessMouseReleased(GdkEventButton* event) { + last_mouse_event_was_move_ = false; + MouseEvent mouse_up(Event::ET_MOUSE_RELEASED, + event->x, event->y, + Event::GetFlagsFromGdkState(event->state)); + // Release the capture first, that way we don't get confused if + // OnMouseReleased blocks. + // + // TODO(port): Enable this once I figure out what capture is. + // if (has_capture_ && ReleaseCaptureOnMouseReleased()) { + // has_capture_ = false; + // current_action_ = FA_NONE; + // ReleaseCapture(); + // } + is_mouse_down_ = false; + root_view_->OnMouseReleased(mouse_up, false); +} + +// static +WidgetGtk* WidgetGtk::GetViewForNative(GtkWidget* widget) { + gpointer user_data = g_object_get_data(G_OBJECT(widget), "chrome-views"); + return static_cast<WidgetGtk*>(user_data); +} + +// static +void WidgetGtk::SetViewForNative(GtkWidget* widget, WidgetGtk* view) { + g_object_set_data(G_OBJECT(widget), "chrome-views", view); +} + +// static +RootView* WidgetGtk::GetRootViewForWidget(GtkWidget* widget) { + gpointer user_data = g_object_get_data(G_OBJECT(widget), "root-view"); + return static_cast<RootView*>(user_data); +} + +// static +void WidgetGtk::SetRootViewForWidget(GtkWidget* widget, RootView* root_view) { + g_object_set_data(G_OBJECT(widget), "root-view", root_view); +} + +// static +void WidgetGtk::CallSizeAllocate(GtkWidget* widget, GtkAllocation* allocation) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return; + + widget_gtk->OnSizeAllocate(widget, allocation); +} + +gboolean WidgetGtk::CallPaint(GtkWidget* widget, GdkEventExpose* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnPaint(widget, event); +} + +gboolean WidgetGtk::CallEnterNotify(GtkWidget* widget, GdkEventCrossing* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnEnterNotify(widget, event); +} + +gboolean WidgetGtk::CallLeaveNotify(GtkWidget* widget, GdkEventCrossing* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnLeaveNotify(widget, event); +} + +gboolean WidgetGtk::CallMotionNotify(GtkWidget* widget, GdkEventMotion* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnMotionNotify(widget, event); +} + +gboolean WidgetGtk::CallButtonPress(GtkWidget* widget, GdkEventButton* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnButtonPress(widget, event); +} + +gboolean WidgetGtk::CallButtonRelease(GtkWidget* widget, GdkEventButton* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnButtonRelease(widget, event); +} + +gboolean WidgetGtk::CallFocusIn(GtkWidget* widget, GdkEventFocus* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnFocusIn(widget, event); +} + +gboolean WidgetGtk::CallFocusOut(GtkWidget* widget, GdkEventFocus* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnFocusOut(widget, event); +} + +gboolean WidgetGtk::CallKeyPress(GtkWidget* widget, GdkEventKey* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnKeyPress(widget, event); +} + +gboolean WidgetGtk::CallKeyRelease(GtkWidget* widget, GdkEventKey* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnKeyRelease(widget, event); +} + +gboolean WidgetGtk::CallScroll(GtkWidget* widget, GdkEventScroll* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnScroll(widget, event); +} + +gboolean WidgetGtk::CallVisibilityNotify(GtkWidget* widget, + GdkEventVisibility* event) { + WidgetGtk* widget_gtk = GetViewForNative(widget); + if (!widget_gtk) + return false; + + return widget_gtk->OnVisibilityNotify(widget, event); +} + +} diff --git a/views/widget/widget_gtk.h b/views/widget/widget_gtk.h new file mode 100644 index 0000000..4bbc0dd --- /dev/null +++ b/views/widget/widget_gtk.h @@ -0,0 +1,125 @@ +// Copyright (c) 2009 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 VIEWS_WIDGET_WIDGET_GTK_H_ +#define VIEWS_WIDGET_WIDGET_GTK_H_ + +#include <gtk/gtk.h> + +#include "base/message_loop.h" +#include "views/widget/widget.h" + +namespace gfx { +class Rect; +} + +namespace views { + +class View; + +class WidgetGtk : public Widget { + public: + static WidgetGtk* Construct() { + // This isn't used, but exists to force WidgetGtk to be instantiable. + return new WidgetGtk; + } + + WidgetGtk(); + virtual ~WidgetGtk(); + + // Initializes this widget and returns the gtk drawing area for the caller to + // add to its hierarchy. (We can't pass in the parent to this method because + // there are no standard adding semantics in gtk...) + void Init(const gfx::Rect& bounds, bool has_own_focus_manager); + + virtual void SetContentsView(View* view); + + // Overridden from Widget: + virtual void GetBounds(gfx::Rect* out, bool including_frame) const; + virtual gfx::NativeView GetNativeView() const; + virtual void PaintNow(const gfx::Rect& update_rect); + virtual RootView* GetRootView(); + virtual bool IsVisible() const; + virtual bool IsActive() const; + virtual TooltipManager* GetTooltipManager(); + virtual bool GetAccelerator(int cmd_id, Accelerator* accelerator); + + protected: + virtual void OnSizeAllocate(GtkWidget* widget, GtkAllocation* allocation) {} + virtual gboolean OnPaint(GtkWidget* widget, GdkEventExpose* event); + virtual gboolean OnEnterNotify(GtkWidget* widget, GdkEventCrossing* event); + virtual gboolean OnLeaveNotify(GtkWidget* widget, GdkEventCrossing* event); + virtual gboolean OnMotionNotify(GtkWidget* widget, GdkEventMotion* event); + virtual gboolean OnButtonPress(GtkWidget* widget, GdkEventButton* event); + virtual gboolean OnButtonRelease(GtkWidget* widget, GdkEventButton* event); + virtual gboolean OnFocusIn(GtkWidget* widget, GdkEventFocus* event) { + return false; + } + virtual gboolean OnFocusOut(GtkWidget* widget, GdkEventFocus* event) { + return false; + } + virtual gboolean OnKeyPress(GtkWidget* widget, GdkEventKey* event); + virtual gboolean OnKeyRelease(GtkWidget* widget, GdkEventKey* event); + virtual gboolean OnScroll(GtkWidget* widget, GdkEventScroll* event) { + return false; + } + virtual gboolean OnVisibilityNotify(GtkWidget* widget, + GdkEventVisibility* event) { + return false; + } + + private: + virtual RootView* CreateRootView(); + + // Process a mouse click + bool ProcessMousePressed(GdkEventButton* event); + void ProcessMouseReleased(GdkEventButton* event); + + // Sets and retrieves the WidgetGtk in the userdata section of the widget. + static WidgetGtk* GetViewForNative(GtkWidget* widget); + static void SetViewForNative(GtkWidget* widget, WidgetGtk* view); + + static RootView* GetRootViewForWidget(GtkWidget* widget); + static void SetRootViewForWidget(GtkWidget* widget, RootView* root_view); + + // A set of static signal handlers that bridge + static void CallSizeAllocate(GtkWidget* widget, GtkAllocation* allocation); + static gboolean CallPaint(GtkWidget* widget, GdkEventExpose* event); + static gboolean CallEnterNotify(GtkWidget* widget, GdkEventCrossing* event); + static gboolean CallLeaveNotify(GtkWidget* widget, GdkEventCrossing* event); + static gboolean CallMotionNotify(GtkWidget* widget, GdkEventMotion* event); + static gboolean CallButtonPress(GtkWidget* widget, GdkEventButton* event); + static gboolean CallButtonRelease(GtkWidget* widget, GdkEventButton* event); + static gboolean CallFocusIn(GtkWidget* widget, GdkEventFocus* event); + static gboolean CallFocusOut(GtkWidget* widget, GdkEventFocus* event); + static gboolean CallKeyPress(GtkWidget* widget, GdkEventKey* event); + static gboolean CallKeyRelease(GtkWidget* widget, GdkEventKey* event); + static gboolean CallScroll(GtkWidget* widget, GdkEventScroll* event); + static gboolean CallVisibilityNotify(GtkWidget* widget, + GdkEventVisibility* event); + + // Our native view. + GtkWidget* widget_; + + // The root of the View hierarchy attached to this window. + scoped_ptr<RootView> root_view_; + + // If true, the mouse is currently down. + bool is_mouse_down_; + + // The following are used to detect duplicate mouse move events and not + // deliver them. Displaying a window may result in the system generating + // duplicate move events even though the mouse hasn't moved. + + // If true, the last event was a mouse move event. + bool last_mouse_event_was_move_; + + // Coordinates of the last mouse move event, in screen coordinates. + int last_mouse_move_x_; + int last_mouse_move_y_; +}; + +} // namespace views + +#endif // VIEWS_WIDGET_WIDGET_GTK_H_ diff --git a/views/widget/widget_win.cc b/views/widget/widget_win.cc new file mode 100644 index 0000000..2bc9b0c --- /dev/null +++ b/views/widget/widget_win.cc @@ -0,0 +1,999 @@ +// Copyright (c) 2006-2008 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/widget/widget_win.h" + +#include "app/gfx/chrome_canvas.h" +#include "base/gfx/native_theme.h" +#include "base/string_util.h" +#include "base/win_util.h" +#include "chrome/app/chrome_dll_resource.h" +#include "chrome/common/win_util.h" +#include "views/accessibility/view_accessibility.h" +#include "views/controls/native_control_win.h" +#include "views/fill_layout.h" +#include "views/focus/focus_util_win.h" +#include "views/widget/aero_tooltip_manager.h" +#include "views/widget/root_view.h" +#include "views/window/window_win.h" + +namespace views { + +static const DWORD kWindowDefaultChildStyle = + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; +static const DWORD kWindowDefaultStyle = WS_OVERLAPPEDWINDOW; +static const DWORD kWindowDefaultExStyle = 0; + +// Property used to link the HWND to its RootView. +static const wchar_t* const kRootViewWindowProperty = L"__ROOT_VIEW__"; + +bool SetRootViewForHWND(HWND hwnd, RootView* root_view) { + return ::SetProp(hwnd, kRootViewWindowProperty, root_view) ? true : false; +} + +RootView* GetRootViewForHWND(HWND hwnd) { + return reinterpret_cast<RootView*>(::GetProp(hwnd, kRootViewWindowProperty)); +} + +NativeControlWin* GetNativeControlWinForHWND(HWND hwnd) { + return reinterpret_cast<NativeControlWin*>( + ::GetProp(hwnd, NativeControlWin::kNativeControlWinKey)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Window class tracking. + +// static +const wchar_t* const WidgetWin::kBaseClassName = + L"Chrome_WidgetWin_"; + +// Window class information used for registering unique windows. +struct ClassInfo { + UINT style; + HBRUSH background; + + explicit ClassInfo(int style) + : style(style), + background(NULL) {} + + // Compares two ClassInfos. Returns true if all members match. + bool Equals(const ClassInfo& other) const { + return (other.style == style && other.background == background); + } +}; + +class ClassRegistrar { + public: + ~ClassRegistrar() { + for (RegisteredClasses::iterator i = registered_classes_.begin(); + i != registered_classes_.end(); ++i) { + UnregisterClass(i->name.c_str(), NULL); + } + } + + // Puts the name for the class matching |class_info| in |class_name|, creating + // a new name if the class is not yet known. + // Returns true if this class was already known, false otherwise. + bool RetrieveClassName(const ClassInfo& class_info, std::wstring* name) { + for (RegisteredClasses::const_iterator i = registered_classes_.begin(); + i != registered_classes_.end(); ++i) { + if (class_info.Equals(i->info)) { + name->assign(i->name); + return true; + } + } + + name->assign(std::wstring(WidgetWin::kBaseClassName) + + IntToWString(registered_count_++)); + return false; + } + + void RegisterClass(const ClassInfo& class_info, + const std::wstring& name, + ATOM atom) { + registered_classes_.push_back(RegisteredClass(class_info, name, atom)); + } + + private: + // Represents a registered window class. + struct RegisteredClass { + RegisteredClass(const ClassInfo& info, + const std::wstring& name, + ATOM atom) + : info(info), + name(name), + atom(atom) { + } + + // Info used to create the class. + ClassInfo info; + + // The name given to the window. + std::wstring name; + + // The ATOM returned from creating the window. + ATOM atom; + }; + + ClassRegistrar() : registered_count_(0) { } + friend struct DefaultSingletonTraits<ClassRegistrar>; + + typedef std::list<RegisteredClass> RegisteredClasses; + RegisteredClasses registered_classes_; + + // Counter of how many classes have ben registered so far. + int registered_count_; + + DISALLOW_COPY_AND_ASSIGN(ClassRegistrar); +}; + +/////////////////////////////////////////////////////////////////////////////// +// WidgetWin, public + +WidgetWin::WidgetWin() + : close_widget_factory_(this), + active_mouse_tracking_flags_(0), + has_capture_(false), + current_action_(FA_NONE), + window_style_(0), + window_ex_style_(kWindowDefaultExStyle), + use_layered_buffer_(true), + layered_alpha_(255), + delete_on_destroy_(true), + can_update_layered_window_(true), + last_mouse_event_was_move_(false), + is_mouse_down_(false), + is_window_(false), + class_style_(CS_DBLCLKS), + hwnd_(NULL) { +} + +WidgetWin::~WidgetWin() { + MessageLoopForUI::current()->RemoveObserver(this); +} + +void WidgetWin::Init(HWND parent, const gfx::Rect& bounds, + bool has_own_focus_manager) { + if (window_style_ == 0) + window_style_ = parent ? kWindowDefaultChildStyle : kWindowDefaultStyle; + + // See if the style has been overridden. + opaque_ = !(window_ex_style_ & WS_EX_TRANSPARENT); + use_layered_buffer_ = (use_layered_buffer_ && + !!(window_ex_style_ & WS_EX_LAYERED)); + + // Force creation of the RootView if it hasn't been created yet. + GetRootView(); + + // Ensures the parent we have been passed is valid, otherwise CreateWindowEx + // will fail. + if (parent && !::IsWindow(parent)) { + NOTREACHED() << "invalid parent window specified."; + parent = NULL; + } + + hwnd_ = CreateWindowEx(window_ex_style_, GetWindowClassName().c_str(), L"", + window_style_, bounds.x(), bounds.y(), bounds.width(), + bounds.height(), parent, NULL, NULL, this); + DCHECK(hwnd_); + TRACK_HWND_CREATION(hwnd_); + SetWindowSupportsRerouteMouseWheel(hwnd_); + + // The window procedure should have set the data for us. + DCHECK(win_util::GetWindowUserData(hwnd_) == this); + + root_view_->OnWidgetCreated(); + + if (has_own_focus_manager) { + FocusManager::CreateFocusManager(hwnd_, GetRootView()); + } else { + // Subclass the window so we get the tab key messages when a view with no + // associated native window is focused. + FocusManager::InstallFocusSubclass(hwnd_, NULL); + } + + // Sets the RootView as a property, so the automation can introspect windows. + SetRootViewForHWND(hwnd_, root_view_.get()); + + MessageLoopForUI::current()->AddObserver(this); + + // Windows special DWM window frame requires a special tooltip manager so + // that window controls in Chrome windows don't flicker when you move your + // mouse over them. See comment in aero_tooltip_manager.h. + if (win_util::ShouldUseVistaFrame()) { + tooltip_manager_.reset(new AeroTooltipManager(this, GetNativeView())); + } else { + tooltip_manager_.reset(new TooltipManager(this, GetNativeView())); + } + + // This message initializes the window so that focus border are shown for + // windows. + ::SendMessage(GetNativeView(), + WM_CHANGEUISTATE, + MAKELPARAM(UIS_CLEAR, UISF_HIDEFOCUS), + 0); + + // Bug 964884: detach the IME attached to this window. + // We should attach IMEs only when we need to input CJK strings. + ::ImmAssociateContextEx(GetNativeView(), NULL, 0); +} + +void WidgetWin::SetContentsView(View* view) { + DCHECK(view && hwnd_) << "Can't be called until after the HWND is created!"; + // The ContentsView must be set up _after_ the window is created so that its + // Widget pointer is valid. + root_view_->SetLayoutManager(new FillLayout); + if (root_view_->GetChildViewCount() != 0) + root_view_->RemoveAllChildViews(true); + root_view_->AddChildView(view); + + // Manually size the window here to ensure the root view is laid out. + RECT wr; + GetWindowRect(&wr); + ChangeSize(0, CSize(wr.right - wr.left, wr.bottom - wr.top)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Widget implementation: + +void WidgetWin::GetBounds(gfx::Rect* out, bool including_frame) const { + CRect crect; + if (including_frame) { + GetWindowRect(&crect); + *out = gfx::Rect(crect); + return; + } + + GetClientRect(&crect); + POINT p = {0, 0}; + ::ClientToScreen(hwnd_, &p); + out->SetRect(crect.left + p.x, crect.top + p.y, + crect.Width(), crect.Height()); +} + +gfx::NativeView WidgetWin::GetNativeView() const { + return hwnd_; +} + +void WidgetWin::PaintNow(const gfx::Rect& update_rect) { + if (use_layered_buffer_) { + PaintLayeredWindow(); + } else if (root_view_->NeedsPainting(false) && IsWindow()) { + if (!opaque_ && GetParent()) { + // We're transparent. Need to force painting to occur from our parent. + CRect parent_update_rect = update_rect.ToRECT(); + POINT location_in_parent = { 0, 0 }; + ClientToScreen(hwnd_, &location_in_parent); + ::ScreenToClient(GetParent(), &location_in_parent); + parent_update_rect.OffsetRect(location_in_parent); + ::RedrawWindow(GetParent(), parent_update_rect, NULL, + RDW_UPDATENOW | RDW_INVALIDATE | RDW_ALLCHILDREN); + } else { + RECT native_update_rect = update_rect.ToRECT(); + RedrawWindow(hwnd_, &native_update_rect, NULL, + RDW_UPDATENOW | RDW_INVALIDATE | RDW_ALLCHILDREN); + } + // As we were created with a style of WS_CLIPCHILDREN redraw requests may + // result in an empty paint rect in WM_PAINT (this'll happen if a + // child HWND completely contains the update _rect). In such a scenario + // RootView would never get a ProcessPaint and always think it needs to + // be painted (leading to a steady stream of RedrawWindow requests on every + // event). For this reason we tell RootView it doesn't need to paint + // here. + root_view_->ClearPaintRect(); + } +} + +RootView* WidgetWin::GetRootView() { + if (!root_view_.get()) { + // First time the root view is being asked for, create it now. + root_view_.reset(CreateRootView()); + } + return root_view_.get(); +} + +bool WidgetWin::IsVisible() const { + return !!::IsWindowVisible(GetNativeView()); +} + +bool WidgetWin::IsActive() const { + return win_util::IsWindowActive(GetNativeView()); +} + +TooltipManager* WidgetWin::GetTooltipManager() { + return tooltip_manager_.get(); +} + +Window* WidgetWin::GetWindow() { + return GetWindowImpl(hwnd_); +} + +const Window* WidgetWin::GetWindow() const { + return GetWindowImpl(hwnd_); +} + +void WidgetWin::SetLayeredAlpha(BYTE layered_alpha) { + layered_alpha_ = layered_alpha; + +// if (hwnd_) +// UpdateWindowFromContents(contents_->getTopPlatformDevice().getBitmapDC()); +} + +void WidgetWin::SetUseLayeredBuffer(bool use_layered_buffer) { + if (use_layered_buffer_ == use_layered_buffer) + return; + + use_layered_buffer_ = use_layered_buffer; + if (!hwnd_) + return; + + if (use_layered_buffer_) { + // Force creation of the buffer at the right size. + RECT wr; + GetWindowRect(&wr); + ChangeSize(0, CSize(wr.right - wr.left, wr.bottom - wr.top)); + } else { + contents_.reset(NULL); + } +} + +static BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM l_param) { + RootView* root_view = + reinterpret_cast<RootView*>(GetProp(hwnd, kRootViewWindowProperty)); + if (root_view) { + *reinterpret_cast<RootView**>(l_param) = root_view; + return FALSE; // Stop enumerating. + } + return TRUE; // Keep enumerating. +} + +// static +RootView* WidgetWin::FindRootView(HWND hwnd) { + RootView* root_view = + reinterpret_cast<RootView*>(GetProp(hwnd, kRootViewWindowProperty)); + if (root_view) + return root_view; + + // Enumerate all children and check if they have a RootView. + EnumChildWindows(hwnd, EnumChildProc, reinterpret_cast<LPARAM>(&root_view)); + + return root_view; +} + +void WidgetWin::Close() { + if (!IsWindow()) + return; // No need to do anything. + + // Let's hide ourselves right away. + Hide(); + if (close_widget_factory_.empty()) { + // And we delay the close so that if we are called from an ATL callback, + // we don't destroy the window before the callback returned (as the caller + // may delete ourselves on destroy and the ATL callback would still + // dereference us when the callback returns). + MessageLoop::current()->PostTask(FROM_HERE, + close_widget_factory_.NewRunnableMethod( + &WidgetWin::CloseNow)); + } +} + +void WidgetWin::Hide() { + if (IsWindow()) { + // NOTE: Be careful not to activate any windows here (for example, calling + // ShowWindow(SW_HIDE) will automatically activate another window). This + // code can be called while a window is being deactivated, and activating + // another window will screw up the activation that is already in progress. + SetWindowPos(NULL, 0, 0, 0, 0, + SWP_HIDEWINDOW | SWP_NOACTIVATE | SWP_NOMOVE | + SWP_NOREPOSITION | SWP_NOSIZE | SWP_NOZORDER); + } +} + +void WidgetWin::Show() { + if (IsWindow()) + ShowWindow(SW_SHOWNOACTIVATE); +} + +void WidgetWin::CloseNow() { + // We may already have been destroyed if the selection resulted in a tab + // switch which will have reactivated the browser window and closed us, so + // we need to check to see if we're still a window before trying to destroy + // ourself. + if (IsWindow()) + DestroyWindow(); +} + +/////////////////////////////////////////////////////////////////////////////// +// MessageLoop::Observer + +void WidgetWin::WillProcessMessage(const MSG& msg) { +} + +void WidgetWin::DidProcessMessage(const MSG& msg) { + if (root_view_->NeedsPainting(true)) { + PaintNow(root_view_->GetScheduledPaintRect()); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// FocusTraversable + +View* WidgetWin::FindNextFocusableView( + View* starting_view, bool reverse, Direction direction, bool dont_loop, + FocusTraversable** focus_traversable, View** focus_traversable_view) { + return root_view_->FindNextFocusableView(starting_view, + reverse, + direction, + dont_loop, + focus_traversable, + focus_traversable_view); +} + +FocusTraversable* WidgetWin::GetFocusTraversableParent() { + // We are a proxy to the root view, so we should be bypassed when traversing + // up and as a result this should not be called. + NOTREACHED(); + return NULL; +} + +void WidgetWin::SetFocusTraversableParent(FocusTraversable* parent) { + root_view_->SetFocusTraversableParent(parent); +} + +View* WidgetWin::GetFocusTraversableParentView() { + // We are a proxy to the root view, so we should be bypassed when traversing + // up and as a result this should not be called. + NOTREACHED(); + return NULL; +} + +void WidgetWin::SetFocusTraversableParentView(View* parent_view) { + root_view_->SetFocusTraversableParentView(parent_view); +} + +/////////////////////////////////////////////////////////////////////////////// +// Message handlers + +void WidgetWin::OnCaptureChanged(HWND hwnd) { + if (has_capture_) { + if (is_mouse_down_) + root_view_->ProcessMouseDragCanceled(); + is_mouse_down_ = false; + has_capture_ = false; + } +} + +void WidgetWin::OnClose() { + Close(); +} + +void WidgetWin::OnDestroy() { + root_view_->OnWidgetDestroyed(); + + RemoveProp(hwnd_, kRootViewWindowProperty); +} + +LRESULT WidgetWin::OnEraseBkgnd(HDC dc) { + // This is needed for magical win32 flicker ju-ju + return 1; +} + +LRESULT WidgetWin::OnGetObject(UINT uMsg, WPARAM w_param, LPARAM l_param) { + LRESULT reference_result = static_cast<LRESULT>(0L); + + // Accessibility readers will send an OBJID_CLIENT message + if (OBJID_CLIENT == l_param) { + // If our MSAA root is already created, reuse that pointer. Otherwise, + // create a new one. + if (!accessibility_root_) { + CComObject<ViewAccessibility>* instance = NULL; + + HRESULT hr = CComObject<ViewAccessibility>::CreateInstance(&instance); + DCHECK(SUCCEEDED(hr)); + + if (!instance) { + // Return with failure. + return static_cast<LRESULT>(0L); + } + + CComPtr<IAccessible> accessibility_instance(instance); + + if (!SUCCEEDED(instance->Initialize(root_view_.get()))) { + // Return with failure. + return static_cast<LRESULT>(0L); + } + + // All is well, assign the temp instance to the class smart pointer + accessibility_root_.Attach(accessibility_instance.Detach()); + + if (!accessibility_root_) { + // Return with failure. + return static_cast<LRESULT>(0L); + } + + // Notify that an instance of IAccessible was allocated for m_hWnd + ::NotifyWinEvent(EVENT_OBJECT_CREATE, GetNativeView(), OBJID_CLIENT, + CHILDID_SELF); + } + + // Create a reference to ViewAccessibility that MSAA will marshall + // to the client. + reference_result = LresultFromObject(IID_IAccessible, w_param, + static_cast<IAccessible*>(accessibility_root_)); + } + return reference_result; +} + +void WidgetWin::OnKeyDown(TCHAR c, UINT rep_cnt, UINT flags) { + KeyEvent event(Event::ET_KEY_PRESSED, c, rep_cnt, flags); + SetMsgHandled(root_view_->ProcessKeyEvent(event)); +} + +void WidgetWin::OnKeyUp(TCHAR c, UINT rep_cnt, UINT flags) { + KeyEvent event(Event::ET_KEY_RELEASED, c, rep_cnt, flags); + SetMsgHandled(root_view_->ProcessKeyEvent(event)); +} + +void WidgetWin::OnLButtonDown(UINT flags, const CPoint& point) { + ProcessMousePressed(point, flags | MK_LBUTTON, false, false); +} + +void WidgetWin::OnLButtonUp(UINT flags, const CPoint& point) { + ProcessMouseReleased(point, flags | MK_LBUTTON); +} + +void WidgetWin::OnLButtonDblClk(UINT flags, const CPoint& point) { + ProcessMousePressed(point, flags | MK_LBUTTON, true, false); +} + +void WidgetWin::OnMButtonDown(UINT flags, const CPoint& point) { + ProcessMousePressed(point, flags | MK_MBUTTON, false, false); +} + +void WidgetWin::OnMButtonUp(UINT flags, const CPoint& point) { + ProcessMouseReleased(point, flags | MK_MBUTTON); +} + +void WidgetWin::OnMButtonDblClk(UINT flags, const CPoint& point) { + ProcessMousePressed(point, flags | MK_MBUTTON, true, false); +} + +LRESULT WidgetWin::OnMouseActivate(HWND window, UINT hittest_code, + UINT message) { + SetMsgHandled(FALSE); + return MA_ACTIVATE; +} + +void WidgetWin::OnMouseMove(UINT flags, const CPoint& point) { + ProcessMouseMoved(point, flags, false); +} + +LRESULT WidgetWin::OnMouseLeave(UINT message, WPARAM w_param, LPARAM l_param) { + tooltip_manager_->OnMouseLeave(); + ProcessMouseExited(); + return 0; +} + +LRESULT WidgetWin::OnMouseWheel(UINT message, WPARAM w_param, LPARAM l_param) { + // Reroute the mouse-wheel to the window under the mouse pointer if + // applicable. + if (message == WM_MOUSEWHEEL && + views::RerouteMouseWheel(hwnd_, w_param, l_param)) { + return 0; + } + + int flags = GET_KEYSTATE_WPARAM(w_param); + short distance = GET_WHEEL_DELTA_WPARAM(w_param); + int x = GET_X_LPARAM(l_param); + int y = GET_Y_LPARAM(l_param); + MouseWheelEvent e(distance, x, y, Event::ConvertWindowsFlags(flags)); + return root_view_->ProcessMouseWheelEvent(e) ? 0 : 1; +} + +LRESULT WidgetWin::OnMouseRange(UINT msg, WPARAM w_param, LPARAM l_param) { + tooltip_manager_->OnMouse(msg, w_param, l_param); + SetMsgHandled(FALSE); + return 0; +} + +void WidgetWin::OnNCLButtonDblClk(UINT flags, const CPoint& point) { + SetMsgHandled(ProcessMousePressed(point, flags | MK_LBUTTON, true, true)); +} + +void WidgetWin::OnNCLButtonDown(UINT flags, const CPoint& point) { + SetMsgHandled(ProcessMousePressed(point, flags | MK_LBUTTON, false, true)); +} + +void WidgetWin::OnNCLButtonUp(UINT flags, const CPoint& point) { + SetMsgHandled(FALSE); +} + +void WidgetWin::OnNCMButtonDblClk(UINT flags, const CPoint& point) { + SetMsgHandled(ProcessMousePressed(point, flags | MK_MBUTTON, true, true)); +} + +void WidgetWin::OnNCMButtonDown(UINT flags, const CPoint& point) { + SetMsgHandled(ProcessMousePressed(point, flags | MK_MBUTTON, false, true)); +} + +void WidgetWin::OnNCMButtonUp(UINT flags, const CPoint& point) { + SetMsgHandled(FALSE); +} + +LRESULT WidgetWin::OnNCMouseLeave(UINT uMsg, WPARAM w_param, LPARAM l_param) { + ProcessMouseExited(); + return 0; +} + +LRESULT WidgetWin::OnNCMouseMove(UINT flags, const CPoint& point) { + // NC points are in screen coordinates. + CPoint temp = point; + MapWindowPoints(HWND_DESKTOP, GetNativeView(), &temp, 1); + ProcessMouseMoved(temp, 0, true); + + // We need to process this message to stop Windows from drawing the window + // controls as the mouse moves over the title bar area when the window is + // maximized. + return 0; +} + +void WidgetWin::OnNCRButtonDblClk(UINT flags, const CPoint& point) { + SetMsgHandled(ProcessMousePressed(point, flags | MK_RBUTTON, true, true)); +} + +void WidgetWin::OnNCRButtonDown(UINT flags, const CPoint& point) { + SetMsgHandled(ProcessMousePressed(point, flags | MK_RBUTTON, false, true)); +} + +void WidgetWin::OnNCRButtonUp(UINT flags, const CPoint& point) { + SetMsgHandled(FALSE); +} + +LRESULT WidgetWin::OnNotify(int w_param, NMHDR* l_param) { + // We can be sent this message before the tooltip manager is created, if a + // subclass overrides OnCreate and creates some kind of Windows control there + // that sends WM_NOTIFY messages. + if (tooltip_manager_.get()) { + bool handled; + LRESULT result = tooltip_manager_->OnNotify(w_param, l_param, &handled); + SetMsgHandled(handled); + return result; + } + SetMsgHandled(FALSE); + return 0; +} + +void WidgetWin::OnPaint(HDC dc) { + root_view_->OnPaint(GetNativeView()); +} + +void WidgetWin::OnRButtonDown(UINT flags, const CPoint& point) { + ProcessMousePressed(point, flags | MK_RBUTTON, false, false); +} + +void WidgetWin::OnRButtonUp(UINT flags, const CPoint& point) { + ProcessMouseReleased(point, flags | MK_RBUTTON); +} + +void WidgetWin::OnRButtonDblClk(UINT flags, const CPoint& point) { + ProcessMousePressed(point, flags | MK_RBUTTON, true, false); +} + +void WidgetWin::OnSize(UINT param, const CSize& size) { + ChangeSize(param, size); +} + +void WidgetWin::OnThemeChanged() { + // Notify NativeTheme. + gfx::NativeTheme::instance()->CloseHandles(); +} + +void WidgetWin::OnFinalMessage(HWND window) { + if (delete_on_destroy_) + delete this; +} + +/////////////////////////////////////////////////////////////////////////////// +// WidgetWin, protected: + +void WidgetWin::TrackMouseEvents(DWORD mouse_tracking_flags) { + // Begin tracking mouse events for this HWND so that we get WM_MOUSELEAVE + // when the user moves the mouse outside this HWND's bounds. + if (active_mouse_tracking_flags_ == 0 || mouse_tracking_flags & TME_CANCEL) { + if (mouse_tracking_flags & TME_CANCEL) { + // We're about to cancel active mouse tracking, so empty out the stored + // state. + active_mouse_tracking_flags_ = 0; + } else { + active_mouse_tracking_flags_ = mouse_tracking_flags; + } + + TRACKMOUSEEVENT tme; + tme.cbSize = sizeof(tme); + tme.dwFlags = mouse_tracking_flags; + tme.hwndTrack = GetNativeView(); + tme.dwHoverTime = 0; + TrackMouseEvent(&tme); + } else if (mouse_tracking_flags != active_mouse_tracking_flags_) { + TrackMouseEvents(active_mouse_tracking_flags_ | TME_CANCEL); + TrackMouseEvents(mouse_tracking_flags); + } +} + +bool WidgetWin::ProcessMousePressed(const CPoint& point, + UINT flags, + bool dbl_click, + bool non_client) { + last_mouse_event_was_move_ = false; + // Windows gives screen coordinates for nonclient events, while the RootView + // expects window coordinates; convert if necessary. + gfx::Point converted_point(point); + if (non_client) + View::ConvertPointToView(NULL, root_view_.get(), &converted_point); + MouseEvent mouse_pressed(Event::ET_MOUSE_PRESSED, + converted_point.x(), + converted_point.y(), + (dbl_click ? MouseEvent::EF_IS_DOUBLE_CLICK : 0) | + (non_client ? MouseEvent::EF_IS_NON_CLIENT : 0) | + Event::ConvertWindowsFlags(flags)); + if (root_view_->OnMousePressed(mouse_pressed)) { + is_mouse_down_ = true; + if (!has_capture_) { + SetCapture(); + has_capture_ = true; + current_action_ = FA_FORWARDING; + } + return true; + } + return false; +} + +void WidgetWin::ProcessMouseDragged(const CPoint& point, UINT flags) { + last_mouse_event_was_move_ = false; + MouseEvent mouse_drag(Event::ET_MOUSE_DRAGGED, + point.x, + point.y, + Event::ConvertWindowsFlags(flags)); + root_view_->OnMouseDragged(mouse_drag); +} + +void WidgetWin::ProcessMouseReleased(const CPoint& point, UINT flags) { + last_mouse_event_was_move_ = false; + MouseEvent mouse_up(Event::ET_MOUSE_RELEASED, + point.x, + point.y, + Event::ConvertWindowsFlags(flags)); + // Release the capture first, that way we don't get confused if + // OnMouseReleased blocks. + if (has_capture_ && ReleaseCaptureOnMouseReleased()) { + has_capture_ = false; + current_action_ = FA_NONE; + ReleaseCapture(); + } + is_mouse_down_ = false; + root_view_->OnMouseReleased(mouse_up, false); +} + +void WidgetWin::ProcessMouseMoved(const CPoint &point, UINT flags, + bool is_nonclient) { + // Windows only fires WM_MOUSELEAVE events if the application begins + // "tracking" mouse events for a given HWND during WM_MOUSEMOVE events. + // We need to call |TrackMouseEvents| to listen for WM_MOUSELEAVE. + if (!has_capture_) + TrackMouseEvents(is_nonclient ? TME_NONCLIENT | TME_LEAVE : TME_LEAVE); + if (has_capture_ && is_mouse_down_) { + ProcessMouseDragged(point, flags); + } else { + gfx::Point screen_loc(point); + View::ConvertPointToScreen(root_view_.get(), &screen_loc); + if (last_mouse_event_was_move_ && last_mouse_move_x_ == screen_loc.x() && + last_mouse_move_y_ == screen_loc.y()) { + // Don't generate a mouse event for the same location as the last. + return; + } + last_mouse_move_x_ = screen_loc.x(); + last_mouse_move_y_ = screen_loc.y(); + last_mouse_event_was_move_ = true; + MouseEvent mouse_move(Event::ET_MOUSE_MOVED, + point.x, + point.y, + Event::ConvertWindowsFlags(flags)); + root_view_->OnMouseMoved(mouse_move); + } +} + +void WidgetWin::ProcessMouseExited() { + last_mouse_event_was_move_ = false; + root_view_->ProcessOnMouseExited(); + // Reset our tracking flag so that future mouse movement over this WidgetWin + // results in a new tracking session. + active_mouse_tracking_flags_ = 0; +} + +void WidgetWin::ChangeSize(UINT size_param, const CSize& size) { + CRect rect; + if (use_layered_buffer_) { + GetWindowRect(&rect); + SizeContents(rect); + } else { + GetClientRect(&rect); + } + + // Resizing changes the size of the view hierarchy and thus forces a + // complete relayout. + root_view_->SetBounds(0, 0, rect.Width(), rect.Height()); + root_view_->Layout(); + root_view_->SchedulePaint(); + + if (use_layered_buffer_) + PaintNow(gfx::Rect(rect)); +} + +RootView* WidgetWin::CreateRootView() { + return new RootView(this); +} + +/////////////////////////////////////////////////////////////////////////////// +// WidgetWin, private: + +// static +Window* WidgetWin::GetWindowImpl(HWND hwnd) { + // NOTE: we can't use GetAncestor here as constrained windows are a Window, + // but not a top level window. + HWND parent = hwnd; + while (parent) { + WidgetWin* widget = + reinterpret_cast<WidgetWin*>(win_util::GetWindowUserData(parent)); + if (widget && widget->is_window_) + return static_cast<WindowWin*>(widget); + parent = ::GetParent(parent); + } + return NULL; +} + +void WidgetWin::SizeContents(const CRect& window_rect) { + contents_.reset(new ChromeCanvas(window_rect.Width(), + window_rect.Height(), + false)); +} + +void WidgetWin::PaintLayeredWindow() { + // Painting monkeys with our cliprect, so we need to save it so that the + // call to UpdateLayeredWindow updates the entire window, not just the + // cliprect. + contents_->save(SkCanvas::kClip_SaveFlag); + gfx::Rect dirty_rect = root_view_->GetScheduledPaintRect(); + contents_->ClipRectInt(dirty_rect.x(), dirty_rect.y(), dirty_rect.width(), + dirty_rect.height()); + root_view_->ProcessPaint(contents_.get()); + contents_->restore(); + + UpdateWindowFromContents(contents_->getTopPlatformDevice().getBitmapDC()); +} + +void WidgetWin::UpdateWindowFromContents(HDC dib_dc) { + DCHECK(use_layered_buffer_); + if (can_update_layered_window_) { + CRect wr; + GetWindowRect(&wr); + CSize size(wr.right - wr.left, wr.bottom - wr.top); + CPoint zero_origin(0, 0); + CPoint window_position = wr.TopLeft(); + + BLENDFUNCTION blend = {AC_SRC_OVER, 0, layered_alpha_, AC_SRC_ALPHA}; + ::UpdateLayeredWindow( + hwnd_, NULL, &window_position, &size, dib_dc, &zero_origin, + RGB(0xFF, 0xFF, 0xFF), &blend, ULW_ALPHA); + } +} + +std::wstring WidgetWin::GetWindowClassName() { + ClassInfo class_info(initial_class_style()); + std::wstring name; + if (Singleton<ClassRegistrar>()->RetrieveClassName(class_info, &name)) + return name; + + // No class found, need to register one. + WNDCLASSEX class_ex; + class_ex.cbSize = sizeof(WNDCLASSEX); + class_ex.style = class_info.style; + class_ex.lpfnWndProc = &WidgetWin::WndProc; + class_ex.cbClsExtra = 0; + class_ex.cbWndExtra = 0; + class_ex.hInstance = NULL; + class_ex.hIcon = LoadIcon(GetModuleHandle(L"chrome.dll"), + MAKEINTRESOURCE(IDR_MAINFRAME)); + class_ex.hCursor = LoadCursor(NULL, IDC_ARROW); + class_ex.hbrBackground = reinterpret_cast<HBRUSH>(class_info.background + 1); + class_ex.lpszMenuName = NULL; + class_ex.lpszClassName = name.c_str(); + class_ex.hIconSm = class_ex.hIcon; + ATOM atom = RegisterClassEx(&class_ex); + DCHECK(atom); + + Singleton<ClassRegistrar>()->RegisterClass(class_info, name, atom); + + return name; +} + +// Get the source HWND of the specified message. Depending on the message, the +// source HWND is encoded in either the WPARAM or the LPARAM value. +HWND GetControlHWNDForMessage(UINT message, WPARAM w_param, LPARAM l_param) { + // Each of the following messages can be sent by a child HWND and must be + // forwarded to its associated NativeControlWin for handling. + switch (message) { + case WM_NOTIFY: + return reinterpret_cast<NMHDR*>(l_param)->hwndFrom; + case WM_COMMAND: + return reinterpret_cast<HWND>(l_param); + case WM_CONTEXTMENU: + return reinterpret_cast<HWND>(w_param); + case WM_CTLCOLORBTN: + case WM_CTLCOLORSTATIC: + return reinterpret_cast<HWND>(l_param); + } + return NULL; +} + +// Some messages may be sent to us by a child HWND managed by +// NativeControlWin. If this is the case, this function will forward those +// messages on to the object associated with the source HWND and return true, +// in which case the window procedure must not do any further processing of +// the message. If there is no associated NativeControlWin, the return value +// will be false and the WndProc can continue processing the message normally. +// |l_result| contains the result of the message processing by the control and +// must be returned by the WndProc if the return value is true. +bool ProcessNativeControlMessage(UINT message, + WPARAM w_param, + LPARAM l_param, + LRESULT* l_result) { + *l_result = 0; + + HWND control_hwnd = GetControlHWNDForMessage(message, w_param, l_param); + if (IsWindow(control_hwnd)) { + NativeControlWin* wrapper = GetNativeControlWinForHWND(control_hwnd); + if (wrapper) + return wrapper->ProcessMessage(message, w_param, l_param, l_result); + } + + return false; +} + +// static +LRESULT CALLBACK WidgetWin::WndProc(HWND window, UINT message, + WPARAM w_param, LPARAM l_param) { + if (message == WM_NCCREATE) { + CREATESTRUCT* cs = reinterpret_cast<CREATESTRUCT*>(l_param); + WidgetWin* widget = reinterpret_cast<WidgetWin*>(cs->lpCreateParams); + DCHECK(widget); + win_util::SetWindowUserData(window, widget); + widget->hwnd_ = window; + return TRUE; + } + + WidgetWin* widget = reinterpret_cast<WidgetWin*>( + win_util::GetWindowUserData(window)); + if (!widget) + return 0; + + LRESULT result = 0; + + // First allow messages sent by child controls to be processed directly by + // their associated views. If such a view is present, it will handle the + // message *instead of* this WidgetWin. + if (ProcessNativeControlMessage(message, w_param, l_param, &result)) + return result; + + // Otherwise we handle everything else. + if (!widget->ProcessWindowMessage(window, message, w_param, l_param, result)) + result = DefWindowProc(window, message, w_param, l_param); + if (message == WM_NCDESTROY) { + TRACK_HWND_DESTRUCTION(window); + widget->hwnd_ = NULL; + widget->OnFinalMessage(window); + } + return result; +} + +} // namespace views diff --git a/views/widget/widget_win.h b/views/widget/widget_win.h new file mode 100644 index 0000000..09efa21 --- /dev/null +++ b/views/widget/widget_win.h @@ -0,0 +1,636 @@ +// Copyright (c) 2006-2008 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 VIEWS_WIDGET_WIDGET_WIN_H_ +#define VIEWS_WIDGET_WIDGET_WIN_H_ + +#include <atlbase.h> +#include <atlcrack.h> + +#include "base/message_loop.h" +#include "base/system_monitor.h" +#include "views/focus/focus_manager.h" +#include "views/layout_manager.h" +#include "views/widget/widget.h" + +class ChromeCanvas; + +namespace gfx { +class Rect; +} + +namespace views { + +class RootView; +class TooltipManager; +class Window; + +bool SetRootViewForHWND(HWND hwnd, RootView* root_view); +RootView* GetRootViewForHWND(HWND hwnd); + +// A Windows message reflected from other windows. This message is sent +// with the following arguments: +// hWnd - Target window +// uMsg - kReflectedMessage +// wParam - Should be 0 +// lParam - Pointer to MSG struct containing the original message. +static const int kReflectedMessage = WM_APP + 3; + +// These two messages aren't defined in winuser.h, but they are sent to windows +// with captions. They appear to paint the window caption and frame. +// Unfortunately if you override the standard non-client rendering as we do +// with CustomFrameWindow, sometimes Windows (not deterministically +// reproducibly but definitely frequently) will send these messages to the +// window and paint the standard caption/title over the top of the custom one. +// So we need to handle these messages in CustomFrameWindow to prevent this +// from happening. +static const int WM_NCUAHDRAWCAPTION = 0xAE; +static const int WM_NCUAHDRAWFRAME = 0xAF; + +/////////////////////////////////////////////////////////////////////////////// +// +// WidgetWin +// A Widget for a views hierarchy used to represent anything that can be +// contained within an HWND, e.g. a control, a window, etc. Specializations +// suitable for specific tasks, e.g. top level window, are derived from this. +// +// This Widget contains a RootView which owns the hierarchy of views within it. +// As long as views are part of this tree, they will be deleted automatically +// when the RootView is destroyed. If you remove a view from the tree, you are +// then responsible for cleaning up after it. +// +/////////////////////////////////////////////////////////////////////////////// +class WidgetWin : public Widget, + public MessageLoopForUI::Observer, + public FocusTraversable, + public AcceleratorTarget { + public: + WidgetWin(); + virtual ~WidgetWin(); + + // Initialize the Widget with a parent and an initial desired size. + // |contents_view| is the view that will be the single child of RootView + // within this Widget. As contents_view is inserted into RootView's tree, + // RootView assumes ownership of this view and cleaning it up. If you remove + // this view, you are responsible for its destruction. If this value is NULL, + // the caller is responsible for populating the RootView, and sizing its + // contents as the window is sized. + // If |has_own_focus_manager| is true, the focus traversal stay confined to + // the window. + void Init(HWND parent, + const gfx::Rect& bounds, + bool has_own_focus_manager); + + // Sets the specified view as the contents of this Widget. There can only + // be one contnets view child of this Widget's RootView. This view is sized to + // fit the entire size of the RootView. The RootView takes ownership of this + // View, unless it is set as not being parent-owned. + virtual void SetContentsView(View* view); + + // Sets the window styles. This is ONLY used when the window is created. + // In other words, if you invoke this after invoking Init, nothing happens. + void set_window_style(DWORD style) { window_style_ = style; } + DWORD window_style() const { return window_style_; } + + // Sets the extended window styles. See comment about |set_window_style|. + void set_window_ex_style(DWORD style) { window_ex_style_ = style; } + DWORD window_ex_style() const { return window_ex_style_; }; + + // Sets the class style to use. The default is CS_DBLCLKS. + void set_initial_class_style(UINT class_style) { + // We dynamically generate the class name, so don't register it globally! + DCHECK((class_style & CS_GLOBALCLASS) == 0); + class_style_ = class_style; + } + UINT initial_class_style() { return class_style_; } + + void set_delete_on_destroy(bool delete_on_destroy) { + delete_on_destroy_ = delete_on_destroy; + } + + // Sets the initial opacity of a layered window, or updates the window's + // opacity if it is on the screen. + void SetLayeredAlpha(BYTE layered_alpha); + + // See description of use_layered_buffer_ for details. + void SetUseLayeredBuffer(bool use_layered_buffer); + + // Disable Layered Window updates by setting to false. + void set_can_update_layered_window(bool can_update_layered_window) { + can_update_layered_window_ = can_update_layered_window; + } + + // Returns the RootView associated with the specified HWND (if any). + static RootView* FindRootView(HWND hwnd); + + // Closes the window asynchronously by scheduling a task for it. The window + // is destroyed as a result. + // This invokes Hide to hide the window, and schedules a task that + // invokes CloseNow. + virtual void Close(); + + // Hides the window. This does NOT delete the window, it just hides it. + virtual void Hide(); + + // Shows the window without changing size/position/activation state. + virtual void Show(); + + // Closes the window synchronously. Note that this should not be called from + // an ATL message callback as it deletes the WidgetWin and ATL will + // dereference it after the callback is processed. + void CloseNow(); + + // All classes registered by WidgetWin start with this name. + static const wchar_t* const kBaseClassName; + + BEGIN_MSG_MAP_EX(0) + // Range handlers must go first! + MESSAGE_RANGE_HANDLER_EX(WM_MOUSEFIRST, WM_MOUSELAST, OnMouseRange) + MESSAGE_RANGE_HANDLER_EX(WM_NCMOUSEMOVE, WM_NCMOUSEMOVE, OnMouseRange) + + // Reflected message handler + MESSAGE_HANDLER_EX(kReflectedMessage, OnReflectedMessage) + + // CustomFrameWindow hacks + MESSAGE_HANDLER_EX(WM_NCUAHDRAWCAPTION, OnNCUAHDrawCaption) + MESSAGE_HANDLER_EX(WM_NCUAHDRAWFRAME, OnNCUAHDrawFrame) + + // Vista and newer + MESSAGE_HANDLER_EX(WM_DWMCOMPOSITIONCHANGED, OnDwmCompositionChanged) + + // Non-atlcrack.h handlers + MESSAGE_HANDLER_EX(WM_GETOBJECT, OnGetObject) + MESSAGE_HANDLER_EX(WM_NCMOUSELEAVE, OnNCMouseLeave) + MESSAGE_HANDLER_EX(WM_MOUSELEAVE, OnMouseLeave) + MESSAGE_HANDLER_EX(WM_MOUSEWHEEL, OnMouseWheel) + + // This list is in _ALPHABETICAL_ order! OR I WILL HURT YOU. + MSG_WM_ACTIVATE(OnActivate) + MSG_WM_ACTIVATEAPP(OnActivateApp) + MSG_WM_APPCOMMAND(OnAppCommand) + MSG_WM_CANCELMODE(OnCancelMode) + MSG_WM_CAPTURECHANGED(OnCaptureChanged) + MSG_WM_CLOSE(OnClose) + MSG_WM_COMMAND(OnCommand) + MSG_WM_CREATE(OnCreate) + MSG_WM_DESTROY(OnDestroy) + MSG_WM_ERASEBKGND(OnEraseBkgnd) + MSG_WM_ENDSESSION(OnEndSession) + MSG_WM_ENTERSIZEMOVE(OnEnterSizeMove) + MSG_WM_EXITMENULOOP(OnExitMenuLoop) + MSG_WM_GETMINMAXINFO(OnGetMinMaxInfo) + MSG_WM_HSCROLL(OnHScroll) + MSG_WM_INITMENU(OnInitMenu) + MSG_WM_INITMENUPOPUP(OnInitMenuPopup) + MSG_WM_KEYDOWN(OnKeyDown) + MSG_WM_KEYUP(OnKeyUp) + MSG_WM_SYSKEYDOWN(OnKeyDown) + MSG_WM_SYSKEYUP(OnKeyUp) + MSG_WM_LBUTTONDBLCLK(OnLButtonDblClk) + MSG_WM_LBUTTONDOWN(OnLButtonDown) + MSG_WM_LBUTTONUP(OnLButtonUp) + MSG_WM_MBUTTONDOWN(OnMButtonDown) + MSG_WM_MBUTTONUP(OnMButtonUp) + MSG_WM_MBUTTONDBLCLK(OnMButtonDblClk) + MSG_WM_MOUSEACTIVATE(OnMouseActivate) + MSG_WM_MOUSEMOVE(OnMouseMove) + MSG_WM_MOVE(OnMove) + MSG_WM_MOVING(OnMoving) + MSG_WM_NCACTIVATE(OnNCActivate) + MSG_WM_NCCALCSIZE(OnNCCalcSize) + MSG_WM_NCHITTEST(OnNCHitTest) + MSG_WM_NCMOUSEMOVE(OnNCMouseMove) + MSG_WM_NCLBUTTONDBLCLK(OnNCLButtonDblClk) + MSG_WM_NCLBUTTONDOWN(OnNCLButtonDown) + MSG_WM_NCLBUTTONUP(OnNCLButtonUp) + MSG_WM_NCMBUTTONDBLCLK(OnNCMButtonDblClk) + MSG_WM_NCMBUTTONDOWN(OnNCMButtonDown) + MSG_WM_NCMBUTTONUP(OnNCMButtonUp) + MSG_WM_NCPAINT(OnNCPaint) + MSG_WM_NCRBUTTONDBLCLK(OnNCRButtonDblClk) + MSG_WM_NCRBUTTONDOWN(OnNCRButtonDown) + MSG_WM_NCRBUTTONUP(OnNCRButtonUp) + MSG_WM_NOTIFY(OnNotify) + MSG_WM_PAINT(OnPaint) + MSG_WM_POWERBROADCAST(OnPowerBroadcast) + MSG_WM_RBUTTONDBLCLK(OnRButtonDblClk) + MSG_WM_RBUTTONDOWN(OnRButtonDown) + MSG_WM_RBUTTONUP(OnRButtonUp) + MSG_WM_SETCURSOR(OnSetCursor) + MSG_WM_SETFOCUS(OnSetFocus) + MSG_WM_SETICON(OnSetIcon) + MSG_WM_SETTEXT(OnSetText) + MSG_WM_SETTINGCHANGE(OnSettingChange) + MSG_WM_SIZE(OnSize) + MSG_WM_SYSCOMMAND(OnSysCommand) + MSG_WM_THEMECHANGED(OnThemeChanged) + MSG_WM_VSCROLL(OnVScroll) + MSG_WM_WINDOWPOSCHANGING(OnWindowPosChanging) + MSG_WM_WINDOWPOSCHANGED(OnWindowPosChanged) + END_MSG_MAP() + + // Overridden from Widget: + virtual void GetBounds(gfx::Rect* out, bool including_frame) const; + virtual gfx::NativeView GetNativeView() const; + virtual void PaintNow(const gfx::Rect& update_rect); + virtual RootView* GetRootView(); + virtual bool IsVisible() const; + virtual bool IsActive() const; + virtual TooltipManager* GetTooltipManager(); + virtual Window* GetWindow(); + virtual const Window* GetWindow() const; + + // Overridden from MessageLoop::Observer: + void WillProcessMessage(const MSG& msg); + virtual void DidProcessMessage(const MSG& msg); + + // Overridden from FocusTraversable: + virtual View* FindNextFocusableView(View* starting_view, + bool reverse, + Direction direction, + bool dont_loop, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + virtual FocusTraversable* GetFocusTraversableParent(); + virtual View* GetFocusTraversableParentView(); + + // Overridden from AcceleratorTarget: + virtual bool AcceleratorPressed(const Accelerator& accelerator) { + return false; + } + + void SetFocusTraversableParent(FocusTraversable* parent); + void SetFocusTraversableParentView(View* parent_view); + + virtual bool GetAccelerator(int cmd_id, Accelerator* accelerator) { + return false; + } + + BOOL IsWindow() const { + return ::IsWindow(GetNativeView()); + } + + BOOL ShowWindow(int command) { + DCHECK(::IsWindow(GetNativeView())); + return ::ShowWindow(GetNativeView(), command); + } + + HWND SetCapture() { + DCHECK(::IsWindow(GetNativeView())); + return ::SetCapture(GetNativeView()); + } + + HWND GetParent() const { + return ::GetParent(GetNativeView()); + } + + LONG GetWindowLong(int index) { + DCHECK(::IsWindow(GetNativeView())); + return ::GetWindowLong(GetNativeView(), index); + } + + BOOL GetWindowRect(RECT* rect) const { + return ::GetWindowRect(GetNativeView(), rect); + } + + LONG SetWindowLong(int index, LONG new_long) { + DCHECK(::IsWindow(GetNativeView())); + return ::SetWindowLong(GetNativeView(), index, new_long); + } + + BOOL SetWindowPos(HWND hwnd_after, int x, int y, int cx, int cy, UINT flags) { + DCHECK(::IsWindow(GetNativeView())); + return ::SetWindowPos(GetNativeView(), hwnd_after, x, y, cx, cy, flags); + } + + BOOL IsZoomed() const { + DCHECK(::IsWindow(GetNativeView())); + return ::IsZoomed(GetNativeView()); + } + + BOOL MoveWindow(int x, int y, int width, int height) { + return MoveWindow(x, y, width, height, TRUE); + } + + BOOL MoveWindow(int x, int y, int width, int height, BOOL repaint) { + DCHECK(::IsWindow(GetNativeView())); + return ::MoveWindow(GetNativeView(), x, y, width, height, repaint); + } + + int SetWindowRgn(HRGN region, BOOL redraw) { + DCHECK(::IsWindow(GetNativeView())); + return ::SetWindowRgn(GetNativeView(), region, redraw); + } + + BOOL GetClientRect(RECT* rect) const { + DCHECK(::IsWindow(GetNativeView())); + return ::GetClientRect(GetNativeView(), rect); + } + + protected: + + // Call close instead of this to Destroy the window. + BOOL DestroyWindow() { + DCHECK(::IsWindow(GetNativeView())); + return ::DestroyWindow(GetNativeView()); + } + + // Message Handlers + // These are all virtual so that specialized Widgets can modify or augment + // processing. + // This list is in _ALPHABETICAL_ order! + // Note: in the base class these functions must do nothing but convert point + // coordinates to client coordinates (if necessary) and forward the + // handling to the appropriate Process* function. This is so that + // subclasses can easily override these methods to do different things + // and have a convenient function to call to get the default behavior. + virtual void OnActivate(UINT action, BOOL minimized, HWND window) { + SetMsgHandled(FALSE); + } + virtual void OnActivateApp(BOOL active, DWORD thread_id) { + SetMsgHandled(FALSE); + } + virtual LRESULT OnAppCommand(HWND window, short app_command, WORD device, + int keystate) { + SetMsgHandled(FALSE); + return 0; + } + virtual void OnCancelMode() {} + virtual void OnCaptureChanged(HWND hwnd); + virtual void OnClose(); + virtual void OnCommand(UINT notification_code, int command_id, HWND window) { + SetMsgHandled(FALSE); + } + virtual LRESULT OnCreate(LPCREATESTRUCT create_struct) { return 0; } + // WARNING: If you override this be sure and invoke super, otherwise we'll + // leak a few things. + virtual void OnDestroy(); + virtual LRESULT OnDwmCompositionChanged(UINT msg, + WPARAM w_param, + LPARAM l_param) { + SetMsgHandled(FALSE); + return 0; + } + virtual void OnEndSession(BOOL ending, UINT logoff) { SetMsgHandled(FALSE); } + virtual void OnEnterSizeMove() { SetMsgHandled(FALSE); } + virtual void OnExitMenuLoop(BOOL is_track_popup_menu) { + SetMsgHandled(FALSE); + } + virtual LRESULT OnEraseBkgnd(HDC dc); + virtual LRESULT OnGetObject(UINT uMsg, WPARAM w_param, LPARAM l_param); + virtual void OnGetMinMaxInfo(MINMAXINFO* minmax_info) { + SetMsgHandled(FALSE); + } + virtual void OnHScroll(int scroll_type, short position, HWND scrollbar) { + SetMsgHandled(FALSE); + } + virtual void OnInitMenu(HMENU menu) { SetMsgHandled(FALSE); } + virtual void OnInitMenuPopup(HMENU menu, UINT position, BOOL is_system_menu) { + SetMsgHandled(FALSE); + } + virtual void OnKeyDown(TCHAR c, UINT rep_cnt, UINT flags); + virtual void OnKeyUp(TCHAR c, UINT rep_cnt, UINT flags); + virtual void OnLButtonDblClk(UINT flags, const CPoint& point); + virtual void OnLButtonDown(UINT flags, const CPoint& point); + virtual void OnLButtonUp(UINT flags, const CPoint& point); + virtual void OnMButtonDblClk(UINT flags, const CPoint& point); + virtual void OnMButtonDown(UINT flags, const CPoint& point); + virtual void OnMButtonUp(UINT flags, const CPoint& point); + virtual LRESULT OnMouseActivate(HWND window, UINT hittest_code, UINT message); + virtual void OnMouseMove(UINT flags, const CPoint& point); + virtual LRESULT OnMouseLeave(UINT message, WPARAM w_param, LPARAM l_param); + virtual LRESULT OnMouseWheel(UINT message, WPARAM w_param, LPARAM l_param); + virtual void OnMove(const CPoint& point) { SetMsgHandled(FALSE); } + virtual void OnMoving(UINT param, const LPRECT new_bounds) { } + virtual LRESULT OnMouseRange(UINT msg, WPARAM w_param, LPARAM l_param); + virtual LRESULT OnNCActivate(BOOL active) { SetMsgHandled(FALSE); return 0; } + virtual LRESULT OnNCCalcSize(BOOL w_param, LPARAM l_param) { + SetMsgHandled(FALSE); + return 0; + } + virtual LRESULT OnNCHitTest(const CPoint& pt) { + SetMsgHandled(FALSE); + return 0; + } + virtual void OnNCLButtonDblClk(UINT flags, const CPoint& point); + virtual void OnNCLButtonDown(UINT flags, const CPoint& point); + virtual void OnNCLButtonUp(UINT flags, const CPoint& point); + virtual void OnNCMButtonDblClk(UINT flags, const CPoint& point); + virtual void OnNCMButtonDown(UINT flags, const CPoint& point); + virtual void OnNCMButtonUp(UINT flags, const CPoint& point); + virtual LRESULT OnNCMouseLeave(UINT uMsg, WPARAM w_param, LPARAM l_param); + virtual LRESULT OnNCMouseMove(UINT flags, const CPoint& point); + virtual void OnNCPaint(HRGN rgn) { SetMsgHandled(FALSE); } + virtual void OnNCRButtonDblClk(UINT flags, const CPoint& point); + virtual void OnNCRButtonDown(UINT flags, const CPoint& point); + virtual void OnNCRButtonUp(UINT flags, const CPoint& point); + virtual LRESULT OnNCUAHDrawCaption(UINT msg, + WPARAM w_param, + LPARAM l_param) { + SetMsgHandled(FALSE); + return 0; + } + virtual LRESULT OnNCUAHDrawFrame(UINT msg, WPARAM w_param, LPARAM l_param) { + SetMsgHandled(FALSE); + return 0; + } + virtual LRESULT OnNotify(int w_param, NMHDR* l_param); + virtual void OnPaint(HDC dc); + virtual LRESULT OnPowerBroadcast(DWORD power_event, DWORD data) { + base::SystemMonitor* monitor = base::SystemMonitor::Get(); + if (monitor) + monitor->ProcessWmPowerBroadcastMessage(power_event); + SetMsgHandled(FALSE); + return 0; + } + virtual void OnRButtonDblClk(UINT flags, const CPoint& point); + virtual void OnRButtonDown(UINT flags, const CPoint& point); + virtual void OnRButtonUp(UINT flags, const CPoint& point); + virtual LRESULT OnReflectedMessage(UINT msg, WPARAM w_param, LPARAM l_param) { + SetMsgHandled(FALSE); + return 0; + } + virtual LRESULT OnSetCursor(HWND window, UINT hittest_code, UINT message) { + SetMsgHandled(FALSE); + return 0; + } + virtual void OnSetFocus(HWND focused_window) { + SetMsgHandled(FALSE); + } + virtual LRESULT OnSetIcon(UINT size_type, HICON new_icon) { + SetMsgHandled(FALSE); + return 0; + } + virtual LRESULT OnSetText(const wchar_t* text) { + SetMsgHandled(FALSE); + return 0; + } + virtual void OnSettingChange(UINT flags, const wchar_t* section) { + SetMsgHandled(FALSE); + } + virtual void OnSize(UINT param, const CSize& size); + virtual void OnSysCommand(UINT notification_code, CPoint click) { } + virtual void OnThemeChanged(); + virtual void OnVScroll(int scroll_type, short position, HWND scrollbar) { + SetMsgHandled(FALSE); + } + virtual void OnWindowPosChanging(WINDOWPOS* window_pos) { + SetMsgHandled(FALSE); + } + virtual void OnWindowPosChanged(WINDOWPOS* window_pos) { + SetMsgHandled(FALSE); + } + + // deletes this window as it is destroyed, override to provide different + // behavior. + virtual void OnFinalMessage(HWND window); + + // Start tracking all mouse events so that this window gets sent mouse leave + // messages too. |is_nonclient| is true when we should track WM_NCMOUSELEAVE + // messages instead of WM_MOUSELEAVE ones. + void TrackMouseEvents(DWORD mouse_tracking_flags); + + // Actually handle mouse events. These functions are called by subclasses who + // override the message handlers above to do the actual real work of handling + // the event in the View system. + bool ProcessMousePressed(const CPoint& point, + UINT flags, + bool dbl_click, + bool non_client); + void ProcessMouseDragged(const CPoint& point, UINT flags); + void ProcessMouseReleased(const CPoint& point, UINT flags); + void ProcessMouseMoved(const CPoint& point, UINT flags, bool is_nonclient); + void ProcessMouseExited(); + + // Handles re-laying out content in response to a window size change. + virtual void ChangeSize(UINT size_param, const CSize& size); + + // Returns whether capture should be released on mouse release. The default + // is true. + virtual bool ReleaseCaptureOnMouseReleased() { return true; } + + enum FrameAction {FA_NONE = 0, FA_RESIZING, FA_MOVING, FA_FORWARDING}; + + virtual RootView* CreateRootView(); + + // Returns true if this WidgetWin is opaque. + bool opaque() const { return opaque_; } + + // The root of the View hierarchy attached to this window. + scoped_ptr<RootView> root_view_; + + // Current frame ui action + FrameAction current_action_; + + // Whether or not we have capture the mouse. + bool has_capture_; + + // If true, the mouse is currently down. + bool is_mouse_down_; + + scoped_ptr<TooltipManager> tooltip_manager_; + + // Are a subclass of WindowWin? + bool is_window_; + + private: + // Implementation of GetWindow. Ascends the parents of |hwnd| returning the + // first ancestor that is a Window. + static Window* GetWindowImpl(HWND hwnd); + + // Resize the bitmap used to contain the contents of the layered window. This + // recreates the entire bitmap. + void SizeContents(const CRect& window_rect); + + // Paint into a DIB and then update the layered window with its contents. + void PaintLayeredWindow(); + + // In layered mode, update the layered window. |dib_dc| represents a handle + // to a device context that contains the contents of the window. + void UpdateWindowFromContents(HDC dib_dc); + + // Invoked from WM_DESTROY. Does appropriate cleanup and invokes OnDestroy + // so that subclasses can do any cleanup they need to. + void OnDestroyImpl(); + + // The windows procedure used by all WidgetWins. + static LRESULT CALLBACK WndProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param); + + // Gets the window class name to use when creating the corresponding HWND. + // If necessary, this registers the window class. + std::wstring GetWindowClassName(); + + // The following factory is used for calls to close the WidgetWin + // instance. + ScopedRunnableMethodFactory<WidgetWin> close_widget_factory_; + + // The flags currently being used with TrackMouseEvent to track mouse + // messages. 0 if there is no active tracking. The value of this member is + // used when tracking is canceled. + DWORD active_mouse_tracking_flags_; + + bool opaque_; + + // Window Styles used when creating the window. + DWORD window_style_; + + // Window Extended Styles used when creating the window. + DWORD window_ex_style_; + + // Style of the class to use. + UINT class_style_; + + // Should we keep an offscreen buffer? This is initially true and if the + // window has WS_EX_LAYERED then it remains true. You can set this to false + // at any time to ditch the buffer, and similarly set back to true to force + // creation of the buffer. + // + // NOTE: this is intended to be used with a layered window (a window with an + // extended window style of WS_EX_LAYERED). If you are using a layered window + // and NOT changing the layered alpha or anything else, then leave this value + // alone. OTOH if you are invoking SetLayeredWindowAttributes then you'll + // must likely want to set this to false, or after changing the alpha toggle + // the extended style bit to false than back to true. See MSDN for more + // details. + bool use_layered_buffer_; + + // The default alpha to be applied to the layered window. + BYTE layered_alpha_; + + // A canvas that contains the window contents in the case of a layered + // window. + scoped_ptr<ChromeCanvas> contents_; + + // Whether or not the window should delete itself when it is destroyed. + // Set this to false via its setter for stack allocated instances. + bool delete_on_destroy_; + + // True if we are allowed to update the layered window from the DIB backing + // store if necessary. + bool can_update_layered_window_; + + // The following are used to detect duplicate mouse move events and not + // deliver them. Displaying a window may result in the system generating + // duplicate move events even though the mouse hasn't moved. + + // If true, the last event was a mouse move event. + bool last_mouse_event_was_move_; + + // Coordinates of the last mouse move event, in screen coordinates. + int last_mouse_move_x_; + int last_mouse_move_y_; + + // Instance of accessibility information and handling for MSAA root + CComPtr<IAccessible> accessibility_root_; + + // Our hwnd. + HWND hwnd_; +}; + +} // namespace views + +#endif // #ifndef VIEWS_WIDGET_WIDGET_WIN_H_ diff --git a/views/window/client_view.cc b/views/window/client_view.cc new file mode 100644 index 0000000..5b5afb1 --- /dev/null +++ b/views/window/client_view.cc @@ -0,0 +1,61 @@ +// Copyright (c) 2006-2008 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/window/client_view.h" +#include "views/window/window.h" +#include "views/window/window_delegate.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// ClientView, public: + +ClientView::ClientView(Window* window, View* contents_view) + : window_(window), + contents_view_(contents_view) { +} + +int ClientView::NonClientHitTest(const gfx::Point& point) { + return bounds().Contains(point) ? HTCLIENT : HTNOWHERE; +} + +void ClientView::WindowClosing() { + window_->GetDelegate()->WindowClosing(); +} + +/////////////////////////////////////////////////////////////////////////////// +// ClientView, View overrides: + +gfx::Size ClientView::GetPreferredSize() { + // |contents_view_| is allowed to be NULL up until the point where this view + // is attached to a Container. + if (contents_view_) + return contents_view_->GetPreferredSize(); + return gfx::Size(); +} + +void ClientView::Layout() { + // |contents_view_| is allowed to be NULL up until the point where this view + // is attached to a Container. + if (contents_view_) + contents_view_->SetBounds(0, 0, width(), height()); +} + +void ClientView::ViewHierarchyChanged(bool is_add, View* parent, View* child) { + if (is_add && child == this) { + DCHECK(GetWidget()); + DCHECK(contents_view_); // |contents_view_| must be valid now! + AddChildView(contents_view_); + } +} + +void ClientView::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + // Overridden to do nothing. The NonClientView manually calls Layout on the + // ClientView when it is itself laid out, see comment in + // NonClientView::Layout. +} + +} // namespace views diff --git a/views/window/client_view.h b/views/window/client_view.h new file mode 100644 index 0000000..b56fcfd --- /dev/null +++ b/views/window/client_view.h @@ -0,0 +1,84 @@ +// Copyright (c) 2006-2008 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 VIEWS_WINDOW_CLIENT_VIEW_H_ +#define VIEWS_WINDOW_CLIENT_VIEW_H_ + +#include "views/view.h" + +namespace views { + +class DialogClientView; +class Window; + +/////////////////////////////////////////////////////////////////////////////// +// ClientView +// +// A ClientView is a View subclass that is used to occupy the "client area" +// of a window. It provides basic information to the window that contains it +// such as non-client hit testing information, sizing etc. Sub-classes of +// ClientView are used to create more elaborate contents, e.g. +// "DialogClientView". +class ClientView : public View { + public: + // Constructs a ClientView object for the specified window with the specified + // contents. Since this object is created during the process of creating + // |window|, |contents_view| must be valid if you want the initial size of + // the window to be based on |contents_view|'s preferred size. + ClientView(Window* window, View* contents_view); + virtual ~ClientView() {} + + // Manual RTTI ftw. + virtual DialogClientView* AsDialogClientView() { return NULL; } + + // Returns true to signal that the Window can be closed. Specialized + // ClientView subclasses can override this default behavior to allow the + // close to be blocked until the user corrects mistakes, accepts a warning + // dialog, etc. + virtual bool CanClose() const { return true; } + + // Notification that the window is closing. The default implementation + // forwards the notification to the delegate. + virtual void WindowClosing(); + + // Tests to see if the specified point (in view coordinates) is within the + // bounds of this view. If so, it returns HTCLIENT in this default + // implementation. If it is outside the bounds of this view, this must return + // HTNOWHERE to tell the caller to do further processing to determine where + // in the non-client area it is (if it is). + // Subclasses of ClientView can extend this logic by overriding this method + // to detect if regions within the client area count as parts of the "non- + // client" area. A good example of this is the size box at the bottom right + // corner of resizable dialog boxes. + virtual int NonClientHitTest(const gfx::Point& point); + + // Overridden from View: + virtual gfx::Size GetPreferredSize(); + virtual void Layout(); + + protected: + // Overridden from View: + virtual void ViewHierarchyChanged(bool is_add, View* parent, View* child); + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + + // Accessors for private data members. + Window* window() const { return window_; } + void set_window(Window* window) { window_ = window; } + View* contents_view() const { return contents_view_; } + void set_contents_view(View* contents_view) { + contents_view_ = contents_view; + } + + private: + // The Window that hosts this ClientView. + Window* window_; + + // The View that this ClientView contains. + View* contents_view_; +}; + +} // namespace views + +#endif // #ifndef VIEWS_WINDOW_CLIENT_VIEW_H_ diff --git a/views/window/custom_frame_view.cc b/views/window/custom_frame_view.cc new file mode 100644 index 0000000..cf8173f --- /dev/null +++ b/views/window/custom_frame_view.cc @@ -0,0 +1,695 @@ +// Copyright (c) 2009 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/window/custom_frame_view.h" + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/gfx/path.h" +#include "app/resource_bundle.h" +#include "base/win_util.h" +#include "chrome/common/win_util.h" +#include "grit/theme_resources.h" +#include "views/window/client_view.h" +#include "views/window/window_delegate.h" + +namespace views { + +// An enumeration of bitmap resources used by this window. +enum { + FRAME_PART_BITMAP_FIRST = 0, // Must be first. + + // Window Controls. + FRAME_CLOSE_BUTTON_ICON, + FRAME_CLOSE_BUTTON_ICON_H, + FRAME_CLOSE_BUTTON_ICON_P, + FRAME_CLOSE_BUTTON_ICON_SA, + FRAME_CLOSE_BUTTON_ICON_SA_H, + FRAME_CLOSE_BUTTON_ICON_SA_P, + FRAME_RESTORE_BUTTON_ICON, + FRAME_RESTORE_BUTTON_ICON_H, + FRAME_RESTORE_BUTTON_ICON_P, + FRAME_MAXIMIZE_BUTTON_ICON, + FRAME_MAXIMIZE_BUTTON_ICON_H, + FRAME_MAXIMIZE_BUTTON_ICON_P, + FRAME_MINIMIZE_BUTTON_ICON, + FRAME_MINIMIZE_BUTTON_ICON_H, + FRAME_MINIMIZE_BUTTON_ICON_P, + + // Window Frame Border. + FRAME_BOTTOM_EDGE, + FRAME_BOTTOM_LEFT_CORNER, + FRAME_BOTTOM_RIGHT_CORNER, + FRAME_LEFT_EDGE, + FRAME_RIGHT_EDGE, + FRAME_TOP_EDGE, + FRAME_TOP_LEFT_CORNER, + FRAME_TOP_RIGHT_CORNER, + + // Client Edge Border. + FRAME_CLIENT_EDGE_TOP_LEFT, + FRAME_CLIENT_EDGE_TOP, + FRAME_CLIENT_EDGE_TOP_RIGHT, + FRAME_CLIENT_EDGE_RIGHT, + FRAME_CLIENT_EDGE_BOTTOM_RIGHT, + FRAME_CLIENT_EDGE_BOTTOM, + FRAME_CLIENT_EDGE_BOTTOM_LEFT, + FRAME_CLIENT_EDGE_LEFT, + + FRAME_PART_BITMAP_COUNT // Must be last. +}; + +class ActiveWindowResources : public WindowResources { + public: + ActiveWindowResources() { + InitClass(); + } + virtual ~ActiveWindowResources() { + } + + // WindowResources implementation: + virtual SkBitmap* GetPartBitmap(FramePartBitmap part) const { + return standard_frame_bitmaps_[part]; + } + + private: + static void InitClass() { + static bool initialized = false; + if (!initialized) { + static const int kFramePartBitmapIds[] = { + 0, + IDR_CLOSE, IDR_CLOSE_H, IDR_CLOSE_P, + IDR_CLOSE_SA, IDR_CLOSE_SA_H, IDR_CLOSE_SA_P, + IDR_RESTORE, IDR_RESTORE_H, IDR_RESTORE_P, + IDR_MAXIMIZE, IDR_MAXIMIZE_H, IDR_MAXIMIZE_P, + IDR_MINIMIZE, IDR_MINIMIZE_H, IDR_MINIMIZE_P, + IDR_WINDOW_BOTTOM_CENTER, IDR_WINDOW_BOTTOM_LEFT_CORNER, + IDR_WINDOW_BOTTOM_RIGHT_CORNER, IDR_WINDOW_LEFT_SIDE, + IDR_WINDOW_RIGHT_SIDE, IDR_WINDOW_TOP_CENTER, + IDR_WINDOW_TOP_LEFT_CORNER, IDR_WINDOW_TOP_RIGHT_CORNER, + IDR_APP_TOP_LEFT, IDR_APP_TOP_CENTER, IDR_APP_TOP_RIGHT, + IDR_CONTENT_RIGHT_SIDE, IDR_CONTENT_BOTTOM_RIGHT_CORNER, + IDR_CONTENT_BOTTOM_CENTER, IDR_CONTENT_BOTTOM_LEFT_CORNER, + IDR_CONTENT_LEFT_SIDE, + 0 + }; + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + for (int i = 0; i < FRAME_PART_BITMAP_COUNT; ++i) { + int id = kFramePartBitmapIds[i]; + if (id != 0) + standard_frame_bitmaps_[i] = rb.GetBitmapNamed(id); + } + initialized = true; + } + } + + static SkBitmap* standard_frame_bitmaps_[FRAME_PART_BITMAP_COUNT]; + + DISALLOW_EVIL_CONSTRUCTORS(ActiveWindowResources); +}; + +class InactiveWindowResources : public WindowResources { + public: + InactiveWindowResources() { + InitClass(); + } + virtual ~InactiveWindowResources() { + } + + // WindowResources implementation: + virtual SkBitmap* GetPartBitmap(FramePartBitmap part) const { + return standard_frame_bitmaps_[part]; + } + + private: + static void InitClass() { + static bool initialized = false; + if (!initialized) { + static const int kFramePartBitmapIds[] = { + 0, + IDR_CLOSE, IDR_CLOSE_H, IDR_CLOSE_P, + IDR_CLOSE_SA, IDR_CLOSE_SA_H, IDR_CLOSE_SA_P, + IDR_RESTORE, IDR_RESTORE_H, IDR_RESTORE_P, + IDR_MAXIMIZE, IDR_MAXIMIZE_H, IDR_MAXIMIZE_P, + IDR_MINIMIZE, IDR_MINIMIZE_H, IDR_MINIMIZE_P, + IDR_DEWINDOW_BOTTOM_CENTER, IDR_DEWINDOW_BOTTOM_LEFT_CORNER, + IDR_DEWINDOW_BOTTOM_RIGHT_CORNER, IDR_DEWINDOW_LEFT_SIDE, + IDR_DEWINDOW_RIGHT_SIDE, IDR_DEWINDOW_TOP_CENTER, + IDR_DEWINDOW_TOP_LEFT_CORNER, IDR_DEWINDOW_TOP_RIGHT_CORNER, + IDR_APP_TOP_LEFT, IDR_APP_TOP_CENTER, IDR_APP_TOP_RIGHT, + IDR_CONTENT_RIGHT_SIDE, IDR_CONTENT_BOTTOM_RIGHT_CORNER, + IDR_CONTENT_BOTTOM_CENTER, IDR_CONTENT_BOTTOM_LEFT_CORNER, + IDR_CONTENT_LEFT_SIDE, + 0 + }; + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + for (int i = 0; i < FRAME_PART_BITMAP_COUNT; ++i) { + int id = kFramePartBitmapIds[i]; + if (id != 0) + standard_frame_bitmaps_[i] = rb.GetBitmapNamed(id); + } + initialized = true; + } + } + + static SkBitmap* standard_frame_bitmaps_[FRAME_PART_BITMAP_COUNT]; + + DISALLOW_EVIL_CONSTRUCTORS(InactiveWindowResources); +}; + +// static +SkBitmap* ActiveWindowResources::standard_frame_bitmaps_[]; +SkBitmap* InactiveWindowResources::standard_frame_bitmaps_[]; + +// static +WindowResources* CustomFrameView::active_resources_ = NULL; +WindowResources* CustomFrameView::inactive_resources_ = NULL; +ChromeFont* CustomFrameView::title_font_ = NULL; + +namespace { +// The frame border is only visible in restored mode and is hardcoded to 4 px on +// each side regardless of the system window border size. +const int kFrameBorderThickness = 4; +// Various edges of the frame border have a 1 px shadow along their edges; in a +// few cases we shift elements based on this amount for visual appeal. +const int kFrameShadowThickness = 1; +// While resize areas on Windows are normally the same size as the window +// borders, our top area is shrunk by 1 px to make it easier to move the window +// around with our thinner top grabbable strip. (Incidentally, our side and +// bottom resize areas don't match the frame border thickness either -- they +// span the whole nonclient area, so there's no "dead zone" for the mouse.) +const int kTopResizeAdjust = 1; +// In the window corners, the resize areas don't actually expand bigger, but the +// 16 px at the end of each edge triggers diagonal resizing. +const int kResizeAreaCornerSize = 16; +// The titlebar never shrinks to less than 18 px tall, plus the height of the +// frame border and any bottom edge. +const int kTitlebarMinimumHeight = 18; +// The icon is inset 2 px from the left frame border. +const int kIconLeftSpacing = 2; +// The icon takes up 16/25th of the available titlebar height. (This is +// expressed as two ints to avoid precision losses leading to off-by-one pixel +// errors.) +const int kIconHeightFractionNumerator = 16; +const int kIconHeightFractionDenominator = 25; +// The icon never shrinks below 16 px on a side. +const int kIconMinimumSize = 16; +// Because our frame border has a different "3D look" than Windows', with a less +// cluttered top edge, we need to shift the icon up by 1 px in restored mode so +// it looks more centered. +const int kIconRestoredAdjust = 1; +// There is a 4 px gap between the icon and the title text. +const int kIconTitleSpacing = 4; +// The title text starts 2 px below the bottom of the top frame border. +const int kTitleTopSpacing = 2; +// There is a 5 px gap between the title text and the caption buttons. +const int kTitleCaptionSpacing = 5; +// The caption buttons are always drawn 1 px down from the visible top of the +// window (the true top in restored mode, or the top of the screen in maximized +// mode). +const int kCaptionTopSpacing = 1; +} + +/////////////////////////////////////////////////////////////////////////////// +// CustomFrameView, public: + +CustomFrameView::CustomFrameView(Window* frame) + : NonClientFrameView(), + close_button_(new ImageButton(this)), + restore_button_(new ImageButton(this)), + maximize_button_(new ImageButton(this)), + minimize_button_(new ImageButton(this)), + system_menu_button_(new ImageButton(this)), + should_show_minmax_buttons_(false), + frame_(frame) { + InitClass(); + WindowResources* resources = active_resources_; + + // Close button images will be set in LayoutWindowControls(). + AddChildView(close_button_); + + restore_button_->SetImage(CustomButton::BS_NORMAL, + resources->GetPartBitmap(FRAME_RESTORE_BUTTON_ICON)); + restore_button_->SetImage(CustomButton::BS_HOT, + resources->GetPartBitmap(FRAME_RESTORE_BUTTON_ICON_H)); + restore_button_->SetImage(CustomButton::BS_PUSHED, + resources->GetPartBitmap(FRAME_RESTORE_BUTTON_ICON_P)); + AddChildView(restore_button_); + + maximize_button_->SetImage(CustomButton::BS_NORMAL, + resources->GetPartBitmap(FRAME_MAXIMIZE_BUTTON_ICON)); + maximize_button_->SetImage(CustomButton::BS_HOT, + resources->GetPartBitmap(FRAME_MAXIMIZE_BUTTON_ICON_H)); + maximize_button_->SetImage(CustomButton::BS_PUSHED, + resources->GetPartBitmap(FRAME_MAXIMIZE_BUTTON_ICON_P)); + AddChildView(maximize_button_); + + minimize_button_->SetImage(CustomButton::BS_NORMAL, + resources->GetPartBitmap(FRAME_MINIMIZE_BUTTON_ICON)); + minimize_button_->SetImage(CustomButton::BS_HOT, + resources->GetPartBitmap(FRAME_MINIMIZE_BUTTON_ICON_H)); + minimize_button_->SetImage(CustomButton::BS_PUSHED, + resources->GetPartBitmap(FRAME_MINIMIZE_BUTTON_ICON_P)); + AddChildView(minimize_button_); + + should_show_minmax_buttons_ = frame_->GetDelegate()->CanMaximize(); + + AddChildView(system_menu_button_); +} + +CustomFrameView::~CustomFrameView() { +} + +/////////////////////////////////////////////////////////////////////////////// +// CustomFrameView, NonClientFrameView implementation: + +gfx::Rect CustomFrameView::GetBoundsForClientView() const { + return client_view_bounds_; +} + +gfx::Rect CustomFrameView::GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const { + int top_height = NonClientTopBorderHeight(); + int border_thickness = NonClientBorderThickness(); + return gfx::Rect(std::max(0, client_bounds.x() - border_thickness), + std::max(0, client_bounds.y() - top_height), + client_bounds.width() + (2 * border_thickness), + client_bounds.height() + top_height + border_thickness); +} + +gfx::Point CustomFrameView::GetSystemMenuPoint() const { + gfx::Point system_menu_point( + MirroredXCoordinateInsideView(FrameBorderThickness()), + NonClientTopBorderHeight() - BottomEdgeThicknessWithinNonClientHeight()); + ConvertPointToScreen(this, &system_menu_point); + return system_menu_point; +} + +int CustomFrameView::NonClientHitTest(const gfx::Point& point) { + // Then see if the point is within any of the window controls. + if (close_button_->GetBounds(APPLY_MIRRORING_TRANSFORMATION).Contains(point)) + return HTCLOSE; + if (restore_button_->GetBounds(APPLY_MIRRORING_TRANSFORMATION).Contains( + point)) + return HTMAXBUTTON; + if (maximize_button_->GetBounds(APPLY_MIRRORING_TRANSFORMATION).Contains( + point)) + return HTMAXBUTTON; + if (minimize_button_->GetBounds(APPLY_MIRRORING_TRANSFORMATION).Contains( + point)) + return HTMINBUTTON; + if (system_menu_button_->GetBounds(APPLY_MIRRORING_TRANSFORMATION).Contains( + point)) + return HTSYSMENU; + + int window_component = GetHTComponentForFrame(point, FrameBorderThickness(), + NonClientBorderThickness(), kResizeAreaCornerSize, kResizeAreaCornerSize, + frame_->GetDelegate()->CanResize()); + // Fall back to the caption if no other component matches. + return (window_component == HTNOWHERE) ? HTCAPTION : window_component; +} + +void CustomFrameView::GetWindowMask(const gfx::Size& size, + gfx::Path* window_mask) { + DCHECK(window_mask); + + if (frame_->IsMaximized()) + return; + + // Redefine the window visible region for the new size. + window_mask->moveTo(0, 3); + window_mask->lineTo(1, 2); + window_mask->lineTo(1, 1); + window_mask->lineTo(2, 1); + window_mask->lineTo(3, 0); + + window_mask->lineTo(SkIntToScalar(size.width() - 3), 0); + window_mask->lineTo(SkIntToScalar(size.width() - 2), 1); + window_mask->lineTo(SkIntToScalar(size.width() - 1), 1); + window_mask->lineTo(SkIntToScalar(size.width() - 1), 2); + window_mask->lineTo(SkIntToScalar(size.width()), 3); + + window_mask->lineTo(SkIntToScalar(size.width()), + SkIntToScalar(size.height())); + window_mask->lineTo(0, SkIntToScalar(size.height())); + window_mask->close(); +} + +void CustomFrameView::EnableClose(bool enable) { + close_button_->SetEnabled(enable); +} + +void CustomFrameView::ResetWindowControls() { + restore_button_->SetState(CustomButton::BS_NORMAL); + minimize_button_->SetState(CustomButton::BS_NORMAL); + maximize_button_->SetState(CustomButton::BS_NORMAL); + // The close button isn't affected by this constraint. +} + +/////////////////////////////////////////////////////////////////////////////// +// CustomFrameView, View overrides: + +void CustomFrameView::Paint(ChromeCanvas* canvas) { + if (frame_->IsMaximized()) + PaintMaximizedFrameBorder(canvas); + else + PaintRestoredFrameBorder(canvas); + PaintTitleBar(canvas); + if (!frame_->IsMaximized()) + PaintRestoredClientEdge(canvas); +} + +void CustomFrameView::Layout() { + LayoutWindowControls(); + LayoutTitleBar(); + LayoutClientView(); +} + +gfx::Size CustomFrameView::GetPreferredSize() { + gfx::Size pref = frame_->GetClientView()->GetPreferredSize(); + gfx::Rect bounds(0, 0, pref.width(), pref.height()); + return frame_->GetNonClientView()->GetWindowBoundsForClientBounds( + bounds).size(); +} + +/////////////////////////////////////////////////////////////////////////////// +// CustomFrameView, ButtonListener implementation: + +void CustomFrameView::ButtonPressed(Button* sender) { + if (sender == close_button_) + frame_->Close(); + else if (sender == minimize_button_) + frame_->Minimize(); + else if (sender == maximize_button_) + frame_->Maximize(); + else if (sender == restore_button_) + frame_->Restore(); +} + +/////////////////////////////////////////////////////////////////////////////// +// CustomFrameView, private: + +int CustomFrameView::FrameBorderThickness() const { + return frame_->IsMaximized() ? 0 : kFrameBorderThickness; +} + +int CustomFrameView::NonClientBorderThickness() const { + // In maximized mode, we don't show a client edge. + return FrameBorderThickness() + + (frame_->IsMaximized() ? 0 : kClientEdgeThickness); +} + +int CustomFrameView::NonClientTopBorderHeight() const { + int title_top_spacing, title_thickness; + return TitleCoordinates(&title_top_spacing, &title_thickness); +} + +int CustomFrameView::BottomEdgeThicknessWithinNonClientHeight() const { + return kFrameShadowThickness + + (frame_->IsMaximized() ? 0 : kClientEdgeThickness); +} + +int CustomFrameView::TitleCoordinates(int* title_top_spacing, + int* title_thickness) const { + int frame_thickness = FrameBorderThickness(); + int min_titlebar_height = kTitlebarMinimumHeight + frame_thickness; + *title_top_spacing = frame_thickness + kTitleTopSpacing; + // The bottom spacing should be the same apparent height as the top spacing. + // Because the actual top spacing height varies based on the system border + // thickness, we calculate this based on the restored top spacing and then + // adjust for maximized mode. We also don't include the frame shadow here, + // since while it's part of the bottom spacing it will be added in at the end. + int title_bottom_spacing = + kFrameBorderThickness + kTitleTopSpacing - kFrameShadowThickness; + if (frame_->IsMaximized()) { + // When we maximize, the top border appears to be chopped off; shift the + // title down to stay centered within the remaining space. + int title_adjust = (kFrameBorderThickness / 2); + *title_top_spacing += title_adjust; + title_bottom_spacing -= title_adjust; + } + *title_thickness = std::max(title_font_->height(), + min_titlebar_height - *title_top_spacing - title_bottom_spacing); + return *title_top_spacing + *title_thickness + title_bottom_spacing + + BottomEdgeThicknessWithinNonClientHeight(); +} + +void CustomFrameView::PaintRestoredFrameBorder(ChromeCanvas* canvas) { + SkBitmap* top_left_corner = resources()->GetPartBitmap(FRAME_TOP_LEFT_CORNER); + SkBitmap* top_right_corner = + resources()->GetPartBitmap(FRAME_TOP_RIGHT_CORNER); + SkBitmap* top_edge = resources()->GetPartBitmap(FRAME_TOP_EDGE); + SkBitmap* right_edge = resources()->GetPartBitmap(FRAME_RIGHT_EDGE); + SkBitmap* left_edge = resources()->GetPartBitmap(FRAME_LEFT_EDGE); + SkBitmap* bottom_left_corner = + resources()->GetPartBitmap(FRAME_BOTTOM_LEFT_CORNER); + SkBitmap* bottom_right_corner = + resources()->GetPartBitmap(FRAME_BOTTOM_RIGHT_CORNER); + SkBitmap* bottom_edge = resources()->GetPartBitmap(FRAME_BOTTOM_EDGE); + + // Top. + canvas->DrawBitmapInt(*top_left_corner, 0, 0); + canvas->TileImageInt(*top_edge, top_left_corner->width(), 0, + width() - top_right_corner->width(), top_edge->height()); + canvas->DrawBitmapInt(*top_right_corner, + width() - top_right_corner->width(), 0); + + // Right. + canvas->TileImageInt(*right_edge, width() - right_edge->width(), + top_right_corner->height(), right_edge->width(), + height() - top_right_corner->height() - + bottom_right_corner->height()); + + // Bottom. + canvas->DrawBitmapInt(*bottom_right_corner, + width() - bottom_right_corner->width(), + height() - bottom_right_corner->height()); + canvas->TileImageInt(*bottom_edge, bottom_left_corner->width(), + height() - bottom_edge->height(), + width() - bottom_left_corner->width() - + bottom_right_corner->width(), + bottom_edge->height()); + canvas->DrawBitmapInt(*bottom_left_corner, 0, + height() - bottom_left_corner->height()); + + // Left. + canvas->TileImageInt(*left_edge, 0, top_left_corner->height(), + left_edge->width(), + height() - top_left_corner->height() - bottom_left_corner->height()); +} + +void CustomFrameView::PaintMaximizedFrameBorder( + ChromeCanvas* canvas) { + SkBitmap* top_edge = resources()->GetPartBitmap(FRAME_TOP_EDGE); + canvas->TileImageInt(*top_edge, 0, FrameBorderThickness(), width(), + top_edge->height()); + + // The bottom of the titlebar actually comes from the top of the Client Edge + // graphic, with the actual client edge clipped off the bottom. + SkBitmap* titlebar_bottom = resources()->GetPartBitmap(FRAME_CLIENT_EDGE_TOP); + int edge_height = titlebar_bottom->height() - kClientEdgeThickness; + canvas->TileImageInt(*titlebar_bottom, 0, + frame_->GetClientView()->y() - edge_height, width(), edge_height); +} + +void CustomFrameView::PaintTitleBar(ChromeCanvas* canvas) { + WindowDelegate* d = frame_->GetDelegate(); + + // It seems like in some conditions we can be asked to paint after the window + // that contains us is WM_DESTROYed. At this point, our delegate is NULL. The + // correct long term fix may be to shut down the RootView in WM_DESTROY. + if (!d) + return; + + canvas->DrawStringInt(d->GetWindowTitle(), *title_font_, SK_ColorWHITE, + MirroredLeftPointForRect(title_bounds_), title_bounds_.y(), + title_bounds_.width(), title_bounds_.height()); +} + +void CustomFrameView::PaintRestoredClientEdge(ChromeCanvas* canvas) { + gfx::Rect client_area_bounds = frame_->GetClientView()->bounds(); + int client_area_top = client_area_bounds.y(); + + SkBitmap* top_left = resources()->GetPartBitmap(FRAME_CLIENT_EDGE_TOP_LEFT); + SkBitmap* top = resources()->GetPartBitmap(FRAME_CLIENT_EDGE_TOP); + SkBitmap* top_right = resources()->GetPartBitmap(FRAME_CLIENT_EDGE_TOP_RIGHT); + SkBitmap* right = resources()->GetPartBitmap(FRAME_CLIENT_EDGE_RIGHT); + SkBitmap* bottom_right = + resources()->GetPartBitmap(FRAME_CLIENT_EDGE_BOTTOM_RIGHT); + SkBitmap* bottom = resources()->GetPartBitmap(FRAME_CLIENT_EDGE_BOTTOM); + SkBitmap* bottom_left = + resources()->GetPartBitmap(FRAME_CLIENT_EDGE_BOTTOM_LEFT); + SkBitmap* left = resources()->GetPartBitmap(FRAME_CLIENT_EDGE_LEFT); + + // Top. + // This next calculation is necessary because the top center bitmap is shorter + // than the top left and right bitmaps. We need their top edges to line up, + // and we need the left and right edges to start below the corners' bottoms. + int top_edge_y = client_area_top - top->height(); + client_area_top = top_edge_y + top_left->height(); + canvas->DrawBitmapInt(*top_left, client_area_bounds.x() - top_left->width(), + top_edge_y); + canvas->TileImageInt(*top, client_area_bounds.x(), top_edge_y, + client_area_bounds.width(), top->height()); + canvas->DrawBitmapInt(*top_right, client_area_bounds.right(), top_edge_y); + + // Right. + int client_area_bottom = + std::max(client_area_top, client_area_bounds.bottom()); + int client_area_height = client_area_bottom - client_area_top; + canvas->TileImageInt(*right, client_area_bounds.right(), client_area_top, + right->width(), client_area_height); + + // Bottom. + canvas->DrawBitmapInt(*bottom_right, client_area_bounds.right(), + client_area_bottom); + canvas->TileImageInt(*bottom, client_area_bounds.x(), client_area_bottom, + client_area_bounds.width(), bottom_right->height()); + canvas->DrawBitmapInt(*bottom_left, + client_area_bounds.x() - bottom_left->width(), client_area_bottom); + + // Left. + canvas->TileImageInt(*left, client_area_bounds.x() - left->width(), + client_area_top, left->width(), client_area_height); +} + +void CustomFrameView::LayoutWindowControls() { + close_button_->SetImageAlignment(ImageButton::ALIGN_LEFT, + ImageButton::ALIGN_BOTTOM); + // Maximized buttons start at window top so that even if their images aren't + // drawn flush with the screen edge, they still obey Fitts' Law. + bool is_maximized = frame_->IsMaximized(); + int frame_thickness = FrameBorderThickness(); + int caption_y = is_maximized ? frame_thickness : kCaptionTopSpacing; + int top_extra_height = is_maximized ? kCaptionTopSpacing : 0; + // There should always be the same number of non-shadow pixels visible to the + // side of the caption buttons. In maximized mode we extend the rightmost + // button to the screen corner to obey Fitts' Law. + int right_extra_width = is_maximized ? + (kFrameBorderThickness - kFrameShadowThickness) : 0; + gfx::Size close_button_size = close_button_->GetPreferredSize(); + close_button_->SetBounds(width() - close_button_size.width() - + right_extra_width - frame_thickness, caption_y, + close_button_size.width() + right_extra_width, + close_button_size.height() + top_extra_height); + + // When the window is restored, we show a maximized button; otherwise, we show + // a restore button. + bool is_restored = !is_maximized && !frame_->IsMinimized(); + views::ImageButton* invisible_button = is_restored ? + restore_button_ : maximize_button_; + invisible_button->SetVisible(false); + + views::ImageButton* visible_button = is_restored ? + maximize_button_ : restore_button_; + FramePartBitmap normal_part, hot_part, pushed_part; + if (should_show_minmax_buttons_) { + visible_button->SetVisible(true); + visible_button->SetImageAlignment(ImageButton::ALIGN_LEFT, + ImageButton::ALIGN_BOTTOM); + gfx::Size visible_button_size = visible_button->GetPreferredSize(); + visible_button->SetBounds(close_button_->x() - visible_button_size.width(), + caption_y, visible_button_size.width(), + visible_button_size.height() + top_extra_height); + + minimize_button_->SetVisible(true); + minimize_button_->SetImageAlignment(ImageButton::ALIGN_LEFT, + ImageButton::ALIGN_BOTTOM); + gfx::Size minimize_button_size = minimize_button_->GetPreferredSize(); + minimize_button_->SetBounds( + visible_button->x() - minimize_button_size.width(), caption_y, + minimize_button_size.width(), + minimize_button_size.height() + top_extra_height); + + normal_part = FRAME_CLOSE_BUTTON_ICON; + hot_part = FRAME_CLOSE_BUTTON_ICON_H; + pushed_part = FRAME_CLOSE_BUTTON_ICON_P; + } else { + visible_button->SetVisible(false); + minimize_button_->SetVisible(false); + + normal_part = FRAME_CLOSE_BUTTON_ICON_SA; + hot_part = FRAME_CLOSE_BUTTON_ICON_SA_H; + pushed_part = FRAME_CLOSE_BUTTON_ICON_SA_P; + } + + close_button_->SetImage(CustomButton::BS_NORMAL, + active_resources_->GetPartBitmap(normal_part)); + close_button_->SetImage(CustomButton::BS_HOT, + active_resources_->GetPartBitmap(hot_part)); + close_button_->SetImage(CustomButton::BS_PUSHED, + active_resources_->GetPartBitmap(pushed_part)); +} + +void CustomFrameView::LayoutTitleBar() { + // Always lay out the icon, even when it's not present, so we can lay out the + // window title based on its position. + int frame_thickness = FrameBorderThickness(); + int icon_x = frame_thickness + kIconLeftSpacing; + + // The usable height of the titlebar area is the total height minus the top + // resize border and any edge area we draw at its bottom. + int title_top_spacing, title_thickness; + int top_height = TitleCoordinates(&title_top_spacing, &title_thickness); + int available_height = top_height - frame_thickness - + BottomEdgeThicknessWithinNonClientHeight(); + + // The icon takes up a constant fraction of the available height, down to a + // minimum size, and is always an even number of pixels on a side (presumably + // to make scaled icons look better). It's centered within the usable height. + int icon_size = std::max((available_height * kIconHeightFractionNumerator / + kIconHeightFractionDenominator) / 2 * 2, kIconMinimumSize); + int icon_y = ((available_height - icon_size) / 2) + frame_thickness; + + // Hack: Our frame border has a different "3D look" than Windows'. Theirs has + // a more complex gradient on the top that they push their icon/title below; + // then the maximized window cuts this off and the icon/title are centered in + // the remaining space. Because the apparent shape of our border is simpler, + // using the same positioning makes things look slightly uncentered with + // restored windows, so we come up to compensate. + if (!frame_->IsMaximized()) + icon_y -= kIconRestoredAdjust; + + views::WindowDelegate* d = frame_->GetDelegate(); + if (!d->ShouldShowWindowIcon()) + icon_size = 0; + system_menu_button_->SetBounds(icon_x, icon_y, icon_size, icon_size); + + // Size the title. + int icon_right = icon_x + icon_size; + int title_x = + icon_right + (d->ShouldShowWindowIcon() ? kIconTitleSpacing : 0); + int title_right = (should_show_minmax_buttons_ ? + minimize_button_->x() : close_button_->x()) - kTitleCaptionSpacing; + title_bounds_.SetRect(title_x, + title_top_spacing + ((title_thickness - title_font_->height()) / 2), + std::max(0, title_right - title_x), title_font_->height()); +} + +void CustomFrameView::LayoutClientView() { + int top_height = NonClientTopBorderHeight(); + int border_thickness = NonClientBorderThickness(); + client_view_bounds_.SetRect( + border_thickness, + top_height, + std::max(0, width() - (2 * border_thickness)), + std::max(0, height() - top_height - border_thickness)); +} + +// static +void CustomFrameView::InitClass() { + static bool initialized = false; + if (!initialized) { + active_resources_ = new ActiveWindowResources; + inactive_resources_ = new InactiveWindowResources; + + title_font_ = new ChromeFont(win_util::GetWindowTitleFont()); + + initialized = true; + } +} + +} // namespace views diff --git a/views/window/custom_frame_view.h b/views/window/custom_frame_view.h new file mode 100644 index 0000000..f071692 --- /dev/null +++ b/views/window/custom_frame_view.h @@ -0,0 +1,122 @@ +// Copyright (c) 2009 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 VIEWS_WINDOW_CUSTOM_FRAME_VIEW_H_ +#define VIEWS_WINDOW_CUSTOM_FRAME_VIEW_H_ + +#include "views/controls/button/image_button.h" +#include "views/window/non_client_view.h" +#include "views/window/window.h" +#include "views/window/window_resources.h" + +namespace gfx{ +class Size; +class Path; +class Point; +} +class ChromeCanvas; +class ChromeFont; + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// +// CustomFrameView +// +// A ChromeView that provides the non client frame for Windows. This means +// rendering the non-standard window caption, border, and controls. +// +//////////////////////////////////////////////////////////////////////////////// +class CustomFrameView : public NonClientFrameView, + public ButtonListener { + public: + explicit CustomFrameView(Window* frame); + virtual ~CustomFrameView(); + + // Overridden from views::NonClientFrameView: + virtual gfx::Rect GetBoundsForClientView() const; + virtual gfx::Rect GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const; + virtual gfx::Point GetSystemMenuPoint() const; + virtual int NonClientHitTest(const gfx::Point& point); + virtual void GetWindowMask(const gfx::Size& size, gfx::Path* window_mask); + virtual void EnableClose(bool enable); + virtual void ResetWindowControls(); + + // View overrides: + virtual void Paint(ChromeCanvas* canvas); + virtual void Layout(); + virtual gfx::Size GetPreferredSize(); + + // ButtonListener implementation: + virtual void ButtonPressed(Button* sender); + + private: + // Returns the thickness of the border that makes up the window frame edges. + // This does not include any client edge. + int FrameBorderThickness() const; + + // Returns the thickness of the entire nonclient left, right, and bottom + // borders, including both the window frame and any client edge. + int NonClientBorderThickness() const; + + // Returns the height of the entire nonclient top border, including the window + // frame, any title area, and any connected client edge. + int NonClientTopBorderHeight() const; + + // A bottom border, and, in restored mode, a client edge are drawn at the + // bottom of the titlebar. This returns the total height drawn. + int BottomEdgeThicknessWithinNonClientHeight() const; + + // Calculates multiple values related to title layout. Returns the height of + // the entire titlebar including any connected client edge. + int TitleCoordinates(int* title_top_spacing, + int* title_thickness) const; + + // Paint various sub-components of this view. + void PaintRestoredFrameBorder(ChromeCanvas* canvas); + void PaintMaximizedFrameBorder(ChromeCanvas* canvas); + void PaintTitleBar(ChromeCanvas* canvas); + void PaintRestoredClientEdge(ChromeCanvas* canvas); + + // Layout various sub-components of this view. + void LayoutWindowControls(); + void LayoutTitleBar(); + void LayoutClientView(); + + // Returns the resource collection to be used when rendering the window. + WindowResources* resources() const { + return frame_->IsActive() || paint_as_active() ? active_resources_ + : inactive_resources_; + } + + // The bounds of the client view, in this view's coordinates. + gfx::Rect client_view_bounds_; + + // The layout rect of the title, if visible. + gfx::Rect title_bounds_; + + // Window controls. + ImageButton* close_button_; + ImageButton* restore_button_; + ImageButton* maximize_button_; + ImageButton* minimize_button_; + ImageButton* system_menu_button_; // Uses the window icon if visible. + bool should_show_minmax_buttons_; + + // The window that owns this view. + Window* frame_; + + // Initialize various static resources. + static void InitClass(); + static WindowResources* active_resources_; + static WindowResources* inactive_resources_; + static ChromeFont* title_font_; + + DISALLOW_EVIL_CONSTRUCTORS(CustomFrameView); +}; + +} // namespace views + +#endif // #ifndef VIEWS_WINDOW_CUSTOM_FRAME_VIEW_H_ diff --git a/views/window/dialog_client_view.cc b/views/window/dialog_client_view.cc new file mode 100644 index 0000000..b7989ac --- /dev/null +++ b/views/window/dialog_client_view.cc @@ -0,0 +1,440 @@ +// Copyright (c) 2006-2008 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/window/dialog_client_view.h" + +#include <windows.h> +#include <uxtheme.h> +#include <vsstyle.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "base/gfx/native_theme.h" +#include "chrome/browser/views/standard_layout.h" +#include "grit/generated_resources.h" +#include "views/controls/button/native_button.h" +#include "views/window/dialog_delegate.h" +#include "views/window/window.h" + +namespace views { + +namespace { + +// Updates any of the standard buttons according to the delegate. +void UpdateButtonHelper(NativeButton* button_view, + DialogDelegate* delegate, + MessageBoxFlags::DialogButton button) { + std::wstring label = delegate->GetDialogButtonLabel(button); + if (!label.empty()) + button_view->SetLabel(label); + button_view->SetEnabled(delegate->IsDialogButtonEnabled(button)); + button_view->SetVisible(delegate->IsDialogButtonVisible(button)); +} + +void FillViewWithSysColor(ChromeCanvas* canvas, View* view, COLORREF color) { + SkColor sk_color = + SkColorSetRGB(GetRValue(color), GetGValue(color), GetBValue(color)); + canvas->FillRectInt(sk_color, 0, 0, view->width(), view->height()); +} + +// DialogButton ---------------------------------------------------------------- + +// DialogButtons is used for the ok/cancel buttons of the window. DialogButton +// forwards AcceleratorPressed to the delegate. + +class DialogButton : public NativeButton { + public: + DialogButton(ButtonListener* listener, + Window* owner, + MessageBoxFlags::DialogButton type, + const std::wstring& title, + bool is_default) + : NativeButton(listener, title), + owner_(owner), + type_(type) { + SetIsDefault(is_default); + } + + // Overridden to forward to the delegate. + virtual bool AcceleratorPressed(const Accelerator& accelerator) { + if (!owner_->GetDelegate()->AsDialogDelegate()-> + AreAcceleratorsEnabled(type_)) { + return false; + } + return NativeButton::AcceleratorPressed(accelerator); + } + + private: + Window* owner_; + const MessageBoxFlags::DialogButton type_; + + DISALLOW_COPY_AND_ASSIGN(DialogButton); +}; + +} // namespace + +// static +ChromeFont* DialogClientView::dialog_button_font_ = NULL; +static const int kDialogMinButtonWidth = 75; +static const int kDialogButtonLabelSpacing = 16; +static const int kDialogButtonContentSpacing = 5; + +// The group used by the buttons. This name is chosen voluntarily big not to +// conflict with other groups that could be in the dialog content. +static const int kButtonGroup = 6666; + +/////////////////////////////////////////////////////////////////////////////// +// DialogClientView, public: + +DialogClientView::DialogClientView(Window* owner, View* contents_view) + : ClientView(owner, contents_view), + ok_button_(NULL), + cancel_button_(NULL), + extra_view_(NULL), + accepted_(false), + default_button_(NULL) { + InitClass(); +} + +DialogClientView::~DialogClientView() { +} + +void DialogClientView::ShowDialogButtons() { + DialogDelegate* dd = GetDialogDelegate(); + int buttons = dd->GetDialogButtons(); + if (buttons & MessageBoxFlags::DIALOGBUTTON_OK && !ok_button_) { + std::wstring label = + dd->GetDialogButtonLabel(MessageBoxFlags::DIALOGBUTTON_OK); + if (label.empty()) + label = l10n_util::GetString(IDS_OK); + bool is_default_button = + (dd->GetDefaultDialogButton() & MessageBoxFlags::DIALOGBUTTON_OK) != 0; + ok_button_ = new DialogButton(this, window(), + MessageBoxFlags::DIALOGBUTTON_OK, label, + is_default_button); + ok_button_->SetGroup(kButtonGroup); + if (is_default_button) + default_button_ = ok_button_; + if (!(buttons & MessageBoxFlags::DIALOGBUTTON_CANCEL)) + ok_button_->AddAccelerator(Accelerator(VK_ESCAPE, false, false, false)); + AddChildView(ok_button_); + } + if (buttons & MessageBoxFlags::DIALOGBUTTON_CANCEL && !cancel_button_) { + std::wstring label = + dd->GetDialogButtonLabel(MessageBoxFlags::DIALOGBUTTON_CANCEL); + if (label.empty()) { + if (buttons & MessageBoxFlags::DIALOGBUTTON_OK) { + label = l10n_util::GetString(IDS_CANCEL); + } else { + label = l10n_util::GetString(IDS_CLOSE); + } + } + bool is_default_button = + (dd->GetDefaultDialogButton() & MessageBoxFlags::DIALOGBUTTON_CANCEL) + != 0; + cancel_button_ = new DialogButton(this, window(), + MessageBoxFlags::DIALOGBUTTON_CANCEL, + label, is_default_button); + cancel_button_->SetGroup(kButtonGroup); + cancel_button_->AddAccelerator(Accelerator(VK_ESCAPE, false, false, false)); + if (is_default_button) + default_button_ = ok_button_; + AddChildView(cancel_button_); + } + if (!buttons) { + // Register the escape key as an accelerator which will close the window + // if there are no dialog buttons. + AddAccelerator(Accelerator(VK_ESCAPE, false, false, false)); + } +} + +void DialogClientView::SetDefaultButton(NativeButton* new_default_button) { + if (default_button_ && default_button_ != new_default_button) { + default_button_->SetIsDefault(false); + default_button_ = NULL; + } + + if (new_default_button) { + default_button_ = new_default_button; + default_button_->SetIsDefault(true); + } +} + +void DialogClientView::FocusWillChange(View* focused_before, + View* focused_now) { + NativeButton* new_default_button = NULL; + if (focused_now && + focused_now->GetClassName() == NativeButton::kViewClassName) { + new_default_button = static_cast<NativeButton*>(focused_now); + } else { + // The focused view is not a button, get the default button from the + // delegate. + DialogDelegate* dd = GetDialogDelegate(); + if ((dd->GetDefaultDialogButton() & MessageBoxFlags::DIALOGBUTTON_OK) != 0) + new_default_button = ok_button_; + if ((dd->GetDefaultDialogButton() & MessageBoxFlags::DIALOGBUTTON_CANCEL) + != 0) + new_default_button = cancel_button_; + } + SetDefaultButton(new_default_button); +} + +// Changing dialog labels will change button widths. +void DialogClientView::UpdateDialogButtons() { + DialogDelegate* dd = GetDialogDelegate(); + int buttons = dd->GetDialogButtons(); + + if (buttons & MessageBoxFlags::DIALOGBUTTON_OK) + UpdateButtonHelper(ok_button_, dd, MessageBoxFlags::DIALOGBUTTON_OK); + + if (buttons & MessageBoxFlags::DIALOGBUTTON_CANCEL) { + UpdateButtonHelper(cancel_button_, dd, + MessageBoxFlags::DIALOGBUTTON_CANCEL); + } + + LayoutDialogButtons(); + SchedulePaint(); +} + +void DialogClientView::AcceptWindow() { + if (accepted_) { + // We should only get into AcceptWindow once. + NOTREACHED(); + return; + } + if (GetDialogDelegate()->Accept(false)) { + accepted_ = true; + window()->Close(); + } +} + +void DialogClientView::CancelWindow() { + // Call the standard Close handler, which checks with the delegate before + // proceeding. This checking _isn't_ done here, but in the WM_CLOSE handler, + // so that the close box on the window also shares this code path. + window()->Close(); +} + +/////////////////////////////////////////////////////////////////////////////// +// DialogClientView, ClientView overrides: + +bool DialogClientView::CanClose() const { + if (!accepted_) { + DialogDelegate* dd = GetDialogDelegate(); + int buttons = dd->GetDialogButtons(); + if (buttons & MessageBoxFlags::DIALOGBUTTON_CANCEL) + return dd->Cancel(); + if (buttons & MessageBoxFlags::DIALOGBUTTON_OK) + return dd->Accept(true); + } + return true; +} + +void DialogClientView::WindowClosing() { + FocusManager* focus_manager = GetFocusManager(); + DCHECK(focus_manager); + if (focus_manager) + focus_manager->RemoveFocusChangeListener(this); + ClientView::WindowClosing(); +} + +int DialogClientView::NonClientHitTest(const gfx::Point& point) { + if (size_box_bounds_.Contains(point.x() - x(), point.y() - y())) + return HTBOTTOMRIGHT; + return ClientView::NonClientHitTest(point); +} + +//////////////////////////////////////////////////////////////////////////////// +// DialogClientView, View overrides: + +void DialogClientView::Paint(ChromeCanvas* canvas) { + FillViewWithSysColor(canvas, this, GetSysColor(COLOR_3DFACE)); +} + +void DialogClientView::PaintChildren(ChromeCanvas* canvas) { + View::PaintChildren(canvas); + if (!window()->IsMaximized() && !window()->IsMinimized()) + PaintSizeBox(canvas); +} + +void DialogClientView::Layout() { + if (has_dialog_buttons()) + LayoutDialogButtons(); + LayoutContentsView(); +} + +void DialogClientView::ViewHierarchyChanged(bool is_add, View* parent, + View* child) { + if (is_add && child == this) { + // Can only add and update the dialog buttons _after_ they are added to the + // view hierarchy since they are native controls and require the + // Container's HWND. + ShowDialogButtons(); + ClientView::ViewHierarchyChanged(is_add, parent, child); + + FocusManager* focus_manager = GetFocusManager(); + // Listen for focus change events so we can update the default button. + DCHECK(focus_manager); // bug #1291225: crash reports seem to indicate it + // can be NULL. + if (focus_manager) + focus_manager->AddFocusChangeListener(this); + + // The "extra view" must be created and installed after the contents view + // has been inserted into the view hierarchy. + CreateExtraView(); + UpdateDialogButtons(); + Layout(); + } +} + +gfx::Size DialogClientView::GetPreferredSize() { + gfx::Size prefsize = contents_view()->GetPreferredSize(); + int button_height = 0; + if (has_dialog_buttons()) { + if (cancel_button_) + button_height = cancel_button_->height(); + else + button_height = ok_button_->height(); + // Account for padding above and below the button. + button_height += kDialogButtonContentSpacing + kButtonVEdgeMargin; + } + prefsize.Enlarge(0, button_height); + return prefsize; +} + +bool DialogClientView::AcceleratorPressed(const Accelerator& accelerator) { + DCHECK(accelerator.GetKeyCode() == VK_ESCAPE); // We only expect Escape key. + window()->Close(); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// DialogClientView, ButtonListener implementation: + +void DialogClientView::ButtonPressed(Button* sender) { + if (sender == ok_button_) { + AcceptWindow(); + } else if (sender == cancel_button_) { + CancelWindow(); + } else { + NOTREACHED(); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// DialogClientView, private: + +void DialogClientView::PaintSizeBox(ChromeCanvas* canvas) { + if (window()->GetDelegate()->CanResize() || + window()->GetDelegate()->CanMaximize()) { + HDC dc = canvas->beginPlatformPaint(); + SIZE gripper_size = { 0, 0 }; + gfx::NativeTheme::instance()->GetThemePartSize( + gfx::NativeTheme::STATUS, dc, SP_GRIPPER, 1, NULL, TS_TRUE, + &gripper_size); + + // TODO(beng): (http://b/1085509) In "classic" rendering mode, there isn't + // a theme-supplied gripper. We should probably improvise + // something, which would also require changing |gripper_size| + // to have different default values, too... + size_box_bounds_ = GetLocalBounds(false); + size_box_bounds_.set_x(size_box_bounds_.right() - gripper_size.cx); + size_box_bounds_.set_y(size_box_bounds_.bottom() - gripper_size.cy); + RECT native_bounds = size_box_bounds_.ToRECT(); + gfx::NativeTheme::instance()->PaintStatusGripper( + dc, SP_PANE, 1, 0, &native_bounds); + canvas->endPlatformPaint(); + } +} + +int DialogClientView::GetButtonWidth(int button) const { + DialogDelegate* dd = GetDialogDelegate(); + std::wstring button_label = dd->GetDialogButtonLabel( + static_cast<MessageBoxFlags::DialogButton>(button)); + int string_width = dialog_button_font_->GetStringWidth(button_label); + return std::max(string_width + kDialogButtonLabelSpacing, + kDialogMinButtonWidth); +} + +int DialogClientView::GetButtonsHeight() const { + if (has_dialog_buttons()) { + if (cancel_button_) + return cancel_button_->height() + kDialogButtonContentSpacing; + return ok_button_->height() + kDialogButtonContentSpacing; + } + return 0; +} + +void DialogClientView::LayoutDialogButtons() { + gfx::Rect extra_bounds; + if (cancel_button_) { + gfx::Size ps = cancel_button_->GetPreferredSize(); + gfx::Rect lb = GetLocalBounds(false); + int button_width = GetButtonWidth(MessageBoxFlags::DIALOGBUTTON_CANCEL); + int button_x = lb.right() - button_width - kButtonHEdgeMargin; + int button_y = lb.bottom() - ps.height() - kButtonVEdgeMargin; + cancel_button_->SetBounds(button_x, button_y, button_width, ps.height()); + // The extra view bounds are dependent on this button. + extra_bounds.set_width(std::max(0, cancel_button_->x())); + extra_bounds.set_y(cancel_button_->y()); + } + if (ok_button_) { + gfx::Size ps = ok_button_->GetPreferredSize(); + gfx::Rect lb = GetLocalBounds(false); + int button_width = GetButtonWidth(MessageBoxFlags::DIALOGBUTTON_OK); + int ok_button_right = lb.right() - kButtonHEdgeMargin; + if (cancel_button_) + ok_button_right = cancel_button_->x() - kRelatedButtonHSpacing; + int button_x = ok_button_right - button_width; + int button_y = lb.bottom() - ps.height() - kButtonVEdgeMargin; + ok_button_->SetBounds(button_x, button_y, ok_button_right - button_x, + ps.height()); + // The extra view bounds are dependent on this button. + extra_bounds.set_width(std::max(0, ok_button_->x())); + extra_bounds.set_y(ok_button_->y()); + } + if (extra_view_) { + gfx::Size ps = extra_view_->GetPreferredSize(); + gfx::Rect lb = GetLocalBounds(false); + extra_bounds.set_x(lb.x() + kButtonHEdgeMargin); + extra_bounds.set_height(ps.height()); + extra_view_->SetBounds(extra_bounds); + } +} + +void DialogClientView::LayoutContentsView() { + gfx::Rect lb = GetLocalBounds(false); + lb.set_height(std::max(0, lb.height() - GetButtonsHeight())); + contents_view()->SetBounds(lb); + contents_view()->Layout(); +} + +void DialogClientView::CreateExtraView() { + View* extra_view = GetDialogDelegate()->GetExtraView(); + if (extra_view && !extra_view_) { + extra_view_ = extra_view; + extra_view_->SetGroup(kButtonGroup); + AddChildView(extra_view_); + } +} + +DialogDelegate* DialogClientView::GetDialogDelegate() const { + DialogDelegate* dd = window()->GetDelegate()->AsDialogDelegate(); + DCHECK(dd); + return dd; +} + +// static +void DialogClientView::InitClass() { + static bool initialized = false; + if (!initialized) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + dialog_button_font_ = new ChromeFont(rb.GetFont(ResourceBundle::BaseFont)); + initialized = true; + } +} + +} // namespace views diff --git a/views/window/dialog_client_view.h b/views/window/dialog_client_view.h new file mode 100644 index 0000000..347b61c --- /dev/null +++ b/views/window/dialog_client_view.h @@ -0,0 +1,123 @@ +// Copyright (c) 2006-2008 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 VIEWS_WINDOW_DIALOG_CLIENT_VIEW_H_ +#define VIEWS_WINDOW_DIALOG_CLIENT_VIEW_H_ + +#include "app/gfx/chrome_font.h" +#include "views/focus/focus_manager.h" +#include "views/controls/button/button.h" +#include "views/window/client_view.h" + +namespace views { + +class DialogDelegate; +class NativeButton; +class Window; + +/////////////////////////////////////////////////////////////////////////////// +// DialogClientView +// +// This ClientView subclass provides the content of a typical dialog box, +// including a strip of buttons at the bottom right of the window, default +// accelerator handlers for accept and cancel, and the ability for the +// embedded contents view to provide extra UI to be shown in the row of +// buttons. +// +class DialogClientView : public ClientView, + public ButtonListener, + public FocusChangeListener { + public: + DialogClientView(Window* window, View* contents_view); + virtual ~DialogClientView(); + + // Adds the dialog buttons required by the supplied WindowDelegate to the + // view. + void ShowDialogButtons(); + + // Updates the enabled state and label of the buttons required by the + // supplied WindowDelegate + void UpdateDialogButtons(); + + // Accept the changes made in the window that contains this ClientView. + void AcceptWindow(); + + // Cancel the changes made in the window that contains this ClientView. + void CancelWindow(); + + // Accessors in case the user wishes to adjust these buttons. + NativeButton* ok_button() const { return ok_button_; } + NativeButton* cancel_button() const { return cancel_button_; } + + // Overridden from ClientView: + virtual bool CanClose() const; + virtual void WindowClosing(); + virtual int NonClientHitTest(const gfx::Point& point); + virtual DialogClientView* AsDialogClientView() { return this; } + + // FocusChangeListener implementation: + virtual void FocusWillChange(View* focused_before, View* focused_now); + + protected: + // View overrides: + virtual void Paint(ChromeCanvas* canvas); + virtual void PaintChildren(ChromeCanvas* canvas); + virtual void Layout(); + virtual void ViewHierarchyChanged(bool is_add, View* parent, View* child); + virtual gfx::Size GetPreferredSize(); + virtual bool AcceleratorPressed(const Accelerator& accelerator); + + // ButtonListener implementation: + virtual void ButtonPressed(Button* sender); + + private: + // Paint the size box in the bottom right corner of the window if it is + // resizable. + void PaintSizeBox(ChromeCanvas* canvas); + + // Returns the width of the specified dialog button using the correct font. + int GetButtonWidth(int button) const; + int GetButtonsHeight() const; + + // Position and size various sub-views. + void LayoutDialogButtons(); + void LayoutContentsView(); + + // Makes the specified button the default button. + void SetDefaultButton(NativeButton* button); + + bool has_dialog_buttons() const { return ok_button_ || cancel_button_; } + + // Create and add the extra view, if supplied by the delegate. + void CreateExtraView(); + + // Returns the DialogDelegate for the window. + DialogDelegate* GetDialogDelegate() const; + + // The dialog buttons. + NativeButton* ok_button_; + NativeButton* cancel_button_; + + // The button that is currently the default button if any. + NativeButton* default_button_; + + // The button-level extra view, NULL unless the dialog delegate supplies one. + View* extra_view_; + + // The layout rect of the size box, when visible. + gfx::Rect size_box_bounds_; + + // True if the window was Accepted by the user using the OK button. + bool accepted_; + + // Static resource initialization + static void InitClass(); + static ChromeFont* dialog_button_font_; + + DISALLOW_COPY_AND_ASSIGN(DialogClientView); +}; + +} // namespace views + +#endif // #ifndef VIEWS_WINDOW_DIALOG_CLIENT_VIEW_H_ diff --git a/views/window/dialog_delegate.cc b/views/window/dialog_delegate.cc new file mode 100644 index 0000000..4ae9a91 --- /dev/null +++ b/views/window/dialog_delegate.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2006-2008 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/window/dialog_delegate.h" + +#include "base/logging.h" +#include "views/controls/button/native_button.h" +#include "views/window/window.h" + +namespace views { + +// Overridden from WindowDelegate: + +int DialogDelegate::GetDefaultDialogButton() const { + if (GetDialogButtons() & MessageBoxFlags::DIALOGBUTTON_OK) + return MessageBoxFlags::DIALOGBUTTON_OK; + if (GetDialogButtons() & MessageBoxFlags::DIALOGBUTTON_CANCEL) + return MessageBoxFlags::DIALOGBUTTON_CANCEL; + return MessageBoxFlags::DIALOGBUTTON_NONE; +} + +View* DialogDelegate::GetInitiallyFocusedView() { + // Focus the default button if any. + DialogClientView* dcv = GetDialogClientView(); + int default_button = GetDefaultDialogButton(); + if (default_button == MessageBoxFlags::DIALOGBUTTON_NONE) + return NULL; + + if ((default_button & GetDialogButtons()) == 0) { + // The default button is a button we don't have. + NOTREACHED(); + return NULL; + } + + if (default_button & MessageBoxFlags::DIALOGBUTTON_OK) + return dcv->ok_button(); + if (default_button & MessageBoxFlags::DIALOGBUTTON_CANCEL) + return dcv->cancel_button(); + return NULL; +} + +ClientView* DialogDelegate::CreateClientView(Window* window) { + return new DialogClientView(window, GetContentsView()); +} + +DialogClientView* DialogDelegate::GetDialogClientView() const { + ClientView* client_view = window()->GetClientView(); + DialogClientView* dialog_client_view = client_view->AsDialogClientView(); + DCHECK(dialog_client_view); + return dialog_client_view; +} + +} // namespace views diff --git a/views/window/dialog_delegate.h b/views/window/dialog_delegate.h new file mode 100644 index 0000000..4418e61f --- /dev/null +++ b/views/window/dialog_delegate.h @@ -0,0 +1,113 @@ +// Copyright (c) 2006-2008 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 VIEWS_WINDOW_DIALOG_DELEGATE_H_ +#define VIEWS_WINDOW_DIALOG_DELEGATE_H_ + +#include "app/message_box_flags.h" +#include "views/window/dialog_client_view.h" +#include "views/window/window_delegate.h" + +namespace views { + +class View; + +/////////////////////////////////////////////////////////////////////////////// +// +// DialogDelegate +// +// DialogDelegate is an interface implemented by objects that wish to show a +// dialog box Window. The window that is displayed uses this interface to +// determine how it should be displayed and notify the delegate object of +// certain events. +// +/////////////////////////////////////////////////////////////////////////////// +class DialogDelegate : public WindowDelegate { + public: + virtual DialogDelegate* AsDialogDelegate() { return this; } + + // Returns a mask specifying which of the available DialogButtons are visible + // for the dialog. Note: If an OK button is provided, you should provide a + // CANCEL button. A dialog box with just an OK button is frowned upon and + // considered a very special case, so if you're planning on including one, + // you should reconsider, or beng says there will be stabbings. + // + // To use the extra button you need to override GetDialogButtons() + virtual int GetDialogButtons() const { + return MessageBoxFlags::DIALOGBUTTON_OK | + MessageBoxFlags::DIALOGBUTTON_CANCEL; + } + + // Returns whether accelerators are enabled on the button. This is invoked + // when an accelerator is pressed, not at construction time. This + // returns true. + virtual bool AreAcceleratorsEnabled(MessageBoxFlags::DialogButton button) { + return true; + } + + // Returns the label of the specified DialogButton. + virtual std::wstring GetDialogButtonLabel( + MessageBoxFlags::DialogButton button) const { + // empty string results in defaults for MessageBoxFlags::DIALOGBUTTON_OK, + // MessageBoxFlags::DIALOGBUTTON_CANCEL. + return L""; + } + + // Override this function if with a view which will be shown in the same + // row as the OK and CANCEL buttons but flush to the left and extending + // up to the buttons. + virtual View* GetExtraView() { return NULL; } + + // Returns the default dialog button. This should not be a mask as only + // one button should ever be the default button. Return + // MessageBoxFlags::DIALOGBUTTON_NONE if there is no default. Default + // behavior is to return MessageBoxFlags::DIALOGBUTTON_OK or + // MessageBoxFlags::DIALOGBUTTON_CANCEL (in that order) if they are + // present, MessageBoxFlags::DIALOGBUTTON_NONE otherwise. + virtual int GetDefaultDialogButton() const; + + // Returns whether the specified dialog button is enabled. + virtual bool IsDialogButtonEnabled( + MessageBoxFlags::DialogButton button) const { + return true; + } + + // Returns whether the specified dialog button is visible. + virtual bool IsDialogButtonVisible( + MessageBoxFlags::DialogButton button) const { + return true; + } + + // For Dialog boxes, if there is a "Cancel" button, this is called when the + // user presses the "Cancel" button or the Close button on the window or + // in the system menu, or presses the Esc key. This function should return + // true if the window can be closed after it returns, or false if it must + // remain open. + virtual bool Cancel() { return true; } + + // For Dialog boxes, this is called when the user presses the "OK" button, + // or the Enter key. Can also be called on Esc key or close button + // presses if there is no "Cancel" button. This function should return + // true if the window can be closed after the window can be closed after + // it returns, or false if it must remain open. If |window_closing| is + // true, it means that this handler is being called because the window is + // being closed (e.g. by Window::Close) and there is no Cancel handler, + // so Accept is being called instead. + virtual bool Accept(bool window_closing) { return Accept(); } + virtual bool Accept() { return true; } + + // Overridden from WindowDelegate: + virtual View* GetInitiallyFocusedView(); + + // Overridden from WindowDelegate: + virtual ClientView* CreateClientView(Window* window); + + // A helper for accessing the DialogClientView object contained by this + // delegate's Window. + DialogClientView* GetDialogClientView() const; +}; + +} // namespace views + +#endif // VIEWS_WINDOW_DIALOG_DELEGATE_H_ diff --git a/views/window/native_frame_view.cc b/views/window/native_frame_view.cc new file mode 100644 index 0000000..05a8dcb --- /dev/null +++ b/views/window/native_frame_view.cc @@ -0,0 +1,60 @@ +// Copyright (c) 2009 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/window/native_frame_view.h" + +#include "views/window/window_win.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// NativeFrameView, public: + +NativeFrameView::NativeFrameView(WindowWin* frame) + : NonClientFrameView(), + frame_(frame) { +} + +NativeFrameView::~NativeFrameView() { +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeFrameView, NonClientFrameView overrides: + +gfx::Rect NativeFrameView::GetBoundsForClientView() const { + return gfx::Rect(0, 0, width(), height()); +} + +gfx::Rect NativeFrameView::GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const { + RECT rect = client_bounds.ToRECT(); + AdjustWindowRectEx(&rect, frame_->window_style(), FALSE, + frame_->window_ex_style()); + return gfx::Rect(rect); +} + +gfx::Point NativeFrameView::GetSystemMenuPoint() const { + POINT temp = {0, -kFrameShadowThickness }; + MapWindowPoints(frame_->GetNativeView(), HWND_DESKTOP, &temp, 1); + return gfx::Point(temp); +} + +int NativeFrameView::NonClientHitTest(const gfx::Point& point) { + return HTNOWHERE; +} + +void NativeFrameView::GetWindowMask(const gfx::Size& size, + gfx::Path* window_mask) { + // Nothing to do, we use the default window mask. +} + +void NativeFrameView::EnableClose(bool enable) { + // Nothing to do, handled automatically by Window. +} + +void NativeFrameView::ResetWindowControls() { + // Nothing to do. +} + +} // namespace views diff --git a/views/window/native_frame_view.h b/views/window/native_frame_view.h new file mode 100644 index 0000000..e2efa9d --- /dev/null +++ b/views/window/native_frame_view.h @@ -0,0 +1,39 @@ +// Copyright (c) 2009 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 VIEWS_WINDOW_NATIVE_FRAME_VIEW_H_ +#define VIEWS_WINDOW_NATIVE_FRAME_VIEW_H_ + +#include "views/window/non_client_view.h" + +namespace views { + +class WindowWin; + +class NativeFrameView : public NonClientFrameView { + public: + explicit NativeFrameView(WindowWin* frame); + virtual ~NativeFrameView(); + + // NonClientFrameView overrides: + virtual gfx::Rect GetBoundsForClientView() const; + virtual gfx::Rect GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const; + virtual gfx::Point GetSystemMenuPoint() const; + virtual int NonClientHitTest(const gfx::Point& point); + virtual void GetWindowMask(const gfx::Size& size, + gfx::Path* window_mask); + virtual void EnableClose(bool enable); + virtual void ResetWindowControls(); + + private: + // Our containing frame. + WindowWin* frame_; + + DISALLOW_COPY_AND_ASSIGN(NativeFrameView); +}; + +} // namespace views + +#endif // #ifndef VIEWS_WINDOW_NATIVE_FRAME_VIEW_H_ diff --git a/views/window/non_client_view.cc b/views/window/non_client_view.cc new file mode 100644 index 0000000..9a06cfc --- /dev/null +++ b/views/window/non_client_view.cc @@ -0,0 +1,255 @@ +// Copyright (c) 2006-2008 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/window/non_client_view.h" + +#include "chrome/common/win_util.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" +#include "views/window/window.h" + +namespace views { + +const int NonClientFrameView::kFrameShadowThickness = 1; +const int NonClientFrameView::kClientEdgeThickness = 1; + +// The frame view and the client view are always at these specific indices, +// because the RootView message dispatch sends messages to items higher in the +// z-order first and we always want the client view to have first crack at +// handling mouse messages. +static const int kFrameViewIndex = 0; +static const int kClientViewIndex = 1; + +//////////////////////////////////////////////////////////////////////////////// +// NonClientView, public: + +NonClientView::NonClientView(Window* frame) + : frame_(frame), + client_view_(NULL), + use_native_frame_(win_util::ShouldUseVistaFrame()) { +} + +NonClientView::~NonClientView() { + // This value may have been reset before the window hierarchy shuts down, + // so we need to manually remove it. + RemoveChildView(frame_view_.get()); +} + +void NonClientView::SetFrameView(NonClientFrameView* frame_view) { + // See comment in header about ownership. + frame_view->SetParentOwned(false); + if (frame_view_.get()) + RemoveChildView(frame_view_.get()); + frame_view_.reset(frame_view); + if (GetParent()) + AddChildView(kFrameViewIndex, frame_view_.get()); +} + +bool NonClientView::CanClose() const { + return client_view_->CanClose(); +} + +void NonClientView::WindowClosing() { + client_view_->WindowClosing(); +} + +void NonClientView::SetUseNativeFrame(bool use_native_frame) { + use_native_frame_ = use_native_frame; + SetFrameView(frame_->CreateFrameViewForWindow()); + GetRootView()->ThemeChanged(); + Layout(); + SchedulePaint(); + frame_->UpdateFrameAfterFrameChange(); +} + +bool NonClientView::UseNativeFrame() const { + // The frame view may always require a custom frame, e.g. Constrained Windows. + bool always_use_custom_frame = + frame_view_.get() && frame_view_->AlwaysUseCustomFrame(); + return !always_use_custom_frame && use_native_frame_; +} + +void NonClientView::DisableInactiveRendering(bool disable) { + frame_view_->DisableInactiveRendering(disable); +} + +gfx::Rect NonClientView::GetWindowBoundsForClientBounds( + const gfx::Rect client_bounds) const { + return frame_view_->GetWindowBoundsForClientBounds(client_bounds); +} + +gfx::Point NonClientView::GetSystemMenuPoint() const { + return frame_view_->GetSystemMenuPoint(); +} + +int NonClientView::NonClientHitTest(const gfx::Point& point) { + // Sanity check. + if (!bounds().Contains(point)) + return HTNOWHERE; + + // The ClientView gets first crack, since it overlays the NonClientFrameView + // in the display stack. + int frame_component = client_view_->NonClientHitTest(point); + if (frame_component != HTNOWHERE) + return frame_component; + + // Finally ask the NonClientFrameView. It's at the back of the display stack + // so it gets asked last. + return frame_view_->NonClientHitTest(point); +} + +void NonClientView::GetWindowMask(const gfx::Size& size, + gfx::Path* window_mask) { + frame_view_->GetWindowMask(size, window_mask); +} + +void NonClientView::EnableClose(bool enable) { + frame_view_->EnableClose(enable); +} + +void NonClientView::ResetWindowControls() { + frame_view_->ResetWindowControls(); +} + +void NonClientView::LayoutFrameView() { + // First layout the NonClientFrameView, which determines the size of the + // ClientView... + frame_view_->SetBounds(0, 0, width(), height()); + + // We need to manually call Layout here because layout for the frame view can + // change independently of the bounds changing - e.g. after the initial + // display of the window the metrics of the native window controls can change, + // which does not change the bounds of the window but requires a re-layout to + // trigger a repaint. We override DidChangeBounds for the NonClientFrameView + // to do nothing so that SetBounds above doesn't cause Layout to be called + // twice. + frame_view_->Layout(); +} + +//////////////////////////////////////////////////////////////////////////////// +// NonClientView, View overrides: + +gfx::Size NonClientView::GetPreferredSize() { + // TODO(pkasting): This should probably be made to look similar to + // GetMinimumSize() below. This will require implementing GetPreferredSize() + // better in the various frame views. + gfx::Rect client_bounds(gfx::Point(), client_view_->GetPreferredSize()); + return GetWindowBoundsForClientBounds(client_bounds).size(); +} + +gfx::Size NonClientView::GetMinimumSize() { + return frame_view_->GetMinimumSize(); +} + +void NonClientView::Layout() { + LayoutFrameView(); + + // Then layout the ClientView, using those bounds. + client_view_->SetBounds(frame_view_->GetBoundsForClientView()); + + // We need to manually call Layout on the ClientView as well for the same + // reason as above. + client_view_->Layout(); +} + +void NonClientView::ViewHierarchyChanged(bool is_add, View* parent, + View* child) { + // Add our two child views here as we are added to the Widget so that if we + // are subsequently resized all the parent-child relationships are + // established. + if (is_add && GetWidget() && child == this) { + AddChildView(kFrameViewIndex, frame_view_.get()); + AddChildView(kClientViewIndex, client_view_); + } +} + +views::View* NonClientView::GetViewForPoint(const gfx::Point& point) { + return GetViewForPoint(point, false); +} + +views::View* NonClientView::GetViewForPoint(const gfx::Point& point, + bool can_create_floating) { + // Because of the z-ordering of our child views (the client view is positioned + // over the non-client frame view, if the client view ever overlaps the frame + // view visually (as it does for the browser window), then it will eat mouse + // events for the window controls. We override this method here so that we can + // detect this condition and re-route the events to the non-client frame view. + // The assumption is that the frame view's implementation of HitTest will only + // return true for area not occupied by the client view. + gfx::Point point_in_child_coords(point); + View::ConvertPointToView(this, frame_view_.get(), &point_in_child_coords); + if (frame_view_->HitTest(point_in_child_coords)) + return frame_view_->GetViewForPoint(point); + + return View::GetViewForPoint(point, can_create_floating); +} + +//////////////////////////////////////////////////////////////////////////////// +// NonClientFrameView, View overrides: + +bool NonClientFrameView::HitTest(const gfx::Point& l) const { + // For the default case, we assume the non-client frame view never overlaps + // the client view. + return !GetWindow()->GetClientView()->bounds().Contains(l); +} + +void NonClientFrameView::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + // Overridden to do nothing. The NonClientView manually calls Layout on the + // FrameView when it is itself laid out, see comment in NonClientView::Layout. +} + +//////////////////////////////////////////////////////////////////////////////// +// NonClientFrameView, protected: + +int NonClientFrameView::GetHTComponentForFrame(const gfx::Point& point, + int top_resize_border_height, + int resize_border_thickness, + int top_resize_corner_height, + int resize_corner_width, + bool can_resize) { + // Tricky: In XP, native behavior is to return HTTOPLEFT and HTTOPRIGHT for + // a |resize_corner_size|-length strip of both the side and top borders, but + // only to return HTBOTTOMLEFT/HTBOTTOMRIGHT along the bottom border + corner + // (not the side border). Vista goes further and doesn't return these on any + // of the side borders. We allow callers to match either behavior. + int component; + if (point.x() < resize_border_thickness) { + if (point.y() < top_resize_corner_height) + component = HTTOPLEFT; + else if (point.y() >= (height() - resize_border_thickness)) + component = HTBOTTOMLEFT; + else + component = HTLEFT; + } else if (point.x() >= (width() - resize_border_thickness)) { + if (point.y() < top_resize_corner_height) + component = HTTOPRIGHT; + else if (point.y() >= (height() - resize_border_thickness)) + component = HTBOTTOMRIGHT; + else + component = HTRIGHT; + } else if (point.y() < top_resize_border_height) { + if (point.x() < resize_corner_width) + component = HTTOPLEFT; + else if (point.x() >= (width() - resize_corner_width)) + component = HTTOPRIGHT; + else + component = HTTOP; + } else if (point.y() >= (height() - resize_border_thickness)) { + if (point.x() < resize_corner_width) + component = HTBOTTOMLEFT; + else if (point.x() >= (width() - resize_corner_width)) + component = HTBOTTOMRIGHT; + else + component = HTBOTTOM; + } else { + return HTNOWHERE; + } + + // If the window can't be resized, there are no resize boundaries, just + // window borders. + return can_resize ? component : HTBORDER; +} + +} // namespace views diff --git a/views/window/non_client_view.h b/views/window/non_client_view.h new file mode 100644 index 0000000..d93f423 --- /dev/null +++ b/views/window/non_client_view.h @@ -0,0 +1,227 @@ +// Copyright (c) 2006-2008 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 VIEWS_WINDOW_NON_CLIENT_VIEW_H_ +#define VIEWS_WINDOW_NON_CLIENT_VIEW_H_ + +#include "base/task.h" +#include "views/view.h" +#include "views/window/client_view.h" + +namespace gfx { +class Path; +} + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// NonClientFrameView +// +// An object that subclasses NonClientFrameView is a View that renders and +// responds to events within the frame portions of the non-client area of a +// window. This view does _not_ contain the ClientView, but rather is a sibling +// of it. +class NonClientFrameView : public View { + public: + // Various edges of the frame border have a 1 px shadow along their edges; in + // a few cases we shift elements based on this amount for visual appeal. + static const int kFrameShadowThickness; + // In restored mode, we draw a 1 px edge around the content area inside the + // frame border. + static const int kClientEdgeThickness; + + void DisableInactiveRendering(bool disable) { + paint_as_active_ = disable; + if (!paint_as_active_) + SchedulePaint(); + } + + // Returns the bounds (in this View's parent's coordinates) that the client + // view should be laid out within. + virtual gfx::Rect GetBoundsForClientView() const = 0; + + // Returns true if this FrameView should always use the custom frame, + // regardless of the system settings. An example is the Constrained Window, + // which is a child window and must always provide its own frame. + virtual bool AlwaysUseCustomFrame() const { return false; } + + virtual gfx::Rect GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const = 0; + virtual gfx::Point GetSystemMenuPoint() const = 0; + virtual int NonClientHitTest(const gfx::Point& point) = 0; + virtual void GetWindowMask(const gfx::Size& size, + gfx::Path* window_mask) = 0; + virtual void EnableClose(bool enable) = 0; + virtual void ResetWindowControls() = 0; + + // Overridden from View: + virtual bool HitTest(const gfx::Point& l) const; + + protected: + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + + NonClientFrameView() : paint_as_active_(false) {} + + + // Helper for non-client view implementations to determine which area of the + // window border the specified |point| falls within. The other parameters are + // the size of the sizing edges, and whether or not the window can be + // resized. + int GetHTComponentForFrame(const gfx::Point& point, + int top_resize_border_height, + int resize_border_thickness, + int top_resize_corner_height, + int resize_corner_width, + bool can_resize); + + // Accessor for paint_as_active_. + bool paint_as_active() const { return paint_as_active_; } + + private: + // True when the non-client view should always be rendered as if the window + // were active, regardless of whether or not the top level window actually + // is active. + bool paint_as_active_; +}; + +//////////////////////////////////////////////////////////////////////////////// +// NonClientView +// +// The NonClientView is the logical root of all Views contained within a +// Window, except for the RootView which is its parent and of which it is the +// sole child. The NonClientView has two children, the NonClientFrameView which +// is responsible for painting and responding to events from the non-client +// portions of the window, and the ClientView, which is responsible for the +// same for the client area of the window: +// +// +- views::Window ------------------------------------+ +// | +- views::RootView ------------------------------+ | +// | | +- views::NonClientView ---------------------+ | | +// | | | +- views::NonClientView subclass ---+ | | | +// | | | | | | | | +// | | | | << all painting and event receiving >> | | | | +// | | | | << of the non-client areas of a >> | | | | +// | | | | << views::Window. >> | | | | +// | | | | | | | | +// | | | +----------------------------------------+ | | | +// | | | +- views::ClientView or subclass --------+ | | | +// | | | | | | | | +// | | | | << all painting and event receiving >> | | | | +// | | | | << of the client areas of a >> | | | | +// | | | | << views::Window. >> | | | | +// | | | | | | | | +// | | | +----------------------------------------+ | | | +// | | +--------------------------------------------+ | | +// | +------------------------------------------------+ | +// +----------------------------------------------------+ +// +// The NonClientFrameView and ClientView are siblings because due to theme +// changes the NonClientFrameView may be replaced with different +// implementations (e.g. during the switch from DWM/Aero-Glass to Vista Basic/ +// Classic rendering). +// +class NonClientView : public View { + public: + explicit NonClientView(Window* frame); + virtual ~NonClientView(); + + // Replaces the current NonClientFrameView (if any) with the specified one. + void SetFrameView(NonClientFrameView* frame_view); + + // Returns true if the ClientView determines that the containing window can be + // closed, false otherwise. + bool CanClose() const; + + // Called by the containing Window when it is closed. + void WindowClosing(); + + // Changes the frame from native to custom depending on the value of + // |use_native_frame|. + void SetUseNativeFrame(bool use_native_frame); + + // Returns true if the native window frame should be used, false if the + // NonClientView provides its own frame implementation. + bool UseNativeFrame() const; + + // Prevents the window from being rendered as deactivated when |disable| is + // true, until called with |disable| false. Used when a sub-window is to be + // shown that shouldn't visually de-activate the window. + // Subclasses can override this to perform additional actions when this value + // changes. + void DisableInactiveRendering(bool disable); + + // Returns the bounds of the window required to display the content area at + // the specified bounds. + gfx::Rect GetWindowBoundsForClientBounds(const gfx::Rect client_bounds) const; + + // Returns the point, in screen coordinates, where the system menu should + // be shown so it shows up anchored to the system menu icon. + gfx::Point GetSystemMenuPoint() const; + + // Determines the windows HT* code when the mouse cursor is at the + // specified point, in window coordinates. + int NonClientHitTest(const gfx::Point& point); + + // Returns a mask to be used to clip the top level window for the given + // size. This is used to create the non-rectangular window shape. + void GetWindowMask(const gfx::Size& size, gfx::Path* window_mask); + + // Toggles the enable state for the Close button (and the Close menu item in + // the system menu). + void EnableClose(bool enable); + + // Tells the window controls as rendered by the NonClientView to reset + // themselves to a normal state. This happens in situations where the + // containing window does not receive a normal sequences of messages that + // would lead to the controls returning to this normal state naturally, e.g. + // when the window is maximized, minimized or restored. + void ResetWindowControls(); + + // Get/Set client_view property. + ClientView* client_view() const { return client_view_; } + void set_client_view(ClientView* client_view) { + client_view_ = client_view; + } + + // Layout just the frame view. This is necessary on Windows when non-client + // metrics such as the position of the window controls changes independently + // of a window resize message. + void LayoutFrameView(); + + // NonClientView, View overrides: + virtual gfx::Size GetPreferredSize(); + virtual gfx::Size GetMinimumSize(); + virtual void Layout(); + + protected: + // NonClientView, View overrides: + virtual void ViewHierarchyChanged(bool is_add, View* parent, View* child); + virtual views::View* GetViewForPoint(const gfx::Point& point); + virtual views::View* GetViewForPoint(const gfx::Point& point, + bool can_create_floating); + + private: + // The frame that hosts this NonClientView. + Window* frame_; + + // A ClientView object or subclass, responsible for sizing the contents view + // of the window, hit testing and perhaps other tasks depending on the + // implementation. + ClientView* client_view_; + + // The NonClientFrameView that renders the non-client portions of the window. + // This object is not owned by the view hierarchy because it can be replaced + // dynamically as the system settings change. + scoped_ptr<NonClientFrameView> frame_view_; + + // Whether or not we should use the native frame. + bool use_native_frame_; + + DISALLOW_COPY_AND_ASSIGN(NonClientView); +}; + +} // namespace views + +#endif // #ifndef VIEWS_WINDOW_NON_CLIENT_VIEW_H_ diff --git a/views/window/window.h b/views/window/window.h new file mode 100644 index 0000000..17a2ff5 --- /dev/null +++ b/views/window/window.h @@ -0,0 +1,135 @@ +// Copyright (c) 2009 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 VIEWS_WINDOW_WINDOW_H_ +#define VIEWS_WINDOW_WINDOW_H_ + +#include "base/gfx/native_widget_types.h" + +namespace gfx { +class Rect; +class Size; +} + +namespace views { + +class ClientView; +class NonClientFrameView; +class NonClientView; +class WindowDelegate; + +// An interface implemented by an object that provides a top level window. +class Window { + public: + virtual ~Window() {} + + // Creates an instance of an object implementing this interface. + static Window* CreateChromeWindow(gfx::NativeWindow parent, + const gfx::Rect& bounds, + WindowDelegate* window_delegate); + + // Returns the preferred size of the contents view of this window based on + // its localized size data. The width in cols is held in a localized string + // resource identified by |col_resource_id|, the height in the same fashion. + // TODO(beng): This should eventually live somewhere else, probably closer to + // ClientView. + static int GetLocalizedContentsWidth(int col_resource_id); + static int GetLocalizedContentsHeight(int row_resource_id); + static gfx::Size GetLocalizedContentsSize(int col_resource_id, + int row_resource_id); + + // Closes all windows that aren't identified as "app windows" via + // IsAppWindow. Called during application shutdown when the last "app window" + // is closed. + static void CloseAllSecondaryWindows(); + + // Retrieves the window's bounds, including its frame. + virtual gfx::Rect GetBounds() const = 0; + + // Retrieves the restored bounds for the window. + virtual gfx::Rect GetNormalBounds() const = 0; + + // Sizes and/or places the window to the specified bounds, size or position. + virtual void SetBounds(const gfx::Rect& bounds) = 0; + + // As above, except the window is inserted after |other_window| in the window + // Z-order. If this window is not yet visible, other_window's monitor is used + // as the constraining rectangle, rather than this window's monitor. + virtual void SetBounds(const gfx::Rect& bounds, + gfx::NativeWindow other_window) = 0; + + // Makes the window visible. + virtual void Show() = 0; + + // Activate the window, assuming it already exists and is visible. + virtual void Activate() = 0; + + // Closes the window, ultimately destroying it. This isn't immediate (it + // occurs after a return to the message loop. Implementors must also make sure + // that invoking Close multiple times doesn't cause bad things to happen, + // since it can happen. + virtual void Close() = 0; + + // Maximizes/minimizes/restores the window. + virtual void Maximize() = 0; + virtual void Minimize() = 0; + virtual void Restore() = 0; + + // Whether or not the window is currently active. + virtual bool IsActive() const = 0; + + // Whether or not the window is currently visible. + virtual bool IsVisible() const = 0; + + // Whether or not the window is maximized or minimized. + virtual bool IsMaximized() const = 0; + virtual bool IsMinimized() const = 0; + + // Accessors for fullscreen state. + virtual void SetFullscreen(bool fullscreen) = 0; + virtual bool IsFullscreen() const = 0; + + // Returns true if the Window is considered to be an "app window" - i.e. + // any window which when it is the last of its type closed causes the + // application to exit. + virtual bool IsAppWindow() const { return false; } + + // Toggles the enable state for the Close button (and the Close menu item in + // the system menu). + virtual void EnableClose(bool enable) = 0; + + // Prevents the window from being rendered as deactivated the next time it is. + // This state is reset automatically as soon as the window becomes activated + // again. There is no ability to control the state through this API as this + // leads to sync problems. + virtual void DisableInactiveRendering() = 0; + + // Tell the window to update its title from the delegate. + virtual void UpdateWindowTitle() = 0; + + // Tell the window to update its icon from the delegate. + virtual void UpdateWindowIcon() = 0; + + // Creates an appropriate NonClientFrameView for this window. + virtual NonClientFrameView* CreateFrameViewForWindow() = 0; + + // Updates the frame after an event caused it to be changed. + virtual void UpdateFrameAfterFrameChange() = 0; + + // Retrieves the Window's delegate. + virtual WindowDelegate* GetDelegate() const = 0; + + // Retrieves the Window's non-client view. + virtual NonClientView* GetNonClientView() const = 0; + + // Retrieves the Window's client view. + virtual ClientView* GetClientView() const = 0; + + // Retrieves the Window's native window handle. + virtual gfx::NativeWindow GetNativeWindow() const = 0; +}; + +} // namespace views + +#endif // #ifndef VIEWS_WINDOW_WINDOW_H_ diff --git a/views/window/window_delegate.cc b/views/window/window_delegate.cc new file mode 100644 index 0000000..7b5f251 --- /dev/null +++ b/views/window/window_delegate.cc @@ -0,0 +1,96 @@ +// Copyright (c) 2006-2008 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/window/window_delegate.h" + +// TODO(beng): hrmp. Fix this in http://crbug.com/4406 +#include "chrome/browser/browser_process.h" +#include "chrome/common/pref_service.h" +#include "views/window/client_view.h" +#include "views/window/window.h" +#include "skia/include/SkBitmap.h" + +namespace views { + +WindowDelegate::WindowDelegate() { +} + +WindowDelegate::~WindowDelegate() { + ReleaseWindow(); +} + +// Returns the icon to be displayed in the window. +SkBitmap WindowDelegate::GetWindowIcon() { + return SkBitmap(); +} + +void WindowDelegate::SaveWindowPlacement(const gfx::Rect& bounds, + bool maximized, + bool always_on_top) { + std::wstring window_name = GetWindowName(); + if (window_name.empty() || !g_browser_process->local_state()) + return; + + DictionaryValue* window_preferences = + g_browser_process->local_state()->GetMutableDictionary( + window_name.c_str()); + window_preferences->SetInteger(L"left", bounds.x()); + window_preferences->SetInteger(L"top", bounds.y()); + window_preferences->SetInteger(L"right", bounds.right()); + window_preferences->SetInteger(L"bottom", bounds.bottom()); + window_preferences->SetBoolean(L"maximized", maximized); + window_preferences->SetBoolean(L"always_on_top", always_on_top); +} + +bool WindowDelegate::GetSavedWindowBounds(gfx::Rect* bounds) const { + std::wstring window_name = GetWindowName(); + if (window_name.empty()) + return false; + + const DictionaryValue* dictionary = + g_browser_process->local_state()->GetDictionary(window_name.c_str()); + int left, top, right, bottom; + if (!dictionary || !dictionary->GetInteger(L"left", &left) || + !dictionary->GetInteger(L"top", &top) || + !dictionary->GetInteger(L"right", &right) || + !dictionary->GetInteger(L"bottom", &bottom)) + return false; + + bounds->SetRect(left, top, right - left, bottom - top); + return true; +} + +bool WindowDelegate::GetSavedMaximizedState(bool* maximized) const { + std::wstring window_name = GetWindowName(); + if (window_name.empty()) + return false; + + const DictionaryValue* dictionary = + g_browser_process->local_state()->GetDictionary(window_name.c_str()); + return dictionary && dictionary->GetBoolean(L"maximized", maximized); +} + +bool WindowDelegate::GetSavedAlwaysOnTopState(bool* always_on_top) const { + if (!g_browser_process->local_state()) + return false; + + std::wstring window_name = GetWindowName(); + if (window_name.empty()) + return false; + + const DictionaryValue* dictionary = + g_browser_process->local_state()->GetDictionary(window_name.c_str()); + return dictionary && dictionary->GetBoolean(L"always_on_top", always_on_top); +} + + +ClientView* WindowDelegate::CreateClientView(Window* window) { + return new ClientView(window, GetContentsView()); +} + +void WindowDelegate::ReleaseWindow() { + window_.release(); +} + +} // namespace views diff --git a/views/window/window_delegate.h b/views/window/window_delegate.h new file mode 100644 index 0000000..af8285c --- /dev/null +++ b/views/window/window_delegate.h @@ -0,0 +1,162 @@ +// Copyright (c) 2006-2008 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 VIEWS_WINDOW_WINDOW_DELEGATE_H_ +#define VIEWS_WINDOW_WINDOW_DELEGATE_H_ + +#include <string> + +#include "base/scoped_ptr.h" + +class SkBitmap; + +namespace gfx { +class Rect; +} +// TODO(maruel): Remove once gfx::Rect is used instead. +namespace WTL { +class CRect; +} +using WTL::CRect; + +namespace views { + +class ClientView; +class DialogDelegate; +class View; +class Window; + +/////////////////////////////////////////////////////////////////////////////// +// +// WindowDelegate +// +// WindowDelegate is an interface implemented by objects that wish to show a +// Window. The window that is displayed uses this interface to determine how +// it should be displayed and notify the delegate object of certain events. +// +/////////////////////////////////////////////////////////////////////////////// +class WindowDelegate { + public: + WindowDelegate(); + virtual ~WindowDelegate(); + + virtual DialogDelegate* AsDialogDelegate() { return NULL; } + + // Returns true if the window can ever be resized. + virtual bool CanResize() const { + return false; + } + + // Returns true if the window can ever be maximized. + virtual bool CanMaximize() const { + return false; + } + + // Returns true if the window should be placed on top of all other windows on + // the system, even when it is not active. If HasAlwaysOnTopMenu() returns + // true, then this method is only used the first time the window is opened, it + // is stored in the preferences for next runs. + virtual bool IsAlwaysOnTop() const { + return false; + } + + // Returns whether an "always on top" menu should be added to the system menu + // of the window. + virtual bool HasAlwaysOnTopMenu() const { + return false; + } + + // Returns true if the dialog should be displayed modally to the window that + // opened it. Only windows with WindowType == DIALOG can be modal. + virtual bool IsModal() const { + return false; + } + + // Returns the text to be displayed in the window title. + virtual std::wstring GetWindowTitle() const { + return L""; + } + + // Returns the view that should have the focus when the dialog is opened. If + // NULL no view is focused. + virtual View* GetInitiallyFocusedView() { return NULL; } + + // Returns true if the window should show a title in the title bar. + virtual bool ShouldShowWindowTitle() const { + return true; + } + + // Returns the icon to be displayed in the window. + virtual SkBitmap GetWindowIcon(); + + // Returns true if a window icon should be shown. + virtual bool ShouldShowWindowIcon() const { + return false; + } + + // Execute a command in the window's controller. Returns true if the command + // was handled, false if it was not. + virtual bool ExecuteWindowsCommand(int command_id) { return false; } + + // Returns the window's name identifier. Used to identify this window for + // state restoration. + virtual std::wstring GetWindowName() const { + return std::wstring(); + } + + // Saves the window's bounds, maximized and always-on-top states. By default + // this uses the process' local state keyed by window name (See GetWindowName + // above). This behavior can be overridden to provide additional + // functionality. + virtual void SaveWindowPlacement(const gfx::Rect& bounds, + bool maximized, + bool always_on_top); + + // Retrieves the window's bounds, maximized and always-on-top states. By + // default, this uses the process' local state keyed by window name (See + // GetWindowName above). This behavior can be overridden to provide + // additional functionality. + virtual bool GetSavedWindowBounds(gfx::Rect* bounds) const; + virtual bool GetSavedMaximizedState(bool* maximized) const; + virtual bool GetSavedAlwaysOnTopState(bool* always_on_top) const; + + // Called when the window closes. + virtual void WindowClosing() { } + + // Called when the window is destroyed. No events must be sent or received + // after this point. The delegate can use this opportunity to delete itself at + // this time if necessary. + virtual void DeleteDelegate() { } + + // Returns the View that is contained within this Window. + virtual View* GetContentsView() { + return NULL; + } + + // Called by the Window to create the Client View used to host the contents + // of the window. + virtual ClientView* CreateClientView(Window* window); + + // An accessor to the Window this delegate is bound to. + Window* window() const { return window_.get(); } + + protected: + // Releases the Window* we maintain. This should be done by a delegate in its + // WindowClosing handler if it intends to be re-cycled to be used on a + // different Window. + void ReleaseWindow(); + + private: + friend class WindowWin; + // This is a little unusual. We use a scoped_ptr here because it's + // initialized to NULL automatically. We do this because we want to allow + // people using this helper to not have to call a ctor on this object. + // Instead we just release the owning ref this pointer has when we are + // destroyed. + scoped_ptr<Window> window_; +}; + +} // namespace views + +#endif // VIEWS_WINDOW_WINDOW_DELEGATE_H_ diff --git a/views/window/window_resources.h b/views/window/window_resources.h new file mode 100644 index 0000000..e79476e --- /dev/null +++ b/views/window/window_resources.h @@ -0,0 +1,30 @@ +// Copyright (c) 2006-2008 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 VIEWS_WINDOW_WINDOW_RESOURCES_H_ +#define VIEWS_WINDOW_WINDOW_RESOURCES_H_ + +class SkBitmap; + +namespace views { + +typedef int FramePartBitmap; + +/////////////////////////////////////////////////////////////////////////////// +// WindowResources +// +// An interface implemented by an object providing bitmaps to render the +// contents of a window frame. The Window may swap in different +// implementations of this interface to render different modes. The definition +// of FramePartBitmap depends on the implementation. +// +class WindowResources { + public: + virtual ~WindowResources() { } + virtual SkBitmap* GetPartBitmap(FramePartBitmap part) const = 0; +}; + +} // namespace views + +#endif // VIEWS_WINDOW_WINDOW_RESOURCES_H_ diff --git a/views/window/window_win.cc b/views/window/window_win.cc new file mode 100644 index 0000000..f41ef18 --- /dev/null +++ b/views/window/window_win.cc @@ -0,0 +1,1446 @@ +// Copyright (c) 2006-2008 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/window/window_win.h" + +#include <shellapi.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/gfx/icon_util.h" +#include "app/gfx/path.h" +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "base/win_util.h" +#include "chrome/app/chrome_dll_resource.h" +#include "chrome/common/win_util.h" +#include "grit/generated_resources.h" +#include "views/widget/root_view.h" +#include "views/window/client_view.h" +#include "views/window/custom_frame_view.h" +#include "views/window/native_frame_view.h" +#include "views/window/non_client_view.h" +#include "views/window/window_delegate.h" + +namespace { + +bool GetMonitorAndRects(const RECT& rect, + HMONITOR* monitor, + gfx::Rect* monitor_rect, + gfx::Rect* work_area) { + DCHECK(monitor); + DCHECK(monitor_rect); + DCHECK(work_area); + *monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONULL); + if (!*monitor) + return false; + MONITORINFO monitor_info = { 0 }; + monitor_info.cbSize = sizeof(monitor_info); + GetMonitorInfo(*monitor, &monitor_info); + *monitor_rect = monitor_info.rcMonitor; + *work_area = monitor_info.rcWork; + return true; +} + +} // namespace + +namespace views { + +// A scoping class that prevents a window from being able to redraw in response +// to invalidations that may occur within it for the lifetime of the object. +// +// Why would we want such a thing? Well, it turns out Windows has some +// "unorthodox" behavior when it comes to painting its non-client areas. +// Occasionally, Windows will paint portions of the default non-client area +// right over the top of the custom frame. This is not simply fixed by handling +// WM_NCPAINT/WM_PAINT, with some investigation it turns out that this +// rendering is being done *inside* the default implementation of some message +// handlers and functions: +// . WM_SETTEXT +// . WM_SETICON +// . WM_NCLBUTTONDOWN +// . EnableMenuItem, called from our WM_INITMENU handler +// The solution is to handle these messages and call DefWindowProc ourselves, +// but prevent the window from being able to update itself for the duration of +// the call. We do this with this class, which automatically calls its +// associated Window's lock and unlock functions as it is created and destroyed. +// See documentation in those methods for the technique used. +// +// IMPORTANT: Do not use this scoping object for large scopes or periods of +// time! IT WILL PREVENT THE WINDOW FROM BEING REDRAWN! (duh). +// +// I would love to hear Raymond Chen's explanation for all this. And maybe a +// list of other messages that this applies to ;-) +class WindowWin::ScopedRedrawLock { + public: + explicit ScopedRedrawLock(WindowWin* window) : window_(window) { + window_->LockUpdates(); + } + + ~ScopedRedrawLock() { + window_->UnlockUpdates(); + } + + private: + // The window having its style changed. + WindowWin* window_; +}; + +HCURSOR WindowWin::resize_cursors_[6]; + +// If the hung renderer warning doesn't fit on screen, the amount of padding to +// be left between the edge of the window and the edge of the nearest monitor, +// after the window is nudged back on screen. Pixels. +static const int kMonitorEdgePadding = 10; + +//////////////////////////////////////////////////////////////////////////////// +// WindowWin, public: + +WindowWin::~WindowWin() { +} + +// static +Window* Window::CreateChromeWindow(gfx::NativeWindow parent, + const gfx::Rect& bounds, + WindowDelegate* window_delegate) { + WindowWin* window = new WindowWin(window_delegate); + window->GetNonClientView()->SetFrameView(window->CreateFrameViewForWindow()); + window->Init(parent, bounds); + return window; +} + +gfx::Rect WindowWin::GetBounds() const { + gfx::Rect bounds; + WidgetWin::GetBounds(&bounds, true); + return bounds; +} + +gfx::Rect WindowWin::GetNormalBounds() const { + // If we're in fullscreen mode, we've changed the normal bounds to the monitor + // rect, so return the saved bounds instead. + if (IsFullscreen()) + return gfx::Rect(saved_window_info_.window_rect); + + WINDOWPLACEMENT wp; + wp.length = sizeof(wp); + const bool ret = !!GetWindowPlacement(GetNativeView(), &wp); + DCHECK(ret); + return gfx::Rect(wp.rcNormalPosition); +} + +void WindowWin::SetBounds(const gfx::Rect& bounds) { + SetBounds(bounds, NULL); +} + +void WindowWin::SetBounds(const gfx::Rect& bounds, + gfx::NativeWindow other_window) { + win_util::SetChildBounds(GetNativeView(), GetParent(), other_window, bounds, + kMonitorEdgePadding, 0); +} + +void WindowWin::Show(int show_state) { + ShowWindow(show_state); + // When launched from certain programs like bash and Windows Live Messenger, + // show_state is set to SW_HIDE, so we need to correct that condition. We + // don't just change show_state to SW_SHOWNORMAL because MSDN says we must + // always first call ShowWindow with the specified value from STARTUPINFO, + // otherwise all future ShowWindow calls will be ignored (!!#@@#!). Instead, + // we call ShowWindow again in this case. + if (show_state == SW_HIDE) { + show_state = SW_SHOWNORMAL; + ShowWindow(show_state); + } + + // We need to explicitly activate the window if we've been shown with a state + // that should activate, because if we're opened from a desktop shortcut while + // an existing window is already running it doesn't seem to be enough to use + // one of these flags to activate the window. + if (show_state == SW_SHOWNORMAL) + Activate(); + + SetInitialFocus(); +} + +int WindowWin::GetShowState() const { + return SW_SHOWNORMAL; +} + +void WindowWin::ExecuteSystemMenuCommand(int command) { + if (command) + SendMessage(GetNativeView(), WM_SYSCOMMAND, command, 0); +} + +void WindowWin::PushForceHidden() { + if (force_hidden_count_++ == 0) + Hide(); +} + +void WindowWin::PopForceHidden() { + if (--force_hidden_count_ <= 0) { + force_hidden_count_ = 0; + ShowWindow(SW_SHOW); + } +} + +// static +int Window::GetLocalizedContentsWidth(int col_resource_id) { + double chars = _wtof(l10n_util::GetString(col_resource_id).c_str()); + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + ChromeFont font = rb.GetFont(ResourceBundle::BaseFont); + int width = font.GetExpectedTextWidth(static_cast<int>(chars)); + DCHECK(width > 0); + return width; +} + +// static +int Window::GetLocalizedContentsHeight(int row_resource_id) { + double lines = _wtof(l10n_util::GetString(row_resource_id).c_str()); + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + ChromeFont font = rb.GetFont(ResourceBundle::BaseFont); + int height = static_cast<int>(font.height() * lines); + DCHECK(height > 0); + return height; +} + +// static +gfx::Size Window::GetLocalizedContentsSize(int col_resource_id, + int row_resource_id) { + return gfx::Size(GetLocalizedContentsWidth(col_resource_id), + GetLocalizedContentsHeight(row_resource_id)); +} + +//////////////////////////////////////////////////////////////////////////////// +// WindowWin, Window implementation: + +void WindowWin::Show() { + int show_state = GetShowState(); + if (saved_maximized_state_) + show_state = SW_SHOWMAXIMIZED; + Show(show_state); +} + +void WindowWin::Activate() { + if (IsMinimized()) + ::ShowWindow(GetNativeView(), SW_RESTORE); + ::SetWindowPos(GetNativeView(), HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(GetNativeView()); +} + +void WindowWin::Close() { + if (window_closed_) { + // It appears we can hit this code path if you close a modal dialog then + // close the last browser before the destructor is hit, which triggers + // invoking Close again. I'm short circuiting this code path to avoid + // calling into the delegate twice, which is problematic. + return; + } + + if (non_client_view_->CanClose()) { + SaveWindowPosition(); + RestoreEnabledIfNecessary(); + WidgetWin::Close(); + // If the user activates another app after opening us, then comes back and + // closes us, we want our owner to gain activation. But only if the owner + // is visible. If we don't manually force that here, the other app will + // regain activation instead. + if (owning_hwnd_ && GetNativeView() == GetForegroundWindow() && + IsWindowVisible(owning_hwnd_)) { + SetForegroundWindow(owning_hwnd_); + } + window_closed_ = true; + } +} + +void WindowWin::Maximize() { + ExecuteSystemMenuCommand(SC_MAXIMIZE); +} + +void WindowWin::Minimize() { + ExecuteSystemMenuCommand(SC_MINIMIZE); +} + +void WindowWin::Restore() { + ExecuteSystemMenuCommand(SC_RESTORE); +} + +bool WindowWin::IsActive() const { + return is_active_; +} + +bool WindowWin::IsVisible() const { + return !!::IsWindowVisible(GetNativeView()); +} + +bool WindowWin::IsMaximized() const { + return !!::IsZoomed(GetNativeView()); +} + +bool WindowWin::IsMinimized() const { + return !!::IsIconic(GetNativeView()); +} + +void WindowWin::SetFullscreen(bool fullscreen) { + if (fullscreen_ == fullscreen) + return; // Nothing to do. + + // Reduce jankiness during the following position changes by hiding the window + // until it's in the final position. + PushForceHidden(); + + // Size/position/style window appropriately. + if (!fullscreen_) { + // Save current window information. We force the window into restored mode + // before going fullscreen because Windows doesn't seem to hide the + // taskbar if the window is in the maximized state. + saved_window_info_.maximized = IsMaximized(); + if (saved_window_info_.maximized) + Restore(); + saved_window_info_.style = GetWindowLong(GWL_STYLE); + saved_window_info_.ex_style = GetWindowLong(GWL_EXSTYLE); + GetWindowRect(&saved_window_info_.window_rect); + } + + // Toggle fullscreen mode. + fullscreen_ = fullscreen; + + if (fullscreen_) { + // Set new window style and size. + MONITORINFO monitor_info; + monitor_info.cbSize = sizeof(monitor_info); + GetMonitorInfo(MonitorFromWindow(GetNativeView(), MONITOR_DEFAULTTONEAREST), + &monitor_info); + gfx::Rect monitor_rect(monitor_info.rcMonitor); + SetWindowLong(GWL_STYLE, + saved_window_info_.style & ~(WS_CAPTION | WS_THICKFRAME)); + SetWindowLong(GWL_EXSTYLE, + saved_window_info_.ex_style & ~(WS_EX_DLGMODALFRAME | + WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE)); + SetWindowPos(NULL, monitor_rect.x(), monitor_rect.y(), + monitor_rect.width(), monitor_rect.height(), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } else { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + gfx::Rect new_rect(saved_window_info_.window_rect); + SetWindowLong(GWL_STYLE, saved_window_info_.style); + SetWindowLong(GWL_EXSTYLE, saved_window_info_.ex_style); + SetWindowPos(NULL, new_rect.x(), new_rect.y(), new_rect.width(), + new_rect.height(), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + if (saved_window_info_.maximized) + Maximize(); + } + + // Undo our anti-jankiness hacks. + PopForceHidden(); +} + +bool WindowWin::IsFullscreen() const { + return fullscreen_; +} + +void WindowWin::EnableClose(bool enable) { + // If the native frame is rendering its own close button, ask it to disable. + non_client_view_->EnableClose(enable); + + // Disable the native frame's close button regardless of whether or not the + // native frame is in use, since this also affects the system menu. + EnableMenuItem(GetSystemMenu(GetNativeView(), false), + SC_CLOSE, enable ? MF_ENABLED : MF_GRAYED); + + // Let the window know the frame changed. + SetWindowPos(NULL, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOCOPYBITS | + SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOREPOSITION | + SWP_NOSENDCHANGING | SWP_NOSIZE | SWP_NOZORDER); +} + +void WindowWin::DisableInactiveRendering() { + disable_inactive_rendering_ = true; + non_client_view_->DisableInactiveRendering(disable_inactive_rendering_); +} + +void WindowWin::UpdateWindowTitle() { + // If the non-client view is rendering its own title, it'll need to relayout + // now. + non_client_view_->Layout(); + + // Update the native frame's text. We do this regardless of whether or not + // the native frame is being used, since this also updates the taskbar, etc. + std::wstring window_title = window_delegate_->GetWindowTitle(); + std::wstring localized_text; + if (l10n_util::AdjustStringForLocaleDirection(window_title, &localized_text)) + window_title.assign(localized_text); + SetWindowText(GetNativeView(), window_title.c_str()); +} + +void WindowWin::UpdateWindowIcon() { + // If the non-client view is rendering its own icon, we need to tell it to + // repaint. + non_client_view_->SchedulePaint(); + + // Update the native frame's icon. We do this regardless of whether or not + // the native frame is being used, since this also updates the taskbar, etc. + SkBitmap icon = window_delegate_->GetWindowIcon(); + if (!icon.isNull()) { + HICON windows_icon = IconUtil::CreateHICONFromSkBitmap(icon); + // We need to make sure to destroy the previous icon, otherwise we'll leak + // these GDI objects until we crash! + HICON old_icon = reinterpret_cast<HICON>( + SendMessage(GetNativeView(), WM_SETICON, ICON_SMALL, + reinterpret_cast<LPARAM>(windows_icon))); + if (old_icon) + DestroyIcon(old_icon); + old_icon = reinterpret_cast<HICON>( + SendMessage(GetNativeView(), WM_SETICON, ICON_BIG, + reinterpret_cast<LPARAM>(windows_icon))); + if (old_icon) + DestroyIcon(old_icon); + } +} + +NonClientFrameView* WindowWin::CreateFrameViewForWindow() { + if (non_client_view_->UseNativeFrame()) + return new NativeFrameView(this); + return new CustomFrameView(this); +} + +void WindowWin::UpdateFrameAfterFrameChange() { + // We've either gained or lost a custom window region, so reset it now. + ResetWindowRegion(true); +} + +WindowDelegate* WindowWin::GetDelegate() const { + return window_delegate_; +} + +NonClientView* WindowWin::GetNonClientView() const { + return non_client_view_; +} + +ClientView* WindowWin::GetClientView() const { + return non_client_view_->client_view(); +} + +gfx::NativeWindow WindowWin::GetNativeWindow() const { + return GetNativeView(); +} + +/////////////////////////////////////////////////////////////////////////////// +// WindowWin, protected: + +WindowWin::WindowWin(WindowDelegate* window_delegate) + : WidgetWin(), + focus_on_creation_(true), + window_delegate_(window_delegate), + non_client_view_(new NonClientView(this)), + owning_hwnd_(NULL), + minimum_size_(100, 100), + is_modal_(false), + restored_enabled_(false), + is_always_on_top_(false), + fullscreen_(false), + window_closed_(false), + disable_inactive_rendering_(false), + is_active_(false), + lock_updates_(false), + saved_window_style_(0), + saved_maximized_state_(0), + ignore_window_pos_changes_(false), + ignore_pos_changes_factory_(this), + force_hidden_count_(0), + last_monitor_(NULL) { + is_window_ = true; + InitClass(); + DCHECK(window_delegate_); + window_delegate_->window_.reset(this); + // Initialize these values to 0 so that subclasses can override the default + // behavior before calling Init. + set_window_style(0); + set_window_ex_style(0); +} + +void WindowWin::Init(HWND parent, const gfx::Rect& bounds) { + // We need to save the parent window, since later calls to GetParent() will + // return NULL. + owning_hwnd_ = parent; + // We call this after initializing our members since our implementations of + // assorted WidgetWin functions may be called during initialization. + is_modal_ = window_delegate_->IsModal(); + if (is_modal_) + BecomeModal(); + is_always_on_top_ = window_delegate_->IsAlwaysOnTop(); + + if (window_style() == 0) + set_window_style(CalculateWindowStyle()); + if (window_ex_style() == 0) + set_window_ex_style(CalculateWindowExStyle()); + + WidgetWin::Init(parent, bounds, true); + win_util::SetWindowUserData(GetNativeView(), this); + + // Create the ClientView, add it to the NonClientView and add the + // NonClientView to the RootView. This will cause everything to be parented. + non_client_view_->set_client_view(window_delegate_->CreateClientView(this)); + WidgetWin::SetContentsView(non_client_view_); + + UpdateWindowTitle(); + + SetInitialBounds(bounds); + InitAlwaysOnTopState(); + + GetMonitorAndRects(bounds.ToRECT(), &last_monitor_, &last_monitor_rect_, + &last_work_area_); + ResetWindowRegion(false); +} + +void WindowWin::SizeWindowToDefault() { + win_util::CenterAndSizeWindow(owning_window(), GetNativeView(), + non_client_view_->GetPreferredSize().ToSIZE(), + false); +} + +void WindowWin::RunSystemMenu(const gfx::Point& point) { + // We need to reset and clean up any currently created system menu objects. + // We need to call this otherwise there's a small chance that we aren't going + // to get a system menu. We also can't take the return value of this + // function. We need to call it *again* to get a valid HMENU. + //::GetSystemMenu(GetNativeView(), TRUE); + UINT flags = TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_RETURNCMD; + if (l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT) + flags |= TPM_RIGHTALIGN; + HMENU system_menu = ::GetSystemMenu(GetNativeView(), FALSE); + int id = ::TrackPopupMenu(system_menu, flags, + point.x(), point.y(), 0, GetNativeView(), NULL); + ExecuteSystemMenuCommand(id); +} + +/////////////////////////////////////////////////////////////////////////////// +// WindowWin, WidgetWin overrides: + +void WindowWin::OnActivate(UINT action, BOOL minimized, HWND window) { + if (action == WA_INACTIVE) + SaveWindowPosition(); +} + +void WindowWin::OnActivateApp(BOOL active, DWORD thread_id) { + if (!active && thread_id != GetCurrentThreadId()) { + // Another application was activated, we should reset any state that + // disables inactive rendering now. + disable_inactive_rendering_ = false; + non_client_view_->DisableInactiveRendering(false); + // Update the native frame too, since it could be rendering the non-client + // area. + CallDefaultNCActivateHandler(FALSE); + } +} + +LRESULT WindowWin::OnAppCommand(HWND window, short app_command, WORD device, + int keystate) { + // We treat APPCOMMAND ids as an extension of our command namespace, and just + // let the delegate figure out what to do... + if (!window_delegate_->ExecuteWindowsCommand(app_command)) + return WidgetWin::OnAppCommand(window, app_command, device, keystate); + return 0; +} + +void WindowWin::OnCommand(UINT notification_code, int command_id, HWND window) { + // If the notification code is > 1 it means it is control specific and we + // should ignore it. + if (notification_code > 1 || + window_delegate_->ExecuteWindowsCommand(command_id)) { + WidgetWin::OnCommand(notification_code, command_id, window); + } +} + +void WindowWin::OnDestroy() { + non_client_view_->WindowClosing(); + RestoreEnabledIfNecessary(); + WidgetWin::OnDestroy(); +} + +namespace { +static BOOL CALLBACK SendDwmCompositionChanged(HWND window, LPARAM param) { + SendMessage(window, WM_DWMCOMPOSITIONCHANGED, 0, 0); + return TRUE; +} +} // namespace + +LRESULT WindowWin::OnDwmCompositionChanged(UINT msg, WPARAM w_param, + LPARAM l_param) { + // The window may try to paint in SetUseNativeFrame, and as a result it can + // get into a state where it is very unhappy with itself - rendering black + // behind the entire client area. This is because for some reason the + // SkPorterDuff::kClear_mode erase done in the RootView thinks the window is + // still opaque. So, to work around this we hide the window as soon as we can + // (now), saving off its placement so it can be properly restored once + // everything has settled down. + WINDOWPLACEMENT saved_window_placement; + saved_window_placement.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(GetNativeView(), &saved_window_placement); + Hide(); + + // Important step: restore the window first, since our hiding hack doesn't + // work for maximized windows! We tell the frame not to allow itself to be + // made visible though, which removes the brief flicker. + ++force_hidden_count_; + ::ShowWindow(GetNativeView(), SW_RESTORE); + --force_hidden_count_; + + // We respond to this in response to WM_DWMCOMPOSITIONCHANGED since that is + // the only thing we care about - we don't actually respond to WM_THEMECHANGED + // messages. + non_client_view_->SetUseNativeFrame(win_util::ShouldUseVistaFrame()); + + // Now that we've updated the frame, we'll want to restore our saved placement + // since the display should have settled down and we can be properly rendered. + SetWindowPlacement(GetNativeView(), &saved_window_placement); + + // WM_DWMCOMPOSITIONCHANGED is only sent to top level windows, however we want + // to notify our children too, since we can have MDI child windows who need to + // update their appearance. + EnumChildWindows(GetNativeView(), &SendDwmCompositionChanged, NULL); + return 0; +} + +void WindowWin::OnFinalMessage(HWND window) { + // Delete and NULL the delegate here once we're guaranteed to get no more + // messages. + window_delegate_->DeleteDelegate(); + window_delegate_ = NULL; + WidgetWin::OnFinalMessage(window); +} + +void WindowWin::OnGetMinMaxInfo(MINMAXINFO* minmax_info) { + gfx::Size min_window_size(GetNonClientView()->GetMinimumSize()); + minmax_info->ptMinTrackSize.x = min_window_size.width(); + minmax_info->ptMinTrackSize.y = min_window_size.height(); + WidgetWin::OnGetMinMaxInfo(minmax_info); +} + +namespace { +static void EnableMenuItem(HMENU menu, UINT command, bool enabled) { + UINT flags = MF_BYCOMMAND | (enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED); + EnableMenuItem(menu, command, flags); +} +} // namespace + +void WindowWin::OnInitMenu(HMENU menu) { + // We only need to manually enable the system menu if we're not using a native + // frame. + if (non_client_view_->UseNativeFrame()) + WidgetWin::OnInitMenu(menu); + + bool is_fullscreen = IsFullscreen(); + bool is_minimized = IsMinimized(); + bool is_maximized = IsMaximized(); + bool is_restored = !is_fullscreen && !is_minimized && !is_maximized; + + ScopedRedrawLock lock(this); + EnableMenuItem(menu, SC_RESTORE, is_minimized || is_maximized); + EnableMenuItem(menu, SC_MOVE, is_restored); + EnableMenuItem(menu, SC_SIZE, window_delegate_->CanResize() && is_restored); + EnableMenuItem(menu, SC_MAXIMIZE, + window_delegate_->CanMaximize() && !is_fullscreen && !is_maximized); + EnableMenuItem(menu, SC_MINIMIZE, + window_delegate_->CanMaximize() && !is_minimized); +} + +void WindowWin::OnMouseLeave() { + // We only need to manually track WM_MOUSELEAVE messages between the client + // and non-client area when we're not using the native frame. + if (non_client_view_->UseNativeFrame()) { + SetMsgHandled(FALSE); + return; + } + + bool process_mouse_exited = true; + POINT pt; + if (GetCursorPos(&pt)) { + LRESULT ht_component = + ::SendMessage(GetNativeView(), WM_NCHITTEST, 0, MAKELPARAM(pt.x, pt.y)); + if (ht_component != HTNOWHERE) { + // If the mouse moved into a part of the window's non-client area, then + // don't send a mouse exited event since the mouse is still within the + // bounds of the ChromeView that's rendering the frame. Note that we do + // _NOT_ do this for windows with native frames, since in that case the + // mouse really will have left the bounds of the RootView. + process_mouse_exited = false; + } + } + + if (process_mouse_exited) + ProcessMouseExited(); +} + +LRESULT WindowWin::OnNCActivate(BOOL active) { + is_active_ = !!active; + + // If we're not using the native frame, we need to force a synchronous repaint + // otherwise we'll be left in the wrong activation state until something else + // causes a repaint later. + if (!non_client_view_->UseNativeFrame()) { + // We can get WM_NCACTIVATE before we're actually visible. If we're not + // visible, no need to paint. + if (IsWindowVisible(GetNativeView())) { + non_client_view_->SchedulePaint(); + // We need to force a paint now, as a user dragging a window will block + // painting operations while the move is in progress. + PaintNow(root_view_->GetScheduledPaintRect()); + } + } + + // If we're active again, we should be allowed to render as inactive, so + // tell the non-client view. This must be done independently of the check for + // disable_inactive_rendering_ since that check is valid even if the frame + // is not active, but this can only be done if we've become active. + if (IsActive()) + non_client_view_->DisableInactiveRendering(false); + + // Reset the disable inactive rendering state since activation has changed. + if (disable_inactive_rendering_) { + disable_inactive_rendering_ = false; + return CallDefaultNCActivateHandler(TRUE); + } + return CallDefaultNCActivateHandler(active); +} + +LRESULT WindowWin::OnNCCalcSize(BOOL mode, LPARAM l_param) { + // We only need to adjust the client size/paint handling when we're not using + // the native frame. + if (non_client_view_->UseNativeFrame()) + return WidgetWin::OnNCCalcSize(mode, l_param); + + RECT* client_rect = mode ? + &reinterpret_cast<NCCALCSIZE_PARAMS*>(l_param)->rgrc[0] : + reinterpret_cast<RECT*>(l_param); + if (IsMaximized()) { + // Make the maximized mode client rect fit the screen exactly, by + // subtracting the border Windows automatically adds for maximized mode. + int border_thickness = GetSystemMetrics(SM_CXSIZEFRAME); + InflateRect(client_rect, -border_thickness, -border_thickness); + + // Find all auto-hide taskbars along the screen edges and adjust in by the + // thickness of the auto-hide taskbar on each such edge, so the window isn't + // treated as a "fullscreen app", which would cause the taskbars to + // disappear. + HMONITOR monitor = MonitorFromWindow(GetNativeView(), + MONITOR_DEFAULTTONULL); + if (win_util::EdgeHasTopmostAutoHideTaskbar(ABE_LEFT, monitor)) + client_rect->left += win_util::kAutoHideTaskbarThicknessPx; + if (win_util::EdgeHasTopmostAutoHideTaskbar(ABE_TOP, monitor)) + client_rect->top += win_util::kAutoHideTaskbarThicknessPx; + if (win_util::EdgeHasTopmostAutoHideTaskbar(ABE_RIGHT, monitor)) + client_rect->right -= win_util::kAutoHideTaskbarThicknessPx; + if (win_util::EdgeHasTopmostAutoHideTaskbar(ABE_BOTTOM, monitor)) + client_rect->bottom -= win_util::kAutoHideTaskbarThicknessPx; + + // We cannot return WVR_REDRAW when there is nonclient area, or Windows + // exhibits bugs where client pixels and child HWNDs are mispositioned by + // the width/height of the upper-left nonclient area. + return 0; + } + + // If the window bounds change, we're going to relayout and repaint anyway. + // Returning WVR_REDRAW avoids an extra paint before that of the old client + // pixels in the (now wrong) location, and thus makes actions like resizing a + // window from the left edge look slightly less broken. + return mode ? WVR_REDRAW : 0; +} + +LRESULT WindowWin::OnNCHitTest(const CPoint& point) { + // First, give the NonClientView a chance to test the point to see if it + // provides any of the non-client area. + CPoint temp = point; + MapWindowPoints(HWND_DESKTOP, GetNativeView(), &temp, 1); + int component = non_client_view_->NonClientHitTest(gfx::Point(temp)); + if (component != HTNOWHERE) + return component; + + // Otherwise, we let Windows do all the native frame non-client handling for + // us. + return WidgetWin::OnNCHitTest(point); +} + +namespace { +struct ClipState { + // The window being painted. + HWND parent; + + // DC painting to. + HDC dc; + + // Origin of the window in terms of the screen. + int x; + int y; +}; + +// See comments in OnNCPaint for details of this function. +static BOOL CALLBACK ClipDCToChild(HWND window, LPARAM param) { + ClipState* clip_state = reinterpret_cast<ClipState*>(param); + if (GetParent(window) == clip_state->parent && IsWindowVisible(window)) { + RECT bounds; + GetWindowRect(window, &bounds); + ExcludeClipRect(clip_state->dc, + bounds.left - clip_state->x, + bounds.top - clip_state->y, + bounds.right - clip_state->x, + bounds.bottom - clip_state->y); + } + return TRUE; +} +} // namespace + +void WindowWin::OnNCPaint(HRGN rgn) { + // We only do non-client painting if we're not using the native frame. + if (non_client_view_->UseNativeFrame()) { + WidgetWin::OnNCPaint(rgn); + return; + } + + // We have an NC region and need to paint it. We expand the NC region to + // include the dirty region of the root view. This is done to minimize + // paints. + CRect window_rect; + GetWindowRect(&window_rect); + + if (window_rect.Width() != root_view_->width() || + window_rect.Height() != root_view_->height()) { + // If the size of the window differs from the size of the root view it + // means we're being asked to paint before we've gotten a WM_SIZE. This can + // happen when the user is interactively resizing the window. To avoid + // mass flickering we don't do anything here. Once we get the WM_SIZE we'll + // reset the region of the window which triggers another WM_NCPAINT and + // all is well. + return; + } + + CRect dirty_region; + // A value of 1 indicates paint all. + if (!rgn || rgn == reinterpret_cast<HRGN>(1)) { + dirty_region = CRect(0, 0, window_rect.Width(), window_rect.Height()); + } else { + RECT rgn_bounding_box; + GetRgnBox(rgn, &rgn_bounding_box); + if (!IntersectRect(&dirty_region, &rgn_bounding_box, &window_rect)) + return; // Dirty region doesn't intersect window bounds, bale. + + // rgn_bounding_box is in screen coordinates. Map it to window coordinates. + OffsetRect(&dirty_region, -window_rect.left, -window_rect.top); + } + + // In theory GetDCEx should do what we want, but I couldn't get it to work. + // In particular the docs mentiond DCX_CLIPCHILDREN, but as far as I can tell + // it doesn't work at all. So, instead we get the DC for the window then + // manually clip out the children. + HDC dc = GetWindowDC(GetNativeView()); + ClipState clip_state; + clip_state.x = window_rect.left; + clip_state.y = window_rect.top; + clip_state.parent = GetNativeView(); + clip_state.dc = dc; + EnumChildWindows(GetNativeView(), &ClipDCToChild, + reinterpret_cast<LPARAM>(&clip_state)); + + RootView* root_view = GetRootView(); + gfx::Rect old_paint_region = + root_view->GetScheduledPaintRectConstrainedToSize(); + + if (!old_paint_region.IsEmpty()) { + // The root view has a region that needs to be painted. Include it in the + // region we're going to paint. + + CRect old_paint_region_crect = old_paint_region.ToRECT(); + CRect tmp = dirty_region; + UnionRect(&dirty_region, &tmp, &old_paint_region_crect); + } + + root_view->SchedulePaint(gfx::Rect(dirty_region), false); + + // ChromeCanvasPaints destructor does the actual painting. As such, wrap the + // following in a block to force paint to occur so that we can release the dc. + { + ChromeCanvasPaint canvas(dc, opaque(), dirty_region.left, dirty_region.top, + dirty_region.Width(), dirty_region.Height()); + + root_view->ProcessPaint(&canvas); + } + + ReleaseDC(GetNativeView(), dc); +} + +void WindowWin::OnNCLButtonDown(UINT ht_component, const CPoint& point) { + // When we're using a native frame, window controls work without us + // interfering. + if (!non_client_view_->UseNativeFrame()) { + switch (ht_component) { + case HTCLOSE: + case HTMINBUTTON: + case HTMAXBUTTON: { + // When the mouse is pressed down in these specific non-client areas, + // we need to tell the RootView to send the mouse pressed event (which + // sets capture, allowing subsequent WM_LBUTTONUP (note, _not_ + // WM_NCLBUTTONUP) to fire so that the appropriate WM_SYSCOMMAND can be + // sent by the applicable button's ButtonListener. We _have_ to do this + // way rather than letting Windows just send the syscommand itself (as + // would happen if we never did this dance) because for some insane + // reason DefWindowProc for WM_NCLBUTTONDOWN also renders the pressed + // window control button appearance, in the Windows classic style, over + // our view! Ick! By handling this message we prevent Windows from + // doing this undesirable thing, but that means we need to roll the + // sys-command handling ourselves. + ProcessNCMousePress(point, MK_LBUTTON); + return; + } + } + } + + // TODO(beng): figure out why we need to run the system menu manually + // ourselves. This is wrong and causes many subtle bugs. + // From my initial research, it looks like DefWindowProc tries + // to run it but fails before sending the initial WM_MENUSELECT + // for the sysmenu. + if (ht_component == HTSYSMENU) + RunSystemMenu(non_client_view_->GetSystemMenuPoint()); + else + WidgetWin::OnNCLButtonDown(ht_component, point); + + /* TODO(beng): Fix the standard non-client over-painting bug. This code + doesn't work but identifies the problem. + if (!IsMsgHandled()) { + // WindowWin::OnNCLButtonDown set the message as unhandled. This normally + // means WidgetWin::ProcessWindowMessage will pass it to + // DefWindowProc. Sadly, DefWindowProc for WM_NCLBUTTONDOWN does weird + // non-client painting, so we need to call it directly here inside a + // scoped update lock. + ScopedRedrawLock lock(this); + DefWindowProc(GetNativeView(), WM_NCLBUTTONDOWN, ht_component, + MAKELPARAM(point.x, point.y)); + SetMsgHandled(TRUE); + } + */ +} + +void WindowWin::OnNCRButtonDown(UINT ht_component, const CPoint& point) { + if (ht_component == HTCAPTION || ht_component == HTSYSMENU) + RunSystemMenu(gfx::Point(point)); + else + WidgetWin::OnNCRButtonDown(ht_component, point); +} + +LRESULT WindowWin::OnNCUAHDrawCaption(UINT msg, WPARAM w_param, + LPARAM l_param) { + // See comment in widget_win.h at the definition of WM_NCUAHDRAWCAPTION for + // an explanation about why we need to handle this message. + SetMsgHandled(!non_client_view_->UseNativeFrame()); + return 0; +} + +LRESULT WindowWin::OnNCUAHDrawFrame(UINT msg, WPARAM w_param, + LPARAM l_param) { + // See comment in widget_win.h at the definition of WM_NCUAHDRAWCAPTION for + // an explanation about why we need to handle this message. + SetMsgHandled(!non_client_view_->UseNativeFrame()); + return 0; +} + +LRESULT WindowWin::OnSetCursor(HWND window, UINT hittest_code, UINT message) { + // If the window is disabled, it's because we're showing a modal dialog box. + // We need to let DefWindowProc handle the message. That's because + // DefWindowProc for WM_SETCURSOR with message = some kind of mouse button + // down message sends the top level window a WM_ACTIVATEAPP message, which we + // otherwise wouldn't get. The symptom of not doing this is that if the user + // has a window in the background with a modal dialog open, they can't click + // on the disabled background window to bring the entire stack to the front. + // This is annoying because they then have to move all the foreground windows + // out of the way to be able to activate said window. I love how on Windows, + // the answer isn't always logical. + if (!IsWindowEnabled(GetNativeView())) + return WidgetWin::OnSetCursor(window, hittest_code, message); + + int index = RC_NORMAL; + switch (hittest_code) { + case HTTOP: + case HTBOTTOM: + index = RC_VERTICAL; + break; + case HTTOPLEFT: + case HTBOTTOMRIGHT: + index = RC_NWSE; + break; + case HTTOPRIGHT: + case HTBOTTOMLEFT: + index = RC_NESW; + break; + case HTLEFT: + case HTRIGHT: + index = RC_HORIZONTAL; + break; + case HTCAPTION: + case HTCLIENT: + index = RC_NORMAL; + break; + } + SetCursor(resize_cursors_[index]); + return 0; +} + +LRESULT WindowWin::OnSetIcon(UINT size_type, HICON new_icon) { + // This shouldn't hurt even if we're using the native frame. + ScopedRedrawLock lock(this); + return DefWindowProc(GetNativeView(), WM_SETICON, size_type, + reinterpret_cast<LPARAM>(new_icon)); +} + +LRESULT WindowWin::OnSetText(const wchar_t* text) { + // This shouldn't hurt even if we're using the native frame. + ScopedRedrawLock lock(this); + return DefWindowProc(GetNativeView(), WM_SETTEXT, NULL, + reinterpret_cast<LPARAM>(text)); +} + +void WindowWin::OnSettingChange(UINT flags, const wchar_t* section) { + if (!GetParent() && (flags == SPI_SETWORKAREA)) { + // Fire a dummy SetWindowPos() call, so we'll trip the code in + // OnWindowPosChanging() below that notices work area changes. + ::SetWindowPos(GetNativeView(), 0, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | + SWP_NOZORDER | SWP_NOREDRAW | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + SetMsgHandled(TRUE); + } else { + WidgetWin::OnSettingChange(flags, section); + } +} + +void WindowWin::OnSize(UINT size_param, const CSize& new_size) { + // Don't no-op if the new_size matches current size. If our normal bounds + // and maximized bounds are the same, then we need to layout (because we + // layout differently when maximized). + SaveWindowPosition(); + ChangeSize(size_param, new_size); + RedrawWindow(GetNativeView(), NULL, NULL, RDW_INVALIDATE | RDW_ALLCHILDREN); + + // ResetWindowRegion is going to trigger WM_NCPAINT. By doing it after we've + // invoked OnSize we ensure the RootView has been laid out. + ResetWindowRegion(false); +} + +void WindowWin::OnSysCommand(UINT notification_code, CPoint click) { + // Windows uses the 4 lower order bits of |notification_code| for type- + // specific information so we must exclude this when comparing. + static const int sc_mask = 0xFFF0; + // Ignore size/move/maximize in fullscreen mode. + if (IsFullscreen() && + (((notification_code & sc_mask) == SC_SIZE) || + ((notification_code & sc_mask) == SC_MOVE) || + ((notification_code & sc_mask) == SC_MAXIMIZE))) + return; + if (!non_client_view_->UseNativeFrame()) { + if ((notification_code & sc_mask) == SC_MINIMIZE || + (notification_code & sc_mask) == SC_MAXIMIZE || + (notification_code & sc_mask) == SC_RESTORE) { + non_client_view_->ResetWindowControls(); + } else if ((notification_code & sc_mask) == SC_MOVE || + (notification_code & sc_mask) == SC_SIZE) { + if (lock_updates_) { + // We were locked, before entering a resize or move modal loop. Now that + // we've begun to move the window, we need to unlock updates so that the + // sizing/moving feedback can be continuous. + UnlockUpdates(); + } + } + } + + // First see if the delegate can handle it. + if (window_delegate_->ExecuteWindowsCommand(notification_code)) + return; + + if (notification_code == IDC_ALWAYS_ON_TOP) { + is_always_on_top_ = !is_always_on_top_; + + // Change the menu check state. + HMENU system_menu = GetSystemMenu(GetNativeView(), FALSE); + MENUITEMINFO menu_info; + memset(&menu_info, 0, sizeof(MENUITEMINFO)); + menu_info.cbSize = sizeof(MENUITEMINFO); + BOOL r = GetMenuItemInfo(system_menu, IDC_ALWAYS_ON_TOP, + FALSE, &menu_info); + DCHECK(r); + menu_info.fMask = MIIM_STATE; + if (is_always_on_top_) + menu_info.fState = MFS_CHECKED; + r = SetMenuItemInfo(system_menu, IDC_ALWAYS_ON_TOP, FALSE, &menu_info); + + // Now change the actual window's behavior. + AlwaysOnTopChanged(); + } else if ((notification_code == SC_KEYMENU) && (click.x == VK_SPACE)) { + // Run the system menu at the NonClientView's desired location. + RunSystemMenu(non_client_view_->GetSystemMenuPoint()); + } else { + // Use the default implementation for any other command. + DefWindowProc(GetNativeView(), WM_SYSCOMMAND, notification_code, + MAKELPARAM(click.y, click.x)); + } +} + +void WindowWin::OnWindowPosChanging(WINDOWPOS* window_pos) { + if (force_hidden_count_) { + // Prevent the window from being made visible if we've been asked to do so. + // See comment in header as to why we might want this. + window_pos->flags &= ~SWP_SHOWWINDOW; + } + + if (ignore_window_pos_changes_) { + // If somebody's trying to toggle our visibility, change the nonclient area, + // change our Z-order, or activate us, we should probably let it go through. + if (!(window_pos->flags & ((IsVisible() ? SWP_HIDEWINDOW : SWP_SHOWWINDOW) | + SWP_FRAMECHANGED)) && + (window_pos->flags & (SWP_NOZORDER | SWP_NOACTIVATE))) { + // Just sizing/moving the window; ignore. + window_pos->flags |= SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW; + window_pos->flags &= ~(SWP_SHOWWINDOW | SWP_HIDEWINDOW); + } + } else if (!GetParent()) { + CRect window_rect; + HMONITOR monitor; + gfx::Rect monitor_rect, work_area; + if (GetWindowRect(&window_rect) && + GetMonitorAndRects(window_rect, &monitor, &monitor_rect, &work_area)) { + if (monitor && (monitor == last_monitor_) && + (IsFullscreen() || ((monitor_rect == last_monitor_rect_) && + (work_area != last_work_area_)))) { + // A rect for the monitor we're on changed. Normally Windows notifies + // us about this (and thus we're reaching here due to the SetWindowPos() + // call in OnSettingChange() above), but with some software (e.g. + // nVidia's nView desktop manager) the work area can change asynchronous + // to any notification, and we're just sent a SetWindowPos() call with a + // new (frequently incorrect) position/size. In either case, the best + // response is to throw away the existing position/size information in + // |window_pos| and recalculate it based on the new work rect. + gfx::Rect new_window_rect; + if (IsFullscreen()) { + new_window_rect = monitor_rect; + } else if (IsZoomed()) { + new_window_rect = work_area; + int border_thickness = GetSystemMetrics(SM_CXSIZEFRAME); + new_window_rect.Inset(-border_thickness, -border_thickness); + } else { + new_window_rect = gfx::Rect(window_rect).AdjustToFit(work_area); + } + window_pos->x = new_window_rect.x(); + window_pos->y = new_window_rect.y(); + window_pos->cx = new_window_rect.width(); + window_pos->cy = new_window_rect.height(); + // WARNING! Don't set SWP_FRAMECHANGED here, it breaks moving the child + // HWNDs for some reason. + window_pos->flags &= ~(SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW); + window_pos->flags |= SWP_NOCOPYBITS; + + // Now ignore all immediately-following SetWindowPos() changes. Windows + // likes to (incorrectly) recalculate what our position/size should be + // and send us further updates. + ignore_window_pos_changes_ = true; + DCHECK(ignore_pos_changes_factory_.empty()); + MessageLoop::current()->PostTask(FROM_HERE, + ignore_pos_changes_factory_.NewRunnableMethod( + &WindowWin::StopIgnoringPosChanges)); + } + last_monitor_ = monitor; + last_monitor_rect_ = monitor_rect; + last_work_area_ = work_area; + } + } + + WidgetWin::OnWindowPosChanging(window_pos); +} + +//////////////////////////////////////////////////////////////////////////////// +// WindowWin, private: + +void WindowWin::BecomeModal() { + // We implement modality by crawling up the hierarchy of windows starting + // at the owner, disabling all of them so that they don't receive input + // messages. + DCHECK(owning_hwnd_) << "Can't create a modal dialog without an owner"; + HWND start = owning_hwnd_; + while (start != NULL) { + ::EnableWindow(start, FALSE); + start = ::GetParent(start); + } +} + +void WindowWin::SetInitialFocus() { + if (!focus_on_creation_) + return; + + View* v = window_delegate_->GetInitiallyFocusedView(); + if (v) { + v->RequestFocus(); + } else { + // The window does not get keyboard messages unless we focus it, not sure + // why. + SetFocus(GetNativeView()); + } +} + +void WindowWin::SetInitialBounds(const gfx::Rect& create_bounds) { + // First we obtain the window's saved show-style and store it. We need to do + // this here, rather than in Show() because by the time Show() is called, + // the window's size will have been reset (below) and the saved maximized + // state will have been lost. Sadly there's no way to tell on Windows when + // a window is restored from maximized state, so we can't more accurately + // track maximized state independently of sizing information. + window_delegate_->GetSavedMaximizedState(&saved_maximized_state_); + + // Restore the window's placement from the controller. + gfx::Rect saved_bounds(create_bounds.ToRECT()); + if (window_delegate_->GetSavedWindowBounds(&saved_bounds)) { + // Make sure the bounds are at least the minimum size. + if (saved_bounds.width() < minimum_size_.cx) { + saved_bounds.SetRect(saved_bounds.x(), saved_bounds.y(), + saved_bounds.right() + minimum_size_.cx - + saved_bounds.width(), + saved_bounds.bottom()); + } + + if (saved_bounds.height() < minimum_size_.cy) { + saved_bounds.SetRect(saved_bounds.x(), saved_bounds.y(), + saved_bounds.right(), + saved_bounds.bottom() + minimum_size_.cy - + saved_bounds.height()); + } + + // "Show state" (maximized, minimized, etc) is handled by Show(). + // Don't use SetBounds here. SetBounds constrains to the size of the + // monitor, but we don't want that when creating a new window as the result + // of dragging out a tab to create a new window. + SetWindowPos(NULL, saved_bounds.x(), saved_bounds.y(), + saved_bounds.width(), saved_bounds.height(), 0); + } else { + if (create_bounds.IsEmpty()) { + // No initial bounds supplied, so size the window to its content and + // center over its parent. + SizeWindowToDefault(); + } else { + // Use the supplied initial bounds. + SetBounds(create_bounds); + } + } +} + +void WindowWin::InitAlwaysOnTopState() { + is_always_on_top_ = false; + if (window_delegate_->GetSavedAlwaysOnTopState(&is_always_on_top_) && + is_always_on_top_ != window_delegate_->IsAlwaysOnTop()) { + AlwaysOnTopChanged(); + } + + if (window_delegate_->HasAlwaysOnTopMenu()) + AddAlwaysOnTopSystemMenuItem(); +} + +void WindowWin::AddAlwaysOnTopSystemMenuItem() { + // The Win32 API requires that we own the text. + always_on_top_menu_text_ = l10n_util::GetString(IDS_ALWAYS_ON_TOP); + + // Let's insert a menu to the window. + HMENU system_menu = ::GetSystemMenu(GetNativeView(), FALSE); + int index = ::GetMenuItemCount(system_menu) - 1; + if (index < 0) { + // Paranoia check. + NOTREACHED(); + index = 0; + } + // First we add the separator. + MENUITEMINFO menu_info; + memset(&menu_info, 0, sizeof(MENUITEMINFO)); + menu_info.cbSize = sizeof(MENUITEMINFO); + menu_info.fMask = MIIM_FTYPE; + menu_info.fType = MFT_SEPARATOR; + ::InsertMenuItem(system_menu, index, TRUE, &menu_info); + + // Then the actual menu. + menu_info.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STRING | MIIM_STATE; + menu_info.fType = MFT_STRING; + menu_info.fState = MFS_ENABLED; + if (is_always_on_top_) + menu_info.fState |= MFS_CHECKED; + menu_info.wID = IDC_ALWAYS_ON_TOP; + menu_info.dwTypeData = const_cast<wchar_t*>(always_on_top_menu_text_.c_str()); + ::InsertMenuItem(system_menu, index, TRUE, &menu_info); +} + +void WindowWin::RestoreEnabledIfNecessary() { + if (is_modal_ && !restored_enabled_) { + restored_enabled_ = true; + // If we were run modally, we need to undo the disabled-ness we inflicted on + // the owner's parent hierarchy. + HWND start = owning_hwnd_; + while (start != NULL) { + ::EnableWindow(start, TRUE); + start = ::GetParent(start); + } + } +} + +void WindowWin::AlwaysOnTopChanged() { + ::SetWindowPos(GetNativeView(), + is_always_on_top_ ? HWND_TOPMOST : HWND_NOTOPMOST, + 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); +} + +DWORD WindowWin::CalculateWindowStyle() { + DWORD window_styles = + WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_SYSMENU | WS_CAPTION; + bool can_resize = window_delegate_->CanResize(); + bool can_maximize = window_delegate_->CanMaximize(); + if (can_maximize) { + window_styles |= WS_OVERLAPPEDWINDOW; + } else if (can_resize) { + window_styles |= WS_OVERLAPPED | WS_THICKFRAME; + } + if (window_delegate_->AsDialogDelegate()) { + window_styles |= DS_MODALFRAME; + // NOTE: Turning this off means we lose the close button, which is bad. + // Turning it on though means the user can maximize or size the window + // from the system menu, which is worse. We may need to provide our own + // menu to get the close button to appear properly. + // window_styles &= ~WS_SYSMENU; + } + return window_styles; +} + +DWORD WindowWin::CalculateWindowExStyle() { + DWORD window_ex_styles = 0; + if (window_delegate_->AsDialogDelegate()) + window_ex_styles |= WS_EX_DLGMODALFRAME; + if (window_delegate_->IsAlwaysOnTop()) + window_ex_styles |= WS_EX_TOPMOST; + return window_ex_styles; +} + +void WindowWin::SaveWindowPosition() { + // The window delegate does the actual saving for us. It seems like (judging + // by go/crash) that in some circumstances we can end up here after + // WM_DESTROY, at which point the window delegate is likely gone. So just + // bail. + if (!window_delegate_) + return; + + WINDOWPLACEMENT win_placement = { 0 }; + win_placement.length = sizeof(WINDOWPLACEMENT); + + BOOL r = GetWindowPlacement(GetNativeView(), &win_placement); + DCHECK(r); + + bool maximized = (win_placement.showCmd == SW_SHOWMAXIMIZED); + CRect window_bounds(win_placement.rcNormalPosition); + window_delegate_->SaveWindowPlacement( + gfx::Rect(win_placement.rcNormalPosition), maximized, is_always_on_top_); +} + +void WindowWin::LockUpdates() { + lock_updates_ = true; + saved_window_style_ = GetWindowLong(GWL_STYLE); + SetWindowLong(GWL_STYLE, saved_window_style_ & ~WS_VISIBLE); +} + +void WindowWin::UnlockUpdates() { + SetWindowLong(GWL_STYLE, saved_window_style_); + lock_updates_ = false; +} + +void WindowWin::ResetWindowRegion(bool force) { + // A native frame uses the native window region, and we don't want to mess + // with it. + if (non_client_view_->UseNativeFrame()) { + if (force) + SetWindowRgn(NULL, TRUE); + return; + } + + // Changing the window region is going to force a paint. Only change the + // window region if the region really differs. + HRGN current_rgn = CreateRectRgn(0, 0, 0, 0); + int current_rgn_result = GetWindowRgn(GetNativeView(), current_rgn); + + CRect window_rect; + GetWindowRect(&window_rect); + HRGN new_region; + gfx::Path window_mask; + non_client_view_->GetWindowMask( + gfx::Size(window_rect.Width(), window_rect.Height()), &window_mask); + new_region = window_mask.CreateHRGN(); + + if (current_rgn_result == ERROR || !EqualRgn(current_rgn, new_region)) { + // SetWindowRgn takes ownership of the HRGN created by CreateHRGN. + SetWindowRgn(new_region, TRUE); + } else { + DeleteObject(new_region); + } + + DeleteObject(current_rgn); +} + +void WindowWin::ProcessNCMousePress(const CPoint& point, int flags) { + CPoint temp = point; + MapWindowPoints(HWND_DESKTOP, GetNativeView(), &temp, 1); + UINT message_flags = 0; + if ((GetKeyState(VK_CONTROL) & 0x80) == 0x80) + message_flags |= MK_CONTROL; + if ((GetKeyState(VK_SHIFT) & 0x80) == 0x80) + message_flags |= MK_SHIFT; + message_flags |= flags; + ProcessMousePressed(temp, message_flags, false, false); +} + +LRESULT WindowWin::CallDefaultNCActivateHandler(BOOL active) { + // The DefWindowProc handling for WM_NCACTIVATE renders the classic-look + // window title bar directly, so we need to use a redraw lock here to prevent + // it from doing so. + ScopedRedrawLock lock(this); + return DefWindowProc(GetNativeView(), WM_NCACTIVATE, active, 0); +} + +void WindowWin::InitClass() { + static bool initialized = false; + if (!initialized) { + resize_cursors_[RC_NORMAL] = LoadCursor(NULL, IDC_ARROW); + resize_cursors_[RC_VERTICAL] = LoadCursor(NULL, IDC_SIZENS); + resize_cursors_[RC_HORIZONTAL] = LoadCursor(NULL, IDC_SIZEWE); + resize_cursors_[RC_NESW] = LoadCursor(NULL, IDC_SIZENESW); + resize_cursors_[RC_NWSE] = LoadCursor(NULL, IDC_SIZENWSE); + initialized = true; + } +} + +namespace { +// static +static BOOL CALLBACK WindowCallbackProc(HWND hwnd, LPARAM lParam) { + WidgetWin* widget = reinterpret_cast<WidgetWin*>( + win_util::GetWindowUserData(hwnd)); + if (!widget) + return TRUE; + + // If the toplevel HWND is a Window, close it if it's identified as a + // secondary window. + Window* window = widget->GetWindow(); + if (window) { + if (!window->IsAppWindow()) + window->Close(); + } else { + // If it's not a Window, then close it anyway since it probably is + // secondary. + widget->Close(); + } + return TRUE; +} +} // namespace + +void Window::CloseAllSecondaryWindows() { + EnumThreadWindows(GetCurrentThreadId(), WindowCallbackProc, 0); +} + +} // namespace views diff --git a/views/window/window_win.h b/views/window/window_win.h new file mode 100644 index 0000000..76d5196 --- /dev/null +++ b/views/window/window_win.h @@ -0,0 +1,307 @@ +// Copyright (c) 2006-2008 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 VIEWS_WINDOW_WINDOW_WIN_H_ +#define VIEWS_WINDOW_WINDOW_WIN_H_ + +#include "views/widget/widget_win.h" +#include "views/window/client_view.h" +#include "views/window/non_client_view.h" +#include "views/window/window.h" + +namespace gfx { +class Point; +class Size; +}; + +namespace views { + +class Client; +class WindowDelegate; + +/////////////////////////////////////////////////////////////////////////////// +// +// WindowWin +// +// A WindowWin is a WidgetWin that has a caption and a border. The frame is +// rendered by the operating system. +// +/////////////////////////////////////////////////////////////////////////////// +class WindowWin : public WidgetWin, + public Window { + public: + virtual ~WindowWin(); + + // Show the window with the specified show command. + void Show(int show_state); + + // Retrieve the show state of the window. This is one of the SW_SHOW* flags + // passed into Windows' ShowWindow method. For normal windows this defaults + // to SW_SHOWNORMAL, however windows (e.g. the main window) can override this + // method to provide different values (e.g. retrieve the user's specified + // show state from the shortcut starutp info). + virtual int GetShowState() const; + + // Executes the specified SC_command. + void ExecuteSystemMenuCommand(int command); + + // Hides the window if it hasn't already been force-hidden, then increments + // |force_hidden_count_| to prevent it from being shown again until + // PopForceHidden()) is called. + void PushForceHidden(); + + // Decrements |force_hidden_count_| and, if it is now zero, shows the window. + void PopForceHidden(); + + // Accessors and setters for various properties. + HWND owning_window() const { return owning_hwnd_; } + void set_focus_on_creation(bool focus_on_creation) { + focus_on_creation_ = focus_on_creation; + } + + // Window overrides: + virtual gfx::Rect GetBounds() const; + virtual gfx::Rect GetNormalBounds() const; + virtual void SetBounds(const gfx::Rect& bounds); + virtual void SetBounds(const gfx::Rect& bounds, + gfx::NativeWindow other_window); + virtual void Show(); + virtual void Activate(); + virtual void Close(); + virtual void Maximize(); + virtual void Minimize(); + virtual void Restore(); + virtual bool IsActive() const; + virtual bool IsVisible() const; + virtual bool IsMaximized() const; + virtual bool IsMinimized() const; + virtual void SetFullscreen(bool fullscreen); + virtual bool IsFullscreen() const; + virtual void EnableClose(bool enable); + virtual void DisableInactiveRendering(); + virtual void UpdateWindowTitle(); + virtual void UpdateWindowIcon(); + virtual NonClientFrameView* CreateFrameViewForWindow(); + virtual void UpdateFrameAfterFrameChange(); + virtual WindowDelegate* GetDelegate() const; + virtual NonClientView* GetNonClientView() const; + virtual ClientView* GetClientView() const; + virtual gfx::NativeWindow GetNativeWindow() const; + + protected: + friend Window; + + // Constructs the WindowWin. |window_delegate| cannot be NULL. + explicit WindowWin(WindowDelegate* window_delegate); + + // Create the Window. + // If parent is NULL, this WindowWin is top level on the desktop. + // If |bounds| is empty, the view is queried for its preferred size and + // centered on screen. + virtual void Init(HWND parent, const gfx::Rect& bounds); + + // Sizes the window to the default size specified by its ClientView. + virtual void SizeWindowToDefault(); + + // Shows the system menu at the specified screen point. + void RunSystemMenu(const gfx::Point& point); + + // Overridden from WidgetWin: + virtual void OnActivate(UINT action, BOOL minimized, HWND window); + virtual void OnActivateApp(BOOL active, DWORD thread_id); + virtual LRESULT OnAppCommand(HWND window, short app_command, WORD device, + int keystate); + virtual void OnCommand(UINT notification_code, int command_id, HWND window); + virtual void OnDestroy(); + virtual LRESULT OnDwmCompositionChanged(UINT msg, WPARAM w_param, + LPARAM l_param); + virtual void OnFinalMessage(HWND window); + virtual void OnGetMinMaxInfo(MINMAXINFO* minmax_info); + virtual void OnInitMenu(HMENU menu); + virtual void OnMouseLeave(); + virtual LRESULT OnNCActivate(BOOL active); + virtual LRESULT OnNCCalcSize(BOOL mode, LPARAM l_param); + virtual LRESULT OnNCHitTest(const CPoint& point); + virtual void OnNCPaint(HRGN rgn); + virtual void OnNCLButtonDown(UINT ht_component, const CPoint& point); + virtual void OnNCRButtonDown(UINT ht_component, const CPoint& point); + virtual LRESULT OnNCUAHDrawCaption(UINT msg, WPARAM w_param, LPARAM l_param); + virtual LRESULT OnNCUAHDrawFrame(UINT msg, WPARAM w_param, LPARAM l_param); + virtual LRESULT OnSetCursor(HWND window, UINT hittest_code, UINT message); + virtual LRESULT OnSetIcon(UINT size_type, HICON new_icon); + virtual LRESULT OnSetText(const wchar_t* text); + virtual void OnSettingChange(UINT flags, const wchar_t* section); + virtual void OnSize(UINT size_param, const CSize& new_size); + virtual void OnSysCommand(UINT notification_code, CPoint click); + virtual void OnWindowPosChanging(WINDOWPOS* window_pos); + virtual Window* GetWindow() { return this; } + virtual const Window* GetWindow() const { return this; } + + // Accessor for disable_inactive_rendering_. + bool disable_inactive_rendering() const { + return disable_inactive_rendering_; + } + + private: + // Information saved before going into fullscreen mode, used to restore the + // window afterwards. + struct SavedWindowInfo { + bool maximized; + LONG style; + LONG ex_style; + RECT window_rect; + }; + + // Set the window as modal (by disabling all the other windows). + void BecomeModal(); + + // Sets-up the focus manager with the view that should have focus when the + // window is shown the first time. If NULL is returned, the focus goes to the + // button if there is one, otherwise the to the Cancel button. + void SetInitialFocus(); + + // Place and size the window when it is created. |create_bounds| are the + // bounds used when the window was created. + void SetInitialBounds(const gfx::Rect& create_bounds); + + // Restore saved always on stop state and add the always on top system menu + // if needed. + void InitAlwaysOnTopState(); + + // Add an item for "Always on Top" to the System Menu. + void AddAlwaysOnTopSystemMenuItem(); + + // If necessary, enables all ancestors. + void RestoreEnabledIfNecessary(); + + // Update the window style to reflect the always on top state. + void AlwaysOnTopChanged(); + + // Calculate the appropriate window styles for this window. + DWORD CalculateWindowStyle(); + DWORD CalculateWindowExStyle(); + + // Asks the delegate if any to save the window's location and size. + void SaveWindowPosition(); + + // Lock or unlock the window from being able to redraw itself in response to + // updates to its invalid region. + class ScopedRedrawLock; + void LockUpdates(); + void UnlockUpdates(); + + // Stops ignoring SetWindowPos() requests (see below). + void StopIgnoringPosChanges() { ignore_window_pos_changes_ = false; } + + // Resets the window region for the current window bounds if necessary. + // If |force| is true, the window region is reset to NULL even for native + // frame windows. + void ResetWindowRegion(bool force); + + // Converts a non-client mouse down message to a regular ChromeViews event + // and handle it. |point| is the mouse position of the message in screen + // coords. |flags| are flags that would be passed with a WM_L/M/RBUTTON* + // message and relate to things like which button was pressed. These are + // combined with flags relating to the current key state. + void ProcessNCMousePress(const CPoint& point, int flags); + + // Calls the default WM_NCACTIVATE handler with the specified activation + // value, safely wrapping the call in a ScopedRedrawLock to prevent frame + // flicker. + LRESULT CallDefaultNCActivateHandler(BOOL active); + + // Static resource initialization. + static void InitClass(); + enum ResizeCursor { + RC_NORMAL = 0, RC_VERTICAL, RC_HORIZONTAL, RC_NESW, RC_NWSE + }; + static HCURSOR resize_cursors_[6]; + + // Our window delegate (see Init method for documentation). + WindowDelegate* window_delegate_; + + // The View that provides the non-client area of the window (title bar, + // window controls, sizing borders etc). To use an implementation other than + // the default, this class must be subclassed and this value set to the + // desired implementation before calling |Init|. + NonClientView* non_client_view_; + + // Whether we should SetFocus() on a newly created window after + // Init(). Defaults to true. + bool focus_on_creation_; + + // We need to save the parent window that spawned us, since GetParent() + // returns NULL for dialogs. + HWND owning_hwnd_; + + // The smallest size the window can be. + CSize minimum_size_; + + // Whether or not the window is modal. This comes from the delegate and is + // cached at Init time to avoid calling back to the delegate from the + // destructor. + bool is_modal_; + + // Whether all ancestors have been enabled. This is only used if is_modal_ is + // true. + bool restored_enabled_; + + // Whether the window is currently always on top. + bool is_always_on_top_; + + // We need to own the text of the menu, the Windows API does not copy it. + std::wstring always_on_top_menu_text_; + + // True if we're in fullscreen mode. + bool fullscreen_; + + // Saved window information from before entering fullscreen mode. + SavedWindowInfo saved_window_info_; + + // Set to true if the window is in the process of closing . + bool window_closed_; + + // True when the window should be rendered as active, regardless of whether + // or not it actually is. + bool disable_inactive_rendering_; + + // True if this window is the active top level window. + bool is_active_; + + // True if updates to this window are currently locked. + bool lock_updates_; + + // The window styles of the window before updates were locked. + DWORD saved_window_style_; + + // The saved maximized state for this window. See note in SetInitialBounds + // that explains why we save this. + bool saved_maximized_state_; + + // When true, this flag makes us discard incoming SetWindowPos() requests that + // only change our position/size. (We still allow changes to Z-order, + // activation, etc.) + bool ignore_window_pos_changes_; + + // The following factory is used to ignore SetWindowPos() calls for short time + // periods. + ScopedRunnableMethodFactory<WindowWin> ignore_pos_changes_factory_; + + // If this is greater than zero, we should prevent attempts to make the window + // visible when we handle WM_WINDOWPOSCHANGING. Some calls like + // ShowWindow(SW_RESTORE) make the window visible in addition to restoring it, + // when all we want to do is restore it. + int force_hidden_count_; + + // The last-seen monitor containing us, and its rect and work area. These are + // used to catch updates to the rect and work area and react accordingly. + HMONITOR last_monitor_; + gfx::Rect last_monitor_rect_, last_work_area_; + + DISALLOW_COPY_AND_ASSIGN(WindowWin); +}; + +} // namespace views + +#endif // VIEWS_WINDOW_WINDOW_WIN_H_ |