25bc67ed22b9b4bbaac16fdbbc8ee70bcf2774cd
[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/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 "content/public/browser/notification_registrar.h"
20 #include "content/public/browser/notification_source.h"
21 #include "grit/generated_resources.h"
22 #include "grit/theme_resources.h"
23 #include "grit/ui_resources.h"
24 #include "ui/base/l10n/l10n_util.h"
25 #include "ui/base/resource/resource_bundle.h"
26 #include "ui/gfx/image/image.h"
27 #include "ui/gfx/text_elider.h"
28
29 namespace {
30
31 // Maximum number of pixels to use for a menu item title.
32 const float kTitlePixelWidth = 400;
33
34 // Number of days to consider when getting the number of visited items.
35 const int kVisitedScope = 90;
36
37 // The number of visisted results to get.
38 const int kVisitedCount = 15;
39
40 // The number of recently closed items to get.
41 const unsigned int kRecentlyClosedCount = 10;
42
43 }  // namespace
44
45 HistoryMenuBridge::HistoryItem::HistoryItem()
46     : icon_requested(false),
47       icon_task_id(base::CancelableTaskTracker::kBadTaskId),
48       menu_item(nil),
49       session_id(0) {}
50
51 HistoryMenuBridge::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
59 HistoryMenuBridge::HistoryItem::~HistoryItem() {
60 }
61
62 HistoryMenuBridge::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.
114 HistoryMenuBridge::~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
141 void 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
166 void 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
259 void HistoryMenuBridge::TabRestoreServiceDestroyed(
260     TabRestoreService* service) {
261   // Intentionally left blank. We hold a weak reference to the service.
262 }
263
264 void HistoryMenuBridge::ResetMenu() {
265   NSMenu* menu = HistoryMenu();
266   ClearMenuSection(menu, kVisited);
267   ClearMenuSection(menu, kRecentlyClosed);
268 }
269
270 void 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
277 HistoryMenuBridge::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
286 HistoryService* HistoryMenuBridge::service() {
287   return history_service_;
288 }
289
290 Profile* HistoryMenuBridge::profile() {
291   return profile_;
292 }
293
294 NSMenu* HistoryMenuBridge::HistoryMenu() {
295   NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU]
296                             submenu];
297   return history_menu;
298 }
299
300 void 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
326 NSMenuItem* 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
362 void 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
371 void 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
392 void 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
420 HistoryMenuBridge::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
437 void 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
450 void 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
467 void 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 }