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