1 /* 2 * Copyright (C) 2010 Stephan Aßmus <superstippi@gmx.de> 3 * 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 #include "DownloadWindow.h" 8 9 #include <stdio.h> 10 11 #include <Alert.h> 12 #include <Button.h> 13 #include <Catalog.h> 14 #include <ControlLook.h> 15 #include <Entry.h> 16 #include <File.h> 17 #include <FindDirectory.h> 18 #include <GroupLayout.h> 19 #include <GroupLayoutBuilder.h> 20 #include <Locale.h> 21 #include <MenuBar.h> 22 #include <MenuItem.h> 23 #include <Path.h> 24 #include <Roster.h> 25 #include <ScrollView.h> 26 #include <SeparatorView.h> 27 #include <SpaceLayoutItem.h> 28 29 #include "BrowserApp.h" 30 #include "BrowserWindow.h" 31 #include "DownloadProgressView.h" 32 #include "SettingsKeys.h" 33 #include "SettingsMessage.h" 34 #include "WebDownload.h" 35 #include "WebPage.h" 36 37 38 #undef B_TRANSLATION_CONTEXT 39 #define B_TRANSLATION_CONTEXT "Download Window" 40 41 enum { 42 INIT = 'init', 43 OPEN_DOWNLOADS_FOLDER = 'odnf', 44 REMOVE_FINISHED_DOWNLOADS = 'rmfd', 45 REMOVE_MISSING_DOWNLOADS = 'rmmd' 46 }; 47 48 49 class DownloadsContainerView : public BGroupView { 50 public: 51 DownloadsContainerView() 52 : 53 BGroupView(B_VERTICAL, 0.0) 54 { 55 SetFlags(Flags() | B_PULSE_NEEDED); 56 SetViewColor(245, 245, 245); 57 AddChild(BSpaceLayoutItem::CreateGlue()); 58 } 59 60 virtual BSize MinSize() 61 { 62 BSize minSize = BGroupView::MinSize(); 63 return BSize(minSize.width, 80); 64 } 65 66 virtual void Pulse() 67 { 68 DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse(); 69 } 70 71 protected: 72 virtual void DoLayout() 73 { 74 BGroupView::DoLayout(); 75 if (BScrollBar* scrollBar = ScrollBar(B_VERTICAL)) { 76 BSize minSize = BGroupView::MinSize(); 77 float height = Bounds().Height(); 78 float max = minSize.height - height; 79 scrollBar->SetRange(0, max); 80 if (minSize.height > 0) 81 scrollBar->SetProportion(height / minSize.height); 82 else 83 scrollBar->SetProportion(1); 84 } 85 } 86 }; 87 88 89 class DownloadContainerScrollView : public BScrollView { 90 public: 91 DownloadContainerScrollView(BView* target) 92 : 93 BScrollView("Downloads scroll view", target, 0, false, true, 94 B_NO_BORDER) 95 { 96 } 97 98 protected: 99 virtual void DoLayout() 100 { 101 BScrollView::DoLayout(); 102 // Tweak scroll bar layout to hide part of the frame for better looks. 103 BScrollBar* scrollBar = ScrollBar(B_VERTICAL); 104 scrollBar->MoveBy(1, -1); 105 scrollBar->ResizeBy(0, 2); 106 Target()->ResizeBy(1, 0); 107 // Set the scroll steps 108 if (BView* item = Target()->ChildAt(0)) { 109 scrollBar->SetSteps(item->MinSize().height + 1, 110 item->MinSize().height + 1); 111 } 112 } 113 }; 114 115 116 // #pragma mark - 117 118 119 DownloadWindow::DownloadWindow(BRect frame, bool visible, 120 SettingsMessage* settings) 121 : BWindow(frame, B_TRANSLATE("Downloads"), 122 B_TITLED_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL, 123 B_AUTO_UPDATE_SIZE_LIMITS | B_ASYNCHRONOUS_CONTROLS | B_NOT_ZOOMABLE), 124 fMinimizeOnClose(false) 125 { 126 SetPulseRate(1000000); 127 128 settings->AddListener(BMessenger(this)); 129 BPath downloadPath; 130 if (find_directory(B_DESKTOP_DIRECTORY, &downloadPath) != B_OK) 131 downloadPath.SetTo("/boot/home/Desktop"); 132 fDownloadPath = settings->GetValue(kSettingsKeyDownloadPath, 133 downloadPath.Path()); 134 settings->SetValue(kSettingsKeyDownloadPath, fDownloadPath); 135 136 SetLayout(new BGroupLayout(B_VERTICAL, 0.0)); 137 138 DownloadsContainerView* downloadsGroupView = new DownloadsContainerView(); 139 fDownloadViewsLayout = downloadsGroupView->GroupLayout(); 140 141 BMenuBar* menuBar = new BMenuBar("Menu bar"); 142 BMenu* menu = new BMenu(B_TRANSLATE("Downloads")); 143 menu->AddItem(new BMenuItem(B_TRANSLATE("Open downloads folder"), 144 new BMessage(OPEN_DOWNLOADS_FOLDER))); 145 BMessage* newWindowMessage = new BMessage(NEW_WINDOW); 146 newWindowMessage->AddString("url", ""); 147 BMenuItem* newWindowItem = new BMenuItem(B_TRANSLATE("New browser window"), 148 newWindowMessage, 'N'); 149 menu->AddItem(newWindowItem); 150 newWindowItem->SetTarget(be_app); 151 menu->AddSeparatorItem(); 152 menu->AddItem(new BMenuItem(B_TRANSLATE("Hide"), 153 new BMessage(B_QUIT_REQUESTED), 'D')); 154 menuBar->AddItem(menu); 155 156 fDownloadsScrollView = new DownloadContainerScrollView(downloadsGroupView); 157 158 fRemoveFinishedButton = new BButton(B_TRANSLATE("Remove finished"), 159 new BMessage(REMOVE_FINISHED_DOWNLOADS)); 160 fRemoveFinishedButton->SetEnabled(false); 161 162 fRemoveMissingButton = new BButton(B_TRANSLATE("Remove missing"), 163 new BMessage(REMOVE_MISSING_DOWNLOADS)); 164 fRemoveMissingButton->SetEnabled(false); 165 166 const float spacing = be_control_look->DefaultItemSpacing(); 167 168 AddChild(BGroupLayoutBuilder(B_VERTICAL, 0.0) 169 .Add(menuBar) 170 .Add(fDownloadsScrollView) 171 .Add(new BSeparatorView(B_HORIZONTAL, B_PLAIN_BORDER)) 172 .Add(BGroupLayoutBuilder(B_HORIZONTAL, spacing) 173 .AddGlue() 174 .Add(fRemoveMissingButton) 175 .Add(fRemoveFinishedButton) 176 .SetInsets(12, 5, 12, 5) 177 ) 178 ); 179 180 PostMessage(INIT); 181 182 if (!visible) 183 Hide(); 184 Show(); 185 } 186 187 188 DownloadWindow::~DownloadWindow() 189 { 190 // Only necessary to save the current progress of unfinished downloads: 191 _SaveSettings(); 192 } 193 194 195 void 196 DownloadWindow::DispatchMessage(BMessage* message, BHandler* target) 197 { 198 // We need to intercept mouse down events inside the area of download 199 // progress views (regardless of whether they have children at the click), 200 // so that they may display a context menu. 201 BPoint where; 202 int32 buttons; 203 if (message->what == B_MOUSE_DOWN 204 && message->FindPoint("screen_where", &where) == B_OK 205 && message->FindInt32("buttons", &buttons) == B_OK 206 && (buttons & B_SECONDARY_MOUSE_BUTTON) != 0) { 207 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 208 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 209 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 210 item->View()); 211 if (!view) 212 continue; 213 BPoint viewWhere(where); 214 view->ConvertFromScreen(&viewWhere); 215 if (view->Bounds().Contains(viewWhere)) { 216 view->ShowContextMenu(where); 217 return; 218 } 219 } 220 } 221 BWindow::DispatchMessage(message, target); 222 } 223 224 225 void 226 DownloadWindow::MessageReceived(BMessage* message) 227 { 228 switch (message->what) { 229 case INIT: 230 { 231 _LoadSettings(); 232 // Small trick to get the correct enabled status of the Remove 233 // finished button 234 _DownloadFinished(NULL); 235 break; 236 } 237 case B_DOWNLOAD_ADDED: 238 { 239 BWebDownload* download; 240 if (message->FindPointer("download", reinterpret_cast<void**>( 241 &download)) == B_OK) { 242 _DownloadStarted(download); 243 } 244 break; 245 } 246 case B_DOWNLOAD_REMOVED: 247 { 248 BWebDownload* download; 249 if (message->FindPointer("download", reinterpret_cast<void**>( 250 &download)) == B_OK) { 251 _DownloadFinished(download); 252 } 253 break; 254 } 255 case OPEN_DOWNLOADS_FOLDER: 256 { 257 entry_ref ref; 258 status_t status = get_ref_for_path(fDownloadPath.String(), &ref); 259 if (status == B_OK) 260 status = be_roster->Launch(&ref); 261 if (status != B_OK && status != B_ALREADY_RUNNING) { 262 BString errorString(B_TRANSLATE_COMMENT("The downloads folder could " 263 "not be opened.\n\nError: %error", "Don't translate " 264 "variable %error")); 265 errorString.ReplaceFirst("%error", strerror(status)); 266 BAlert* alert = new BAlert(B_TRANSLATE("Error opening downloads " 267 "folder"), errorString.String(), B_TRANSLATE("OK")); 268 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 269 alert->Go(NULL); 270 } 271 break; 272 } 273 case REMOVE_FINISHED_DOWNLOADS: 274 _RemoveFinishedDownloads(); 275 break; 276 case REMOVE_MISSING_DOWNLOADS: 277 _RemoveMissingDownloads(); 278 break; 279 case SAVE_SETTINGS: 280 _ValidateButtonStatus(); 281 _SaveSettings(); 282 break; 283 284 case SETTINGS_VALUE_CHANGED: 285 { 286 BString string; 287 if (message->FindString("name", &string) == B_OK 288 && string == kSettingsKeyDownloadPath 289 && message->FindString("value", &string) == B_OK) { 290 fDownloadPath = string; 291 } 292 break; 293 } 294 default: 295 BWindow::MessageReceived(message); 296 break; 297 } 298 } 299 300 301 bool 302 DownloadWindow::QuitRequested() 303 { 304 if (fMinimizeOnClose) { 305 if (!IsMinimized()) 306 Minimize(true); 307 } else { 308 if (!IsHidden()) 309 Hide(); 310 } 311 return false; 312 } 313 314 315 bool 316 DownloadWindow::DownloadsInProgress() 317 { 318 bool downloadsInProgress = false; 319 if (!Lock()) 320 return downloadsInProgress; 321 322 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 323 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 324 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 325 item->View()); 326 if (!view) 327 continue; 328 if (view->Download() != NULL) { 329 downloadsInProgress = true; 330 break; 331 } 332 } 333 334 Unlock(); 335 336 return downloadsInProgress; 337 } 338 339 340 void 341 DownloadWindow::SetMinimizeOnClose(bool minimize) 342 { 343 if (Lock()) { 344 fMinimizeOnClose = minimize; 345 Unlock(); 346 } 347 } 348 349 350 // #pragma mark - private 351 352 353 void 354 DownloadWindow::_DownloadStarted(BWebDownload* download) 355 { 356 download->Start(BPath(fDownloadPath.String())); 357 358 int32 finishedCount = 0; 359 int32 missingCount = 0; 360 int32 index = 0; 361 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 362 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 363 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 364 item->View()); 365 if (!view) 366 continue; 367 if (view->URL() == download->URL()) { 368 index = i; 369 view->RemoveSelf(); 370 delete view; 371 continue; 372 } 373 if (view->IsFinished()) 374 finishedCount++; 375 if (view->IsMissing()) 376 missingCount++; 377 } 378 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 379 fRemoveMissingButton->SetEnabled(missingCount > 0); 380 DownloadProgressView* view = new DownloadProgressView(download); 381 if (!view->Init()) { 382 delete view; 383 return; 384 } 385 fDownloadViewsLayout->AddView(index, view); 386 387 // Scroll new download into view 388 if (BScrollBar* scrollBar = fDownloadsScrollView->ScrollBar(B_VERTICAL)) { 389 float min; 390 float max; 391 scrollBar->GetRange(&min, &max); 392 float viewHeight = view->MinSize().height + 1; 393 float scrollOffset = min + index * viewHeight; 394 float scrollBarHeight = scrollBar->Bounds().Height() - 1; 395 float value = scrollBar->Value(); 396 if (scrollOffset < value) 397 scrollBar->SetValue(scrollOffset); 398 else if (scrollOffset + viewHeight > value + scrollBarHeight) { 399 float diff = scrollOffset + viewHeight - (value + scrollBarHeight); 400 scrollBar->SetValue(value + diff); 401 } 402 } 403 404 _SaveSettings(); 405 406 SetWorkspaces(B_CURRENT_WORKSPACE); 407 if (IsHidden()) 408 Show(); 409 410 Activate(true); 411 } 412 413 414 void 415 DownloadWindow::_DownloadFinished(BWebDownload* download) 416 { 417 int32 finishedCount = 0; 418 int32 missingCount = 0; 419 for (int32 i = 0; 420 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i++) { 421 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 422 item->View()); 423 if (!view) 424 continue; 425 if (download && view->Download() == download) { 426 view->DownloadFinished(); 427 finishedCount++; 428 continue; 429 } 430 if (view->IsFinished()) 431 finishedCount++; 432 if (view->IsMissing()) 433 missingCount++; 434 } 435 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 436 fRemoveMissingButton->SetEnabled(missingCount > 0); 437 if (download) 438 _SaveSettings(); 439 } 440 441 442 void 443 DownloadWindow::_RemoveFinishedDownloads() 444 { 445 int32 missingCount = 0; 446 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 447 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 448 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 449 item->View()); 450 if (!view) 451 continue; 452 if (view->IsFinished()) { 453 view->RemoveSelf(); 454 delete view; 455 } else if (view->IsMissing()) 456 missingCount++; 457 } 458 fRemoveFinishedButton->SetEnabled(false); 459 fRemoveMissingButton->SetEnabled(missingCount > 0); 460 _SaveSettings(); 461 } 462 463 464 void 465 DownloadWindow::_RemoveMissingDownloads() 466 { 467 int32 finishedCount = 0; 468 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 469 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 470 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 471 item->View()); 472 if (!view) 473 continue; 474 if (view->IsMissing()) { 475 view->RemoveSelf(); 476 delete view; 477 } else if (view->IsFinished()) 478 finishedCount++; 479 } 480 fRemoveMissingButton->SetEnabled(false); 481 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 482 _SaveSettings(); 483 } 484 485 486 void 487 DownloadWindow::_ValidateButtonStatus() 488 { 489 int32 finishedCount = 0; 490 int32 missingCount = 0; 491 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 492 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 493 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 494 item->View()); 495 if (!view) 496 continue; 497 if (view->IsFinished()) 498 finishedCount++; 499 if (view->IsMissing()) 500 missingCount++; 501 } 502 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 503 fRemoveMissingButton->SetEnabled(missingCount > 0); 504 } 505 506 507 void 508 DownloadWindow::_SaveSettings() 509 { 510 BFile file; 511 if (!_OpenSettingsFile(file, B_ERASE_FILE | B_CREATE_FILE | B_WRITE_ONLY)) 512 return; 513 BMessage message; 514 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 515 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 516 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 517 item->View()); 518 if (!view) 519 continue; 520 521 BMessage downloadArchive; 522 if (view->SaveSettings(&downloadArchive) == B_OK) 523 message.AddMessage("download", &downloadArchive); 524 } 525 message.Flatten(&file); 526 } 527 528 529 void 530 DownloadWindow::_LoadSettings() 531 { 532 BFile file; 533 if (!_OpenSettingsFile(file, B_READ_ONLY)) 534 return; 535 BMessage message; 536 if (message.Unflatten(&file) != B_OK) 537 return; 538 BMessage downloadArchive; 539 for (int32 i = 0; 540 message.FindMessage("download", i, &downloadArchive) == B_OK; 541 i++) { 542 DownloadProgressView* view = new DownloadProgressView( 543 &downloadArchive); 544 if (!view->Init(&downloadArchive)) 545 continue; 546 fDownloadViewsLayout->AddView(0, view); 547 } 548 } 549 550 551 bool 552 DownloadWindow::_OpenSettingsFile(BFile& file, uint32 mode) 553 { 554 BPath path; 555 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK 556 || path.Append(kApplicationName) != B_OK 557 || path.Append("Downloads") != B_OK) { 558 return false; 559 } 560 return file.SetTo(path.Path(), mode) == B_OK; 561 } 562 563 564