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.
5 #include "chrome/browser/ui/gtk/global_history_menu.h"
10 #include "base/bind_helpers.h"
11 #include "base/memory/weak_ptr.h"
12 #include "base/stl_util.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/browser/chrome_notification_types.h"
16 #include "chrome/browser/history/top_sites.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/sessions/tab_restore_service.h"
19 #include "chrome/browser/sessions/tab_restore_service_factory.h"
20 #include "chrome/browser/themes/theme_service_factory.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/browser_tab_restore_service_delegate.h"
23 #include "chrome/browser/ui/gtk/event_utils.h"
24 #include "chrome/browser/ui/gtk/global_menu_bar.h"
25 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
26 #include "chrome/browser/ui/gtk/gtk_util.h"
27 #include "chrome/common/url_constants.h"
28 #include "content/public/browser/notification_source.h"
29 #include "grit/generated_resources.h"
30 #include "ui/base/gtk/owned_widget_gtk.h"
31 #include "ui/base/l10n/l10n_util.h"
32 #include "ui/gfx/codec/png_codec.h"
33 #include "ui/gfx/gtk_util.h"
34 #include "ui/gfx/text_elider.h"
36 using content::OpenURLParams
;
40 // The maximum number of most visited items to display.
41 const unsigned int kMostVisitedCount
= 8;
43 // The number of recently closed items to get.
44 const unsigned int kRecentlyClosedCount
= 8;
46 // Menus more than this many chars long will get trimmed.
47 const int kMaximumMenuWidthInChars
= 50;
51 struct GlobalHistoryMenu::ClearMenuClosure
{
53 GlobalHistoryMenu
* menu_bar
;
57 struct GlobalHistoryMenu::GetIndexClosure
{
63 class GlobalHistoryMenu::HistoryItem
{
69 // The title for the menu item.
71 // The URL that will be navigated to if the user selects this item.
74 // A pointer to the menu_item. This is a weak reference in the GTK+ version
75 // because the GtkMenu must sink the reference.
78 // This ID is unique for a browser session and can be passed to the
79 // TabRestoreService to re-open the closed window or tab that this
80 // references. A non-0 session ID indicates that this is an entry can be
81 // restored that way. Otherwise, the URL will be used to open the item and
83 SessionID::id_type session_id
;
85 // If the HistoryItem is a window, this will be the vector of tabs. Note
86 // that this is a list of weak references. The |menu_item_map_| is the owner
87 // of all items. If it is not a window, then the entry is a single page and
88 // the vector will be empty.
89 std::vector
<HistoryItem
*> tabs
;
92 DISALLOW_COPY_AND_ASSIGN(HistoryItem
);
95 GlobalHistoryMenu::GlobalHistoryMenu(Browser
* browser
)
97 profile_(browser_
->profile()),
100 tab_restore_service_(NULL
),
101 weak_ptr_factory_(this) {
104 GlobalHistoryMenu::~GlobalHistoryMenu() {
105 if (tab_restore_service_
)
106 tab_restore_service_
->RemoveObserver(this);
108 STLDeleteContainerPairSecondPointers(menu_item_history_map_
.begin(),
109 menu_item_history_map_
.end());
110 menu_item_history_map_
.clear();
113 gtk_widget_destroy(history_menu_
);
114 g_object_unref(history_menu_
);
118 void GlobalHistoryMenu::Init(GtkWidget
* history_menu
,
119 GtkWidget
* history_menu_item
) {
120 history_menu_
= history_menu
;
121 g_object_ref_sink(history_menu_
);
123 // We have to connect to |history_menu_item|'s "activate" signal instead of
124 // |history_menu|'s "show" signal because we are not supposed to modify the
125 // menu during "show"
126 g_signal_connect(history_menu_item
, "activate",
127 G_CALLBACK(OnMenuActivateThunk
), this);
130 top_sites_
= profile_
->GetTopSites();
134 // Register for notification when TopSites changes so that we can update
136 registrar_
.Add(this, chrome::NOTIFICATION_TOP_SITES_CHANGED
,
137 content::Source
<history::TopSites
>(top_sites_
));
142 void GlobalHistoryMenu::GetTopSitesData() {
145 top_sites_
->GetMostVisitedURLs(
146 base::Bind(&GlobalHistoryMenu::OnTopSitesReceived
,
147 weak_ptr_factory_
.GetWeakPtr()), false);
150 void GlobalHistoryMenu::OnTopSitesReceived(
151 const history::MostVisitedURLList
& visited_list
) {
152 ClearMenuSection(history_menu_
, GlobalMenuBar::TAG_MOST_VISITED
);
154 int index
= GetIndexOfMenuItemWithTag(
156 GlobalMenuBar::TAG_MOST_VISITED_HEADER
) + 1;
158 for (size_t i
= 0; i
< visited_list
.size() && i
< kMostVisitedCount
; ++i
) {
159 const history::MostVisitedURL
& visited
= visited_list
[i
];
160 if (visited
.url
.spec().empty())
161 break; // This is the signal that there are no more real visited sites.
163 HistoryItem
* item
= new HistoryItem();
164 item
->title
= visited
.title
;
165 item
->url
= visited
.url
;
167 AddHistoryItemToMenu(item
,
169 GlobalMenuBar::TAG_MOST_VISITED
,
174 GlobalHistoryMenu::HistoryItem
* GlobalHistoryMenu::HistoryItemForMenuItem(
175 GtkWidget
* menu_item
) {
176 MenuItemToHistoryMap::iterator it
= menu_item_history_map_
.find(menu_item
);
177 return it
!= menu_item_history_map_
.end() ? it
->second
: NULL
;
180 GlobalHistoryMenu::HistoryItem
* GlobalHistoryMenu::HistoryItemForTab(
181 const TabRestoreService::Tab
& entry
) {
182 const sessions::SerializedNavigationEntry
& current_navigation
=
183 entry
.navigations
.at(entry
.current_navigation_index
);
184 HistoryItem
* item
= new HistoryItem();
185 item
->title
= current_navigation
.title();
186 item
->url
= current_navigation
.virtual_url();
187 item
->session_id
= entry
.id
;
192 GtkWidget
* GlobalHistoryMenu::AddHistoryItemToMenu(HistoryItem
* item
,
196 base::string16 title
= item
->title
;
197 std::string url_string
= item
->url
.possibly_invalid_spec();
200 title
= base::UTF8ToUTF16(url_string
);
201 gfx::ElideString(title
, kMaximumMenuWidthInChars
, &title
);
203 GtkWidget
* menu_item
= gtk_menu_item_new_with_label(
204 base::UTF16ToUTF8(title
).c_str());
206 item
->menu_item
= menu_item
;
207 gtk_widget_show(menu_item
);
208 g_object_set_data(G_OBJECT(menu_item
), "type-tag", GINT_TO_POINTER(tag
));
209 g_signal_connect(menu_item
, "activate",
210 G_CALLBACK(OnRecentlyClosedItemActivatedThunk
), this);
212 std::string tooltip
= gtk_util::BuildTooltipTitleFor(item
->title
, item
->url
);
213 gtk_widget_set_tooltip_markup(menu_item
, tooltip
.c_str());
215 menu_item_history_map_
.insert(std::make_pair(menu_item
, item
));
216 gtk_menu_shell_insert(GTK_MENU_SHELL(menu
), menu_item
, index
);
221 int GlobalHistoryMenu::GetIndexOfMenuItemWithTag(GtkWidget
* menu
, int tag_id
) {
222 GetIndexClosure closure
;
223 closure
.found
= false;
224 closure
.current_index
= 0;
225 closure
.tag
= tag_id
;
227 gtk_container_foreach(
229 reinterpret_cast<void (*)(GtkWidget
*, void*)>(GetIndexCallback
),
232 return closure
.current_index
;
235 void GlobalHistoryMenu::ClearMenuSection(GtkWidget
* menu
, int tag
) {
236 ClearMenuClosure closure
;
237 closure
.container
= menu
;
238 closure
.menu_bar
= this;
241 gtk_container_foreach(
243 reinterpret_cast<void (*)(GtkWidget
*, void*)>(ClearMenuCallback
),
248 void GlobalHistoryMenu::GetIndexCallback(GtkWidget
* menu_item
,
249 GetIndexClosure
* closure
) {
250 int tag
= GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menu_item
), "type-tag"));
251 if (tag
== closure
->tag
)
252 closure
->found
= true;
255 closure
->current_index
++;
259 void GlobalHistoryMenu::ClearMenuCallback(GtkWidget
* menu_item
,
260 ClearMenuClosure
* closure
) {
261 DCHECK_NE(closure
->tag
, 0);
263 int tag
= GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menu_item
), "type-tag"));
264 if (closure
->tag
== tag
) {
265 HistoryItem
* item
= closure
->menu_bar
->HistoryItemForMenuItem(menu_item
);
268 closure
->menu_bar
->menu_item_history_map_
.erase(menu_item
);
272 GtkWidget
* submenu
= gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item
));
274 closure
->menu_bar
->ClearMenuSection(submenu
, closure
->tag
);
276 gtk_container_remove(GTK_CONTAINER(closure
->container
), menu_item
);
280 void GlobalHistoryMenu::Observe(int type
,
281 const content::NotificationSource
& source
,
282 const content::NotificationDetails
& details
) {
283 if (type
== chrome::NOTIFICATION_TOP_SITES_CHANGED
) {
290 void GlobalHistoryMenu::TabRestoreServiceChanged(TabRestoreService
* service
) {
291 const TabRestoreService::Entries
& entries
= service
->entries();
293 ClearMenuSection(history_menu_
, GlobalMenuBar::TAG_RECENTLY_CLOSED
);
295 // We'll get the index the "Recently Closed" header. (This can vary depending
296 // on the number of "Most Visited" items.
297 int index
= GetIndexOfMenuItemWithTag(
299 GlobalMenuBar::TAG_RECENTLY_CLOSED_HEADER
) + 1;
301 unsigned int added_count
= 0;
302 for (TabRestoreService::Entries::const_iterator it
= entries
.begin();
303 it
!= entries
.end() && added_count
< kRecentlyClosedCount
; ++it
) {
304 TabRestoreService::Entry
* entry
= *it
;
306 if (entry
->type
== TabRestoreService::WINDOW
) {
307 TabRestoreService::Window
* entry_win
=
308 static_cast<TabRestoreService::Window
*>(entry
);
309 std::vector
<TabRestoreService::Tab
>& tabs
= entry_win
->tabs
;
313 // Create the item for the parent/window.
314 HistoryItem
* item
= new HistoryItem();
315 item
->session_id
= entry_win
->id
;
317 GtkWidget
* submenu
= gtk_menu_new();
318 GtkWidget
* restore_item
= gtk_menu_item_new_with_label(
319 l10n_util::GetStringUTF8(
320 IDS_HISTORY_CLOSED_RESTORE_WINDOW_LINUX
).c_str());
321 g_object_set_data(G_OBJECT(restore_item
), "type-tag",
322 GINT_TO_POINTER(GlobalMenuBar::TAG_RECENTLY_CLOSED
));
323 g_signal_connect(restore_item
, "activate",
324 G_CALLBACK(OnRecentlyClosedItemActivatedThunk
), this);
325 gtk_widget_show(restore_item
);
327 // The mac version of this code allows the user to click on the parent
328 // menu item to have the same effect as clicking the restore window
329 // submenu item. GTK+ helpfully activates a menu item when it shows a
330 // submenu so toss that feature out.
331 menu_item_history_map_
.insert(std::make_pair(restore_item
, item
));
332 gtk_menu_shell_append(GTK_MENU_SHELL(submenu
), restore_item
);
334 GtkWidget
* separator
= gtk_separator_menu_item_new();
335 gtk_widget_show(separator
);
336 gtk_menu_shell_append(GTK_MENU_SHELL(submenu
), separator
);
338 // Loop over the window's tabs and add them to the submenu.
340 std::vector
<TabRestoreService::Tab
>::const_iterator iter
;
341 for (iter
= tabs
.begin(); iter
!= tabs
.end(); ++iter
) {
342 TabRestoreService::Tab tab
= *iter
;
343 HistoryItem
* tab_item
= HistoryItemForTab(tab
);
344 item
->tabs
.push_back(tab_item
);
345 AddHistoryItemToMenu(tab_item
,
347 GlobalMenuBar::TAG_RECENTLY_CLOSED
,
351 std::string title
= item
->tabs
.size() == 1 ?
352 l10n_util::GetStringUTF8(
353 IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE
) :
354 l10n_util::GetStringFUTF8(
355 IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE
,
356 base::IntToString16(item
->tabs
.size()));
358 // Create the menu item parent. Unlike mac, it's can't be activated.
359 GtkWidget
* parent_item
= gtk_menu_item_new_with_label(title
.c_str());
360 gtk_widget_show(parent_item
);
361 g_object_set_data(G_OBJECT(parent_item
), "type-tag",
362 GINT_TO_POINTER(GlobalMenuBar::TAG_RECENTLY_CLOSED
));
363 gtk_menu_item_set_submenu(GTK_MENU_ITEM(parent_item
), submenu
);
365 gtk_menu_shell_insert(GTK_MENU_SHELL(history_menu_
), parent_item
,
368 } else if (entry
->type
== TabRestoreService::TAB
) {
369 TabRestoreService::Tab
* tab
= static_cast<TabRestoreService::Tab
*>(entry
);
370 HistoryItem
* item
= HistoryItemForTab(*tab
);
371 AddHistoryItemToMenu(item
,
373 GlobalMenuBar::TAG_RECENTLY_CLOSED
,
380 void GlobalHistoryMenu::TabRestoreServiceDestroyed(
381 TabRestoreService
* service
) {
382 tab_restore_service_
= NULL
;
385 void GlobalHistoryMenu::OnRecentlyClosedItemActivated(GtkWidget
* sender
) {
386 WindowOpenDisposition disposition
=
387 event_utils::DispositionForCurrentButtonPressEvent();
388 HistoryItem
* item
= HistoryItemForMenuItem(sender
);
390 // If this item can be restored using TabRestoreService, do so. Otherwise,
391 // just load the URL.
392 TabRestoreService
* service
=
393 TabRestoreServiceFactory::GetForProfile(browser_
->profile());
394 if (item
->session_id
&& service
) {
395 service
->RestoreEntryById(browser_
->tab_restore_service_delegate(),
396 item
->session_id
, browser_
->host_desktop_type(),
399 DCHECK(item
->url
.is_valid());
400 browser_
->OpenURL(OpenURLParams(item
->url
, content::Referrer(), disposition
,
401 content::PAGE_TRANSITION_AUTO_BOOKMARK
, false));
405 void GlobalHistoryMenu::OnMenuActivate(GtkWidget
* sender
) {
406 if (!tab_restore_service_
) {
407 tab_restore_service_
= TabRestoreServiceFactory::GetForProfile(profile_
);
408 if (tab_restore_service_
) {
409 tab_restore_service_
->LoadTabsFromLastSession();
410 tab_restore_service_
->AddObserver(this);
412 // If LoadTabsFromLastSession doesn't load tabs, it won't call
413 // TabRestoreServiceChanged(). This ensures that all new windows after
414 // the first one will have their menus populated correctly.
415 TabRestoreServiceChanged(tab_restore_service_
);