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