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