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