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