- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / history_menu_bridge.mm
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
38 namespace {
39
40 // Menus more than this many chars long will get trimmed.
41 const NSUInteger kMaximumMenuWidthInChars = 50;
42
43 // When trimming, use this many chars from each side.
44 const NSUInteger kMenuTrimSizeInChars = 25;
45
46 // Number of days to consider when getting the number of visited items.
47 const int kVisitedScope = 90;
48
49 // The number of visisted results to get.
50 const int kVisitedCount = 15;
51
52 // The number of recently closed items to get.
53 const unsigned int kRecentlyClosedCount = 10;
54
55 }  // namespace
56
57 HistoryMenuBridge::HistoryItem::HistoryItem()
58    : icon_requested(false),
59      icon_task_id(CancelableTaskTracker::kBadTaskId),
60      menu_item(nil),
61      session_id(0) {
62 }
63
64 HistoryMenuBridge::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
73 HistoryMenuBridge::HistoryItem::~HistoryItem() {
74 }
75
76 HistoryMenuBridge::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.
128 HistoryMenuBridge::~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
155 void 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
180 void 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
273 void HistoryMenuBridge::TabRestoreServiceDestroyed(
274     TabRestoreService* service) {
275   // Intentionally left blank. We hold a weak reference to the service.
276 }
277
278 void HistoryMenuBridge::ResetMenu() {
279   NSMenu* menu = HistoryMenu();
280   ClearMenuSection(menu, kVisited);
281   ClearMenuSection(menu, kRecentlyClosed);
282 }
283
284 void 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
291 HistoryMenuBridge::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
300 HistoryService* HistoryMenuBridge::service() {
301   return history_service_;
302 }
303
304 Profile* HistoryMenuBridge::profile() {
305   return profile_;
306 }
307
308 NSMenu* HistoryMenuBridge::HistoryMenu() {
309   NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU]
310                             submenu];
311   return history_menu;
312 }
313
314 void 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
340 NSMenuItem* 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 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
382 void 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
391 void 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       string16(),
406       options,
407       &cancelable_request_consumer_,
408       base::Bind(&HistoryMenuBridge::OnVisitedHistoryResults,
409                  base::Unretained(this)));
410 }
411
412 void 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
441 HistoryMenuBridge::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
458 void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) {
459   FaviconService* service =
460       FaviconServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS);
461   CancelableTaskTracker::TaskId task_id = service->GetFaviconImageForURL(
462       FaviconService::FaviconForURLParams(profile_,
463                                           item->url,
464                                           chrome::FAVICON,
465                                           gfx::kFaviconSize),
466       base::Bind(&HistoryMenuBridge::GotFaviconData,
467                  base::Unretained(this),
468                  item),
469       &cancelable_task_tracker_);
470   item->icon_task_id = task_id;
471   item->icon_requested = true;
472 }
473
474 void HistoryMenuBridge::GotFaviconData(
475     HistoryItem* item,
476     const chrome::FaviconImageResult& image_result) {
477   // Since we're going to do Cocoa-y things, make sure this is the main thread.
478   DCHECK([NSThread isMainThread]);
479
480   DCHECK(item);
481   item->icon_requested = false;
482   item->icon_task_id = CancelableTaskTracker::kBadTaskId;
483
484   NSImage* image = image_result.image.AsNSImage();
485   if (image) {
486     item->icon.reset([image retain]);
487     [item->menu_item setImage:item->icon.get()];
488   }
489 }
490
491 void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) {
492   DCHECK(item);
493   if (item->icon_requested) {
494     cancelable_task_tracker_.TryCancel(item->icon_task_id);
495     item->icon_requested = false;
496     item->icon_task_id = CancelableTaskTracker::kBadTaskId;
497   }
498 }