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
DownloadShelfGtk(Browser * browser,GtkWidget * parent)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
~DownloadShelfGtk()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
AddDownload(BaseDownloadItemModel * download_model_)170 void DownloadShelfGtk::AddDownload(BaseDownloadItemModel* download_model_) {
171 download_items_.push_back(new DownloadItemGtk(this, download_model_));
172 Show();
173 }
174
IsShowing() const175 bool DownloadShelfGtk::IsShowing() const {
176 return slide_widget_->IsShowing();
177 }
178
IsClosing() const179 bool DownloadShelfGtk::IsClosing() const {
180 return slide_widget_->IsClosing();
181 }
182
Show()183 void DownloadShelfGtk::Show() {
184 slide_widget_->Open();
185 browser_->UpdateDownloadShelfVisibility(true);
186 CancelAutoClose();
187 }
188
Close()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
browser() const198 Browser* DownloadShelfGtk::browser() const {
199 return browser_;
200 }
201
Closed()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
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)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
GetHeight() const257 int DownloadShelfGtk::GetHeight() const {
258 return slide_widget_->widget()->allocation.height;
259 }
260
RemoveDownloadItem(DownloadItemGtk * download_item)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
GetHBox() const276 GtkWidget* DownloadShelfGtk::GetHBox() const {
277 return items_hbox_.get();
278 }
279
MaybeShowMoreDownloadItems()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
OnButtonClick(GtkWidget * button)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
AutoCloseIfPossible()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
CancelAutoClose()305 void DownloadShelfGtk::CancelAutoClose() {
306 SetCloseOnMouseOut(false);
307 auto_close_factory_.RevokeAll();
308 }
309
ItemOpened()310 void DownloadShelfGtk::ItemOpened() {
311 AutoCloseIfPossible();
312 }
313
SetCloseOnMouseOut(bool close)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
WillProcessEvent(GdkEvent * event)326 void DownloadShelfGtk::WillProcessEvent(GdkEvent* event) {
327 }
328
DidProcessEvent(GdkEvent * event)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
IsCursorInShelfZone(const gfx::Point & cursor_screen_coords)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
MouseLeftShelf()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
MouseEnteredShelf()377 void DownloadShelfGtk::MouseEnteredShelf() {
378 auto_close_factory_.RevokeAll();
379 }
380