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.
4
5#include "chrome/browser/ui/cocoa/history_menu_bridge.h"
6
7#include "base/bind.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/chrome_notification_types.h"
14#include "chrome/browser/favicon/favicon_service_factory.h"
15#include "chrome/browser/history/history_service_factory.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/browser/sessions/tab_restore_service_factory.h"
18#import "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h"
19#include "chrome/grit/generated_resources.h"
20#include "content/public/browser/notification_registrar.h"
21#include "content/public/browser/notification_source.h"
22#include "grit/theme_resources.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "ui/base/resource/resource_bundle.h"
25#include "ui/gfx/image/image.h"
26#include "ui/gfx/text_elider.h"
27#include "ui/resources/grit/ui_resources.h"
28
29namespace {
30
31// Maximum number of pixels to use for a menu item title.
32const float kTitlePixelWidth = 400;
33
34// Number of days to consider when getting the number of visited items.
35const int kVisitedScope = 90;
36
37// The number of visisted results to get.
38const int kVisitedCount = 15;
39
40// The number of recently closed items to get.
41const unsigned int kRecentlyClosedCount = 10;
42
43}  // namespace
44
45HistoryMenuBridge::HistoryItem::HistoryItem()
46    : icon_requested(false),
47      icon_task_id(base::CancelableTaskTracker::kBadTaskId),
48      menu_item(nil),
49      session_id(0) {}
50
51HistoryMenuBridge::HistoryItem::HistoryItem(const HistoryItem& copy)
52    : title(copy.title),
53      url(copy.url),
54      icon_requested(false),
55      icon_task_id(base::CancelableTaskTracker::kBadTaskId),
56      menu_item(nil),
57      session_id(copy.session_id) {}
58
59HistoryMenuBridge::HistoryItem::~HistoryItem() {
60}
61
62HistoryMenuBridge::HistoryMenuBridge(Profile* profile)
63    : controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]),
64      profile_(profile),
65      history_service_(NULL),
66      tab_restore_service_(NULL),
67      create_in_progress_(false),
68      need_recreate_(false) {
69  // If we don't have a profile, do not bother initializing our data sources.
70  // This shouldn't happen except in unit tests.
71  if (profile_) {
72    // Check to see if the history service is ready. Because it loads async, it
73    // may not be ready when the Bridge is created. If this happens, register
74    // for a notification that tells us the HistoryService is ready.
75    HistoryService* hs = HistoryServiceFactory::GetForProfile(
76        profile_, Profile::EXPLICIT_ACCESS);
77    if (hs != NULL && hs->BackendLoaded()) {
78      history_service_ = hs;
79      Init();
80    }
81
82    tab_restore_service_ = TabRestoreServiceFactory::GetForProfile(profile_);
83    if (tab_restore_service_) {
84      tab_restore_service_->AddObserver(this);
85      // If the tab entries are already loaded, invoke the observer method to
86      // build the "Recently Closed" section. Otherwise it will be when the
87      // backend loads.
88      if (!tab_restore_service_->IsLoaded())
89        tab_restore_service_->LoadTabsFromLastSession();
90      else
91        TabRestoreServiceChanged(tab_restore_service_);
92    }
93  }
94
95  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
96  default_favicon_.reset(
97      rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
98
99  // Set the static icons in the menu.
100  NSMenuItem* item = [HistoryMenu() itemWithTag:IDC_SHOW_HISTORY];
101  [item setImage:rb.GetNativeImageNamed(IDR_HISTORY_FAVICON).ToNSImage()];
102
103  // The service is not ready for use yet, so become notified when it does.
104  if (!history_service_) {
105    registrar_.Add(
106        this, chrome::NOTIFICATION_HISTORY_LOADED,
107        content::Source<Profile>(profile_));
108  }
109}
110
111// Note that all requests sent to either the history service or the favicon
112// service will be automatically cancelled by their respective Consumers, so
113// task cancellation is not done manually here in the dtor.
114HistoryMenuBridge::~HistoryMenuBridge() {
115  // Unregister ourselves as observers and notifications.
116  DCHECK(profile_);
117  if (history_service_) {
118    registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
119                      content::Source<Profile>(profile_));
120    registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
121                      content::Source<Profile>(profile_));
122    registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
123                      content::Source<Profile>(profile_));
124  } else {
125    registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_LOADED,
126                      content::Source<Profile>(profile_));
127  }
128
129  if (tab_restore_service_)
130    tab_restore_service_->RemoveObserver(this);
131
132  // Since the map owns the HistoryItems, delete anything that still exists.
133  std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.begin();
134  while (it != menu_item_map_.end()) {
135    HistoryItem* item  = it->second;
136    menu_item_map_.erase(it++);
137    delete item;
138  }
139}
140
141void HistoryMenuBridge::Observe(int type,
142                                const content::NotificationSource& source,
143                                const content::NotificationDetails& details) {
144  // A history service is now ready. Check to see if it's the one for the main
145  // profile. If so, perform final initialization.
146  if (type == chrome::NOTIFICATION_HISTORY_LOADED) {
147    HistoryService* hs = HistoryServiceFactory::GetForProfile(
148        profile_, Profile::EXPLICIT_ACCESS);
149    if (hs != NULL && hs->BackendLoaded()) {
150      history_service_ = hs;
151      Init();
152
153      // Found our HistoryService, so stop listening for this notification.
154      registrar_.Remove(this,
155                        chrome::NOTIFICATION_HISTORY_LOADED,
156                        content::Source<Profile>(profile_));
157    }
158  }
159
160  // All other notification types that we observe indicate that the history has
161  // changed and we need to rebuild.
162  need_recreate_ = true;
163  CreateMenu();
164}
165
166void HistoryMenuBridge::TabRestoreServiceChanged(TabRestoreService* service) {
167  const TabRestoreService::Entries& entries = service->entries();
168
169  // Clear the history menu before rebuilding.
170  NSMenu* menu = HistoryMenu();
171  ClearMenuSection(menu, kRecentlyClosed);
172
173  // Index for the next menu item.
174  NSInteger index = [menu indexOfItemWithTag:kRecentlyClosedTitle] + 1;
175  NSUInteger added_count = 0;
176
177  for (TabRestoreService::Entries::const_iterator it = entries.begin();
178       it != entries.end() && added_count < kRecentlyClosedCount; ++it) {
179    TabRestoreService::Entry* entry = *it;
180
181    // If this is a window, create a submenu for all of its tabs.
182    if (entry->type == TabRestoreService::WINDOW) {
183      TabRestoreService::Window* entry_win = (TabRestoreService::Window*)entry;
184      std::vector<TabRestoreService::Tab>& tabs = entry_win->tabs;
185      if (!tabs.size())
186        continue;
187
188      // Create the item for the parent/window. Do not set the title yet because
189      // the actual number of items that are in the menu will not be known until
190      // things like the NTP are filtered out, which is done when the tab items
191      // are actually created.
192      HistoryItem* item = new HistoryItem();
193      item->session_id = entry_win->id;
194
195      // Create the submenu.
196      base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] init]);
197
198      // Create standard items within the window submenu.
199      NSString* restore_title = l10n_util::GetNSString(
200          IDS_HISTORY_CLOSED_RESTORE_WINDOW_MAC);
201      base::scoped_nsobject<NSMenuItem> restore_item(
202          [[NSMenuItem alloc] initWithTitle:restore_title
203                                     action:@selector(openHistoryMenuItem:)
204                              keyEquivalent:@""]);
205      [restore_item setTarget:controller_.get()];
206      // Duplicate the HistoryItem otherwise the different NSMenuItems will
207      // point to the same HistoryItem, which would then be double-freed when
208      // removing the items from the map or in the dtor.
209      HistoryItem* dup_item = new HistoryItem(*item);
210      menu_item_map_.insert(std::make_pair(restore_item.get(), dup_item));
211      [submenu addItem:restore_item.get()];
212      [submenu addItem:[NSMenuItem separatorItem]];
213
214      // Loop over the window's tabs and add them to the submenu.
215      NSInteger subindex = [[submenu itemArray] count];
216      std::vector<TabRestoreService::Tab>::const_iterator it;
217      for (it = tabs.begin(); it != tabs.end(); ++it) {
218        TabRestoreService::Tab tab = *it;
219        HistoryItem* tab_item = HistoryItemForTab(tab);
220        if (tab_item) {
221          item->tabs.push_back(tab_item);
222          AddItemToMenu(tab_item, submenu.get(), kRecentlyClosed + 1,
223                        subindex++);
224        }
225      }
226
227      // Now that the number of tabs that has been added is known, set the title
228      // of the parent menu item.
229      if (item->tabs.size() == 1) {
230        item->title = l10n_util::GetStringUTF16(
231            IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE);
232      } else {
233        item->title =l10n_util::GetStringFUTF16(
234            IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE,
235                base::IntToString16(item->tabs.size()));
236      }
237
238      // Sometimes it is possible for there to not be any subitems for a given
239      // window; if that is the case, do not add the entry to the main menu.
240      if ([[submenu itemArray] count] > 2) {
241        // Create the menu item parent.
242        NSMenuItem* parent_item =
243            AddItemToMenu(item, menu, kRecentlyClosed, index++);
244        [parent_item setSubmenu:submenu.get()];
245        ++added_count;
246      }
247    } else if (entry->type == TabRestoreService::TAB) {
248      TabRestoreService::Tab* tab =
249          static_cast<TabRestoreService::Tab*>(entry);
250      HistoryItem* item = HistoryItemForTab(*tab);
251      if (item) {
252        AddItemToMenu(item, menu, kRecentlyClosed, index++);
253        ++added_count;
254      }
255    }
256  }
257}
258
259void HistoryMenuBridge::TabRestoreServiceDestroyed(
260    TabRestoreService* service) {
261  // Intentionally left blank. We hold a weak reference to the service.
262}
263
264void HistoryMenuBridge::ResetMenu() {
265  NSMenu* menu = HistoryMenu();
266  ClearMenuSection(menu, kVisited);
267  ClearMenuSection(menu, kRecentlyClosed);
268}
269
270void HistoryMenuBridge::BuildMenu() {
271  // If the history service is ready, use it. Otherwise, a Notification will
272  // force an update when it's loaded.
273  if (history_service_)
274    CreateMenu();
275}
276
277HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForMenuItem(
278    NSMenuItem* item) {
279  std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.find(item);
280  if (it != menu_item_map_.end()) {
281    return it->second;
282  }
283  return NULL;
284}
285
286HistoryService* HistoryMenuBridge::service() {
287  return history_service_;
288}
289
290Profile* HistoryMenuBridge::profile() {
291  return profile_;
292}
293
294NSMenu* HistoryMenuBridge::HistoryMenu() {
295  NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU]
296                            submenu];
297  return history_menu;
298}
299
300void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, NSInteger tag) {
301  for (NSMenuItem* menu_item in [menu itemArray]) {
302    if ([menu_item tag] == tag  && [menu_item target] == controller_.get()) {
303      // This is an item that should be removed, so find the corresponding model
304      // item.
305      HistoryItem* item = HistoryItemForMenuItem(menu_item);
306
307      // Cancel favicon requests that could hold onto stale pointers. Also
308      // remove the item from the mapping.
309      if (item) {
310        CancelFaviconRequest(item);
311        menu_item_map_.erase(menu_item);
312        delete item;
313      }
314
315      // If this menu item has a submenu, recurse.
316      if ([menu_item hasSubmenu]) {
317        ClearMenuSection([menu_item submenu], tag + 1);
318      }
319
320      // Now actually remove the item from the menu.
321      [menu removeItem:menu_item];
322    }
323  }
324}
325
326NSMenuItem* HistoryMenuBridge::AddItemToMenu(HistoryItem* item,
327                                             NSMenu* menu,
328                                             NSInteger tag,
329                                             NSInteger index) {
330  // Elide the title of the history item, or use the URL if there is none.
331  std::string url = item->url.possibly_invalid_spec();
332  base::string16 full_title = item->title;
333  base::string16 title =
334      gfx::ElideText(full_title.empty() ? base::UTF8ToUTF16(url) : full_title,
335                     gfx::FontList(gfx::Font([NSFont menuFontOfSize:0])),
336                     kTitlePixelWidth,
337                     gfx::ELIDE_MIDDLE);
338
339  item->menu_item.reset(
340      [[NSMenuItem alloc] initWithTitle:base::SysUTF16ToNSString(title)
341                                 action:nil
342                          keyEquivalent:@""]);
343  [item->menu_item setTarget:controller_];
344  [item->menu_item setAction:@selector(openHistoryMenuItem:)];
345  [item->menu_item setTag:tag];
346  if (item->icon.get())
347    [item->menu_item setImage:item->icon.get()];
348  else if (!item->tabs.size())
349    [item->menu_item setImage:default_favicon_.get()];
350
351  // Add a tooltip.
352  NSString* tooltip = [NSString stringWithFormat:@"%@\n%@",
353      base::SysUTF16ToNSString(full_title), base::SysUTF8ToNSString(url)];
354  [item->menu_item setToolTip:tooltip];
355
356  [menu insertItem:item->menu_item.get() atIndex:index];
357  menu_item_map_.insert(std::make_pair(item->menu_item.get(), item));
358
359  return item->menu_item.get();
360}
361
362void HistoryMenuBridge::Init() {
363  registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
364                 content::Source<Profile>(profile_));
365  registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
366                 content::Source<Profile>(profile_));
367  registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
368                 content::Source<Profile>(profile_));
369}
370
371void HistoryMenuBridge::CreateMenu() {
372  // If we're currently running CreateMenu(), wait until it finishes.
373  if (create_in_progress_)
374    return;
375  create_in_progress_ = true;
376  need_recreate_ = false;
377
378  DCHECK(history_service_);
379
380  history::QueryOptions options;
381  options.max_count = kVisitedCount;
382  options.SetRecentDayRange(kVisitedScope);
383
384  history_service_->QueryHistory(
385      base::string16(),
386      options,
387      base::Bind(&HistoryMenuBridge::OnVisitedHistoryResults,
388                 base::Unretained(this)),
389      &cancelable_task_tracker_);
390}
391
392void HistoryMenuBridge::OnVisitedHistoryResults(
393    history::QueryResults* results) {
394  NSMenu* menu = HistoryMenu();
395  ClearMenuSection(menu, kVisited);
396  NSInteger top_item = [menu indexOfItemWithTag:kVisitedTitle] + 1;
397
398  size_t count = results->size();
399  for (size_t i = 0; i < count; ++i) {
400    const history::URLResult& result = (*results)[i];
401
402    HistoryItem* item = new HistoryItem;
403    item->title = result.title();
404    item->url = result.url();
405
406    // Need to explicitly get the favicon for each row.
407    GetFaviconForHistoryItem(item);
408
409    // This will add |item| to the |menu_item_map_|, which takes ownership.
410    AddItemToMenu(item, HistoryMenu(), kVisited, top_item + i);
411  }
412
413  // We are already invalid by the time we finished, darn.
414  if (need_recreate_)
415    CreateMenu();
416
417  create_in_progress_ = false;
418}
419
420HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForTab(
421    const TabRestoreService::Tab& entry) {
422  DCHECK(!entry.navigations.empty());
423
424  const sessions::SerializedNavigationEntry& current_navigation =
425      entry.navigations.at(entry.current_navigation_index);
426  HistoryItem* item = new HistoryItem();
427  item->title = current_navigation.title();
428  item->url = current_navigation.virtual_url();
429  item->session_id = entry.id;
430
431  // Tab navigations don't come with icons, so we always have to request them.
432  GetFaviconForHistoryItem(item);
433
434  return item;
435}
436
437void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) {
438  FaviconService* service =
439      FaviconServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS);
440  base::CancelableTaskTracker::TaskId task_id =
441      service->GetFaviconImageForPageURL(
442          item->url,
443          base::Bind(
444              &HistoryMenuBridge::GotFaviconData, base::Unretained(this), item),
445          &cancelable_task_tracker_);
446  item->icon_task_id = task_id;
447  item->icon_requested = true;
448}
449
450void HistoryMenuBridge::GotFaviconData(
451    HistoryItem* item,
452    const favicon_base::FaviconImageResult& image_result) {
453  // Since we're going to do Cocoa-y things, make sure this is the main thread.
454  DCHECK([NSThread isMainThread]);
455
456  DCHECK(item);
457  item->icon_requested = false;
458  item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;
459
460  NSImage* image = image_result.image.AsNSImage();
461  if (image) {
462    item->icon.reset([image retain]);
463    [item->menu_item setImage:item->icon.get()];
464  }
465}
466
467void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) {
468  DCHECK(item);
469  if (item->icon_requested) {
470    cancelable_task_tracker_.TryCancel(item->icon_task_id);
471    item->icon_requested = false;
472    item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;
473  }
474}
475