diff options
author | rohitrao@chromium.org <rohitrao@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-27 15:37:25 +0000 |
---|---|---|
committer | rohitrao@chromium.org <rohitrao@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-27 15:37:25 +0000 |
commit | 1c84c82c18f7bae04a6b6f309979fc0ad979ec3f (patch) | |
tree | 51870daa38dbb42cdf4314a8bd5465f0d5228b70 | |
parent | 81067e05989c787c6b31a19626aa3f8039dcd485 (diff) | |
download | chromium_src-1c84c82c18f7bae04a6b6f309979fc0ad979ec3f.zip chromium_src-1c84c82c18f7bae04a6b6f309979fc0ad979ec3f.tar.gz chromium_src-1c84c82c18f7bae04a6b6f309979fc0ad979ec3f.tar.bz2 |
First cut at Mac history menu.
* The menu has two sections: most visited and recently closed.
* Creates a HistoryMenuBridge that observes different data sources and stores
results for use in the menu.
* Creates a HistoryMenuController to respond to Cocoa IBActions from the menu.
BUG=14933
TEST=History menu in mac should populate with most visited and recently closed
sites.
RELEASE_NOTES=Add initial implementation of the Mac history menu.
Patch by Robert Sesek.
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@21639 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/app/chrome_dll_resource.h | 3 | ||||
-rw-r--r-- | chrome/app/generated_resources.grd | 6 | ||||
-rw-r--r-- | chrome/app/nibs/MainMenu.xib | 55 | ||||
-rw-r--r-- | chrome/browser/app_controller_mac.h | 4 | ||||
-rw-r--r-- | chrome/browser/app_controller_mac.mm | 2 | ||||
-rw-r--r-- | chrome/browser/cocoa/history_menu_bridge.h | 131 | ||||
-rw-r--r-- | chrome/browser/cocoa/history_menu_bridge.mm | 317 | ||||
-rw-r--r-- | chrome/browser/cocoa/history_menu_bridge_unittest.mm | 164 | ||||
-rw-r--r-- | chrome/browser/cocoa/history_menu_cocoa_controller.h | 32 | ||||
-rw-r--r-- | chrome/browser/cocoa/history_menu_cocoa_controller.mm | 69 | ||||
-rw-r--r-- | chrome/browser/cocoa/history_menu_cocoa_controller_unittest.mm | 62 | ||||
-rw-r--r-- | chrome/chrome.gyp | 6 |
12 files changed, 842 insertions, 9 deletions
diff --git a/chrome/app/chrome_dll_resource.h b/chrome/app/chrome_dll_resource.h index 1434b53..1502512 100644 --- a/chrome/app/chrome_dll_resource.h +++ b/chrome/app/chrome_dll_resource.h @@ -206,4 +206,7 @@ #define IDC_BOOKMARK_MENU 43000 // OSX only #define IDC_VIEW_MENU 44000 // OSX only #define IDC_CONTROL_PANEL 45000 // Linux2 only +#define IDC_HISTORY_MENU 46000 // OSX only +#define IDC_HISTORY_MENU_VISITED 46100 // OSX only +#define IDC_HISTORY_MENU_CLOSED 46200 // OSX only diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index 226a341..c9b03f3a 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -4421,6 +4421,12 @@ each locale. --> <message name="IDS_HISTORY_FORWARD_MAC" desc="The menu item for forward in the history menu."> Forward </message> + <message name="IDS_HISTORY_VISITED_MAC" desc="The menu item for the header of most visited items in the history menu."> + Most Visited + </message> + <message name="IDS_HISTORY_CLOSED_MAC" desc="The menu item for the header of recently closed items in the history menu."> + Recently Closed + </message> <!-- Bookmarks menu --> <message name="IDS_BOOKMARK_CURRENT_PAGE_MAC" desc="The menu item for booking the current page in the bookmark menu."> Bookmark Current Page... diff --git a/chrome/app/nibs/MainMenu.xib b/chrome/app/nibs/MainMenu.xib index 1c71785..70c0b8a 100644 --- a/chrome/app/nibs/MainMenu.xib +++ b/chrome/app/nibs/MainMenu.xib @@ -8,7 +8,7 @@ <string key="IBDocument.HIToolboxVersion">353.00</string> <object class="NSMutableArray" key="IBDocument.EditedObjectIDs"> <bool key="EncodedWithXMLCoder">YES</bool> - <integer value="634"/> + <integer value="640"/> </object> <object class="NSArray" key="IBDocument.PluginDependencies"> <bool key="EncodedWithXMLCoder">YES</bool> @@ -868,6 +868,7 @@ <reference key="NSOnImage" ref="353210768"/> <reference key="NSMixedImage" ref="549394948"/> <string key="NSAction">submenuAction:</string> + <int key="NSTag">46000</int> <object class="NSMenu" key="NSSubmenu" id="436720301"> <string key="NSTitle">History</string> <object class="NSMutableArray" key="NSMenuItems"> @@ -933,11 +934,33 @@ </object> <object class="NSMenuItem" id="792145602"> <reference key="NSMenu" ref="436720301"/> - <string key="NSTitle">history item...</string> + <bool key="NSIsDisabled">YES</bool> + <string key="NSTitle">^IDS_HISTORY_VISITED_MAC</string> <string key="NSKeyEquiv"/> <int key="NSMnemonicLoc">2147483647</int> <reference key="NSOnImage" ref="353210768"/> <reference key="NSMixedImage" ref="549394948"/> + <int key="NSTag">46100</int> + </object> + <object class="NSMenuItem" id="259488787"> + <reference key="NSMenu" ref="436720301"/> + <bool key="NSIsDisabled">YES</bool> + <bool key="NSIsSeparator">YES</bool> + <string key="NSTitle"/> + <string key="NSKeyEquiv"/> + <int key="NSMnemonicLoc">2147483647</int> + <reference key="NSOnImage" ref="353210768"/> + <reference key="NSMixedImage" ref="549394948"/> + </object> + <object class="NSMenuItem" id="101838950"> + <reference key="NSMenu" ref="436720301"/> + <bool key="NSIsDisabled">YES</bool> + <string key="NSTitle">^IDS_HISTORY_CLOSED_MAC</string> + <string key="NSKeyEquiv"/> + <int key="NSMnemonicLoc">2147483647</int> + <reference key="NSOnImage" ref="353210768"/> + <reference key="NSMixedImage" ref="549394948"/> + <int key="NSTag">46200</int> </object> </object> </object> @@ -2389,6 +2412,8 @@ <reference ref="792145602"/> <reference ref="64100325"/> <reference ref="517951834"/> + <reference ref="259488787"/> + <reference ref="101838950"/> </object> <reference key="parent" ref="445514911"/> </object> @@ -2492,6 +2517,16 @@ <reference key="object" ref="693413486"/> <reference key="parent" ref="99609113"/> </object> + <object class="IBObjectRecord"> + <int key="objectID">639</int> + <reference key="object" ref="259488787"/> + <reference key="parent" ref="436720301"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">640</int> + <reference key="object" ref="101838950"/> + <reference key="parent" ref="436720301"/> + </object> </object> </object> <object class="NSMutableDictionary" key="flattenedProperties"> @@ -2700,6 +2735,8 @@ <string>631.IBPluginDependency</string> <string>634.IBPluginDependency</string> <string>636.IBPluginDependency</string> + <string>639.IBPluginDependency</string> + <string>640.IBPluginDependency</string> <string>72.IBPluginDependency</string> <string>72.ImportedFromIB2</string> <string>73.IBPluginDependency</string> @@ -2842,13 +2879,13 @@ <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <reference ref="9"/> <string>{{525, 802}, {197, 73}}</string> - <string>{{153, 1136}, {535, 20}}</string> + <string>{{332, 764}, {535, 20}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <reference ref="9"/> <string>{74, 862}</string> <string>{{11, 977}, {478, 20}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - <string>{{345, 923}, {345, 213}}</string> + <string>{{524, 551}, {345, 213}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>{{475, 832}, {234, 43}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> @@ -2890,18 +2927,18 @@ <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - <string>{{462, 1103}, {347, 33}}</string> + <string>{{641, 731}, {347, 33}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <reference ref="9"/> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - <string>{{690, 883}, {241, 63}}</string> + <string>{{869, 511}, {241, 63}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> - <string>{{395, 1013}, {304, 123}}</string> + <string>{{574, 611}, {304, 153}}</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> @@ -2930,6 +2967,8 @@ <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> + <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <reference ref="9"/> <string>com.apple.InterfaceBuilder.CocoaPlugin</string> <reference ref="9"/> @@ -2975,7 +3014,7 @@ </object> </object> <nil key="sourceID"/> - <int key="maxID">638</int> + <int key="maxID">640</int> </object> <object class="IBClassDescriber" key="IBDocument.Classes"> <object class="NSMutableArray" key="referencedPartialClassDescriptions"> diff --git a/chrome/browser/app_controller_mac.h b/chrome/browser/app_controller_mac.h index e47bc00..091891c 100644 --- a/chrome/browser/app_controller_mac.h +++ b/chrome/browser/app_controller_mac.h @@ -15,6 +15,7 @@ class BookmarkMenuBridge; class CommandUpdater; class GURL; +class HistoryMenuBridge; @class PreferencesWindowController; class Profile; @@ -27,6 +28,7 @@ class Profile; // Management of the bookmark menu which spans across all windows // (and Browser*s). scoped_ptr<BookmarkMenuBridge> bookmarkMenuBridge_; + scoped_ptr<HistoryMenuBridge> historyMenuBridge_; scoped_nsobject<PreferencesWindowController> prefsController_; scoped_nsobject<AboutWindowController> aboutController_; @@ -34,7 +36,7 @@ class Profile; // only needed during early startup, it points to a valid vector during early // startup and is NULL during the rest of app execution. scoped_ptr<std::vector<GURL> > pendingURLs_; - + // Outlets for the close tab/window menu items so that we can adjust the // commmand-key equivalent depending on the kind of window and how many // tabs it has. diff --git a/chrome/browser/app_controller_mac.mm b/chrome/browser/app_controller_mac.mm index 5cdaa1c..08882bc 100644 --- a/chrome/browser/app_controller_mac.mm +++ b/chrome/browser/app_controller_mac.mm @@ -16,6 +16,7 @@ #include "chrome/browser/browser_window.h" #import "chrome/browser/cocoa/about_window_controller.h" #import "chrome/browser/cocoa/bookmark_menu_bridge.h" +#import "chrome/browser/cocoa/history_menu_bridge.h" #import "chrome/browser/cocoa/clear_browsing_data_controller.h" #import "chrome/browser/cocoa/encoding_menu_controller_delegate_mac.h" #import "chrome/browser/cocoa/preferences_window_controller.h" @@ -227,6 +228,7 @@ recursively:YES]; bookmarkMenuBridge_.reset(new BookmarkMenuBridge()); + historyMenuBridge_.reset(new HistoryMenuBridge([self defaultProfile])); [self setUpdateCheckInterval]; diff --git a/chrome/browser/cocoa/history_menu_bridge.h b/chrome/browser/cocoa/history_menu_bridge.h new file mode 100644 index 0000000..c67ba83 --- /dev/null +++ b/chrome/browser/cocoa/history_menu_bridge.h @@ -0,0 +1,131 @@ +// 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. + +// C++ controller for the history menu; one per AppController (means there +// is only one). This class observes various data sources, namely the +// HistoryService and the TabRestoreService, and then updates the NSMenu when +// there is new data. +// +// The history menu is broken up into sections: most visisted and recently +// closed. The overall menu has a tag of IDC_HISTORY_MENU, with two sections +// IDC_HISTORY_MENU_VISITED and IDC_HISTORY_MENU_CLOSED, which are used to +// delineate the two sections. Items within these sections are assigned tags +// within IDC_HISTORY_MENU_* + 1..99. Most Chromium Cocoa menu items are static +// from a nib (e.g. New Tab), but may be enabled/disabled under certain +// circumstances (e.g. Cut and Paste). In addition, most Cocoa menu items have +// firstResponder: as a target. Unusually, bookmark menu items are created +// dynamically. They also have a target of HistoryMenuCocoaController, not +// firstResponder. See HistoryMenuBridge::AddItemToMenu()). Unlike most of our +// Cocoa-Bridge classes, the HMCC is not at the root of the ownership model +// because its only function is to respond to menu item actions; everything +// else is done in this bridge. + +#ifndef CHROME_BROWSER_COCOA_HISTORY_MENU_BRIDGE_H_ +#define CHROME_BROWSER_COCOA_HISTORY_MENU_BRIDGE_H_ + +#import <Cocoa/Cocoa.h> +#include "base/scoped_nsobject.h" +#include "chrome/browser/cancelable_request.h" +#include "chrome/browser/history/history.h" +#include "chrome/browser/sessions/tab_restore_service.h" +#include "chrome/common/notification_observer.h" + +class NavigationEntry; +class NotificationRegistrar; +class PageUsageData; +class Profile; +class TabNavigationEntry; +@class HistoryMenuCocoaController; + +class HistoryMenuBridge : public NotificationObserver, + public TabRestoreService::Observer { + public: + // This is a generalization of the data we store in the history menu because + // we pull things from different sources with different data types. + struct HistoryItem { + HistoryItem() {} + ~HistoryItem() {} + + string16 title; + GURL url; + }; + + HistoryMenuBridge(Profile* profile); + virtual ~HistoryMenuBridge(); + + // Overriden from NotificationObserver. + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + // For TabRestoreService::Observer + virtual void TabRestoreServiceChanged(TabRestoreService* service); + virtual void TabRestoreServiceDestroyed(TabRestoreService* service); + + // I wish I has a "friend @class" construct. These are used by the HMCC + // to access model information when responding to actions. + HistoryService* service(); + Profile* profile(); + std::vector<HistoryItem>* visited_results(); + std::vector<HistoryItem>* closed_results(); + + protected: + // Return the History menu. + NSMenu* HistoryMenu(); + + // Clear items in the given |menu|. The menu is broken into sections, defined + // by IDC_HISTORY_MENU_* constants. This function will clear |count| menu + // items, starting from |tag|. + void ClearMenuSection(NSMenu* menu, NSInteger tag, unsigned int count); + + // Adds a given title and URL to HistoryMenu() with a certain tag and index. + void AddItemToMenu(const HistoryItem& item, + NSMenu* menu, + NSInteger tag, + NSInteger index); + + // Called by the ctor if |service_| is ready at the time, or by a + // notification receiver. Finishes initialization tasks by subscribing for + // change notifications and calling CreateMenu(). + void Init(); + + // Does the query for the history information to create the menu. + void CreateMenu(); + + // Callback method for when HistoryService query results are ready with the + // most recently-visited sites. + void OnVisitedHistoryResults(CancelableRequestProvider::Handle handle, + std::vector<PageUsageData*>* results); + + // Tries to add the current Tab's TabNavigationEntry's NavigationEntry object + // to |closed_results_|. Return TRUE if the operation completed successfully. + bool AddNavigationForTab(const TabRestoreService::Tab& entry); + + private: + friend class HistoryMenuBridgeTest; + + scoped_nsobject<HistoryMenuCocoaController> controller_; // strong + + Profile* profile_; // weak + HistoryService* history_service_; // weak + TabRestoreService* tab_restore_service_; // weak + + NotificationRegistrar registrar_; + CancelableRequestConsumer cancelable_request_consumer_; + + // The most recent results we've received. + std::vector<HistoryItem> visited_results_; + std::vector<HistoryItem> closed_results_; + + // We coalesce requests to re-create the menu. |create_in_progress_| is true + // whenever we are either waiting for the history service to return query + // results, or when we are rebuilding. |need_recreate_| is true whenever a + // rebuild has been scheduled but is waiting for the current one to finish. + bool create_in_progress_; + bool need_recreate_; + + DISALLOW_COPY_AND_ASSIGN(HistoryMenuBridge); +}; + +#endif // CHROME_BROWSER_COCOA_HISTORY_MENU_BRIDGE_H_ diff --git a/chrome/browser/cocoa/history_menu_bridge.mm b/chrome/browser/cocoa/history_menu_bridge.mm new file mode 100644 index 0000000..1603e8c --- /dev/null +++ b/chrome/browser/cocoa/history_menu_bridge.mm @@ -0,0 +1,317 @@ +// 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 "chrome/browser/cocoa/history_menu_bridge.h" +#include "base/sys_string_conversions.h" +#include "base/string_util.h" +#include "chrome/app/chrome_dll_resource.h" // IDC_HISTORY_MENU +#import "chrome/browser/app_controller_mac.h" +#import "chrome/browser/cocoa/history_menu_cocoa_controller.h" +#include "chrome/browser/history/page_usage_data.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/sessions/session_types.h" +#include "chrome/common/notification_registrar.h" +#include "chrome/common/notification_service.h" +#include "chrome/common/url_constants.h" + +namespace { + +// Menus more than this many chars long will get trimmed. +const static NSUInteger kMaximumMenuWidthInChars = 65; + +// When trimming, use this many chars from each side. +const static NSUInteger kMenuTrimSizeInChars = 30; + +// Number of days to consider when getting the number of most visited items. +const static int kMostVisitedScope = 90; + +// The number of most visisted results to get. +const static int kMostVisitedCount = 9; + +// The number of recently closed items to get. +const static unsigned int kRecentlyClosedCount = 4; + +} + +HistoryMenuBridge::HistoryMenuBridge(Profile* profile) + : controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]), + profile_(profile), + history_service_(NULL), + tab_restore_service_(NULL), + create_in_progress_(false), + need_recreate_(false) { + // If we don't have a profile, do not bother initializing our data sources. + // This shouldn't happen except in unit tests. + if (profile_) { + // Check to see if the history service is ready. Because it loads async, it + // may not be ready when the Bridge is created. If this happens, register + // for a notification that tells us the HistoryService is ready. + HistoryService* hs = profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (hs != NULL && hs->backend_loaded()) { + history_service_ = hs; + Init(); + } + + // TODO(???): NULL here means we're OTR. Show this in the GUI somehow? + tab_restore_service_ = profile_->GetTabRestoreService(); + if (tab_restore_service_) + tab_restore_service_->AddObserver(this); + } + + // The service is not ready for use yet, so become notified when it does. + if (!history_service_) { + registrar_.Add(this, + NotificationType::HISTORY_LOADED, + NotificationService::AllSources()); + } +} + +HistoryMenuBridge::~HistoryMenuBridge() { + // Unregister ourselves as observers and notifications. + const NotificationSource& src = NotificationService::AllSources(); + if (history_service_) { + registrar_.Remove(this, NotificationType::HISTORY_TYPED_URLS_MODIFIED, src); + registrar_.Remove(this, NotificationType::HISTORY_URL_VISITED, src); + registrar_.Remove(this, NotificationType::HISTORY_URLS_DELETED, src); + } else { + registrar_.Remove(this, NotificationType::HISTORY_LOADED, src); + } + + if (tab_restore_service_) + tab_restore_service_->RemoveObserver(this); +} + +void HistoryMenuBridge::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + // A history service is now ready. Check to see if it's the one for the main + // profile. If so, perform final initialization. + if (type == NotificationType::HISTORY_LOADED) { + HistoryService* hs = + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (hs != NULL && hs->backend_loaded()) { + history_service_ = hs; + Init(); + + // Found our HistoryService, so stop listening for this notification. + registrar_.Remove(this, + NotificationType::HISTORY_LOADED, + NotificationService::AllSources()); + } + } + + // All other notification types that we observe indicate that the history has + // changed and we need to rebuild. + need_recreate_ = true; + CreateMenu(); +} + +void HistoryMenuBridge::TabRestoreServiceChanged(TabRestoreService* service) { + const TabRestoreService::Entries& entries = service->entries(); + + // Clear the history menu before modifying |closed_results_|. + NSMenu* menu = HistoryMenu(); + ClearMenuSection(menu, IDC_HISTORY_MENU_CLOSED, closed_results_.size()); + + unsigned int added_count = 0; + for (TabRestoreService::Entries::const_iterator it = entries.begin(); + it != entries.end() && added_count < kRecentlyClosedCount; ++it) { + TabRestoreService::Entry* entry = *it; + + // If we have a window, loop over all of its tabs. This could consume all + // of |kRecentlyClosedCount| in a given outer loop iteration. + if (entry->type == TabRestoreService::WINDOW) { + TabRestoreService::Window* entry_win = (TabRestoreService::Window*)entry; + std::vector<TabRestoreService::Tab> tabs = entry_win->tabs; + std::vector<TabRestoreService::Tab>::const_iterator it; + for (it = tabs.begin(); it != tabs.end() && + added_count < kRecentlyClosedCount; ++it) { + TabRestoreService::Tab tab = *it; + if (AddNavigationForTab(tab)) + ++added_count; + } + } else if (entry->type == TabRestoreService::TAB) { + TabRestoreService::Tab* tab = + static_cast<TabRestoreService::Tab*>(entry); + if (AddNavigationForTab(*tab)) + ++added_count; + } + } + + // Remove extraneous/old results. + if (closed_results_.size() > kRecentlyClosedCount) + closed_results_.erase(closed_results_.begin(), + closed_results_.end() - kRecentlyClosedCount); + + NSInteger top_index = [menu indexOfItemWithTag:IDC_HISTORY_MENU_CLOSED] + 1; + + int i = 0; // Count offsets for |tag| and |index| in AddItemToMenu(). + for (std::vector<HistoryItem>::const_iterator it = closed_results_.begin(); + it != closed_results_.end(); ++it) { + HistoryItem item = *it; + NSInteger tag = IDC_HISTORY_MENU_CLOSED + 1 + i; + AddItemToMenu(item, HistoryMenu(), tag, top_index + i); + ++i; + } +} + +void HistoryMenuBridge::TabRestoreServiceDestroyed( + TabRestoreService* service) { + // Intentionally left blank. We hold a weak reference to the service. +} + +HistoryService* HistoryMenuBridge::service() { + return history_service_; +} + +Profile* HistoryMenuBridge::profile() { + return profile_; +} + +std::vector<HistoryMenuBridge::HistoryItem>* + HistoryMenuBridge::visited_results() { + return &visited_results_; +} + +std::vector<HistoryMenuBridge::HistoryItem>* + HistoryMenuBridge::closed_results() { + return &closed_results_; +} + +NSMenu* HistoryMenuBridge::HistoryMenu() { + NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU] + submenu]; + return history_menu; +} + +void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, + NSInteger tag, + unsigned int count) { + const NSInteger max_tag = tag + count + 1; + + // Get the index of the first item in the section, excluding the header. + NSInteger index = [menu indexOfItemWithTag:tag] + 1; + if (index <= 0 || index >= [menu numberOfItems]) + return; // The section is at the end, empty. + + // Remove at the same index, usually, because the menu will shrink by one + // item each time, shifting all the lower elements up. If we hit a "unhooked" + // menu item, don't remove it, but advance the index to skip the item. + NSInteger item_tag = tag; + while (count > 0 && item_tag < max_tag && index < [menu numberOfItems]) { + NSMenuItem* menu_item = [menu itemAtIndex:index]; + item_tag = [menu_item tag]; + if ([menu_item action] == @selector(openHistoryMenuItem:)) { + [menu removeItemAtIndex:index]; + --count; + } + else { + ++index; + } + } +} + +void HistoryMenuBridge::AddItemToMenu(const HistoryItem& item, + NSMenu* menu, + NSInteger tag, + NSInteger index) { + NSString* title = base::SysUTF16ToNSString(item.title); + std::string url_string = item.url.possibly_invalid_spec(); + + // If we don't have a title, use the URL. + if ([title isEqualToString:@""]) + title = base::SysUTF8ToNSString(url_string); + NSString* full_title = title; + if (false && [title length] > kMaximumMenuWidthInChars) { + // TODO(rsesek): use app/gfx/text_elider.h once it uses string16 and can + // take out the middle of strings. + title = [NSString stringWithFormat:@"%@…%@", + [title substringToIndex:kMenuTrimSizeInChars], + [title substringFromIndex:([title length] - + kMenuTrimSizeInChars)]]; + } + scoped_nsobject<NSMenuItem> menu_item( + [[NSMenuItem alloc] initWithTitle:title + action:nil + keyEquivalent:@""]); + [menu_item setTarget:controller_]; + [menu_item setAction:@selector(openHistoryMenuItem:)]; + [menu_item setTag:tag]; + + // Add a tooltip. + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", full_title, + url_string.c_str()]; + [menu_item setToolTip:tooltip]; + + [menu insertItem:menu_item atIndex:index]; +} + +void HistoryMenuBridge::Init() { + const NotificationSource& source = NotificationService::AllSources(); + registrar_.Add(this, NotificationType::HISTORY_TYPED_URLS_MODIFIED, source); + registrar_.Add(this, NotificationType::HISTORY_URL_VISITED, source); + registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED, source); +} + +void HistoryMenuBridge::CreateMenu() { + // If we're currently running CreateMenu(), wait until it finishes. + if (create_in_progress_) + return; + create_in_progress_ = true; + need_recreate_ = false; + + history_service_->QuerySegmentUsageSince( + &cancelable_request_consumer_, + base::Time::Now() - base::TimeDelta::FromDays(kMostVisitedScope), + kMostVisitedCount, + NewCallback(this, &HistoryMenuBridge::OnVisitedHistoryResults)); +} + +void HistoryMenuBridge::OnVisitedHistoryResults( + CancelableRequestProvider::Handle handle, + std::vector<PageUsageData*>* results) { + NSMenu* menu = HistoryMenu(); + NSInteger top_item = [menu indexOfItemWithTag:IDC_HISTORY_MENU_VISITED] + 1; + + ClearMenuSection(menu, IDC_HISTORY_MENU_VISITED, visited_results_.size()); + visited_results_.clear(); + + size_t count = results->size(); + for (size_t i = 0; i < count; ++i) { + PageUsageData* history_item = (*results)[i]; + + HistoryItem item; + item.title = history_item->GetTitle(); + item.url = history_item->GetURL(); + visited_results_.push_back(item); + + // Use the large gaps in tags assignment to create the tag for history menu + // items. + NSInteger tag = IDC_HISTORY_MENU_VISITED + 1 + i; + AddItemToMenu(item, HistoryMenu(), tag, top_item + i); + } + + // We are already invalid by the time we finished, darn. + if (need_recreate_) + CreateMenu(); + + create_in_progress_ = false; +} + +bool HistoryMenuBridge::AddNavigationForTab( + const TabRestoreService::Tab& entry) { + if (entry.navigations.empty()) + return false; + + const TabNavigation& current_navigation = + entry.navigations.at(entry.current_navigation_index); + if (current_navigation.url() == GURL(chrome::kChromeUINewTabURL)) + return false; + + HistoryItem item; + item.title = current_navigation.title(); + item.url = current_navigation.url(); + closed_results_.push_back(item); + return true; +} diff --git a/chrome/browser/cocoa/history_menu_bridge_unittest.mm b/chrome/browser/cocoa/history_menu_bridge_unittest.mm new file mode 100644 index 0000000..a0e45e6 --- /dev/null +++ b/chrome/browser/cocoa/history_menu_bridge_unittest.mm @@ -0,0 +1,164 @@ +// 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. + +#import <Cocoa/Cocoa.h> +#include "base/sys_string_conversions.h" +#include "chrome/app/chrome_dll_resource.h" +#include "chrome/browser/browser.h" +#include "chrome/browser/cocoa/history_menu_bridge.h" +#include "chrome/browser/cocoa/browser_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +class HistoryMenuBridgeTest : public PlatformTest { + public: + + // We are a friend of HistoryMenuBridge (and have access to + // protected methods), but none of the classes generated by TEST_F() + // are. Wraps common commands. + void ClearMenuSection(HistoryMenuBridge* bridge, + NSMenu* menu, + NSInteger tag, + unsigned int count) { + bridge->ClearMenuSection(menu, tag, count); + } + + void AddItemToBridgeMenu(HistoryMenuBridge* bridge, + HistoryMenuBridge::HistoryItem& item, + NSMenu* menu, + NSInteger tag, + NSInteger index) { + bridge->AddItemToMenu(item, menu, tag, index); + } + + NSMenuItem* AddItemToMenu(NSMenu* menu, + NSString* title, + SEL selector, + int tag) { + NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title action:NULL + keyEquivalent:@""] autorelease]; + [item setTag:tag]; + if (selector) + [item setAction:selector]; + [menu addItem:item]; + return item; + } + + HistoryMenuBridge::HistoryItem CreateItem(const string16& title) { + HistoryMenuBridge::HistoryItem item; + item.title = title; + item.url = GURL("http://google.com"); + return item; + } + + BrowserTestHelper browser_test_helper_; +}; + +// Edge case test for clearing until the end of a menu. +TEST_F(HistoryMenuBridgeTest, TestClearHistoryMenuUntilEnd) { + scoped_ptr<HistoryMenuBridge> bridge( + new HistoryMenuBridge(browser_test_helper_.profile())); + EXPECT_TRUE(bridge.get()); + + NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; + NSInteger section_tag = 9990; + AddItemToMenu(menu, @"HEADER", NULL, section_tag); + NSInteger tag = section_tag; + + AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), ++tag); + AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), ++tag); + AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), ++tag); + AddItemToMenu(menu, @"delta", @selector(openHistoryMenuItem:), ++tag); + + ClearMenuSection(bridge.get(), menu, section_tag, 4); + + EXPECT_EQ(1, [menu numberOfItems]); + EXPECT_EQ(@"HEADER", [[menu itemWithTag:section_tag] title]); +} + +// Skip menu items that are not hooked up to |-openHistoryMenuItem:|. +TEST_F(HistoryMenuBridgeTest, TestClearHistoryMenuSkipping) { + scoped_ptr<HistoryMenuBridge> bridge( + new HistoryMenuBridge(browser_test_helper_.profile())); + EXPECT_TRUE(bridge.get()); + + NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; + NSInteger section_tag = 9990; + AddItemToMenu(menu, @"HEADER", NULL, section_tag); + NSInteger tag = section_tag; + + AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), ++tag); + AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), ++tag); + AddItemToMenu(menu, @"unhooked", NULL, ++tag); + AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), ++tag); + + ClearMenuSection(bridge.get(), menu, section_tag, 4); + + EXPECT_EQ(2, [menu numberOfItems]); + EXPECT_EQ(@"HEADER", [[menu itemWithTag:section_tag] title]); + EXPECT_EQ(@"unhooked", [[menu itemAtIndex:1] title]); +} + +// Edge case test for clearing an empty menu. +TEST_F(HistoryMenuBridgeTest, TestClearHistoryMenuEmpty) { + scoped_ptr<HistoryMenuBridge> bridge( + new HistoryMenuBridge(browser_test_helper_.profile())); + EXPECT_TRUE(bridge.get()); + + NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; + NSInteger section_tag = 9990; + AddItemToMenu(menu, @"HEADER", NULL, section_tag); + + ClearMenuSection(bridge.get(), menu, section_tag, 1); + + EXPECT_EQ(1, [menu numberOfItems]); + EXPECT_EQ(@"HEADER", [[menu itemWithTag:section_tag] title]); +} + +// Test that AddItemToMenu() properly adds HistoryItem objects as menus. +TEST_F(HistoryMenuBridgeTest, TestAddItemToMenu) { + scoped_ptr<HistoryMenuBridge> bridge( + new HistoryMenuBridge(browser_test_helper_.profile())); + EXPECT_TRUE(bridge.get()); + + NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; + + string16 short_url = ASCIIToUTF16("http://foo/"); + string16 long_url = ASCIIToUTF16("http://super-duper-long-url--." + "that.cannot.possibly.fit.even-in-80-columns" + "or.be.reasonably-displayed-in-a-menu" + "without.looking-ridiculous.com/"); // 140 chars total + + HistoryMenuBridge::HistoryItem item1; + item1.title = short_url; + item1.url = GURL(short_url); + AddItemToBridgeMenu(bridge.get(), item1, menu, 100, 0); + + HistoryMenuBridge::HistoryItem item2; + item2.title = long_url; + item2.url = GURL(long_url); + AddItemToBridgeMenu(bridge.get(), item2, menu, 101, 1); + + EXPECT_EQ(2, [menu numberOfItems]); + + EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:0] action]); + EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:1] action]); + + EXPECT_EQ(100, [[menu itemAtIndex:0] tag]); + EXPECT_EQ(101, [[menu itemAtIndex:1] tag]); + + // Make sure a short title looks fine + NSString* s = [[menu itemAtIndex:0] title]; + EXPECT_EQ(base::SysNSStringToUTF16(s), short_url); + + // Make sure a super-long title gets trimmed + s = [[menu itemAtIndex:0] title]; + EXPECT_TRUE([s length] < long_url.length()); + + // Confirm tooltips and confirm they are not trimmed (like the item + // name might be). Add tolerance for URL fixer-upping; + // e.g. http://foo becomes http://foo/) + EXPECT_GE([[[menu itemAtIndex:0] toolTip] length], (2*short_url.length()-5)); + EXPECT_GE([[[menu itemAtIndex:1] toolTip] length], (2*long_url.length()-5)); +} diff --git a/chrome/browser/cocoa/history_menu_cocoa_controller.h b/chrome/browser/cocoa/history_menu_cocoa_controller.h new file mode 100644 index 0000000..32a7939 --- /dev/null +++ b/chrome/browser/cocoa/history_menu_cocoa_controller.h @@ -0,0 +1,32 @@ +// 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. + +// Controller (MVC) for the history menu. All history menu item commands get +// directed here. This class only responds to menu events, but the actual +// creation and maintenance of the menu happens in the Bridge. + +#ifndef CHROME_BROWSER_COCOA_HISTORY_MENU_COCOA_CONTROLLER_H_ +#define CHROME_BROWSER_COCOA_HISTORY_MENU_COCOA_CONTROLLER_H_ + +#import <Cocoa/Cocoa.h> +#import "chrome/browser/cocoa/history_menu_bridge.h" + +@interface HistoryMenuCocoaController : NSObject { + @private + HistoryMenuBridge* bridge_; // weak; owns us +} + +- (id)initWithBridge:(HistoryMenuBridge*)bridge; + +// Called by any history menu item. +- (IBAction)openHistoryMenuItem:(id)sender; + +@end // HistoryMenuCocoaController + +@interface HistoryMenuCocoaController (ExposedForUnitTests) +- (HistoryMenuBridge::HistoryItem)itemForTag:(NSInteger)tag; +- (void)openURLForItem:(HistoryMenuBridge::HistoryItem&)node; +@end // HistoryMenuCocoaController (ExposedForUnitTests) + +#endif // CHROME_BROWSER_COCOA_HISTORY_MENU_COCOA_CONTROLLER_H_ diff --git a/chrome/browser/cocoa/history_menu_cocoa_controller.mm b/chrome/browser/cocoa/history_menu_cocoa_controller.mm new file mode 100644 index 0000000..ac69916 --- /dev/null +++ b/chrome/browser/cocoa/history_menu_cocoa_controller.mm @@ -0,0 +1,69 @@ +// 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 "chrome/app/chrome_dll_resource.h" // IDC_HISTORY_MENU +#include "chrome/browser/browser.h" +#import "chrome/browser/cocoa/history_menu_cocoa_controller.h" +#include "chrome/browser/browser_list.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/history/history.h" +#include "chrome/browser/history/history_types.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "webkit/glue/window_open_disposition.h" // CURRENT_TAB + +@implementation HistoryMenuCocoaController + +- (id)initWithBridge:(HistoryMenuBridge*)bridge { + if ((self = [super init])) { + bridge_ = bridge; + DCHECK(bridge_); + } + return self; +} + +// Open the URL of the given history item in the current tab. +- (void)openURLForItem:(HistoryMenuBridge::HistoryItem&)node { + Browser* browser = BrowserList::GetLastActive(); + + if (!browser) { // No windows open? + Browser::OpenEmptyWindow(bridge_->profile()); + browser = BrowserList::GetLastActive(); + } + DCHECK(browser); + TabContents* tab_contents = browser->GetSelectedTabContents(); + DCHECK(tab_contents); + + // A TabContents is a PageNavigator, so we can OpenURL() on it. + tab_contents->OpenURL(node.url, GURL(), CURRENT_TAB, + PageTransition::AUTO_BOOKMARK); +} + +- (HistoryMenuBridge::HistoryItem)itemForTag:(NSInteger)tag { + std::vector<HistoryMenuBridge::HistoryItem>* results = NULL; + NSInteger tag_base = 0; + if (tag > IDC_HISTORY_MENU_VISITED && tag < IDC_HISTORY_MENU_CLOSED) { + results = bridge_->visited_results(); + tag_base = IDC_HISTORY_MENU_VISITED; + } else if (tag > IDC_HISTORY_MENU_CLOSED) { + results = bridge_->closed_results(); + tag_base = IDC_HISTORY_MENU_CLOSED; + } else { + NOTREACHED(); + } + + DCHECK(tag > tag_base); + size_t index = tag - tag_base - 1; + if (index >= results->size()) + return HistoryMenuBridge::HistoryItem(); + return (*results)[index]; +} + +- (IBAction)openHistoryMenuItem:(id)sender { + NSInteger tag = [sender tag]; + HistoryMenuBridge::HistoryItem item = [self itemForTag:tag]; + DCHECK(item.url.is_valid()); + [self openURLForItem:item]; +} + +@end // HistoryMenuCocoaController diff --git a/chrome/browser/cocoa/history_menu_cocoa_controller_unittest.mm b/chrome/browser/cocoa/history_menu_cocoa_controller_unittest.mm new file mode 100644 index 0000000..112f798 --- /dev/null +++ b/chrome/browser/cocoa/history_menu_cocoa_controller_unittest.mm @@ -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. + +#include "base/sys_string_conversions.h" +#include "chrome/app/chrome_dll_resource.h" +#include "chrome/browser/browser.h" +#include "chrome/browser/cocoa/history_menu_bridge.h" +#include "chrome/browser/cocoa/history_menu_cocoa_controller.h" +#include "testing/gtest/include/gtest/gtest.h" + +@interface FakeHistoryMenuController : HistoryMenuCocoaController { + @public + BOOL opened_[2]; +} +@end + +@implementation FakeHistoryMenuController + +- (id)init { + if ((self = [super init])) { + opened_[0] = NO; + opened_[1] = NO; + } + return self; +} + +- (HistoryMenuBridge::HistoryItem)itemForTag:(NSInteger)tag { + HistoryMenuBridge::HistoryItem item; + if (tag == 0) { + item.title = ASCIIToUTF16("uno"); + item.url = GURL("http://google.com"); + } else if (tag == 1) { + item.title = ASCIIToUTF16("duo"); + item.url = GURL("http://apple.com"); + } else { + NOTREACHED(); + } + return item; +} + +- (void)openURLForItem:(HistoryMenuBridge::HistoryItem&)item { + std::string url = item.url.possibly_invalid_spec(); + if (url.find("http://google.com") != std::string::npos) + opened_[0] = YES; + if (url.find("http://apple.com") != std::string::npos) + opened_[1] = YES; +} + +@end // FakeHistoryMenuController + +TEST(HistoryMenuCocoaControllerTest, TestOpenItem) { + FakeHistoryMenuController *c = [[FakeHistoryMenuController alloc] init]; + NSMenuItem* item = [[[NSMenuItem alloc] init] autorelease]; + for (int i = 0; i < 2; ++i) { + [item setTag:i]; + ASSERT_EQ(c->opened_[i], NO); + [c openHistoryMenuItem:item]; + ASSERT_EQ(c->opened_[i], YES); + } + [c release]; +} diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 571b364..e94ca05 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -796,6 +796,10 @@ 'browser/cocoa/fullscreen_window.mm', 'browser/cocoa/gradient_button_cell.h', 'browser/cocoa/gradient_button_cell.mm', + 'browser/cocoa/history_menu_bridge.h', + 'browser/cocoa/history_menu_bridge.mm', + 'browser/cocoa/history_menu_cocoa_controller.h', + 'browser/cocoa/history_menu_cocoa_controller.mm', 'browser/cocoa/hung_renderer_controller.h', 'browser/cocoa/hung_renderer_controller.mm', 'browser/cocoa/infobar.h', @@ -3719,6 +3723,8 @@ 'browser/cocoa/infobar_text_field_unittest.mm', 'browser/cocoa/location_bar_view_mac_unittest.mm', 'browser/cocoa/gradient_button_cell_unittest.mm', + 'browser/cocoa/history_menu_bridge_unittest.mm', + 'browser/cocoa/history_menu_cocoa_controller_unittest.mm', 'browser/cocoa/nsimage_cache_unittest.mm', 'browser/cocoa/preferences_window_controller_unittest.mm', 'browser/cocoa/rwhvm_editcommand_helper_unittest.mm', |