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("Close"), 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 MoveOnScreen(B_MOVE_IF_PARTIALLY_OFFSCREEN); 186 } 187 188 189 DownloadWindow::~DownloadWindow() 190 { 191 // Only necessary to save the current progress of unfinished downloads: 192 _SaveSettings(); 193 } 194 195 196 void 197 DownloadWindow::DispatchMessage(BMessage* message, BHandler* target) 198 { 199 // We need to intercept mouse down events inside the area of download 200 // progress views (regardless of whether they have children at the click), 201 // so that they may display a context menu. 202 BPoint where; 203 int32 buttons; 204 if (message->what == B_MOUSE_DOWN 205 && message->FindPoint("screen_where", &where) == B_OK 206 && message->FindInt32("buttons", &buttons) == B_OK 207 && (buttons & B_SECONDARY_MOUSE_BUTTON) != 0) { 208 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 209 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 210 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 211 item->View()); 212 if (!view) 213 continue; 214 BPoint viewWhere(where); 215 view->ConvertFromScreen(&viewWhere); 216 if (view->Bounds().Contains(viewWhere)) { 217 view->ShowContextMenu(where); 218 return; 219 } 220 } 221 } 222 BWindow::DispatchMessage(message, target); 223 } 224 225 226 void 227 DownloadWindow::FrameResized(float newWidth, float newHeight) 228 { 229 MoveOnScreen(B_DO_NOT_RESIZE_TO_FIT | B_MOVE_IF_PARTIALLY_OFFSCREEN); 230 } 231 232 233 void 234 DownloadWindow::MessageReceived(BMessage* message) 235 { 236 switch (message->what) { 237 case INIT: 238 { 239 _LoadSettings(); 240 // Small trick to get the correct enabled status of the Remove 241 // finished button 242 _DownloadFinished(NULL); 243 break; 244 } 245 case B_DOWNLOAD_ADDED: 246 { 247 BWebDownload* download; 248 if (message->FindPointer("download", reinterpret_cast<void**>( 249 &download)) == B_OK) { 250 _DownloadStarted(download); 251 } 252 break; 253 } 254 case B_DOWNLOAD_REMOVED: 255 { 256 BWebDownload* download; 257 if (message->FindPointer("download", reinterpret_cast<void**>( 258 &download)) == B_OK) { 259 _DownloadFinished(download); 260 } 261 break; 262 } 263 case OPEN_DOWNLOADS_FOLDER: 264 { 265 entry_ref ref; 266 status_t status = get_ref_for_path(fDownloadPath.String(), &ref); 267 if (status == B_OK) 268 status = be_roster->Launch(&ref); 269 if (status != B_OK && status != B_ALREADY_RUNNING) { 270 BString errorString(B_TRANSLATE_COMMENT("The downloads folder could " 271 "not be opened.\n\nError: %error", "Don't translate " 272 "variable %error")); 273 errorString.ReplaceFirst("%error", strerror(status)); 274 BAlert* alert = new BAlert(B_TRANSLATE("Error opening downloads " 275 "folder"), errorString.String(), B_TRANSLATE("OK")); 276 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 277 alert->Go(NULL); 278 } 279 break; 280 } 281 case REMOVE_FINISHED_DOWNLOADS: 282 _RemoveFinishedDownloads(); 283 break; 284 case REMOVE_MISSING_DOWNLOADS: 285 _RemoveMissingDownloads(); 286 break; 287 case SAVE_SETTINGS: 288 _ValidateButtonStatus(); 289 _SaveSettings(); 290 break; 291 292 case SETTINGS_VALUE_CHANGED: 293 { 294 BString string; 295 if (message->FindString("name", &string) == B_OK 296 && string == kSettingsKeyDownloadPath 297 && message->FindString("value", &string) == B_OK) { 298 fDownloadPath = string; 299 } 300 break; 301 } 302 default: 303 BWindow::MessageReceived(message); 304 break; 305 } 306 } 307 308 309 bool 310 DownloadWindow::QuitRequested() 311 { 312 if (fMinimizeOnClose) { 313 if (!IsMinimized()) 314 Minimize(true); 315 } else { 316 if (!IsHidden()) 317 Hide(); 318 } 319 return false; 320 } 321 322 323 bool 324 DownloadWindow::DownloadsInProgress() 325 { 326 bool downloadsInProgress = false; 327 if (!Lock()) 328 return downloadsInProgress; 329 330 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 331 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 332 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 333 item->View()); 334 if (!view) 335 continue; 336 if (view->Download() != NULL) { 337 downloadsInProgress = true; 338 break; 339 } 340 } 341 342 Unlock(); 343 344 return downloadsInProgress; 345 } 346 347 348 void 349 DownloadWindow::SetMinimizeOnClose(bool minimize) 350 { 351 if (Lock()) { 352 fMinimizeOnClose = minimize; 353 Unlock(); 354 } 355 } 356 357 358 // #pragma mark - private 359 360 361 void 362 DownloadWindow::_DownloadStarted(BWebDownload* download) 363 { 364 download->Start(BPath(fDownloadPath.String())); 365 366 int32 finishedCount = 0; 367 int32 missingCount = 0; 368 int32 index = 0; 369 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 370 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 371 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 372 item->View()); 373 if (!view) 374 continue; 375 if (view->URL() == download->URL()) { 376 index = i; 377 view->RemoveSelf(); 378 delete view; 379 continue; 380 } 381 if (view->IsFinished()) 382 finishedCount++; 383 if (view->IsMissing()) 384 missingCount++; 385 } 386 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 387 fRemoveMissingButton->SetEnabled(missingCount > 0); 388 DownloadProgressView* view = new DownloadProgressView(download); 389 if (!view->Init()) { 390 delete view; 391 return; 392 } 393 fDownloadViewsLayout->AddView(index, view); 394 395 // Scroll new download into view 396 if (BScrollBar* scrollBar = fDownloadsScrollView->ScrollBar(B_VERTICAL)) { 397 float min; 398 float max; 399 scrollBar->GetRange(&min, &max); 400 float viewHeight = view->MinSize().height + 1; 401 float scrollOffset = min + index * viewHeight; 402 float scrollBarHeight = scrollBar->Bounds().Height() - 1; 403 float value = scrollBar->Value(); 404 if (scrollOffset < value) 405 scrollBar->SetValue(scrollOffset); 406 else if (scrollOffset + viewHeight > value + scrollBarHeight) { 407 float diff = scrollOffset + viewHeight - (value + scrollBarHeight); 408 scrollBar->SetValue(value + diff); 409 } 410 } 411 412 _SaveSettings(); 413 414 SetWorkspaces(B_CURRENT_WORKSPACE); 415 if (IsHidden()) 416 Show(); 417 418 Activate(true); 419 } 420 421 422 void 423 DownloadWindow::_DownloadFinished(BWebDownload* download) 424 { 425 int32 finishedCount = 0; 426 int32 missingCount = 0; 427 for (int32 i = 0; 428 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i++) { 429 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 430 item->View()); 431 if (!view) 432 continue; 433 if (download && view->Download() == download) { 434 view->DownloadFinished(); 435 finishedCount++; 436 continue; 437 } 438 if (view->IsFinished()) 439 finishedCount++; 440 if (view->IsMissing()) 441 missingCount++; 442 } 443 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 444 fRemoveMissingButton->SetEnabled(missingCount > 0); 445 if (download) 446 _SaveSettings(); 447 } 448 449 450 void 451 DownloadWindow::_RemoveFinishedDownloads() 452 { 453 int32 missingCount = 0; 454 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 455 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 456 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 457 item->View()); 458 if (!view) 459 continue; 460 if (view->IsFinished()) { 461 view->RemoveSelf(); 462 delete view; 463 } else if (view->IsMissing()) 464 missingCount++; 465 } 466 fRemoveFinishedButton->SetEnabled(false); 467 fRemoveMissingButton->SetEnabled(missingCount > 0); 468 _SaveSettings(); 469 } 470 471 472 void 473 DownloadWindow::_RemoveMissingDownloads() 474 { 475 int32 finishedCount = 0; 476 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 477 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 478 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 479 item->View()); 480 if (!view) 481 continue; 482 if (view->IsMissing()) { 483 view->RemoveSelf(); 484 delete view; 485 } else if (view->IsFinished()) 486 finishedCount++; 487 } 488 fRemoveMissingButton->SetEnabled(false); 489 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 490 _SaveSettings(); 491 } 492 493 494 void 495 DownloadWindow::_ValidateButtonStatus() 496 { 497 int32 finishedCount = 0; 498 int32 missingCount = 0; 499 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 500 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 501 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 502 item->View()); 503 if (!view) 504 continue; 505 if (view->IsFinished()) 506 finishedCount++; 507 if (view->IsMissing()) 508 missingCount++; 509 } 510 fRemoveFinishedButton->SetEnabled(finishedCount > 0); 511 fRemoveMissingButton->SetEnabled(missingCount > 0); 512 } 513 514 515 void 516 DownloadWindow::_SaveSettings() 517 { 518 BFile file; 519 if (!_OpenSettingsFile(file, B_ERASE_FILE | B_CREATE_FILE | B_WRITE_ONLY)) 520 return; 521 BMessage message; 522 for (int32 i = fDownloadViewsLayout->CountItems() - 1; 523 BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) { 524 DownloadProgressView* view = dynamic_cast<DownloadProgressView*>( 525 item->View()); 526 if (!view) 527 continue; 528 529 BMessage downloadArchive; 530 if (view->SaveSettings(&downloadArchive) == B_OK) 531 message.AddMessage("download", &downloadArchive); 532 } 533 message.Flatten(&file); 534 } 535 536 537 void 538 DownloadWindow::_LoadSettings() 539 { 540 BFile file; 541 if (!_OpenSettingsFile(file, B_READ_ONLY)) 542 return; 543 BMessage message; 544 if (message.Unflatten(&file) != B_OK) 545 return; 546 BMessage downloadArchive; 547 for (int32 i = 0; 548 message.FindMessage("download", i, &downloadArchive) == B_OK; 549 i++) { 550 DownloadProgressView* view = new DownloadProgressView( 551 &downloadArchive); 552 if (!view->Init(&downloadArchive)) 553 continue; 554 fDownloadViewsLayout->AddView(0, view); 555 } 556 } 557 558 559 bool 560 DownloadWindow::_OpenSettingsFile(BFile& file, uint32 mode) 561 { 562 BPath path; 563 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK 564 || path.Append(kApplicationName) != B_OK 565 || path.Append("Downloads") != B_OK) { 566 return false; 567 } 568 return file.SetTo(path.Path(), mode) == B_OK; 569 } 570 571 572