diff options
author | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-08 00:34:05 +0000 |
---|---|---|
committer | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-08 00:34:05 +0000 |
commit | 2362e4fe2905ab75d3230ebc3e307ae53e2b8362 (patch) | |
tree | e6d88357a2021811e0e354f618247217be8bb3da /views/controls | |
parent | db23ac3e713dc17509b2b15d3ee634968da45715 (diff) | |
download | chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.zip chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.tar.gz chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.tar.bz2 |
Move src/chrome/views to src/views. RS=darin http://crbug.com/11387
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15604 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'views/controls')
75 files changed, 20073 insertions, 0 deletions
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_ |