Home | History | Annotate | Download | only in gtk
      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