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/base/text/text_elider.h" 33 #include "ui/gfx/codec/png_codec.h" 34 #include "ui/gfx/gtk_util.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 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 weak_ptr_factory_(this), 101 tab_restore_service_(NULL) { 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())); 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 string16 title = item->title; 197 std::string url_string = item->url.possibly_invalid_spec(); 198 199 if (title.empty()) 200 title = UTF8ToUTF16(url_string); 201 ui::ElideString(title, kMaximumMenuWidthInChars, &title); 202 203 GtkWidget* menu_item = gtk_menu_item_new_with_label( 204 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 } 419