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