OLD | NEW |
| (Empty) |
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/gtk/global_history_menu.h" | |
6 | |
7 #include <gtk/gtk.h> | |
8 | |
9 #include "base/bind.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" | |
35 | |
36 using content::OpenURLParams; | |
37 | |
38 namespace { | |
39 | |
40 // The maximum number of most visited items to display. | |
41 const unsigned int kMostVisitedCount = 8; | |
42 | |
43 // The number of recently closed items to get. | |
44 const unsigned int kRecentlyClosedCount = 8; | |
45 | |
46 // Menus more than this many chars long will get trimmed. | |
47 const int kMaximumMenuWidthInChars = 50; | |
48 | |
49 } // namespace | |
50 | |
51 struct GlobalHistoryMenu::ClearMenuClosure { | |
52 GtkWidget* container; | |
53 GlobalHistoryMenu* menu_bar; | |
54 int tag; | |
55 }; | |
56 | |
57 struct GlobalHistoryMenu::GetIndexClosure { | |
58 bool found; | |
59 int current_index; | |
60 int tag; | |
61 }; | |
62 | |
63 class GlobalHistoryMenu::HistoryItem { | |
64 public: | |
65 HistoryItem() | |
66 : menu_item(NULL), | |
67 session_id(0) {} | |
68 | |
69 // The title for the menu item. | |
70 base::string16 title; | |
71 // The URL that will be navigated to if the user selects this item. | |
72 GURL url; | |
73 | |
74 // A pointer to the menu_item. This is a weak reference in the GTK+ version | |
75 // because the GtkMenu must sink the reference. | |
76 GtkWidget* menu_item; | |
77 | |
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 | |
82 // this ID will be 0. | |
83 SessionID::id_type session_id; | |
84 | |
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; | |
90 | |
91 private: | |
92 DISALLOW_COPY_AND_ASSIGN(HistoryItem); | |
93 }; | |
94 | |
95 GlobalHistoryMenu::GlobalHistoryMenu(Browser* browser) | |
96 : browser_(browser), | |
97 profile_(browser_->profile()), | |
98 history_menu_(NULL), | |
99 top_sites_(NULL), | |
100 tab_restore_service_(NULL), | |
101 weak_ptr_factory_(this) { | |
102 } | |
103 | |
104 GlobalHistoryMenu::~GlobalHistoryMenu() { | |
105 if (tab_restore_service_) | |
106 tab_restore_service_->RemoveObserver(this); | |
107 | |
108 STLDeleteContainerPairSecondPointers(menu_item_history_map_.begin(), | |
109 menu_item_history_map_.end()); | |
110 menu_item_history_map_.clear(); | |
111 | |
112 if (history_menu_) { | |
113 gtk_widget_destroy(history_menu_); | |
114 g_object_unref(history_menu_); | |
115 } | |
116 } | |
117 | |
118 void GlobalHistoryMenu::Init(GtkWidget* history_menu, | |
119 GtkWidget* history_menu_item) { | |
120 history_menu_ = history_menu; | |
121 g_object_ref_sink(history_menu_); | |
122 | |
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); | |
128 | |
129 if (profile_) { | |
130 top_sites_ = profile_->GetTopSites(); | |
131 if (top_sites_) { | |
132 GetTopSitesData(); | |
133 | |
134 // Register for notification when TopSites changes so that we can update | |
135 // ourself. | |
136 registrar_.Add(this, chrome::NOTIFICATION_TOP_SITES_CHANGED, | |
137 content::Source<history::TopSites>(top_sites_)); | |
138 } | |
139 } | |
140 } | |
141 | |
142 void GlobalHistoryMenu::GetTopSitesData() { | |
143 DCHECK(top_sites_); | |
144 | |
145 top_sites_->GetMostVisitedURLs( | |
146 base::Bind(&GlobalHistoryMenu::OnTopSitesReceived, | |
147 weak_ptr_factory_.GetWeakPtr()), false); | |
148 } | |
149 | |
150 void GlobalHistoryMenu::OnTopSitesReceived( | |
151 const history::MostVisitedURLList& visited_list) { | |
152 ClearMenuSection(history_menu_, GlobalMenuBar::TAG_MOST_VISITED); | |
153 | |
154 int index = GetIndexOfMenuItemWithTag( | |
155 history_menu_, | |
156 GlobalMenuBar::TAG_MOST_VISITED_HEADER) + 1; | |
157 | |
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. | |
162 | |
163 HistoryItem* item = new HistoryItem(); | |
164 item->title = visited.title; | |
165 item->url = visited.url; | |
166 | |
167 AddHistoryItemToMenu(item, | |
168 history_menu_, | |
169 GlobalMenuBar::TAG_MOST_VISITED, | |
170 index++); | |
171 } | |
172 } | |
173 | |
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; | |
178 } | |
179 | |
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; | |
188 | |
189 return item; | |
190 } | |
191 | |
192 GtkWidget* GlobalHistoryMenu::AddHistoryItemToMenu(HistoryItem* item, | |
193 GtkWidget* menu, | |
194 int tag, | |
195 int index) { | |
196 base::string16 title = item->title; | |
197 std::string url_string = item->url.possibly_invalid_spec(); | |
198 | |
199 if (title.empty()) | |
200 title = base::UTF8ToUTF16(url_string); | |
201 gfx::ElideString(title, kMaximumMenuWidthInChars, &title); | |
202 | |
203 GtkWidget* menu_item = gtk_menu_item_new_with_label( | |
204 base::UTF16ToUTF8(title).c_str()); | |
205 | |
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); | |
211 | |
212 std::string tooltip = gtk_util::BuildTooltipTitleFor(item->title, item->url); | |
213 gtk_widget_set_tooltip_markup(menu_item, tooltip.c_str()); | |
214 | |
215 menu_item_history_map_.insert(std::make_pair(menu_item, item)); | |
216 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menu_item, index); | |
217 | |
218 return menu_item; | |
219 } | |
220 | |
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; | |
226 | |
227 gtk_container_foreach( | |
228 GTK_CONTAINER(menu), | |
229 reinterpret_cast<void (*)(GtkWidget*, void*)>(GetIndexCallback), | |
230 &closure); | |
231 | |
232 return closure.current_index; | |
233 } | |
234 | |
235 void GlobalHistoryMenu::ClearMenuSection(GtkWidget* menu, int tag) { | |
236 ClearMenuClosure closure; | |
237 closure.container = menu; | |
238 closure.menu_bar = this; | |
239 closure.tag = tag; | |
240 | |
241 gtk_container_foreach( | |
242 GTK_CONTAINER(menu), | |
243 reinterpret_cast<void (*)(GtkWidget*, void*)>(ClearMenuCallback), | |
244 &closure); | |
245 } | |
246 | |
247 // static | |
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; | |
253 | |
254 if (!closure->found) | |
255 closure->current_index++; | |
256 } | |
257 | |
258 // static | |
259 void GlobalHistoryMenu::ClearMenuCallback(GtkWidget* menu_item, | |
260 ClearMenuClosure* closure) { | |
261 DCHECK_NE(closure->tag, 0); | |
262 | |
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); | |
266 | |
267 if (item) { | |
268 closure->menu_bar->menu_item_history_map_.erase(menu_item); | |
269 delete item; | |
270 } | |
271 | |
272 GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)); | |
273 if (submenu) | |
274 closure->menu_bar->ClearMenuSection(submenu, closure->tag); | |
275 | |
276 gtk_container_remove(GTK_CONTAINER(closure->container), menu_item); | |
277 } | |
278 } | |
279 | |
280 void GlobalHistoryMenu::Observe(int type, | |
281 const content::NotificationSource& source, | |
282 const content::NotificationDetails& details) { | |
283 if (type == chrome::NOTIFICATION_TOP_SITES_CHANGED) { | |
284 GetTopSitesData(); | |
285 } else { | |
286 NOTREACHED(); | |
287 } | |
288 } | |
289 | |
290 void GlobalHistoryMenu::TabRestoreServiceChanged(TabRestoreService* service) { | |
291 const TabRestoreService::Entries& entries = service->entries(); | |
292 | |
293 ClearMenuSection(history_menu_, GlobalMenuBar::TAG_RECENTLY_CLOSED); | |
294 | |
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( | |
298 history_menu_, | |
299 GlobalMenuBar::TAG_RECENTLY_CLOSED_HEADER) + 1; | |
300 | |
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; | |
305 | |
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; | |
310 if (tabs.empty()) | |
311 continue; | |
312 | |
313 // Create the item for the parent/window. | |
314 HistoryItem* item = new HistoryItem(); | |
315 item->session_id = entry_win->id; | |
316 | |
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); | |
326 | |
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); | |
333 | |
334 GtkWidget* separator = gtk_separator_menu_item_new(); | |
335 gtk_widget_show(separator); | |
336 gtk_menu_shell_append(GTK_MENU_SHELL(submenu), separator); | |
337 | |
338 // Loop over the window's tabs and add them to the submenu. | |
339 int subindex = 2; | |
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, | |
346 submenu, | |
347 GlobalMenuBar::TAG_RECENTLY_CLOSED, | |
348 subindex++); | |
349 } | |
350 | |
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())); | |
357 | |
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); | |
364 | |
365 gtk_menu_shell_insert(GTK_MENU_SHELL(history_menu_), parent_item, | |
366 index++); | |
367 ++added_count; | |
368 } else if (entry->type == TabRestoreService::TAB) { | |
369 TabRestoreService::Tab* tab = static_cast<TabRestoreService::Tab*>(entry); | |
370 HistoryItem* item = HistoryItemForTab(*tab); | |
371 AddHistoryItemToMenu(item, | |
372 history_menu_, | |
373 GlobalMenuBar::TAG_RECENTLY_CLOSED, | |
374 index++); | |
375 ++added_count; | |
376 } | |
377 } | |
378 } | |
379 | |
380 void GlobalHistoryMenu::TabRestoreServiceDestroyed( | |
381 TabRestoreService* service) { | |
382 tab_restore_service_ = NULL; | |
383 } | |
384 | |
385 void GlobalHistoryMenu::OnRecentlyClosedItemActivated(GtkWidget* sender) { | |
386 WindowOpenDisposition disposition = | |
387 event_utils::DispositionForCurrentButtonPressEvent(); | |
388 HistoryItem* item = HistoryItemForMenuItem(sender); | |
389 | |
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(), | |
397 UNKNOWN); | |
398 } else { | |
399 DCHECK(item->url.is_valid()); | |
400 browser_->OpenURL(OpenURLParams(item->url, content::Referrer(), disposition, | |
401 content::PAGE_TRANSITION_AUTO_BOOKMARK, false)); | |
402 } | |
403 } | |
404 | |
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); | |
411 | |
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_); | |
416 } | |
417 } | |
418 } | |
OLD | NEW |