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