Home | History | Annotate | Download | only in download
      1 // Copyright (c) 2011 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/download/download_shelf_gtk.h"
      6 
      7 #include <string>
      8 
      9 #include "chrome/browser/download/download_item.h"
     10 #include "chrome/browser/download/download_item_model.h"
     11 #include "chrome/browser/download/download_util.h"
     12 #include "chrome/browser/ui/browser.h"
     13 #include "chrome/browser/ui/gtk/browser_window_gtk.h"
     14 #include "chrome/browser/ui/gtk/custom_button.h"
     15 #include "chrome/browser/ui/gtk/download/download_item_gtk.h"
     16 #include "chrome/browser/ui/gtk/gtk_chrome_link_button.h"
     17 #include "chrome/browser/ui/gtk/gtk_chrome_shrinkable_hbox.h"
     18 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
     19 #include "chrome/browser/ui/gtk/gtk_util.h"
     20 #include "content/common/notification_service.h"
     21 #include "grit/generated_resources.h"
     22 #include "grit/theme_resources.h"
     23 #include "ui/base/l10n/l10n_util.h"
     24 #include "ui/base/resource/resource_bundle.h"
     25 #include "ui/gfx/gtk_util.h"
     26 #include "ui/gfx/insets.h"
     27 #include "ui/gfx/point.h"
     28 #include "ui/gfx/rect.h"
     29 
     30 namespace {
     31 
     32 // The height of the download items.
     33 const int kDownloadItemHeight = download_util::kSmallProgressIconSize;
     34 
     35 // Padding between the download widgets.
     36 const int kDownloadItemPadding = 10;
     37 
     38 // Padding between the top/bottom of the download widgets and the edge of the
     39 // shelf.
     40 const int kTopBottomPadding = 4;
     41 
     42 // Padding between the left side of the shelf and the first download item.
     43 const int kLeftPadding = 2;
     44 
     45 // Padding between the right side of the shelf and the close button.
     46 const int kRightPadding = 10;
     47 
     48 // Speed of the shelf show/hide animation.
     49 const int kShelfAnimationDurationMs = 120;
     50 
     51 // The time between when the user mouses out of the download shelf zone and
     52 // when the shelf closes (when auto-close is enabled).
     53 const int kAutoCloseDelayMs = 300;
     54 
     55 // The area to the top of the shelf that is considered part of its "zone".
     56 const int kShelfAuraSize = 40;
     57 
     58 }  // namespace
     59 
     60 DownloadShelfGtk::DownloadShelfGtk(Browser* browser, GtkWidget* parent)
     61     : browser_(browser),
     62       is_showing_(false),
     63       theme_service_(GtkThemeService::GetFrom(browser->profile())),
     64       close_on_mouse_out_(false),
     65       mouse_in_shelf_(false),
     66       auto_close_factory_(this) {
     67   // Logically, the shelf is a vbox that contains two children: a one pixel
     68   // tall event box, which serves as the top border, and an hbox, which holds
     69   // the download items and other shelf widgets (close button, show-all-
     70   // downloads link).
     71   // To make things pretty, we have to add a few more widgets. To get padding
     72   // right, we stick the hbox in an alignment. We put that alignment in an
     73   // event box so we can color the background.
     74 
     75   // Create the top border.
     76   top_border_ = gtk_event_box_new();
     77   gtk_widget_set_size_request(GTK_WIDGET(top_border_), 0, 1);
     78 
     79   // Create |items_hbox_|. We use GtkChromeShrinkableHBox, so that download
     80   // items can be hid automatically when there is no enough space to show them.
     81   items_hbox_.Own(gtk_chrome_shrinkable_hbox_new(
     82       TRUE, FALSE, kDownloadItemPadding));
     83   // We want the download shelf to be horizontally shrinkable, so that the
     84   // Chrome window can be resized freely even with many download items.
     85   gtk_widget_set_size_request(items_hbox_.get(), 0, kDownloadItemHeight);
     86 
     87   // Create a hbox that holds |items_hbox_| and other shelf widgets.
     88   GtkWidget* outer_hbox = gtk_hbox_new(FALSE, kDownloadItemPadding);
     89 
     90   // Pack the |items_hbox_| in the outer hbox.
     91   gtk_box_pack_start(GTK_BOX(outer_hbox), items_hbox_.get(), TRUE, TRUE, 0);
     92 
     93   // Get the padding and background color for |outer_hbox| right.
     94   GtkWidget* padding = gtk_alignment_new(0, 0, 1, 1);
     95   // Subtract 1 from top spacing to account for top border.
     96   gtk_alignment_set_padding(GTK_ALIGNMENT(padding),
     97       kTopBottomPadding - 1, kTopBottomPadding, kLeftPadding, kRightPadding);
     98   padding_bg_ = gtk_event_box_new();
     99   gtk_container_add(GTK_CONTAINER(padding_bg_), padding);
    100   gtk_container_add(GTK_CONTAINER(padding), outer_hbox);
    101 
    102   GtkWidget* vbox = gtk_vbox_new(FALSE, 0);
    103   gtk_box_pack_start(GTK_BOX(vbox), top_border_, FALSE, FALSE, 0);
    104   gtk_box_pack_start(GTK_BOX(vbox), padding_bg_, FALSE, FALSE, 0);
    105 
    106   // Put the shelf in an event box so it gets its own window, which makes it
    107   // easier to get z-ordering right.
    108   shelf_.Own(gtk_event_box_new());
    109   gtk_container_add(GTK_CONTAINER(shelf_.get()), vbox);
    110 
    111   // Create and pack the close button.
    112   close_button_.reset(CustomDrawButton::CloseButton(theme_service_));
    113   gtk_util::CenterWidgetInHBox(outer_hbox, close_button_->widget(), true, 0);
    114   g_signal_connect(close_button_->widget(), "clicked",
    115                    G_CALLBACK(OnButtonClickThunk), this);
    116 
    117   // Create the "Show all downloads..." link and connect to the click event.
    118   std::string link_text =
    119       l10n_util::GetStringUTF8(IDS_SHOW_ALL_DOWNLOADS);
    120   link_button_ = gtk_chrome_link_button_new(link_text.c_str());
    121   g_signal_connect(link_button_, "clicked",
    122                    G_CALLBACK(OnButtonClickThunk), this);
    123   gtk_util::SetButtonTriggersNavigation(link_button_);
    124   // Until we switch to vector graphics, force the font size.
    125   // 13.4px == 10pt @ 96dpi
    126   gtk_util::ForceFontSizePixels(GTK_CHROME_LINK_BUTTON(link_button_)->label,
    127                                 13.4);
    128 
    129   // Make the download arrow icon.
    130   ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    131   GdkPixbuf* download_pixbuf = rb.GetPixbufNamed(IDR_DOWNLOADS_FAVICON);
    132   GtkWidget* download_image = gtk_image_new_from_pixbuf(download_pixbuf);
    133 
    134   // Pack the link and the icon in outer hbox.
    135   gtk_util::CenterWidgetInHBox(outer_hbox, link_button_, true, 0);
    136   gtk_util::CenterWidgetInHBox(outer_hbox, download_image, true, 0);
    137 
    138   slide_widget_.reset(new SlideAnimatorGtk(shelf_.get(),
    139                                            SlideAnimatorGtk::UP,
    140                                            kShelfAnimationDurationMs,
    141                                            false, true, this));
    142 
    143   theme_service_->InitThemesFor(this);
    144   registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED,
    145                  NotificationService::AllSources());
    146 
    147   gtk_widget_show_all(shelf_.get());
    148 
    149   // Stick ourselves at the bottom of the parent browser.
    150   gtk_box_pack_end(GTK_BOX(parent), slide_widget_->widget(),
    151                    FALSE, FALSE, 0);
    152   // Make sure we are at the very end.
    153   gtk_box_reorder_child(GTK_BOX(parent), slide_widget_->widget(), 0);
    154   Show();
    155 }
    156 
    157 DownloadShelfGtk::~DownloadShelfGtk() {
    158   for (std::vector<DownloadItemGtk*>::iterator iter = download_items_.begin();
    159        iter != download_items_.end(); ++iter) {
    160     delete *iter;
    161   }
    162 
    163   shelf_.Destroy();
    164   items_hbox_.Destroy();
    165 
    166   // Make sure we're no longer an observer of the message loop.
    167   SetCloseOnMouseOut(false);
    168 }
    169 
    170 void DownloadShelfGtk::AddDownload(BaseDownloadItemModel* download_model_) {
    171   download_items_.push_back(new DownloadItemGtk(this, download_model_));
    172   Show();
    173 }
    174 
    175 bool DownloadShelfGtk::IsShowing() const {
    176   return slide_widget_->IsShowing();
    177 }
    178 
    179 bool DownloadShelfGtk::IsClosing() const {
    180   return slide_widget_->IsClosing();
    181 }
    182 
    183 void DownloadShelfGtk::Show() {
    184   slide_widget_->Open();
    185   browser_->UpdateDownloadShelfVisibility(true);
    186   CancelAutoClose();
    187 }
    188 
    189 void DownloadShelfGtk::Close() {
    190   // When we are closing, we can vertically overlap the render view. Make sure
    191   // we are on top.
    192   gdk_window_raise(shelf_.get()->window);
    193   slide_widget_->Close();
    194   browser_->UpdateDownloadShelfVisibility(false);
    195   SetCloseOnMouseOut(false);
    196 }
    197 
    198 Browser* DownloadShelfGtk::browser() const {
    199   return browser_;
    200 }
    201 
    202 void DownloadShelfGtk::Closed() {
    203   // When the close animation is complete, remove all completed downloads.
    204   size_t i = 0;
    205   while (i < download_items_.size()) {
    206     DownloadItem* download = download_items_[i]->get_download();
    207     bool is_transfer_done = download->IsComplete() ||
    208                             download->IsCancelled() ||
    209                             download->IsInterrupted();
    210     if (is_transfer_done &&
    211         download->safety_state() != DownloadItem::DANGEROUS) {
    212       RemoveDownloadItem(download_items_[i]);
    213     } else {
    214       // We set all remaining items as "opened", so that the shelf will auto-
    215       // close in the future without the user clicking on them.
    216       download->set_opened(true);
    217       ++i;
    218     }
    219   }
    220 }
    221 
    222 void DownloadShelfGtk::Observe(NotificationType type,
    223                                const NotificationSource& source,
    224                                const NotificationDetails& details) {
    225   if (type == NotificationType::BROWSER_THEME_CHANGED) {
    226     GdkColor color = theme_service_->GetGdkColor(
    227         ThemeService::COLOR_TOOLBAR);
    228     gtk_widget_modify_bg(padding_bg_, GTK_STATE_NORMAL, &color);
    229 
    230     color = theme_service_->GetBorderColor();
    231     gtk_widget_modify_bg(top_border_, GTK_STATE_NORMAL, &color);
    232 
    233     gtk_chrome_link_button_set_use_gtk_theme(
    234         GTK_CHROME_LINK_BUTTON(link_button_), theme_service_->UseGtkTheme());
    235 
    236     // When using a non-standard, non-gtk theme, we make the link color match
    237     // the bookmark text color. Otherwise, standard link blue can look very
    238     // bad for some dark themes.
    239     bool use_default_color = theme_service_->GetColor(
    240         ThemeService::COLOR_BOOKMARK_TEXT) ==
    241         ThemeService::GetDefaultColor(
    242             ThemeService::COLOR_BOOKMARK_TEXT);
    243     GdkColor bookmark_color = theme_service_->GetGdkColor(
    244         ThemeService::COLOR_BOOKMARK_TEXT);
    245     gtk_chrome_link_button_set_normal_color(
    246         GTK_CHROME_LINK_BUTTON(link_button_),
    247         use_default_color ? NULL : &bookmark_color);
    248 
    249     ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    250     close_button_->SetBackground(
    251         theme_service_->GetColor(ThemeService::COLOR_TAB_TEXT),
    252         rb.GetBitmapNamed(IDR_CLOSE_BAR),
    253         rb.GetBitmapNamed(IDR_CLOSE_BAR_MASK));
    254   }
    255 }
    256 
    257 int DownloadShelfGtk::GetHeight() const {
    258   return slide_widget_->widget()->allocation.height;
    259 }
    260 
    261 void DownloadShelfGtk::RemoveDownloadItem(DownloadItemGtk* download_item) {
    262   DCHECK(download_item);
    263   std::vector<DownloadItemGtk*>::iterator i =
    264       find(download_items_.begin(), download_items_.end(), download_item);
    265   DCHECK(i != download_items_.end());
    266   download_items_.erase(i);
    267   delete download_item;
    268   if (download_items_.empty()) {
    269     slide_widget_->CloseWithoutAnimation();
    270     browser_->UpdateDownloadShelfVisibility(false);
    271   } else {
    272     AutoCloseIfPossible();
    273   }
    274 }
    275 
    276 GtkWidget* DownloadShelfGtk::GetHBox() const {
    277   return items_hbox_.get();
    278 }
    279 
    280 void DownloadShelfGtk::MaybeShowMoreDownloadItems() {
    281   // Show all existing download items. It'll trigger "size-allocate" signal,
    282   // which will hide download items that don't have enough space to show.
    283   gtk_widget_show_all(items_hbox_.get());
    284 }
    285 
    286 void DownloadShelfGtk::OnButtonClick(GtkWidget* button) {
    287   if (button == close_button_->widget()) {
    288     Close();
    289   } else {
    290     // The link button was clicked.
    291     browser_->ShowDownloadsTab();
    292   }
    293 }
    294 
    295 void DownloadShelfGtk::AutoCloseIfPossible() {
    296   for (std::vector<DownloadItemGtk*>::iterator iter = download_items_.begin();
    297        iter != download_items_.end(); ++iter) {
    298     if (!(*iter)->get_download()->opened())
    299       return;
    300   }
    301 
    302   SetCloseOnMouseOut(true);
    303 }
    304 
    305 void DownloadShelfGtk::CancelAutoClose() {
    306   SetCloseOnMouseOut(false);
    307   auto_close_factory_.RevokeAll();
    308 }
    309 
    310 void DownloadShelfGtk::ItemOpened() {
    311   AutoCloseIfPossible();
    312 }
    313 
    314 void DownloadShelfGtk::SetCloseOnMouseOut(bool close) {
    315   if (close_on_mouse_out_ == close)
    316     return;
    317 
    318   close_on_mouse_out_ = close;
    319   mouse_in_shelf_ = close;
    320   if (close)
    321     MessageLoopForUI::current()->AddObserver(this);
    322   else
    323     MessageLoopForUI::current()->RemoveObserver(this);
    324 }
    325 
    326 void DownloadShelfGtk::WillProcessEvent(GdkEvent* event) {
    327 }
    328 
    329 void DownloadShelfGtk::DidProcessEvent(GdkEvent* event) {
    330   gfx::Point cursor_screen_coords;
    331 
    332   switch (event->type) {
    333     case GDK_MOTION_NOTIFY:
    334       cursor_screen_coords =
    335           gfx::Point(event->motion.x_root, event->motion.y_root);
    336       break;
    337     case GDK_LEAVE_NOTIFY:
    338       cursor_screen_coords =
    339           gfx::Point(event->crossing.x_root, event->crossing.y_root);
    340       break;
    341     default:
    342       return;
    343   }
    344 
    345   bool mouse_in_shelf = IsCursorInShelfZone(cursor_screen_coords);
    346   if (mouse_in_shelf == mouse_in_shelf_)
    347     return;
    348   mouse_in_shelf_ = mouse_in_shelf;
    349 
    350   if (mouse_in_shelf)
    351     MouseEnteredShelf();
    352   else
    353     MouseLeftShelf();
    354 }
    355 
    356 bool DownloadShelfGtk::IsCursorInShelfZone(
    357     const gfx::Point& cursor_screen_coords) {
    358   gfx::Rect bounds(gtk_util::GetWidgetScreenPosition(shelf_.get()),
    359                    gfx::Size(shelf_.get()->allocation.width,
    360                              shelf_.get()->allocation.height));
    361 
    362   // Negative insets expand the rectangle. We only expand the top.
    363   bounds.Inset(gfx::Insets(-kShelfAuraSize, 0, 0, 0));
    364 
    365   return bounds.Contains(cursor_screen_coords);
    366 }
    367 
    368 void DownloadShelfGtk::MouseLeftShelf() {
    369   DCHECK(close_on_mouse_out_);
    370 
    371   MessageLoop::current()->PostDelayedTask(
    372       FROM_HERE,
    373       auto_close_factory_.NewRunnableMethod(&DownloadShelfGtk::Close),
    374       kAutoCloseDelayMs);
    375 }
    376 
    377 void DownloadShelfGtk::MouseEnteredShelf() {
    378   auto_close_factory_.RevokeAll();
    379 }
    380