// Copyright 2008, Google Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include "chrome/browser/views/bookmark_bar_view.h" #include "base/base_drag_source.h" #include "base/gfx/skia_utils.h" #include "chrome/app/theme/theme_resources.h" #include "chrome/browser/browser.h" #include "chrome/browser/browser_list.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/chrome_frame.h" #include "chrome/browser/drag_utils.h" #include "chrome/browser/download_util.h" #include "chrome/browser/history/history_backend.h" #include "chrome/browser/history/history_database.h" #include "chrome/browser/history/history.h" #include "chrome/browser/page_navigator.h" #include "chrome/browser/profile.h" #include "chrome/browser/user_metrics.h" #include "chrome/browser/view_ids.h" #include "chrome/browser/views/bookmark_editor_view.h" #include "chrome/browser/views/event_utils.h" #include "chrome/browser/views/input_window.h" #include "chrome/common/gfx/chrome_canvas.h" #include "chrome/common/gfx/favicon_size.h" #include "chrome/common/gfx/url_elider.h" #include "chrome/common/l10n_util.h" #include "chrome/common/notification_service.h" #include "chrome/common/notification_types.h" #include "chrome/common/os_exchange_data.h" #include "chrome/common/page_transition_types.h" #include "chrome/common/pref_names.h" #include "chrome/common/pref_service.h" #include "chrome/common/resource_bundle.h" #include "chrome/common/win_util.h" #include "chrome/views/chrome_menu.h" #include "chrome/views/menu_button.h" #include "chrome/views/tooltip_manager.h" #include "generated_resources.h" using ChromeViews::BaseButton; using ChromeViews::DropTargetEvent; using ChromeViews::MenuButton; using ChromeViews::MenuItemView; using ChromeViews::View; // Margins around the content. static const int kTopMargin = 2; static const int kBottomMargin = 3; static const int kLeftMargin = 1; static const int kRightMargin = 0; // Preferred height of the bookmarks bar. static const int kBarHeight = 29; // Preferred height of the bookmarks bar when only shown on the new tab page. static const int kNewtabBarHeight = 57; // How inset the bookmarks bar is when displayed on the new tab page. This is // in addition to the margins above. static const int kNewtabHorizontalPadding = 8; static const int kNewtabVerticalPadding = 12; // Padding between buttons. static const int kButtonPadding = 0; // Command ids used in the menu allowing the user to choose when we're visible. static const int kAlwaysShowCommandID = 1; // Whether the pref height has been calculated. static bool calcedSize = false; // The preferred height is the height needed for one button. As it is a constant // it is calculated once. static int prefButtonHeight = 0; // Icon to display when one isn't found for the page. static SkBitmap* kDefaultFavIcon = NULL; // Icon used for folders. static SkBitmap* kFolderIcon = NULL; // Icon used for most other menu. static SkBitmap* kBookmarkedOtherIcon = NULL; // Background color. static const SkColor kBackgroundColor = SkColorSetRGB(237, 244, 252); // Border colors for the BookmarBarView. static const SkColor kTopBorderColor = SkColorSetRGB(222, 234, 248); static const SkColor kBottomBorderColor = SkColorSetRGB(178, 178, 178); // Background color for when the bookmarks bar is only being displayed on the // new tab page - this color should match the background color of the new tab // page (white, most likely). static const SkColor kNewtabBackgroundColor = SkColorSetRGB(255, 255, 255); // Border color for the 'new tab' style bookmarks bar. static const SkColor kNewtabBorderColor = SkColorSetRGB(195, 206, 224); // How round the 'new tab' style bookmarks bar is. static const int kNewtabBarRoundness = 5; // Offset for where the menu is shown relative to the bottom of the // BookmarkBarView. static const int kMenuOffset = 3; // Delay during drag and drop before the menu pops up. This is only used if // we can't get the value from the OS. static const int kShowFolderDropMenuDelay = 400; // Color of the drop indicator. static const SkColor kDropIndicatorColor = SK_ColorBLACK; // Width of the drop indicator. static const int kDropIndicatorWidth = 2; // Distance between the bottom of the bar and the separator. static const int kSeparatorMargin = 1; // Width of the separator between the recently bookmarked button and the // overflow indicator. static const int kSeparatorWidth = 4; // Starting x-coordinate of the separator line within a separator. static const int kSeparatorStartX = 2; // Border color along the left edge of the view representing the most recently // view pages. static const SkColor kSeparatorColor = SkColorSetRGB(194, 205, 212); // Left-padding for the instructional text. static const int kInstructionsPadding = 6; // Color of the instructional text. static const SkColor kInstructionsColor = SkColorSetRGB(128, 128, 142); namespace { // Returns the tooltip text for the specified url and title. The returned // text is clipped to fit within the bounds of the monitor. // // Note that we adjust the direction of both the URL and the title based on the // locale so that pure LTR strings are displayed properly in RTL locales. static std::wstring CreateToolTipForURLAndTitle(const gfx::Point& screen_loc, const GURL& url, const std::wstring& title, const std::wstring& languages) { const gfx::Rect monitor_bounds = win_util::GetMonitorBoundsForRect( gfx::Rect(screen_loc.x(), screen_loc.y(), 1, 1)); ChromeFont tt_font = ChromeViews::TooltipManager::GetDefaultFont(); std::wstring result; // First the title. if (!title.empty()) { std::wstring localized_title; if (l10n_util::AdjustStringForLocaleDirection(title, &localized_title)) result.append(gfx::ElideText(localized_title, tt_font, monitor_bounds.width())); else result.append(gfx::ElideText(title, tt_font, monitor_bounds.width())); } // Only show the URL if the url and title differ. if (title != UTF8ToWide(url.spec())) { if (!result.empty()) result.append(ChromeViews::TooltipManager::GetLineSeparator()); // We need to explicitly specify the directionality of the URL's text to // make sure it is treated as an LTR string when the context is RTL. For // example, the URL "http://www.yahoo.com/" appears as // "/http://www.yahoo.com" when rendered, as is, in an RTL context since // the Unicode BiDi algorithm puts certain characters on the left by // default. std::wstring elided_url(gfx::ElideUrl(url, tt_font, monitor_bounds.width(), languages)); if (l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT) l10n_util::WrapStringWithLTRFormatting(&elided_url); result.append(elided_url); } return result; } // Returns the drag operations for the specified node. static int GetDragOperationsForNode(BookmarkBarNode* node) { if (node->GetType() == history::StarredEntry::URL) { return DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_MOVE | DragDropTypes::DRAG_LINK; } return DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_MOVE; } // BookmarkButton ------------------------------------------------------------- // Buttons used for the bookmarks on the bookmark bar. class BookmarkButton : public ChromeViews::TextButton { public: BookmarkButton(const GURL& url, const std::wstring& title, Profile* profile) : TextButton(title), url_(url), profile_(profile) { show_animation_.reset(new SlideAnimation(this)); show_animation_->Show(); } bool GetTooltipText(int x, int y, std::wstring* tooltip) { CPoint location(x, y); ConvertPointToScreen(this, &location); *tooltip = CreateToolTipForURLAndTitle( gfx::Point(location.x, location.y), url_, GetText(), profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)); return !tooltip->empty(); } virtual bool IsTriggerableEvent(const ChromeViews::MouseEvent& e) { return event_utils::IsPossibleDispositionEvent(e); } virtual void Paint(ChromeCanvas *canvas) { ChromeViews::TextButton::Paint(canvas); // Since we can't change the alpha of the button (it contains un-alphable // text), we paint the bar background over the front of the button. As the // bar background is a gradient, we have to paint the gradient at the // size of the parent (hence all the margin math below). We can't use // the parent's actual bounds because they differ from what is painted. SkPaint paint; paint.setAlpha(static_cast( (1.0 - show_animation_->GetCurrentValue()) * 255)); paint.setShader(gfx::CreateGradientShader(0, GetHeight() + kTopMargin + kBottomMargin, kTopBorderColor, kBackgroundColor))->safeUnref(); canvas->FillRectInt(0, -kTopMargin, GetWidth(), GetHeight() + kTopMargin + kBottomMargin, paint); } virtual void AnimationProgressed(const Animation* animation) { ChromeViews::TextButton::AnimationProgressed(animation); SchedulePaint(); } private: const GURL& url_; Profile* profile_; scoped_ptr show_animation_; DISALLOW_EVIL_CONSTRUCTORS(BookmarkButton); }; // DropInfo ------------------------------------------------------------------- // Tracks drops on the BookmarkBarView. struct DropInfo { DropInfo() : drop_index(-1), is_menu_showing(false), valid(false) {} // Whether the data is valid. bool valid; // Index into the model the drop is over. This is relative to the root node. int drop_index; // If true, the menu is being shown. bool is_menu_showing; // If true, the user is dropping on a node. This is only used for group // nodes. bool drop_on; // If true, the user is over the overflow button. bool is_over_overflow; // If true, the user is over the other button. bool is_over_other; // Coordinates of the drag (in terms of the BookmarkBarView). int x; int y; // The current drag operation. int drag_operation; // DropData for the drop. BookmarkDragData data; }; // ModelChangedListener ------------------------------------------------------- // Interface implemented by controllers/views that need to be notified any // time the model changes, typically to cancel an operation that is showing // data from the model such as a menu. This isn't intended as a general // way to be notified of changes, rather for cases where a controller/view is // showing data from the model in a modal like setting and needs to cleanly // exit the modal loop if the model changes out from under it. // // A controller/view that needs this notification should install itself as the // ModelChangeListener via the SetModelChangedListener method when shown and // reset the ModelChangeListener of the BookmarkBarView when it closes by way // of either the SetModelChangedListener method or the // ClearModelChangedListenerIfEquals method. // class ModelChangedListener { public: virtual ~ModelChangedListener() {} // Invoked when the model changes. Should cancel the edit and close any // dialogs. virtual void ModelChanged() = 0; }; // EditFolderController ------------------------------------------------------- // EditFolderController manages the editing and/or creation of a folder. If the // user presses ok, the name change is committed to the database. // // EditFolderController deletes itself when the window is closed. // class EditFolderController : public InputWindowDelegate, public ModelChangedListener { public: EditFolderController(BookmarkBarView* view, BookmarkBarNode* node, int visual_order, bool is_new) : view_(view), node_(node), visual_order_(visual_order), is_new_(is_new) { DCHECK(is_new_ || node); window_ = CreateInputWindow(view->GetViewContainer()->GetHWND(), this); view_->SetModelChangedListener(this); } void Show() { window_->Show(); } virtual void ModelChanged() { window_->Close(); } private: virtual std::wstring GetTextFieldLabel() { return l10n_util::GetString(IDS_BOOMARK_BAR_EDIT_FOLDER_LABEL); } virtual std::wstring GetTextFieldContents() { if (is_new_) return l10n_util::GetString(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME); return node_->GetTitle(); } virtual bool IsValid(const std::wstring& text) { return !text.empty(); } virtual void InputAccepted(const std::wstring& text) { view_->ClearModelChangedListenerIfEquals(this); BookmarkBarModel* model = view_->GetProfile()->GetBookmarkBarModel(); if (is_new_) model->AddGroup(node_, visual_order_, text); else model->SetTitle(node_, text); } virtual void InputCanceled() { view_->ClearModelChangedListenerIfEquals(this); } virtual void WindowClosing() { view_->ClearModelChangedListenerIfEquals(this); delete this; } virtual std::wstring GetWindowTitle() const { return is_new_ ? l10n_util::GetString(IDS_BOOMARK_FOLDER_EDITOR_WINDOW_TITLE_NEW) : l10n_util::GetString(IDS_BOOMARK_FOLDER_EDITOR_WINDOW_TITLE); } BookmarkBarView* view_; // If is_new is true, this is the parent to create the new node under. // Otherwise this is the node to change the title of. BookmarkBarNode* node_; int visual_order_; bool is_new_; ChromeViews::Window* window_; DISALLOW_EVIL_CONSTRUCTORS(EditFolderController); }; // BookmarkNodeMenuController ------------------------------------------------- // IDs for the menus we create. static const int kOpenBookmarkID = 2; static const int kOpenBookmarkInNewWindowID = 3; static const int kOpenBookmarkInNewTabID = 4; static const int kOpenAllBookmarksID = 5; static const int kOpenAllBookmarksInNewWindowID = 6; static const int kEditBookmarkID = 7; static const int kDeleteBookmarkID = 8; static const int kAddBookmarkID = 9; static const int kNewFolderID = 10; // BookmarkNodeMenuController manages the context menus shown for the bookmark // bar and buttons on the bookmark bar. class BookmarkNodeMenuController : public ChromeViews::MenuDelegate, public ModelChangedListener { public: BookmarkNodeMenuController(BookmarkBarView* view, BookmarkBarNode* node) : view_(view), node_(node), menu_(this) { if (node->GetType() == history::StarredEntry::URL) { menu_.AppendMenuItemWithLabel(kOpenBookmarkID, l10n_util::GetString(IDS_BOOMARK_BAR_OPEN)); menu_.AppendMenuItemWithLabel(kOpenBookmarkInNewTabID, l10n_util::GetString(IDS_BOOMARK_BAR_OPEN_IN_NEW_TAB)); menu_.AppendMenuItemWithLabel(kOpenBookmarkInNewWindowID, l10n_util::GetString(IDS_BOOMARK_BAR_OPEN_IN_NEW_WINDOW)); } else { menu_.AppendMenuItemWithLabel(kOpenAllBookmarksID, l10n_util::GetString(IDS_BOOMARK_BAR_OPEN_ALL)); menu_.AppendMenuItemWithLabel(kOpenAllBookmarksInNewWindowID, l10n_util::GetString(IDS_BOOMARK_BAR_OPEN_ALL_NEW_WINDOW)); } menu_.AppendSeparator(); if (node->GetParent() != view->GetProfile()->GetBookmarkBarModel()->root_node()) { menu_.AppendMenuItemWithLabel(kEditBookmarkID, l10n_util::GetString(IDS_BOOKMARK_BAR_EDIT)); menu_.AppendMenuItemWithLabel(kDeleteBookmarkID, l10n_util::GetString(IDS_BOOKMARK_BAR_REMOVE)); } menu_.AppendMenuItemWithLabel(kAddBookmarkID, l10n_util::GetString(IDS_BOOMARK_BAR_ADD_NEW_BOOKMARK)); menu_.AppendMenuItemWithLabel(kNewFolderID, l10n_util::GetString(IDS_BOOMARK_BAR_NEW_FOLDER)); menu_.AppendSeparator(); menu_.AppendMenuItem(kAlwaysShowCommandID, l10n_util::GetString(IDS_BOOMARK_BAR_ALWAYS_SHOW), MenuItemView::CHECKBOX); } void RunMenuAt(int x, int y) { // Record the current ModelChangedListener. It will be non-null when we're // used as the context menu for another menu. ModelChangedListener* last_listener = view_->GetModelChangedListener(); view_->SetModelChangedListener(this); // width/height don't matter here. menu_.RunMenuAt(view_->GetViewContainer()->GetHWND(), gfx::Rect(x, y, 0, 0), MenuItemView::TOPLEFT, false); if (view_->GetModelChangedListener() == this) view_->SetModelChangedListener(last_listener); } virtual void ModelChanged() { menu_.Cancel(); } private: // Menu::Delegate method. Does the appropriate operation based on chosen // menu item. virtual void ExecuteCommand(int id) { Profile* profile = view_->GetProfile(); switch (id) { case kOpenBookmarkID: UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_Open", profile); view_->GetPageNavigator()->OpenURL(node_->GetURL(), CURRENT_TAB, PageTransition::AUTO_BOOKMARK); break; case kOpenBookmarkInNewWindowID: UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_OpenInNewWindow", profile); view_->GetPageNavigator()->OpenURL(node_->GetURL(), NEW_WINDOW, PageTransition::AUTO_BOOKMARK); break; case kOpenBookmarkInNewTabID: UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_OpenInNewTab", profile); view_->GetPageNavigator()->OpenURL(node_->GetURL(), NEW_FOREGROUND_TAB, PageTransition::AUTO_BOOKMARK); break; case kOpenAllBookmarksID: case kOpenAllBookmarksInNewWindowID: { if (id == kOpenAllBookmarksID) { UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_OpenAll", profile); } else { UserMetrics::RecordAction( L"BookmarkBar_ContextMenu_OpenAllInNewWindow", profile); } BookmarkBarNode* node = node_; PageNavigator* navigator = view_->GetPageNavigator(); bool opened_url = false; OpenAll(node, (id == kOpenAllBookmarksInNewWindowID), &navigator, &opened_url); break; } case kEditBookmarkID: UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_Edit", profile); if (node_->GetType() == history::StarredEntry::URL) { BookmarkEditorView::Show(view_->GetViewContainer()->GetHWND(), view_->GetProfile(), node_->GetURL(), node_->GetTitle()); } else { // Controller deletes itself when done. EditFolderController* controller = new EditFolderController( view_, node_, -1, false); controller->Show(); } break; case kDeleteBookmarkID: { UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_Remove", profile); view_->model_->Remove(node_->GetParent(), node_->GetParent()->IndexOfChild(node_)); break; } case kAddBookmarkID: { UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_Add", profile); BookmarkEditorView::Show(view_->GetViewContainer()->GetHWND(), view_->GetProfile(), GURL(), std::wstring()); break; } case kNewFolderID: { UserMetrics::RecordAction(L"BookmarkBar_ContextMenu_NewFolder", profile); int visual_order; BookmarkBarNode* parent = GetParentAndVisualOrderForNewNode(&visual_order); GetParentAndVisualOrderForNewNode(&visual_order); // Controller deletes itself when done. EditFolderController* controller = new EditFolderController(view_, parent, visual_order, true); controller->Show(); break; } case kAlwaysShowCommandID: view_->ToggleWhenVisible(); break; default: NOTREACHED(); } } bool IsItemChecked(int id) const { DCHECK(id == kAlwaysShowCommandID); return view_->GetProfile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); } // Opens a tab/window for node and recursively opens all descendants. // If open_first_in_new_window is true, the first opened node is opened // in a new window. navigator indicates the PageNavigator to use for // new tabs. It is reset if open_first_in_new_window is true. // opened_url is set to true the first time a new tab is opened. void OpenAll(BookmarkBarNode* node, bool open_first_in_new_window, PageNavigator** navigator, bool* opened_url) { if (node->GetType() == history::StarredEntry::URL) { WindowOpenDisposition disposition; if (*opened_url) disposition = NEW_BACKGROUND_TAB; else if (open_first_in_new_window) disposition = NEW_WINDOW; else // Open in current window. disposition = CURRENT_TAB; (*navigator)->OpenURL(node->GetURL(), disposition, PageTransition::AUTO_BOOKMARK); if (!*opened_url) { *opened_url = true; if (open_first_in_new_window || disposition == CURRENT_TAB) { // We opened the tab in a new window or in the current tab which // likely reset the navigator. Need to reset the page navigator // appropriately. Browser* new_browser = BrowserList::GetLastActive(); TabContents* current_tab = new_browser->GetSelectedTabContents(); DCHECK(new_browser && current_tab); if (new_browser && current_tab) *navigator = current_tab; } } } else { // Group, recurse through children. for (int i = 0; i < node->GetChildCount(); ++i) { OpenAll(node->GetChild(i), open_first_in_new_window, navigator, opened_url); } } } virtual bool IsCommandEnabled(int id) const { if (id == kOpenAllBookmarksID || id == kOpenAllBookmarksInNewWindowID) return NodeHasURLs(node_); return true; } // Returns true if the specified node is of type URL, or has a descendant // of type URL. static bool NodeHasURLs(BookmarkBarNode* node) { if (node->GetType() == history::StarredEntry::URL) return true; for (int i = 0; i < node->GetChildCount(); ++i) { if (NodeHasURLs(node->GetChild(i))) return true; } return false; } // Returns the parent node and visual_order to use when adding new // bookmarks/folders. BookmarkBarNode* GetParentAndVisualOrderForNewNode(int* visual_order) { if (node_->GetType() != history::StarredEntry::URL) { // Adding to a group always adds to the end. *visual_order = node_->GetChildCount(); return node_; } else { DCHECK(node_->GetParent()); *visual_order = node_->GetParent()->IndexOfChild(node_) + 1; return node_->GetParent(); } } MenuItemView menu_; BookmarkBarView* view_; BookmarkBarNode* node_; DISALLOW_EVIL_CONSTRUCTORS(BookmarkNodeMenuController); }; // MenuRunner ----------------------------------------------------------------- // MenuRunner manages creation and showing of a menu containing BookmarkNodes. // MenuRunner is used to show the contents of bookmark folders on the // bookmark bar, other folder, or overflow bookmarks. // class MenuRunner : public ChromeViews::MenuDelegate, public ModelChangedListener { public: // start_child_index is the index of the first child in node to add to the // menu. MenuRunner(BookmarkBarView* view, BookmarkBarNode* node, int start_child_index) : view_(view), node_(node), menu_(this) { int next_menu_id = 1; menu_id_to_node_map_[menu_.GetCommand()] = node; BuildMenu(node, start_child_index, &menu_, &next_menu_id); } // Returns the node the menu is being run for. BookmarkBarNode* GetNode() { return node_; } void RunMenuAt(HWND hwnd, const gfx::Rect& bounds, MenuItemView::AnchorPosition position, bool for_drop) { view_->SetModelChangedListener(this); if (for_drop) menu_.RunMenuForDropAt(hwnd, bounds, position); else menu_.RunMenuAt(hwnd, bounds, position, false); view_->ClearModelChangedListenerIfEquals(this); } // Notification that the favicon has finished loading. Reset the icon // of the menu item. void FavIconLoaded(BookmarkBarNode* node) { if (node_to_menu_id_map_.find(node) != node_to_menu_id_map_.end()) { menu_.SetIcon(node->GetFavIcon(), node_to_menu_id_map_[node]); } } virtual void ModelChanged() { if (context_menu_.get()) context_menu_->ModelChanged(); menu_.Cancel(); } private: // Creates an entry in menu for each child node of parent starting at // start_child_index, recursively invoking this for any star groups. void BuildMenu(BookmarkBarNode* parent, int start_child_index, MenuItemView* menu, int* next_menu_id) { DCHECK(!parent->GetChildCount() || start_child_index < parent->GetChildCount()); for (int i = start_child_index; i < parent->GetChildCount(); ++i) { BookmarkBarNode* node = parent->GetChild(i); int id = *next_menu_id; (*next_menu_id)++; if (node->GetType() == history::StarredEntry::URL) { SkBitmap icon = node->GetFavIcon(); if (icon.width() == 0) icon = *kDefaultFavIcon; menu->AppendMenuItemWithIcon(id, node->GetTitle(), icon); node_to_menu_id_map_[node] = id; } else { SkBitmap* folder_icon = ResourceBundle::GetSharedInstance().GetBitmapNamed( IDR_BOOKMARK_BAR_FOLDER); MenuItemView* submenu = menu->AppendSubMenuWithIcon( id, node->GetTitle(), *folder_icon); BuildMenu(node, 0, submenu, next_menu_id); } menu_id_to_node_map_[id] = node; } } // ViewMenuDelegate method. Overridden to forward to the PageNavigator so // that we accept any events that may trigger opening a url. virtual bool IsTriggerableEvent(const ChromeViews::MouseEvent& e) { return event_utils::IsPossibleDispositionEvent(e); } // Invoked when a menu item is selected. Uses the PageNavigator set on // the BookmarkBarView to open the URL. virtual void ExecuteCommand(int id, int mouse_event_flags) { DCHECK(view_->GetPageNavigator()); GURL url; DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end()); url = menu_id_to_node_map_[id]->GetURL(); view_->GetPageNavigator()->OpenURL( url, event_utils::DispositionFromEventFlags(mouse_event_flags), PageTransition::AUTO_BOOKMARK); } virtual bool CanDrop(MenuItemView* menu, const OSExchangeData& data) { if (!drop_data_.Read(data)) return false; if (drop_data_.is_url) return true; if (drop_data_.profile_id != view_->GetProfile()->GetID()) { // Always accept drags of bookmark groups from other profiles. return true; } // Drag originated from same profile and is not a URL. Only accept it if // the dragged node is not a parent of the node menu represents. BookmarkBarNode* drop_node = menu_id_to_node_map_[menu->GetCommand()]; DCHECK(drop_node); BookmarkBarNode* drag_node = drop_data_.GetNode(view_->GetProfile()-> GetBookmarkBarModel()); if (!drag_node) { // Hmmm, can't find the dragged node. This is generally an error // condition and we won't try and do anything fancy. NOTREACHED(); return false; } BookmarkBarNode* node = drop_node; while (drop_node && drop_node != drag_node) drop_node = drop_node->GetParent(); return (drop_node == NULL); } virtual int GetDropOperation(MenuItemView* item, const ChromeViews::DropTargetEvent& event, DropPosition* position) { DCHECK(drop_data_.is_valid); BookmarkBarNode* node = menu_id_to_node_map_[item->GetCommand()]; BookmarkBarNode* drop_parent = node->GetParent(); int index_to_drop_at = drop_parent->IndexOfChild(node); if (*position == DROP_AFTER) { index_to_drop_at++; } else if (*position == DROP_ON) { drop_parent = node; index_to_drop_at = node->GetChildCount(); } DCHECK(drop_parent); return view_->CalculateDropOperation(drop_data_, drop_parent, index_to_drop_at); } virtual int OnPerformDrop(MenuItemView* menu, DropPosition position, const DropTargetEvent& event) { BookmarkBarNode* drop_node = menu_id_to_node_map_[menu->GetCommand()]; DCHECK(drop_node); BookmarkBarModel* model = view_->GetModel(); DCHECK(model); BookmarkBarNode* drop_parent = drop_node->GetParent(); DCHECK(drop_parent); int index_to_drop_at = drop_parent->IndexOfChild(drop_node); if (position == DROP_AFTER) { index_to_drop_at++; } else if (position == DROP_ON) { DCHECK(drop_node->GetType() != history::StarredEntry::URL); drop_parent = drop_node; index_to_drop_at = drop_node->GetChildCount(); } const int result = view_->PerformDropImpl(drop_data_, drop_parent, index_to_drop_at); if (view_->drop_menu_runner_.get() == this) view_->drop_menu_runner_.reset(); // WARNING: we've been deleted! return result; } virtual void ShowContextMenu(MenuItemView* source, int id, int x, int y, bool is_mouse_gesture) { DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end()); context_menu_.reset( new BookmarkNodeMenuController(view_, menu_id_to_node_map_[id])); context_menu_->RunMenuAt(x, y); context_menu_.reset(NULL); } virtual void DropMenuClosed(MenuItemView* menu) { if (view_->drop_menu_runner_.get() == this) view_->drop_menu_runner_.reset(); } virtual bool CanDrag(MenuItemView* menu) { DCHECK(menu); return true; } virtual void WriteDragData(MenuItemView* sender, OSExchangeData* data) { DCHECK(sender && data); UserMetrics::RecordAction(L"BookmarkBar_DragFromFolder", view_->GetProfile()); view_->WriteDragData(menu_id_to_node_map_[sender->GetCommand()], data); } virtual int GetDragOperations(MenuItemView* sender) { return GetDragOperationsForNode( menu_id_to_node_map_[sender->GetCommand()]); } // The node we're showing the contents of. BookmarkBarNode* node_; // The view that created us. BookmarkBarView* view_; // The menu. MenuItemView menu_; // Mapping from menu id to the BookmarkBarNode. std::map menu_id_to_node_map_; // Mapping from node to menu id. This only contains entries for nodes of type // URL. std::map node_to_menu_id_map_; // Data for the drop. BookmarkDragData drop_data_; scoped_ptr context_menu_; DISALLOW_EVIL_CONSTRUCTORS(MenuRunner); }; // ButtonSeparatorView -------------------------------------------------------- // TODO(sky/glen): this is temporary, need to decide on what this should // look like. class ButtonSeparatorView : public ChromeViews::View { public: ButtonSeparatorView() {} virtual ~ButtonSeparatorView() {} virtual void Paint(ChromeCanvas* canvas) { SkPaint paint; paint.setShader(gfx::CreateGradientShader(0, GetHeight() / 2, kTopBorderColor, kSeparatorColor))->safeUnref(); SkRect rc = {SkIntToScalar(kSeparatorStartX), SkIntToScalar(0), SkIntToScalar(1), SkIntToScalar(GetHeight() / 2) }; canvas->drawRect(rc, paint); SkPaint paint_down; paint_down.setShader(gfx::CreateGradientShader(GetHeight() / 2, GetHeight(), kSeparatorColor, kBackgroundColor))->safeUnref(); SkRect rc_down = { SkIntToScalar(kSeparatorStartX), SkIntToScalar(GetHeight() / 2), SkIntToScalar(1), SkIntToScalar(GetHeight() - 1) }; canvas->drawRect(rc_down, paint_down); } virtual void GetPreferredSize(CSize* out) { // We get the full height of the bookmark bar, so that the height returned // here doesn't matter. out->SetSize(kSeparatorWidth, 1); } private: DISALLOW_EVIL_CONSTRUCTORS(ButtonSeparatorView); }; } // namespace // BookmarkBarView ------------------------------------------------------------ // static const int BookmarkBarView::kMaxButtonWidth = 150; // Returns the bitmap to use for starred groups. static const SkBitmap& GetGroupIcon() { if (!kFolderIcon) { kFolderIcon = ResourceBundle::GetSharedInstance(). GetBitmapNamed(IDR_BOOKMARK_BAR_FOLDER); } return *kFolderIcon; } // static void BookmarkBarView::RegisterUserPrefs(PrefService* prefs) { prefs->RegisterBooleanPref(prefs::kShowBookmarkBar, false); } BookmarkBarView::BookmarkBarView(Profile* profile, Browser* browser) : profile_(NULL), browser_(browser), page_navigator_(NULL), model_(NULL), other_bookmarked_button_(NULL), model_changed_listener_(NULL), show_folder_drop_menu_task_(NULL), overflow_button_(NULL), instructions_(NULL), bookmarks_separator_view_(NULL), throbbing_view_(NULL) { SetID(VIEW_ID_BOOKMARK_BAR); Init(); SetProfile(profile); if (IsAlwaysShown()) { size_animation_->Reset(1); } else { size_animation_->Reset(0); } } BookmarkBarView::~BookmarkBarView() { NotifyModelChanged(); RemoveNotificationObservers(); if (model_) model_->RemoveObserver(this); StopShowFolderDropMenuTimer(); } void BookmarkBarView::SetProfile(Profile* profile) { DCHECK(profile); if (profile_ == profile) return; StopThrobbing(true); // Cancels the current cancelable. NotifyModelChanged(); // Remove the current buttons. for (int i = GetBookmarkButtonCount() - 1; i >= 0; --i) { View* child = GetChildViewAt(i); RemoveChildView(child); delete child; } profile_ = profile; if (model_) model_->RemoveObserver(this); // Disable the other bookmarked button, we'll re-enable when the model is // loaded. other_bookmarked_button_->SetEnabled(false); NotificationService* ns = NotificationService::current(); Source ns_source(profile_->GetOriginalProfile()); ns->AddObserver(this, NOTIFY_HISTORY_CREATED, ns_source); ns->AddObserver(this, NOTIFY_BOOKMARK_BUBBLE_SHOWN, ns_source); ns->AddObserver(this, NOTIFY_BOOKMARK_BUBBLE_HIDDEN, ns_source); ns->AddObserver(this, NOTIFY_BOOKMARK_BAR_VISIBILITY_PREF_CHANGED, NotificationService::AllSources()); if (!profile->HasHistoryService()) { // The history service hasn't been loaded yet. We don't want to trigger // loading it. Instead we install an observer that is notified when the // history service has loaded. model_ = NULL; } else { ProfileHasValidHistoryService(); } } void BookmarkBarView::SetPageNavigator(PageNavigator* navigator) { page_navigator_ = navigator; } void BookmarkBarView::GetPreferredSize(CSize *out) { if (!prefButtonHeight) { ChromeViews::TextButton text_button(L"X"); CSize text_button_pref; text_button.GetMinimumSize(&text_button_pref); prefButtonHeight = static_cast(text_button_pref.cy); } if (IsNewTabPage()) { out->cy = kBarHeight + static_cast(static_cast (kNewtabBarHeight - kBarHeight) * (1 - size_animation_->GetCurrentValue())); } else { out->cy = std::max(static_cast(static_cast(kBarHeight) * size_animation_->GetCurrentValue()), 1); } // Width doesn't matter, we're always given a width based on the browser size. out->cx = 1; } void BookmarkBarView::Layout() { if (!GetParent()) return; // First layout out the buttons. Any buttons that are placed beyond the // visible region and made invisible. int x = kLeftMargin; int y = kTopMargin; int width = GetWidth() - kRightMargin - kLeftMargin; int height = GetHeight() - kTopMargin - kBottomMargin; int separator_margin = kSeparatorMargin; if (IsNewTabPage()) { double current_state = 1 - size_animation_->GetCurrentValue(); x += static_cast(static_cast (kNewtabHorizontalPadding) * current_state); y += static_cast(static_cast (kNewtabVerticalPadding) * current_state); width -= static_cast(static_cast (kNewtabHorizontalPadding) * current_state); height -= static_cast(static_cast (kNewtabVerticalPadding * 2) * current_state); separator_margin -= static_cast(static_cast (kSeparatorMargin) * current_state); } CSize other_bookmarked_pref; other_bookmarked_button_->GetPreferredSize(&other_bookmarked_pref); CSize overflow_pref; overflow_button_->GetPreferredSize(&overflow_pref); CSize bookmarks_separator_pref; bookmarks_separator_view_->GetPreferredSize(&bookmarks_separator_pref); const int max_x = width - other_bookmarked_pref.cx - kButtonPadding - overflow_pref.cx - kButtonPadding - bookmarks_separator_pref.cx; if (GetBookmarkButtonCount() == 0 && model_ && model_->IsLoaded()) { CSize pref; instructions_->GetPreferredSize(&pref); instructions_->SetBounds(x + kInstructionsPadding, y, std::min(static_cast(pref.cx), max_x - x), height); instructions_->SetVisible(true); } else { instructions_->SetVisible(false); for (int i = 0; i < GetBookmarkButtonCount(); ++i) { ChromeViews::View* child = GetChildViewAt(i); CSize pref; child->GetPreferredSize(&pref); int next_x = x + pref.cx + kButtonPadding; child->SetVisible(next_x < max_x); child->SetBounds(x, y, pref.cx, height); x = next_x; } } // Layout the right side of the bar. const bool all_visible = (GetBookmarkButtonCount() == 0 || GetChildViewAt(GetBookmarkButtonCount() - 1)->IsVisible()); // Layout the recently bookmarked button. x = max_x + kButtonPadding; // The overflow button. overflow_button_->SetBounds(x, y, overflow_pref.cx, height); overflow_button_->SetVisible(!all_visible); x += overflow_pref.cx; // Separator. bookmarks_separator_view_->SetBounds(x, y - kTopMargin, bookmarks_separator_pref.cx, height + kTopMargin + kBottomMargin - separator_margin); x += bookmarks_separator_pref.cx; other_bookmarked_button_->SetBounds(x, y, other_bookmarked_pref.cx, height); x += other_bookmarked_pref.cx + kButtonPadding; } void BookmarkBarView::DidChangeBounds(const CRect& previous, const CRect& current) { Layout(); } void BookmarkBarView::ViewHierarchyChanged(bool is_add, View* parent, View* child) { if (is_add && child == this && GetHeight() > 0) { // We only layout while parented. When we become parented, if our bounds // haven't changed, DidChangeBounds won't get invoked and we won't layout. // Therefore we always force a layout when added. Layout(); } } void BookmarkBarView::Paint(ChromeCanvas* canvas) { int width = GetWidth(); int height = GetHeight(); if (IsNewTabPage() && (!IsAlwaysShown() || size_animation_->IsAnimating())) { // Draw the background to match the new tab page. canvas->FillRectInt(kNewtabBackgroundColor, 0, 0, width, height); // Draw the 'bottom' of the toolbar above our bubble. canvas->FillRectInt(kBottomBorderColor, 0, 0, width, 1); SkRect rect; // As 'hidden' according to the animation is the full in-tab state, // we invert the value - when current_state is at '0', we expect the // bar to be docked. double current_state = 1 - size_animation_->GetCurrentValue(); // The 0.5 is to correct for Skia's "draw on pixel boundaries"ness. double h_padding = static_cast (kNewtabHorizontalPadding) * current_state; double v_padding = static_cast (kNewtabVerticalPadding) * current_state; rect.set(SkDoubleToScalar(h_padding - 0.5), SkDoubleToScalar(v_padding - 0.5), SkDoubleToScalar(width - h_padding - 0.5), SkDoubleToScalar(height - v_padding - 0.5)); double roundness = static_cast (kNewtabBarRoundness) * current_state; // Draw our background. SkPaint paint; paint.setAntiAlias(true); paint.setShader(gfx::CreateGradientShader(0, height, kTopBorderColor, kBackgroundColor))->safeUnref(); canvas->drawRoundRect(rect, SkDoubleToScalar(roundness), SkDoubleToScalar(roundness), paint); // Draw border SkPaint border_paint; border_paint.setColor(kNewtabBorderColor); border_paint.setStyle(SkPaint::kStroke_Style); border_paint.setAntiAlias(true); canvas->drawRoundRect(rect, SkDoubleToScalar(roundness), SkDoubleToScalar(roundness), border_paint); } else { SkPaint paint; paint.setShader(gfx::CreateGradientShader(0, height, kTopBorderColor, kBackgroundColor))->safeUnref(); canvas->FillRectInt(0, 0, width, height, paint); canvas->FillRectInt(kTopBorderColor, 0, 0, width, 1); canvas->FillRectInt(kBottomBorderColor, 0, height - 1, width, 1); } } void BookmarkBarView::PaintChildren(ChromeCanvas* canvas) { View::PaintChildren(canvas); if (drop_info_.get() && drop_info_->valid && drop_info_->drag_operation != 0 && drop_info_->drop_index != -1 && !drop_info_->is_over_overflow && !drop_info_->drop_on) { int index = drop_info_->drop_index; DCHECK(index <= GetBookmarkButtonCount()); int x = 0; int y = 0; int h = GetHeight(); if (index == GetBookmarkButtonCount()) { if (index == 0) { x = kLeftMargin; } else { x = GetBookmarkButton(index - 1)->GetX() + GetBookmarkButton(index - 1)->GetWidth(); } } else { x = GetBookmarkButton(index)->GetX(); } if (GetBookmarkButtonCount() > 0 && GetBookmarkButton(0)->IsVisible()) { y = GetBookmarkButton(0)->GetY(); h = GetBookmarkButton(0)->GetHeight(); } // Since the drop indicator is painted directly onto the canvas, we must // make sure it is painted in the right location if the locale is RTL. gfx::Rect indicator_bounds(x - kDropIndicatorWidth / 2, y, kDropIndicatorWidth, h); indicator_bounds.set_x(MirroredLeftPointForRect(indicator_bounds)); // TODO(sky/glen): make me pretty! canvas->FillRectInt(kDropIndicatorColor, indicator_bounds.x(), indicator_bounds.y(), indicator_bounds.width(), indicator_bounds.height()); } } bool BookmarkBarView::CanDrop(const OSExchangeData& data) { if (!model_ || !model_->IsLoaded()) return false; if (!drop_info_.get()) drop_info_.reset(new DropInfo()); return drop_info_->data.Read(data); } void BookmarkBarView::OnDragEntered(const DropTargetEvent& event) { } int BookmarkBarView::OnDragUpdated(const DropTargetEvent& event) { if (!drop_info_.get()) return 0; if (drop_info_->valid && (drop_info_->x == event.GetX() && drop_info_->y == event.GetY())) { return drop_info_->drag_operation; } drop_info_->x = event.GetX(); drop_info_->y = event.GetY(); int drop_index; bool drop_on; bool is_over_overflow; bool is_over_other; drop_info_->drag_operation = CalculateDropOperation( event, drop_info_->data, &drop_index, &drop_on, &is_over_overflow, &is_over_other); if (drop_info_->valid && drop_info_->drop_index == drop_index && drop_info_->drop_on == drop_on && drop_info_->is_over_overflow == is_over_overflow && drop_info_->is_over_other == is_over_other) { return drop_info_->drag_operation; } drop_info_->valid = true; StopShowFolderDropMenuTimer(); // TODO(sky): Optimize paint region. SchedulePaint(); drop_info_->drop_index = drop_index; drop_info_->drop_on = drop_on; drop_info_->is_over_overflow = is_over_overflow; drop_info_->is_over_other = is_over_other; if (drop_info_->is_menu_showing) { drop_menu_runner_.reset(); drop_info_->is_menu_showing = false; } if (drop_on || is_over_overflow || is_over_other) { BookmarkBarNode* node; if (is_over_other) node = model_->other_node(); else if (is_over_overflow) node = model_->GetBookmarkBarNode(); else node = model_->GetBookmarkBarNode()->GetChild(drop_index); StartShowFolderDropMenuTimer(node); } return drop_info_->drag_operation; } void BookmarkBarView::OnDragExited() { StopShowFolderDropMenuTimer(); // NOTE: we don't hide the menu on exit as it's possible the user moved the // mouse over the menu, which triggers an exit on us. drop_info_->valid = false; if (drop_info_->drop_index != -1) { // TODO(sky): optimize the paint region. SchedulePaint(); } drop_info_.reset(); } int BookmarkBarView::OnPerformDrop(const DropTargetEvent& event) { StopShowFolderDropMenuTimer(); drop_menu_runner_.reset(); if (!drop_info_.get() || !drop_info_->drag_operation) return DragDropTypes::DRAG_NONE; BookmarkBarNode* root = drop_info_->is_over_other ? model_->other_node() : model_->GetBookmarkBarNode(); int index = drop_info_->drop_index; const bool drop_on = drop_info_->drop_on; const BookmarkDragData data = drop_info_->data; const bool is_over_other = drop_info_->is_over_other; DCHECK(data.is_valid); if (drop_info_->drop_index != -1) { // TODO(sky): optimize the SchedulePaint region. SchedulePaint(); } drop_info_.reset(); BookmarkBarNode* parent_node; if (is_over_other) { parent_node = root; index = parent_node->GetChildCount(); } else if (drop_on) { parent_node = root->GetChild(index); index = parent_node->GetChildCount(); } else { parent_node = root; } return PerformDropImpl(data, parent_node, index); } void BookmarkBarView::ToggleWhenVisible() { PrefService* prefs = profile_->GetPrefs(); const bool always_show = !prefs->GetBoolean(prefs::kShowBookmarkBar); // The user changed when the bookmark bar is shown, update the preferences. prefs->SetBoolean(prefs::kShowBookmarkBar, always_show); prefs->ScheduleSavePersistentPrefs(g_browser_process->file_thread()); // And notify the notification service. Source source(profile_); NotificationService::current()->Notify( NOTIFY_BOOKMARK_BAR_VISIBILITY_PREF_CHANGED, source, NotificationService::NoDetails()); // May need to redraw the bar with a new style. SchedulePaint(); } bool BookmarkBarView::IsAlwaysShown() { return profile_->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); } bool BookmarkBarView::IsNewTabPage() { DCHECK(browser_); if (browser_->GetSelectedTabContents()) { return browser_->GetSelectedTabContents()->IsBookmarkBarAlwaysVisible(); } return false; } void BookmarkBarView::AnimationProgressed(const Animation* animation) { browser_->ToolbarSizeChanged(NULL, true); } void BookmarkBarView::AnimationEnded(const Animation* animation) { browser_->ToolbarSizeChanged(NULL, false); SchedulePaint(); } void BookmarkBarView::Init() { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); if (!calcedSize) calcedSize = false; if (!kDefaultFavIcon) kDefaultFavIcon = rb.GetBitmapNamed(IDR_DEFAULT_FAVICON); other_bookmarked_button_ = CreateOtherBookmarkedButton(); AddChildView(other_bookmarked_button_); overflow_button_ = CreateOverflowButton(); AddChildView(overflow_button_); bookmarks_separator_view_ = new ButtonSeparatorView(); AddChildView(bookmarks_separator_view_); instructions_ = new ChromeViews::Label( l10n_util::GetString(IDS_BOOKMARKS_NO_ITEMS), rb.GetFont(ResourceBundle::BaseFont)); instructions_->SetColor(kInstructionsColor); AddChildView(instructions_); SetContextMenuController(this); size_animation_.reset(new SlideAnimation(this)); } MenuButton* BookmarkBarView::CreateOtherBookmarkedButton() { MenuButton* button = new MenuButton( l10n_util::GetString(IDS_BOOMARK_BAR_OTHER_BOOKMARKED), this, false); button->SetIcon(GetGroupIcon()); button->SetContextMenuController(this); return button; } MenuButton* BookmarkBarView::CreateOverflowButton() { MenuButton* button = new MenuButton(std::wstring(), this, false); button->SetIcon(*ResourceBundle::GetSharedInstance(). GetBitmapNamed(IDR_BOOKMARK_BAR_CHEVRONS)); // The overflow button's image contains an arrow and therefore it is a // direction sensitive image and we need to flip it if the UI layout is // right-to-left. // // By default, menu buttons are not flipped because they generally contain // text and flipping the ChromeCanvas object will break text rendering. Since // the overflow button does not contain text, we can safely flip it. button->EnableCanvasFlippingForRTLUI(true); // Make visible as necessary. button->SetVisible(false); return button; } int BookmarkBarView::GetBookmarkButtonCount() { // We contain four non-bookmark button views: recently bookmarked, // bookmarks separator, chevrons (for overflow) and the instruction // label. return GetChildViewCount() - 4; } ChromeViews::TextButton* BookmarkBarView::GetBookmarkButton(int index) { DCHECK(index >= 0 && index < GetBookmarkButtonCount()); return static_cast(GetChildViewAt(index)); } void BookmarkBarView::Loaded(BookmarkBarModel* model) { BookmarkBarNode* node = model_->GetBookmarkBarNode(); DCHECK(node && model_->other_node()); // Create a button for each of the children on the bookmark bar. for (int i = 0; i < node->GetChildCount(); ++i) AddChildView(i, CreateBookmarkButton(node->GetChild(i))); other_bookmarked_button_->SetEnabled(true); Layout(); SchedulePaint(); } void BookmarkBarView::BookmarkNodeMoved(BookmarkBarModel* model, BookmarkBarNode* old_parent, int old_index, BookmarkBarNode* new_parent, int new_index) { StopThrobbing(true); BookmarkNodeRemovedImpl(model, old_parent, old_index); BookmarkNodeAddedImpl(model, new_parent, new_index); StartThrobbing(); } void BookmarkBarView::BookmarkNodeAdded(BookmarkBarModel* model, BookmarkBarNode* parent, int index) { StopThrobbing(true); BookmarkNodeAddedImpl(model, parent, index); StartThrobbing(); } void BookmarkBarView::BookmarkNodeAddedImpl(BookmarkBarModel* model, BookmarkBarNode* parent, int index) { NotifyModelChanged(); if (parent != model_->GetBookmarkBarNode()) { // We only care about nodes on the bookmark bar. return; } DCHECK(index >= 0 && index <= GetBookmarkButtonCount()); AddChildView(index, CreateBookmarkButton(parent->GetChild(index))); Layout(); SchedulePaint(); } void BookmarkBarView::BookmarkNodeRemoved(BookmarkBarModel* model, BookmarkBarNode* parent, int index) { StopThrobbing(true); BookmarkNodeRemovedImpl(model, parent, index); StartThrobbing(); } void BookmarkBarView::BookmarkNodeRemovedImpl(BookmarkBarModel* model, BookmarkBarNode* parent, int index) { NotifyModelChanged(); if (parent != model_->GetBookmarkBarNode()) { // We only care about nodes on the bookmark bar. return; } DCHECK(index >= 0 && index < GetBookmarkButtonCount()); ChromeViews::View* button = GetChildViewAt(index); RemoveChildView(button); MessageLoop::current()->DeleteSoon(FROM_HERE, button); Layout(); SchedulePaint(); } void BookmarkBarView::BookmarkNodeChanged(BookmarkBarModel* model, BookmarkBarNode* node) { NotifyModelChanged(); BookmarkNodeChangedImpl(model, node); } void BookmarkBarView::BookmarkNodeChangedImpl(BookmarkBarModel* model, BookmarkBarNode* node) { if (node->GetParent() != model_->GetBookmarkBarNode()) { // We only care about nodes on the bookmark bar. return; } int index = model_->GetBookmarkBarNode()->IndexOfChild(node); DCHECK(index != -1); ChromeViews::TextButton* button = GetBookmarkButton(index); CSize old_pref; button->GetPreferredSize(&old_pref); ConfigureButton(node, button); CSize new_pref; button->GetPreferredSize(&new_pref); if (old_pref.cx != new_pref.cx) { Layout(); SchedulePaint(); } else if (button->IsVisible()) { button->SchedulePaint(); } } void BookmarkBarView::BookmarkNodeFavIconLoaded(BookmarkBarModel* model, BookmarkBarNode* node) { if (menu_runner_.get()) menu_runner_->FavIconLoaded(node); if (drop_menu_runner_.get()) drop_menu_runner_->FavIconLoaded(node); BookmarkNodeChangedImpl(model, node); } void BookmarkBarView::WriteDragData(View* sender, int press_x, int press_y, OSExchangeData* data) { UserMetrics::RecordAction(L"BookmarkBar_DragButton", profile_); for (int i = 0; i < GetBookmarkButtonCount(); ++i) { if (sender == GetBookmarkButton(i)) { ChromeViews::TextButton* button = GetBookmarkButton(i); ChromeCanvas canvas(button->GetWidth(), button->GetHeight(), false); button->Paint(&canvas, true); drag_utils::SetDragImageOnDataObject(canvas, button->GetWidth(), button->GetHeight(), press_x, press_y, data); WriteDragData(model_->GetBookmarkBarNode()->GetChild(i), data); return; } } NOTREACHED(); } void BookmarkBarView::WriteDragData(BookmarkBarNode* node, OSExchangeData* data) { DCHECK(node && data); BookmarkDragData drag_data(node); drag_data.profile_id = GetProfile()->GetID(); drag_data.Write(data); } int BookmarkBarView::GetDragOperations(View* sender, int x, int y) { for (int i = 0; i < GetBookmarkButtonCount(); ++i) { if (sender == GetBookmarkButton(i)) { return GetDragOperationsForNode( model_->GetBookmarkBarNode()->GetChild(i)); } } NOTREACHED(); return DragDropTypes::DRAG_NONE; } void BookmarkBarView::RunMenu(ChromeViews::View* view, const CPoint& pt, HWND hwnd) { BookmarkBarNode* node; MenuItemView::AnchorPosition anchor_point = MenuItemView::TOPLEFT; // When we set the menu's position, we must take into account the mirrored // position of the View relative to its parent. This can be easily done by // passing the right flag to View::GetX(). int x = view->GetX(APPLY_MIRRORING_TRANSFORMATION); int height = GetHeight() - kMenuOffset; if (IsNewTabPage() && !IsAlwaysShown()) height -= kNewtabVerticalPadding; int start_index = 0; if (view == other_bookmarked_button_) { UserMetrics::RecordAction(L"BookmarkBar_ShowOtherBookmarks", profile_); node = model_->other_node(); if (UILayoutIsRightToLeft()) anchor_point = MenuItemView::TOPLEFT; else anchor_point = MenuItemView::TOPRIGHT; } else if (view == overflow_button_) { node = model_->GetBookmarkBarNode(); start_index = GetFirstHiddenNodeIndex(); if (UILayoutIsRightToLeft()) anchor_point = MenuItemView::TOPLEFT; else anchor_point = MenuItemView::TOPRIGHT; } else { int button_index = GetChildIndex(view); DCHECK(button_index != -1); node = model_->GetBookmarkBarNode()->GetChild(button_index); // When the UI layout is RTL, the bookmarks are laid out from right to left // and therefore when we display the menu we want it to be aligned with the // bottom right corner of the bookmark item. if (UILayoutIsRightToLeft()) anchor_point = MenuItemView::TOPRIGHT; else anchor_point = MenuItemView::TOPLEFT; } CPoint screen_loc(x, 0); View::ConvertPointToScreen(this, &screen_loc); menu_runner_.reset(new MenuRunner(this, node, start_index)); HWND parent_hwnd = reinterpret_cast( browser_->frame()->GetPlatformID()); menu_runner_->RunMenuAt(parent_hwnd, gfx::Rect(screen_loc.x, screen_loc.y, view->GetWidth(), height), anchor_point, false); } void BookmarkBarView::ButtonPressed(ChromeViews::BaseButton* sender) { int index = GetChildIndex(sender); DCHECK(index != -1); BookmarkBarNode* node = model_->GetBookmarkBarNode()->GetChild(index); DCHECK(page_navigator_); page_navigator_->OpenURL( node->GetURL(), event_utils::DispositionFromEventFlags(sender->mouse_event_flags()), PageTransition::AUTO_BOOKMARK); UserMetrics::RecordAction(L"ClickedBookmarkBarURLButton", profile_); } void BookmarkBarView::ShowContextMenu(View* source, int x, int y, bool is_mouse_gesture) { if (!model_->IsLoaded()) { // Don't do anything if the model isn't loaded. return; } BookmarkBarNode* node = model_->GetBookmarkBarNode(); if (source == other_bookmarked_button_) { node = model_->other_node(); } else if (source != this) { // User clicked on one of the bookmark buttons, find which one they // clicked on. int bookmark_button_index = GetChildIndex(source); DCHECK(bookmark_button_index != -1 && bookmark_button_index < GetBookmarkButtonCount()); node = model_->GetBookmarkBarNode()->GetChild(bookmark_button_index); } BookmarkNodeMenuController controller(this, node); controller.RunMenuAt(x, y); } ChromeViews::View* BookmarkBarView::CreateBookmarkButton( BookmarkBarNode* node) { if (node->GetType() == history::StarredEntry::URL) { BookmarkButton* button = new BookmarkButton(node->GetURL(), node->GetTitle(), GetProfile()); button->SetListener(this, 0); ConfigureButton(node, button); return button; } else { ChromeViews::MenuButton* button = new ChromeViews::MenuButton(node->GetTitle(), this, false); button->SetIcon(GetGroupIcon()); ConfigureButton(node, button); return button; } } void BookmarkBarView::ConfigureButton(BookmarkBarNode* node, ChromeViews::TextButton* button) { button->SetText(node->GetTitle()); button->ClearMaxTextSize(); button->SetContextMenuController(this); button->SetDragController(this); if (node->GetType() == history::StarredEntry::URL) { if (node->GetFavIcon().width() != 0) button->SetIcon(node->GetFavIcon()); else button->SetIcon(*kDefaultFavIcon); } button->set_max_width(kMaxButtonWidth); } bool BookmarkBarView::IsItemChecked(int id) const { DCHECK(id == kAlwaysShowCommandID); return profile_->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); } void BookmarkBarView::ExecuteCommand(int id) { ToggleWhenVisible(); } void BookmarkBarView::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { DCHECK(profile_); if (type == NOTIFY_BOOKMARK_BAR_VISIBILITY_PREF_CHANGED) { if (IsAlwaysShown()) { size_animation_->Show(); } else { size_animation_->Hide(); } } else if (type == NOTIFY_HISTORY_CREATED) { ProfileHasValidHistoryService(); } else if (type == NOTIFY_BOOKMARK_BUBBLE_SHOWN) { StopThrobbing(true); bubble_url_ = *(Details(details).ptr()); StartThrobbing(); } else if (type == NOTIFY_BOOKMARK_BUBBLE_HIDDEN) { StopThrobbing(false); bubble_url_ = GURL(); } } void BookmarkBarView::ProfileHasValidHistoryService() { DCHECK(profile_); model_ = profile_->GetBookmarkBarModel(); DCHECK(model_); model_->AddObserver(this); if (model_->IsLoaded()) Loaded(model_); } void BookmarkBarView::RemoveNotificationObservers() { NotificationService* ns = NotificationService::current(); Source ns_source(profile_->GetOriginalProfile()); ns->RemoveObserver(this, NOTIFY_HISTORY_CREATED, ns_source); ns->RemoveObserver(this, NOTIFY_BOOKMARK_BUBBLE_SHOWN, ns_source); ns->RemoveObserver(this, NOTIFY_BOOKMARK_BUBBLE_HIDDEN, ns_source); ns->RemoveObserver(this, NOTIFY_BOOKMARK_BAR_VISIBILITY_PREF_CHANGED, NotificationService::AllSources()); } void BookmarkBarView::NotifyModelChanged() { if (model_changed_listener_) model_changed_listener_->ModelChanged(); } void BookmarkBarView::ShowDropFolderForNode(BookmarkBarNode* node) { if (drop_menu_runner_.get() && drop_menu_runner_->GetNode() == node) { // Already showing for the specified node. return; } int start_index = 0; View* view_to_position_menu_from; // Note that both the anchor position and the position of the menu itself // change depending on the locale. Also note that we must apply the // mirroring transformation when querying for the child View bounds // (View::GetX(), specifically) so that we end up with the correct screen // coordinates if the View in question is mirrored. MenuItemView::AnchorPosition anchor = MenuItemView::TOPLEFT; if (node == model_->other_node()) { view_to_position_menu_from = other_bookmarked_button_; if (!UILayoutIsRightToLeft()) anchor = MenuItemView::TOPRIGHT; } else if (node == model_->GetBookmarkBarNode()) { DCHECK(overflow_button_->IsVisible()); view_to_position_menu_from = overflow_button_; start_index = GetFirstHiddenNodeIndex(); if (!UILayoutIsRightToLeft()) anchor = MenuItemView::TOPRIGHT; } else { // Make sure node is still valid. int index = -1; for (int i = 0; i < GetBookmarkButtonCount(); ++i) { if (model_->GetBookmarkBarNode()->GetChild(i) == node) { index = i; break; } } if (index == -1) return; view_to_position_menu_from = GetBookmarkButton(index); if (UILayoutIsRightToLeft()) anchor = MenuItemView::TOPRIGHT; } drop_info_->is_menu_showing = true; drop_menu_runner_.reset(new MenuRunner(this, node, start_index)); CPoint screen_loc(0, 0); View::ConvertPointToScreen(view_to_position_menu_from, &screen_loc); drop_menu_runner_->RunMenuAt( GetViewContainer()->GetHWND(), gfx::Rect(screen_loc.x, screen_loc.y, view_to_position_menu_from->GetWidth(), view_to_position_menu_from->GetHeight()), anchor, true); } void BookmarkBarView::StopShowFolderDropMenuTimer() { if (show_folder_drop_menu_task_) show_folder_drop_menu_task_->Cancel(); } void BookmarkBarView::StartShowFolderDropMenuTimer(BookmarkBarNode* node) { DCHECK(!show_folder_drop_menu_task_); show_folder_drop_menu_task_ = new ShowFolderDropMenuTask(this, node); static DWORD delay = 0; if (!delay && !SystemParametersInfo(SPI_GETMENUSHOWDELAY, 0, &delay, 0)) { delay = kShowFolderDropMenuDelay; } MessageLoop::current()->PostDelayedTask(FROM_HERE, show_folder_drop_menu_task_, delay); } int BookmarkBarView::CalculateDropOperation(const DropTargetEvent& event, const BookmarkDragData& data, int* index, bool* drop_on, bool* is_over_overflow, bool* is_over_other) { DCHECK(model_); DCHECK(model_->IsLoaded()); DCHECK(data.is_valid); // The drop event uses the screen coordinates while the child Views are // always laid out from left to right (even though they are rendered from // right-to-left on RTL locales). Thus, in order to make sure the drop // coordinates calculation works, we mirror the event's X coordinate if the // locale is RTL. int mirrored_x = MirroredXCoordinateInsideView(event.GetX()); *index = -1; *drop_on = false; *is_over_other = *is_over_overflow = false; if (event.GetY() < other_bookmarked_button_->GetY() || event.GetY() >= other_bookmarked_button_->GetY() + other_bookmarked_button_->GetHeight()) { // Mouse isn't over a button. return DragDropTypes::DRAG_NONE; } bool found = false; const int other_delta_x = mirrored_x - other_bookmarked_button_->GetX(); if (other_delta_x >= 0 && other_delta_x < other_bookmarked_button_->GetWidth()) { // Mouse is over 'other' folder. *is_over_other = true; *drop_on = true; found = true; } else if (!GetBookmarkButtonCount()) { // No bookmarks, accept the drop. *index = 0; return DragDropTypes::DRAG_COPY; } for (int i = 0; i < GetBookmarkButtonCount() && GetBookmarkButton(i)->IsVisible() && !found; i++) { ChromeViews::TextButton* button = GetBookmarkButton(i); int button_x = mirrored_x - button->GetX(); int button_w = button->GetWidth(); if (button_x < button_w) { found = true; BookmarkBarNode* node = model_->GetBookmarkBarNode()->GetChild(i); if (node->GetType() != history::StarredEntry::URL) { if (button_x <= MenuItemView::kDropBetweenPixels) { *index = i; } else if (button_x < button_w - MenuItemView::kDropBetweenPixels) { *index = i; *drop_on = true; } else { *index = i + 1; } } else if (button_x < button_w / 2) { *index = i; } else { *index = i + 1; } break; } } if (!found) { if (overflow_button_->IsVisible()) { // Are we over the overflow button? int overflow_delta_x = mirrored_x - overflow_button_->GetX(); if (overflow_delta_x >= 0 && overflow_delta_x < overflow_button_->GetWidth()) { // Mouse is over overflow button. *index = GetFirstHiddenNodeIndex(); *is_over_overflow = true; } else if (overflow_delta_x < 0) { // Mouse is after the last visible button but before overflow button; // use the last visible index. *index = GetFirstHiddenNodeIndex(); } else { return DragDropTypes::DRAG_NONE; } } else if (mirrored_x < other_bookmarked_button_->GetX()) { // Mouse is after the last visible button but before more recently // bookmarked; use the last visible index. *index = GetFirstHiddenNodeIndex(); } else { return DragDropTypes::DRAG_NONE; } } if (*drop_on) { BookmarkBarNode* parent = *is_over_other ? model_->other_node() : model_->GetBookmarkBarNode()->GetChild(*index); int operation = CalculateDropOperation(data, parent, parent->GetChildCount()); if (!operation && !data.is_url && data.profile_id == GetProfile()->GetID()) { if (data.GetNode(model_) == parent) { // Don't open a menu if the node being dragged is the the menu to // open. *drop_on = false; } } return operation; } else { return CalculateDropOperation(data, model_->GetBookmarkBarNode(), *index); } } int BookmarkBarView::CalculateDropOperation(const BookmarkDragData& data, BookmarkBarNode* parent, int index) { if (!CanDropAt(data, parent, index)) return DragDropTypes::DRAG_NONE; if (data.is_url) { // User is dragging a URL. BookmarkBarNode* node = model_->GetNodeByURL(data.url); if (!node) { // We don't have a node with this url. return DragDropTypes::DRAG_COPY; } // Technically we're going to move, but most sources export as copy so that // if we don't accept copy we won't accept the drop. return DragDropTypes::DRAG_MOVE | DragDropTypes::DRAG_COPY; } else if (data.profile_id == GetProfile()->GetID()) { // Dropping a group from the same profile results in a move. BookmarkBarNode* node = data.GetNode(model_); if (!node) { // Generally shouldn't get here, we originated the drag but couldn't // find the node. return DragDropTypes::DRAG_NONE; } return DragDropTypes::DRAG_MOVE; } else { // Dropping a group from different profile. Always accept. return DragDropTypes::DRAG_COPY; } } bool BookmarkBarView::CanDropAt(const BookmarkDragData& data, BookmarkBarNode* parent, int index) { DCHECK(data.is_valid); if (data.is_url) { BookmarkBarNode* existing_node = model_->GetNodeByURL(data.url); if (existing_node && existing_node->GetParent() == parent) { const int existing_index = parent->IndexOfChild(existing_node); if (index == existing_index || existing_index + 1 == index) return false; } return true; } else if (data.profile_id == profile_->GetID()) { BookmarkBarNode* existing_node = data.GetNode(model_); if (existing_node) { if (existing_node->GetParent() == parent) { const int existing_index = parent->IndexOfChild(existing_node); if (index == existing_index || existing_index + 1 == index) return false; } // Allow the drop only if the node we're going to drop on isn't a // descendant of the dragged node. BookmarkBarNode* test_node = parent; while (test_node && test_node != existing_node) test_node = test_node->GetParent(); return (test_node == NULL); } } // else case clones, always allow. return true; } int BookmarkBarView::PerformDropImpl(const BookmarkDragData& data, BookmarkBarNode* parent_node, int index) { if (data.is_url) { // User is dragging a URL. BookmarkBarNode* node = model_->GetNodeByURL(data.url); if (!node) { std::wstring title = data.title; if (title.empty()) { // No title, use the host. title = UTF8ToWide(data.url.host()); if (title.empty()) title = l10n_util::GetString(IDS_BOOMARK_BAR_UNKNOWN_DRAG_TITLE); } model_->AddURL(parent_node, index, title, data.url); return DragDropTypes::DRAG_COPY; } model_->Move(node, parent_node, index); return DragDropTypes::DRAG_MOVE; } else if (data.profile_id == GetProfile()->GetID()) { BookmarkBarNode* node = data.GetNode(model_); if (!node) { // Generally shouldn't get here, we originated the drag but couldn't // find the node. Do nothing. return DragDropTypes::DRAG_COPY; } model_->Move(node, parent_node, index); return DragDropTypes::DRAG_MOVE; } else { // Dropping a group from different profile. Always accept. CloneDragData(data, parent_node, index); return DragDropTypes::DRAG_COPY; } } void BookmarkBarView::CloneDragData(const BookmarkDragData& data, BookmarkBarNode* parent, int index_to_add_at) { DCHECK(data.is_valid && model_); if (data.is_url) { BookmarkBarNode* node = model_->GetNodeByURL(data.url); if (node) { model_->Move(node, parent, index_to_add_at); } else { model_->AddURL(parent, index_to_add_at, data.title, data.url); } } else { BookmarkBarNode* new_folder = model_->AddGroup(parent, index_to_add_at, data.title); for (int i = 0; i < static_cast(data.children.size()); ++i) CloneDragData(data.children[i], new_folder, i); } } int BookmarkBarView::GetFirstHiddenNodeIndex() { const int bb_count = GetBookmarkButtonCount(); for (int i = 0; i < bb_count; ++i) { if (!GetBookmarkButton(i)->IsVisible()) return i; } return bb_count; } void BookmarkBarView::StartThrobbing() { DCHECK(!throbbing_view_); if (bubble_url_.is_empty()) return; // Bubble isn't showing; nothing to throb. if (!GetViewContainer()) return; // We're not showing, don't do anything. BookmarkBarNode* node = model_->GetNodeByURL(bubble_url_); if (!node) return; // Generally shouldn't happen. // Determine which visible button is showing the url (or is an ancestor of // the url). if (node->HasAncestor(model_->GetBookmarkBarNode())) { BookmarkBarNode* bbn = model_->GetBookmarkBarNode(); BookmarkBarNode* parent_on_bb = node; while (parent_on_bb->GetParent() != bbn) parent_on_bb = parent_on_bb->GetParent(); int index = bbn->IndexOfChild(parent_on_bb); if (index >= GetFirstHiddenNodeIndex()) { // Node is hidden, animate the overflow button. throbbing_view_ = overflow_button_; } else { throbbing_view_ = static_cast(GetChildViewAt(index)); } } else { throbbing_view_ = other_bookmarked_button_; } // Use a large number so that the button continues to throb. throbbing_view_->StartThrobbing(std::numeric_limits::max()); } void BookmarkBarView::StopThrobbing(bool immediate) { if (!throbbing_view_) return; // If not immediate, cycle through 2 more complete cycles. throbbing_view_->StartThrobbing(immediate ? 0 : 4); throbbing_view_ = NULL; }