summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrohitrao@chromium.org <rohitrao@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-07-27 15:37:25 +0000
committerrohitrao@chromium.org <rohitrao@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-07-27 15:37:25 +0000
commit1c84c82c18f7bae04a6b6f309979fc0ad979ec3f (patch)
tree51870daa38dbb42cdf4314a8bd5465f0d5228b70
parent81067e05989c787c6b31a19626aa3f8039dcd485 (diff)
downloadchromium_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.h3
-rw-r--r--chrome/app/generated_resources.grd6
-rw-r--r--chrome/app/nibs/MainMenu.xib55
-rw-r--r--chrome/browser/app_controller_mac.h4
-rw-r--r--chrome/browser/app_controller_mac.mm2
-rw-r--r--chrome/browser/cocoa/history_menu_bridge.h131
-rw-r--r--chrome/browser/cocoa/history_menu_bridge.mm317
-rw-r--r--chrome/browser/cocoa/history_menu_bridge_unittest.mm164
-rw-r--r--chrome/browser/cocoa/history_menu_cocoa_controller.h32
-rw-r--r--chrome/browser/cocoa/history_menu_cocoa_controller.mm69
-rw-r--r--chrome/browser/cocoa/history_menu_cocoa_controller_unittest.mm62
-rw-r--r--chrome/chrome.gyp6
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',