1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/cocoa/history_menu_bridge.h"
8 #include "base/strings/string_number_conversions.h"
9 #include "base/strings/string_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/app/chrome_command_ids.h" // IDC_HISTORY_MENU
13 #include "chrome/browser/favicon/favicon_service_factory.h"
14 #include "chrome/browser/history/history_service_factory.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/sessions/tab_restore_service_factory.h"
17 #import "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h"
18 #include "chrome/grit/generated_resources.h"
19 #include "grit/theme_resources.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/gfx/image/image.h"
23 #include "ui/gfx/text_elider.h"
24 #include "ui/resources/grit/ui_resources.h"
28 // Maximum number of pixels to use for a menu item title.
29 const float kTitlePixelWidth = 400;
31 // Number of days to consider when getting the number of visited items.
32 const int kVisitedScope = 90;
34 // The number of visisted results to get.
35 const int kVisitedCount = 15;
37 // The number of recently closed items to get.
38 const unsigned int kRecentlyClosedCount = 10;
42 HistoryMenuBridge::HistoryItem::HistoryItem()
43 : icon_requested(false),
44 icon_task_id(base::CancelableTaskTracker::kBadTaskId),
48 HistoryMenuBridge::HistoryItem::HistoryItem(const HistoryItem& copy)
51 icon_requested(false),
52 icon_task_id(base::CancelableTaskTracker::kBadTaskId),
54 session_id(copy.session_id) {}
56 HistoryMenuBridge::HistoryItem::~HistoryItem() {
59 HistoryMenuBridge::HistoryMenuBridge(Profile* profile)
60 : controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]),
62 history_service_(NULL),
63 tab_restore_service_(NULL),
64 create_in_progress_(false),
65 need_recreate_(false),
66 history_service_observer_(this) {
67 // If we don't have a profile, do not bother initializing our data sources.
68 // This shouldn't happen except in unit tests.
70 // Check to see if the history service is ready. Because it loads async, it
71 // may not be ready when the Bridge is created. If this happens, register
72 // for a notification that tells us the HistoryService is ready.
73 history::HistoryService* hs = HistoryServiceFactory::GetForProfile(
74 profile_, ServiceAccessType::EXPLICIT_ACCESS);
76 history_service_observer_.Add(hs);
77 if (hs->BackendLoaded()) {
78 history_service_ = hs;
83 tab_restore_service_ = TabRestoreServiceFactory::GetForProfile(profile_);
84 if (tab_restore_service_) {
85 tab_restore_service_->AddObserver(this);
86 // If the tab entries are already loaded, invoke the observer method to
87 // build the "Recently Closed" section. Otherwise it will be when the
89 if (!tab_restore_service_->IsLoaded())
90 tab_restore_service_->LoadTabsFromLastSession();
92 TabRestoreServiceChanged(tab_restore_service_);
96 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
97 default_favicon_.reset(
98 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
100 // Set the static icons in the menu.
101 NSMenuItem* item = [HistoryMenu() itemWithTag:IDC_SHOW_HISTORY];
102 [item setImage:rb.GetNativeImageNamed(IDR_HISTORY_FAVICON).ToNSImage()];
106 // Note that all requests sent to either the history service or the favicon
107 // service will be automatically cancelled by their respective Consumers, so
108 // task cancellation is not done manually here in the dtor.
109 HistoryMenuBridge::~HistoryMenuBridge() {
110 // Unregister ourselves as observers and notifications.
113 if (tab_restore_service_)
114 tab_restore_service_->RemoveObserver(this);
116 // Since the map owns the HistoryItems, delete anything that still exists.
117 std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.begin();
118 while (it != menu_item_map_.end()) {
119 HistoryItem* item = it->second;
120 menu_item_map_.erase(it++);
125 void HistoryMenuBridge::TabRestoreServiceChanged(
126 sessions::TabRestoreService* service) {
127 const sessions::TabRestoreService::Entries& entries = service->entries();
129 // Clear the history menu before rebuilding.
130 NSMenu* menu = HistoryMenu();
131 ClearMenuSection(menu, kRecentlyClosed);
133 // Index for the next menu item.
134 NSInteger index = [menu indexOfItemWithTag:kRecentlyClosedTitle] + 1;
135 NSUInteger added_count = 0;
137 for (sessions::TabRestoreService::Entries::const_iterator it =
139 it != entries.end() && added_count < kRecentlyClosedCount; ++it) {
140 sessions::TabRestoreService::Entry* entry = *it;
142 // If this is a window, create a submenu for all of its tabs.
143 if (entry->type == sessions::TabRestoreService::WINDOW) {
144 sessions::TabRestoreService::Window* entry_win =
145 (sessions::TabRestoreService::Window*)entry;
146 std::vector<sessions::TabRestoreService::Tab>& tabs = entry_win->tabs;
150 // Create the item for the parent/window. Do not set the title yet because
151 // the actual number of items that are in the menu will not be known until
152 // things like the NTP are filtered out, which is done when the tab items
153 // are actually created.
154 HistoryItem* item = new HistoryItem();
155 item->session_id = entry_win->id;
157 // Create the submenu.
158 base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] init]);
160 // Create standard items within the window submenu.
161 NSString* restore_title = l10n_util::GetNSString(
162 IDS_HISTORY_CLOSED_RESTORE_WINDOW_MAC);
163 base::scoped_nsobject<NSMenuItem> restore_item(
164 [[NSMenuItem alloc] initWithTitle:restore_title
165 action:@selector(openHistoryMenuItem:)
167 [restore_item setTarget:controller_.get()];
168 // Duplicate the HistoryItem otherwise the different NSMenuItems will
169 // point to the same HistoryItem, which would then be double-freed when
170 // removing the items from the map or in the dtor.
171 HistoryItem* dup_item = new HistoryItem(*item);
172 menu_item_map_.insert(std::make_pair(restore_item.get(), dup_item));
173 [submenu addItem:restore_item.get()];
174 [submenu addItem:[NSMenuItem separatorItem]];
176 // Loop over the window's tabs and add them to the submenu.
177 NSInteger subindex = [[submenu itemArray] count];
178 std::vector<sessions::TabRestoreService::Tab>::const_iterator it;
179 for (it = tabs.begin(); it != tabs.end(); ++it) {
180 sessions::TabRestoreService::Tab tab = *it;
181 HistoryItem* tab_item = HistoryItemForTab(tab);
183 item->tabs.push_back(tab_item);
184 AddItemToMenu(tab_item, submenu.get(), kRecentlyClosed + 1,
189 // Now that the number of tabs that has been added is known, set the title
190 // of the parent menu item.
191 item->title = l10n_util::GetPluralStringFUTF16(
192 IDS_RECENTLY_CLOSED_WINDOW, item->tabs.size());
194 // Sometimes it is possible for there to not be any subitems for a given
195 // window; if that is the case, do not add the entry to the main menu.
196 if ([[submenu itemArray] count] > 2) {
197 // Create the menu item parent.
198 NSMenuItem* parent_item =
199 AddItemToMenu(item, menu, kRecentlyClosed, index++);
200 [parent_item setSubmenu:submenu.get()];
203 } else if (entry->type == sessions::TabRestoreService::TAB) {
204 sessions::TabRestoreService::Tab* tab =
205 static_cast<sessions::TabRestoreService::Tab*>(entry);
206 HistoryItem* item = HistoryItemForTab(*tab);
208 AddItemToMenu(item, menu, kRecentlyClosed, index++);
215 void HistoryMenuBridge::TabRestoreServiceDestroyed(
216 sessions::TabRestoreService* service) {
217 // Intentionally left blank. We hold a weak reference to the service.
220 void HistoryMenuBridge::ResetMenu() {
221 NSMenu* menu = HistoryMenu();
222 ClearMenuSection(menu, kVisited);
223 ClearMenuSection(menu, kRecentlyClosed);
226 void HistoryMenuBridge::BuildMenu() {
227 // If the history service is ready, use it. Otherwise, a Notification will
228 // force an update when it's loaded.
229 if (history_service_)
233 HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForMenuItem(
235 std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.find(item);
236 if (it != menu_item_map_.end()) {
242 history::HistoryService* HistoryMenuBridge::service() {
243 return history_service_;
246 Profile* HistoryMenuBridge::profile() {
250 NSMenu* HistoryMenuBridge::HistoryMenu() {
251 NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU]
256 void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, NSInteger tag) {
257 for (NSMenuItem* menu_item in [menu itemArray]) {
258 if ([menu_item tag] == tag && [menu_item target] == controller_.get()) {
259 // This is an item that should be removed, so find the corresponding model
261 HistoryItem* item = HistoryItemForMenuItem(menu_item);
263 // Cancel favicon requests that could hold onto stale pointers. Also
264 // remove the item from the mapping.
266 CancelFaviconRequest(item);
267 menu_item_map_.erase(menu_item);
271 // If this menu item has a submenu, recurse.
272 if ([menu_item hasSubmenu]) {
273 ClearMenuSection([menu_item submenu], tag + 1);
276 // Now actually remove the item from the menu.
277 [menu removeItem:menu_item];
282 NSMenuItem* HistoryMenuBridge::AddItemToMenu(HistoryItem* item,
286 // Elide the title of the history item, or use the URL if there is none.
287 std::string url = item->url.possibly_invalid_spec();
288 base::string16 full_title = item->title;
289 base::string16 title =
290 gfx::ElideText(full_title.empty() ? base::UTF8ToUTF16(url) : full_title,
291 gfx::FontList(gfx::Font([NSFont menuFontOfSize:0])),
295 item->menu_item.reset(
296 [[NSMenuItem alloc] initWithTitle:base::SysUTF16ToNSString(title)
299 [item->menu_item setTarget:controller_];
300 [item->menu_item setAction:@selector(openHistoryMenuItem:)];
301 [item->menu_item setTag:tag];
302 if (item->icon.get())
303 [item->menu_item setImage:item->icon.get()];
304 else if (!item->tabs.size())
305 [item->menu_item setImage:default_favicon_.get()];
308 NSString* tooltip = [NSString stringWithFormat:@"%@\n%@",
309 base::SysUTF16ToNSString(full_title), base::SysUTF8ToNSString(url)];
310 [item->menu_item setToolTip:tooltip];
312 [menu insertItem:item->menu_item.get() atIndex:index];
313 menu_item_map_.insert(std::make_pair(item->menu_item.get(), item));
315 return item->menu_item.get();
318 void HistoryMenuBridge::Init() {
319 DCHECK(history_service_);
322 void HistoryMenuBridge::CreateMenu() {
323 // If we're currently running CreateMenu(), wait until it finishes.
324 if (create_in_progress_)
326 create_in_progress_ = true;
327 need_recreate_ = false;
329 DCHECK(history_service_);
331 history::QueryOptions options;
332 options.max_count = kVisitedCount;
333 options.SetRecentDayRange(kVisitedScope);
335 history_service_->QueryHistory(
338 base::Bind(&HistoryMenuBridge::OnVisitedHistoryResults,
339 base::Unretained(this)),
340 &cancelable_task_tracker_);
343 void HistoryMenuBridge::OnHistoryChanged() {
344 // History has changed, rebuild menu.
345 need_recreate_ = true;
349 void HistoryMenuBridge::OnVisitedHistoryResults(
350 history::QueryResults* results) {
351 NSMenu* menu = HistoryMenu();
352 ClearMenuSection(menu, kVisited);
353 NSInteger top_item = [menu indexOfItemWithTag:kVisitedTitle] + 1;
355 size_t count = results->size();
356 for (size_t i = 0; i < count; ++i) {
357 const history::URLResult& result = (*results)[i];
359 HistoryItem* item = new HistoryItem;
360 item->title = result.title();
361 item->url = result.url();
363 // Need to explicitly get the favicon for each row.
364 GetFaviconForHistoryItem(item);
366 // This will add |item| to the |menu_item_map_|, which takes ownership.
367 AddItemToMenu(item, HistoryMenu(), kVisited, top_item + i);
370 // We are already invalid by the time we finished, darn.
374 create_in_progress_ = false;
377 HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForTab(
378 const sessions::TabRestoreService::Tab& entry) {
379 DCHECK(!entry.navigations.empty());
381 const sessions::SerializedNavigationEntry& current_navigation =
382 entry.navigations.at(entry.current_navigation_index);
383 HistoryItem* item = new HistoryItem();
384 item->title = current_navigation.title();
385 item->url = current_navigation.virtual_url();
386 item->session_id = entry.id;
388 // Tab navigations don't come with icons, so we always have to request them.
389 GetFaviconForHistoryItem(item);
394 void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) {
395 favicon::FaviconService* service = FaviconServiceFactory::GetForProfile(
396 profile_, ServiceAccessType::EXPLICIT_ACCESS);
397 base::CancelableTaskTracker::TaskId task_id =
398 service->GetFaviconImageForPageURL(
401 &HistoryMenuBridge::GotFaviconData, base::Unretained(this), item),
402 &cancelable_task_tracker_);
403 item->icon_task_id = task_id;
404 item->icon_requested = true;
407 void HistoryMenuBridge::GotFaviconData(
409 const favicon_base::FaviconImageResult& image_result) {
410 // Since we're going to do Cocoa-y things, make sure this is the main thread.
411 DCHECK([NSThread isMainThread]);
414 item->icon_requested = false;
415 item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;
417 NSImage* image = image_result.image.AsNSImage();
419 item->icon.reset([image retain]);
420 [item->menu_item setImage:item->icon.get()];
424 void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) {
426 if (item->icon_requested) {
427 cancelable_task_tracker_.TryCancel(item->icon_task_id);
428 item->icon_requested = false;
429 item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;
433 void HistoryMenuBridge::OnURLVisited(history::HistoryService* history_service,
434 ui::PageTransition transition,
435 const history::URLRow& row,
436 const history::RedirectList& redirects,
437 base::Time visit_time) {
441 void HistoryMenuBridge::OnURLsModified(history::HistoryService* history_service,
442 const history::URLRows& changed_urls) {
446 void HistoryMenuBridge::OnURLsDeleted(history::HistoryService* history_service,
449 const history::URLRows& deleted_rows,
450 const std::set<GURL>& favicon_urls) {
454 void HistoryMenuBridge::OnHistoryServiceLoaded(
455 history::HistoryService* history_service) {
456 history_service_ = history_service;