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
30namespace {
31
32// The height of the download items.
33const int kDownloadItemHeight = download_util::kSmallProgressIconSize;
34
35// Padding between the download widgets.
36const int kDownloadItemPadding = 10;
37
38// Padding between the top/bottom of the download widgets and the edge of the
39// shelf.
40const int kTopBottomPadding = 4;
41
42// Padding between the left side of the shelf and the first download item.
43const int kLeftPadding = 2;
44
45// Padding between the right side of the shelf and the close button.
46const int kRightPadding = 10;
47
48// Speed of the shelf show/hide animation.
49const 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).
53const int kAutoCloseDelayMs = 300;
54
55// The area to the top of the shelf that is considered part of its "zone".
56const int kShelfAuraSize = 40;
57
58}  // namespace
59
60DownloadShelfGtk::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
157DownloadShelfGtk::~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
170void DownloadShelfGtk::AddDownload(BaseDownloadItemModel* download_model_) {
171  download_items_.push_back(new DownloadItemGtk(this, download_model_));
172  Show();
173}
174
175bool DownloadShelfGtk::IsShowing() const {
176  return slide_widget_->IsShowing();
177}
178
179bool DownloadShelfGtk::IsClosing() const {
180  return slide_widget_->IsClosing();
181}
182
183void DownloadShelfGtk::Show() {
184  slide_widget_->Open();
185  browser_->UpdateDownloadShelfVisibility(true);
186  CancelAutoClose();
187}
188
189void 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
198Browser* DownloadShelfGtk::browser() const {
199  return browser_;
200}
201
202void 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
222void 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
257int DownloadShelfGtk::GetHeight() const {
258  return slide_widget_->widget()->allocation.height;
259}
260
261void 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
276GtkWidget* DownloadShelfGtk::GetHBox() const {
277  return items_hbox_.get();
278}
279
280void 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
286void 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
295void 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
305void DownloadShelfGtk::CancelAutoClose() {
306  SetCloseOnMouseOut(false);
307  auto_close_factory_.RevokeAll();
308}
309
310void DownloadShelfGtk::ItemOpened() {
311  AutoCloseIfPossible();
312}
313
314void 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
326void DownloadShelfGtk::WillProcessEvent(GdkEvent* event) {
327}
328
329void 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
356bool 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
368void 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
377void DownloadShelfGtk::MouseEnteredShelf() {
378  auto_close_factory_.RevokeAll();
379}
380