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 "DownloadProgressView.h" 8 9 #include <stdio.h> 10 11 #include <Alert.h> 12 #include <Bitmap.h> 13 #include <Button.h> 14 #include <Catalog.h> 15 #include <Clipboard.h> 16 #include <Directory.h> 17 #include <Entry.h> 18 #include <FindDirectory.h> 19 #include <GroupLayoutBuilder.h> 20 #include <Locale.h> 21 #include <MenuItem.h> 22 #include <NodeInfo.h> 23 #include <NodeMonitor.h> 24 #include <Notification.h> 25 #include <PopUpMenu.h> 26 #include <Roster.h> 27 #include <SpaceLayoutItem.h> 28 #include <StatusBar.h> 29 #include <StringView.h> 30 31 #include "WebDownload.h" 32 #include "WebPage.h" 33 #include "StringForSize.h" 34 35 36 #undef B_TRANSLATION_CONTEXT 37 #define B_TRANSLATION_CONTEXT "Download Window" 38 39 enum { 40 OPEN_DOWNLOAD = 'opdn', 41 RESTART_DOWNLOAD = 'rsdn', 42 CANCEL_DOWNLOAD = 'cndn', 43 REMOVE_DOWNLOAD = 'rmdn', 44 COPY_URL_TO_CLIPBOARD = 'curl', 45 OPEN_CONTAINING_FOLDER = 'opfd', 46 }; 47 48 const bigtime_t kMaxUpdateInterval = 100000LL; 49 const bigtime_t kSpeedReferenceInterval = 500000LL; 50 const bigtime_t kShowSpeedInterval = 8000000LL; 51 const bigtime_t kShowEstimatedFinishInterval = 4000000LL; 52 53 bigtime_t DownloadProgressView::sLastEstimatedFinishSpeedToggleTime = -1; 54 bool DownloadProgressView::sShowSpeed = true; 55 56 57 class IconView : public BView { 58 public: 59 IconView(const BEntry& entry) 60 : 61 BView("Download icon", B_WILL_DRAW), 62 fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32), 63 fDimmedIcon(false) 64 { 65 SetDrawingMode(B_OP_OVER); 66 SetTo(entry); 67 } 68 69 IconView() 70 : 71 BView("Download icon", B_WILL_DRAW), 72 fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32), 73 fDimmedIcon(false) 74 { 75 SetDrawingMode(B_OP_OVER); 76 memset(fIconBitmap.Bits(), 0, fIconBitmap.BitsLength()); 77 } 78 79 IconView(BMessage* archive) 80 : 81 BView("Download icon", B_WILL_DRAW), 82 fIconBitmap(archive), 83 fDimmedIcon(true) 84 { 85 SetDrawingMode(B_OP_OVER); 86 } 87 88 void SetTo(const BEntry& entry) 89 { 90 BNode node(&entry); 91 BNodeInfo info(&node); 92 info.GetTrackerIcon(&fIconBitmap, B_LARGE_ICON); 93 Invalidate(); 94 } 95 96 void SetIconDimmed(bool iconDimmed) 97 { 98 if (fDimmedIcon != iconDimmed) { 99 fDimmedIcon = iconDimmed; 100 Invalidate(); 101 } 102 } 103 104 bool IsIconDimmed() const 105 { 106 return fDimmedIcon; 107 } 108 109 status_t SaveSettings(BMessage* archive) 110 { 111 return fIconBitmap.Archive(archive); 112 } 113 114 virtual void AttachedToWindow() 115 { 116 SetViewColor(Parent()->ViewColor()); 117 } 118 119 virtual void Draw(BRect updateRect) 120 { 121 if (fDimmedIcon) { 122 SetDrawingMode(B_OP_ALPHA); 123 SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 124 SetHighColor(0, 0, 0, 100); 125 } 126 DrawBitmapAsync(&fIconBitmap); 127 } 128 129 virtual BSize MinSize() 130 { 131 return BSize(fIconBitmap.Bounds().Width(), 132 fIconBitmap.Bounds().Height()); 133 } 134 135 virtual BSize PreferredSize() 136 { 137 return MinSize(); 138 } 139 140 virtual BSize MaxSize() 141 { 142 return MinSize(); 143 } 144 145 BBitmap* Bitmap() 146 { 147 return &fIconBitmap; 148 } 149 150 private: 151 BBitmap fIconBitmap; 152 bool fDimmedIcon; 153 }; 154 155 156 class SmallButton : public BButton { 157 public: 158 SmallButton(const char* label, BMessage* message = NULL) 159 : 160 BButton(label, message) 161 { 162 BFont font; 163 GetFont(&font); 164 float size = ceilf(font.Size() * 0.8); 165 font.SetSize(max_c(8, size)); 166 SetFont(&font, B_FONT_SIZE); 167 } 168 }; 169 170 171 // #pragma mark - DownloadProgressView 172 173 174 DownloadProgressView::DownloadProgressView(BWebDownload* download) 175 : 176 BGroupView(B_HORIZONTAL, 8), 177 fDownload(download), 178 fURL(download->URL()), 179 fPath(download->Path()) 180 { 181 } 182 183 184 DownloadProgressView::DownloadProgressView(const BMessage* archive) 185 : 186 BGroupView(B_HORIZONTAL, 8), 187 fDownload(NULL), 188 fURL(), 189 fPath() 190 { 191 const char* string; 192 if (archive->FindString("path", &string) == B_OK) 193 fPath.SetTo(string); 194 if (archive->FindString("url", &string) == B_OK) 195 fURL = string; 196 } 197 198 199 bool 200 DownloadProgressView::Init(BMessage* archive) 201 { 202 fCurrentSize = 0; 203 fExpectedSize = 0; 204 fLastUpdateTime = 0; 205 fBytesPerSecond = 0.0; 206 for (size_t i = 0; i < kBytesPerSecondSlots; i++) 207 fBytesPerSecondSlot[i] = 0.0; 208 fCurrentBytesPerSecondSlot = 0; 209 fLastSpeedReferenceSize = 0; 210 fEstimatedFinishReferenceSize = 0; 211 212 fProcessStartTime = fLastSpeedReferenceTime 213 = fEstimatedFinishReferenceTime = system_time(); 214 215 SetViewColor(245, 245, 245); 216 SetFlags(Flags() | B_FULL_UPDATE_ON_RESIZE | B_WILL_DRAW); 217 218 if (archive) { 219 fStatusBar = new BStatusBar("download progress", fPath.Leaf()); 220 float value; 221 if (archive->FindFloat("value", &value) == B_OK) 222 fStatusBar->SetTo(value); 223 } else 224 fStatusBar = new BStatusBar("download progress", "Download"); 225 fStatusBar->SetMaxValue(100); 226 fStatusBar->SetBarHeight(12); 227 228 // fPath is only valid when constructed from archive (fDownload == NULL) 229 BEntry entry(fPath.Path()); 230 231 if (archive) { 232 if (!entry.Exists()) 233 fIconView = new IconView(archive); 234 else 235 fIconView = new IconView(entry); 236 } else 237 fIconView = new IconView(); 238 239 if (!fDownload && (fStatusBar->CurrentValue() < 100 || !entry.Exists())) { 240 fTopButton = new SmallButton(B_TRANSLATE("Restart"), 241 new BMessage(RESTART_DOWNLOAD)); 242 } else { 243 fTopButton = new SmallButton(B_TRANSLATE("Open"), 244 new BMessage(OPEN_DOWNLOAD)); 245 fTopButton->SetEnabled(fDownload == NULL); 246 } 247 if (fDownload) { 248 fBottomButton = new SmallButton(B_TRANSLATE("Cancel"), 249 new BMessage(CANCEL_DOWNLOAD)); 250 } else { 251 fBottomButton = new SmallButton(B_TRANSLATE("Remove"), 252 new BMessage(REMOVE_DOWNLOAD)); 253 fBottomButton->SetEnabled(fDownload == NULL); 254 } 255 256 fInfoView = new BStringView("info view", ""); 257 258 BGroupLayout* layout = GroupLayout(); 259 layout->SetInsets(8, 5, 5, 6); 260 layout->AddView(fIconView); 261 BView* verticalGroup = BGroupLayoutBuilder(B_VERTICAL, 3) 262 .Add(fStatusBar) 263 .Add(fInfoView) 264 .TopView() 265 ; 266 verticalGroup->SetViewColor(ViewColor()); 267 layout->AddView(verticalGroup); 268 verticalGroup = BGroupLayoutBuilder(B_VERTICAL, 3) 269 .Add(fTopButton) 270 .Add(fBottomButton) 271 .TopView() 272 ; 273 verticalGroup->SetViewColor(ViewColor()); 274 layout->AddView(verticalGroup); 275 276 BFont font; 277 fInfoView->GetFont(&font); 278 float fontSize = font.Size() * 0.8f; 279 font.SetSize(max_c(8.0f, fontSize)); 280 fInfoView->SetFont(&font, B_FONT_SIZE); 281 fInfoView->SetHighColor(tint_color(fInfoView->LowColor(), 282 B_DARKEN_4_TINT)); 283 fInfoView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 284 285 return true; 286 } 287 288 289 status_t 290 DownloadProgressView::SaveSettings(BMessage* archive) 291 { 292 if (!archive) 293 return B_BAD_VALUE; 294 status_t ret = archive->AddString("path", fPath.Path()); 295 if (ret == B_OK) 296 ret = archive->AddString("url", fURL.String()); 297 if (ret == B_OK) 298 ret = archive->AddFloat("value", fStatusBar->CurrentValue()); 299 if (ret == B_OK) 300 ret = fIconView->SaveSettings(archive); 301 return ret; 302 } 303 304 305 void 306 DownloadProgressView::AttachedToWindow() 307 { 308 if (fDownload) { 309 fDownload->SetProgressListener(BMessenger(this)); 310 // Will start node monitor upon receiving the B_DOWNLOAD_STARTED 311 // message. 312 } else { 313 BEntry entry(fPath.Path()); 314 if (entry.Exists()) 315 _StartNodeMonitor(entry); 316 } 317 318 fTopButton->SetTarget(this); 319 fBottomButton->SetTarget(this); 320 } 321 322 323 void 324 DownloadProgressView::DetachedFromWindow() 325 { 326 _StopNodeMonitor(); 327 } 328 329 330 void 331 DownloadProgressView::AllAttached() 332 { 333 SetViewColor(B_TRANSPARENT_COLOR); 334 SetLowColor(245, 245, 245); 335 SetHighColor(tint_color(LowColor(), B_DARKEN_1_TINT)); 336 } 337 338 339 void 340 DownloadProgressView::Draw(BRect updateRect) 341 { 342 BRect bounds(Bounds()); 343 bounds.bottom--; 344 FillRect(bounds, B_SOLID_LOW); 345 bounds.bottom++; 346 StrokeLine(bounds.LeftBottom(), bounds.RightBottom()); 347 } 348 349 350 void 351 DownloadProgressView::MessageReceived(BMessage* message) 352 { 353 switch (message->what) { 354 case B_DOWNLOAD_STARTED: 355 { 356 BString path; 357 if (message->FindString("path", &path) != B_OK) 358 break; 359 fPath.SetTo(path); 360 BEntry entry(fPath.Path()); 361 fIconView->SetTo(entry); 362 fStatusBar->Reset(fPath.Leaf()); 363 _StartNodeMonitor(entry); 364 365 // Immediately switch to speed display whenever a new download 366 // starts. 367 sShowSpeed = true; 368 sLastEstimatedFinishSpeedToggleTime 369 = fProcessStartTime = fLastSpeedReferenceTime 370 = fEstimatedFinishReferenceTime = system_time(); 371 break; 372 } 373 case B_DOWNLOAD_PROGRESS: 374 { 375 int64 currentSize; 376 int64 expectedSize; 377 if (message->FindInt64("current size", ¤tSize) == B_OK 378 && message->FindInt64("expected size", &expectedSize) == B_OK) { 379 _UpdateStatus(currentSize, expectedSize); 380 } 381 break; 382 } 383 case B_DOWNLOAD_REMOVED: 384 // TODO: This is a bit asymetric. The removed notification 385 // arrives here, but it would be nicer if it arrived 386 // at the window... 387 Window()->PostMessage(message); 388 break; 389 case OPEN_DOWNLOAD: 390 { 391 // TODO: In case of executable files, ask the user first! 392 entry_ref ref; 393 status_t status = get_ref_for_path(fPath.Path(), &ref); 394 if (status == B_OK) 395 status = be_roster->Launch(&ref); 396 if (status != B_OK && status != B_ALREADY_RUNNING) { 397 BAlert* alert = new BAlert(B_TRANSLATE("Open download error"), 398 B_TRANSLATE("The download could not be opened."), 399 B_TRANSLATE("OK")); 400 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 401 alert->Go(NULL); 402 } 403 break; 404 } 405 case RESTART_DOWNLOAD: 406 BWebPage::RequestDownload(fURL); 407 break; 408 409 case CANCEL_DOWNLOAD: 410 fDownload->Cancel(); 411 DownloadCanceled(); 412 break; 413 414 case REMOVE_DOWNLOAD: 415 { 416 Window()->PostMessage(SAVE_SETTINGS); 417 RemoveSelf(); 418 delete this; 419 // TOAST! 420 return; 421 } 422 case B_NODE_MONITOR: 423 { 424 int32 opCode; 425 if (message->FindInt32("opcode", &opCode) != B_OK) 426 break; 427 switch (opCode) { 428 case B_ENTRY_REMOVED: 429 fIconView->SetIconDimmed(true); 430 fDownload->Cancel(); 431 DownloadCanceled(); 432 break; 433 case B_ENTRY_MOVED: 434 { 435 // Follow the entry to the new location 436 dev_t device; 437 ino_t directory; 438 const char* name; 439 if (message->FindInt32("device", 440 reinterpret_cast<int32*>(&device)) != B_OK 441 || message->FindInt64("to directory", 442 reinterpret_cast<int64*>(&directory)) != B_OK 443 || message->FindString("name", &name) != B_OK 444 || strlen(name) == 0) { 445 break; 446 } 447 // Construct the BEntry and update fPath 448 entry_ref ref(device, directory, name); 449 BEntry entry(&ref); 450 if (entry.GetPath(&fPath) != B_OK) 451 break; 452 453 // Find out if the directory is the Trash for this 454 // volume 455 char trashPath[B_PATH_NAME_LENGTH]; 456 if (find_directory(B_TRASH_DIRECTORY, device, false, 457 trashPath, B_PATH_NAME_LENGTH) == B_OK) { 458 BPath trashDirectory(trashPath); 459 BPath parentDirectory; 460 fPath.GetParent(&parentDirectory); 461 if (parentDirectory == trashDirectory) { 462 // The entry was moved into the Trash. 463 // If the download is still in progress, 464 // cancel it. 465 if (fDownload) 466 fDownload->Cancel(); 467 fIconView->SetIconDimmed(true); 468 DownloadCanceled(); 469 break; 470 } else if (fIconView->IsIconDimmed()) { 471 // Maybe it was moved out of the trash. 472 fIconView->SetIconDimmed(false); 473 } 474 } 475 476 // Inform download of the new path 477 if (fDownload) 478 fDownload->HasMovedTo(fPath); 479 480 float value = fStatusBar->CurrentValue(); 481 fStatusBar->Reset(name); 482 fStatusBar->SetTo(value); 483 Window()->PostMessage(SAVE_SETTINGS); 484 break; 485 } 486 case B_ATTR_CHANGED: 487 { 488 BEntry entry(fPath.Path()); 489 fIconView->SetIconDimmed(false); 490 fIconView->SetTo(entry); 491 break; 492 } 493 } 494 break; 495 } 496 497 // Context menu messages 498 case COPY_URL_TO_CLIPBOARD: 499 if (be_clipboard->Lock()) { 500 BMessage* data = be_clipboard->Data(); 501 if (data != NULL) { 502 be_clipboard->Clear(); 503 data->AddData("text/plain", B_MIME_TYPE, fURL.String(), 504 fURL.Length()); 505 } 506 be_clipboard->Commit(); 507 be_clipboard->Unlock(); 508 } 509 break; 510 case OPEN_CONTAINING_FOLDER: 511 if (fPath.InitCheck() == B_OK) { 512 BPath containingFolder; 513 if (fPath.GetParent(&containingFolder) != B_OK) 514 break; 515 BEntry entry(containingFolder.Path()); 516 if (!entry.Exists()) 517 break; 518 entry_ref ref; 519 if (entry.GetRef(&ref) != B_OK) 520 break; 521 be_roster->Launch(&ref); 522 523 // Use Tracker scripting and select the download pose 524 // in the window. 525 // TODO: We should somehow get the window that just openend. 526 // Using the name like this is broken when there are multiple 527 // windows open with this name. Also Tracker does not scroll 528 // to this entry. 529 BString windowName = ref.name; 530 BString fullWindowName = containingFolder.Path(); 531 532 BMessenger trackerMessenger("application/x-vnd.Be-TRAK"); 533 if (trackerMessenger.IsValid() 534 && get_ref_for_path(fPath.Path(), &ref) == B_OK) { 535 // We need to wait a bit until the folder is open. 536 // TODO: This is also too fragile... we should be able 537 // to wait for the roster message. 538 snooze(250000); 539 int32 tries = 2; 540 while (tries > 0) { 541 BMessage selectionCommand(B_SET_PROPERTY); 542 selectionCommand.AddSpecifier("Selection"); 543 selectionCommand.AddSpecifier("Poses"); 544 selectionCommand.AddSpecifier("Window", 545 windowName.String()); 546 selectionCommand.AddRef("data", &ref); 547 BMessage reply; 548 trackerMessenger.SendMessage(&selectionCommand, &reply); 549 int32 error; 550 if (reply.FindInt32("error", &error) != B_OK 551 || error == B_OK) { 552 break; 553 } 554 windowName = fullWindowName; 555 tries--; 556 } 557 } 558 } 559 break; 560 561 default: 562 BGroupView::MessageReceived(message); 563 } 564 } 565 566 567 void 568 DownloadProgressView::ShowContextMenu(BPoint screenWhere) 569 { 570 screenWhere += BPoint(2, 2); 571 572 BPopUpMenu* contextMenu = new BPopUpMenu("download context"); 573 BMenuItem* copyURL = new BMenuItem(B_TRANSLATE("Copy URL to clipboard"), 574 new BMessage(COPY_URL_TO_CLIPBOARD)); 575 copyURL->SetEnabled(fURL.Length() > 0); 576 contextMenu->AddItem(copyURL); 577 BMenuItem* openFolder = new BMenuItem(B_TRANSLATE("Open containing folder"), 578 new BMessage(OPEN_CONTAINING_FOLDER)); 579 contextMenu->AddItem(openFolder); 580 581 contextMenu->SetTargetForItems(this); 582 contextMenu->Go(screenWhere, true, true, true); 583 } 584 585 586 BWebDownload* 587 DownloadProgressView::Download() const 588 { 589 return fDownload; 590 } 591 592 593 const BString& 594 DownloadProgressView::URL() const 595 { 596 return fURL; 597 } 598 599 600 bool 601 DownloadProgressView::IsMissing() const 602 { 603 return fIconView->IsIconDimmed(); 604 } 605 606 607 bool 608 DownloadProgressView::IsFinished() const 609 { 610 return !fDownload && fStatusBar->CurrentValue() == 100; 611 } 612 613 614 void 615 DownloadProgressView::DownloadFinished() 616 { 617 fDownload = NULL; 618 if (fExpectedSize == -1) { 619 fStatusBar->SetTo(100.0); 620 fExpectedSize = fCurrentSize; 621 } 622 fTopButton->SetEnabled(true); 623 fBottomButton->SetLabel(B_TRANSLATE("Remove")); 624 fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD)); 625 fBottomButton->SetEnabled(true); 626 fInfoView->SetText(""); 627 fStatusBar->SetBarColor(ui_color(B_SUCCESS_COLOR)); 628 629 BNotification success(B_INFORMATION_NOTIFICATION); 630 success.SetTitle(B_TRANSLATE("Download finished")); 631 success.SetContent(fPath.Leaf()); 632 BEntry entry(fPath.Path()); 633 entry_ref ref; 634 entry.GetRef(&ref); 635 success.SetOnClickFile(&ref); 636 success.SetIcon(fIconView->Bitmap()); 637 success.Send(); 638 639 } 640 641 642 void 643 DownloadProgressView::DownloadCanceled() 644 { 645 // Show the cancel notification, and set the progress bar red, only if the 646 // download was still running. In cases where the file is deleted after 647 // the download was finished, we don't want these things to happen. 648 if (fDownload) { 649 BNotification success(B_ERROR_NOTIFICATION); 650 success.SetTitle(B_TRANSLATE("Download aborted")); 651 success.SetContent(fPath.Leaf()); 652 // Don't make a click on the notification open the file: it is not 653 // complete 654 success.SetIcon(fIconView->Bitmap()); 655 success.Send(); 656 657 fStatusBar->SetBarColor(ui_color(B_FAILURE_COLOR)); 658 } 659 660 fDownload = NULL; 661 fTopButton->SetLabel(B_TRANSLATE("Restart")); 662 fTopButton->SetMessage(new BMessage(RESTART_DOWNLOAD)); 663 fTopButton->SetEnabled(true); 664 fBottomButton->SetLabel(B_TRANSLATE("Remove")); 665 fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD)); 666 fBottomButton->SetEnabled(true); 667 fInfoView->SetText(""); 668 669 fPath.Unset(); 670 } 671 672 673 /*static*/ void 674 DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse() 675 { 676 bigtime_t now = system_time(); 677 if (sShowSpeed 678 && sLastEstimatedFinishSpeedToggleTime + kShowSpeedInterval 679 <= now) { 680 sShowSpeed = false; 681 sLastEstimatedFinishSpeedToggleTime = now; 682 } else if (!sShowSpeed 683 && sLastEstimatedFinishSpeedToggleTime 684 + kShowEstimatedFinishInterval <= now) { 685 sShowSpeed = true; 686 sLastEstimatedFinishSpeedToggleTime = now; 687 } 688 } 689 690 691 // #pragma mark - private 692 693 694 void 695 DownloadProgressView::_UpdateStatus(off_t currentSize, off_t expectedSize) 696 { 697 fCurrentSize = currentSize; 698 fExpectedSize = expectedSize; 699 700 fStatusBar->SetTo(100.0 * currentSize / expectedSize); 701 702 bigtime_t currentTime = system_time(); 703 if ((currentTime - fLastUpdateTime) > kMaxUpdateInterval) { 704 fLastUpdateTime = currentTime; 705 706 if (currentTime >= fLastSpeedReferenceTime + kSpeedReferenceInterval) { 707 // update current speed every kSpeedReferenceInterval 708 fCurrentBytesPerSecondSlot 709 = (fCurrentBytesPerSecondSlot + 1) % kBytesPerSecondSlots; 710 fBytesPerSecondSlot[fCurrentBytesPerSecondSlot] 711 = (double)(currentSize - fLastSpeedReferenceSize) 712 * 1000000LL / (currentTime - fLastSpeedReferenceTime); 713 fLastSpeedReferenceSize = currentSize; 714 fLastSpeedReferenceTime = currentTime; 715 fBytesPerSecond = 0.0; 716 size_t count = 0; 717 for (size_t i = 0; i < kBytesPerSecondSlots; i++) { 718 if (fBytesPerSecondSlot[i] != 0.0) { 719 fBytesPerSecond += fBytesPerSecondSlot[i]; 720 count++; 721 } 722 } 723 if (count > 0) 724 fBytesPerSecond /= count; 725 } 726 _UpdateStatusText(); 727 } 728 } 729 730 731 void 732 DownloadProgressView::_UpdateStatusText() 733 { 734 fInfoView->SetText(""); 735 BString buffer; 736 if (sShowSpeed && fBytesPerSecond != 0.0) { 737 // Draw speed info 738 char sizeBuffer[128]; 739 buffer = "("; 740 // Get strings for current and expected size and remove the unit 741 // from the current size string if it's the same as the expected 742 // size unit. 743 BString currentSize = string_for_size((double)fCurrentSize, sizeBuffer, 744 sizeof(sizeBuffer)); 745 BString expectedSize = string_for_size((double)fExpectedSize, sizeBuffer, 746 sizeof(sizeBuffer)); 747 int currentSizeUnitPos = currentSize.FindLast(' '); 748 int expectedSizeUnitPos = expectedSize.FindLast(' '); 749 if (currentSizeUnitPos >= 0 && expectedSizeUnitPos >= 0 750 && strcmp(currentSize.String() + currentSizeUnitPos, 751 expectedSize.String() + expectedSizeUnitPos) == 0) { 752 currentSize.Truncate(currentSizeUnitPos); 753 } 754 buffer << currentSize; 755 buffer << " "; 756 buffer << B_TRANSLATE_COMMENT("of", "...as in '12kB of 256kB'"); 757 buffer << " "; 758 buffer << expectedSize; 759 buffer << ", "; 760 buffer << string_for_size(fBytesPerSecond, sizeBuffer, 761 sizeof(sizeBuffer)); 762 buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'"); 763 float stringWidth = fInfoView->StringWidth(buffer.String()); 764 if (stringWidth < fInfoView->Bounds().Width()) 765 fInfoView->SetText(buffer.String()); 766 else { 767 // complete string too wide, try with shorter version 768 buffer << string_for_size(fBytesPerSecond, sizeBuffer, 769 sizeof(sizeBuffer)); 770 buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'"); 771 stringWidth = fInfoView->StringWidth(buffer.String()); 772 if (stringWidth < fInfoView->Bounds().Width()) 773 fInfoView->SetText(buffer.String()); 774 } 775 } else if (!sShowSpeed && fCurrentSize < fExpectedSize) { 776 double totalBytesPerSecond = (double)(fCurrentSize 777 - fEstimatedFinishReferenceSize) 778 * 1000000LL / (system_time() - fEstimatedFinishReferenceTime); 779 double secondsRemaining = (fExpectedSize - fCurrentSize) 780 / totalBytesPerSecond; 781 time_t now = (time_t)real_time_clock(); 782 time_t finishTime = (time_t)(now + secondsRemaining); 783 784 tm _time; 785 tm* time = localtime_r(&finishTime, &_time); 786 int32 year = time->tm_year + 1900; 787 788 char timeText[32]; 789 time_t secondsPerDay = 24 * 60 * 60; 790 // TODO: Localization of time string... 791 if (now < finishTime - secondsPerDay) { 792 // process is going to take more than a day! 793 sprintf(timeText, "%0*d:%0*d %0*d/%0*d/%" B_PRId32, 794 2, time->tm_hour, 2, time->tm_min, 795 2, time->tm_mon + 1, 2, time->tm_mday, year); 796 } else { 797 sprintf(timeText, "%0*d:%0*d", 798 2, time->tm_hour, 2, time->tm_min); 799 } 800 801 BString buffer1(B_TRANSLATE_COMMENT("Finish: ", "Finishing time")); 802 buffer1 << timeText; 803 finishTime -= now; 804 time = gmtime(&finishTime); 805 806 BString buffer2; 807 if (finishTime > secondsPerDay) { 808 int64 days = finishTime / secondsPerDay; 809 if (days == 1) 810 buffer2 << B_TRANSLATE("Over 1 day left"); 811 else { 812 buffer2 << B_TRANSLATE("Over %days days left"); 813 buffer2.ReplaceFirst("%days", BString() << days); 814 } 815 } else if (finishTime > 60 * 60) { 816 int64 hours = finishTime / (60 * 60); 817 if (hours == 1) 818 buffer2 << B_TRANSLATE("Over 1 hour left"); 819 else { 820 buffer2 << B_TRANSLATE("Over %hours hours left"); 821 buffer2.ReplaceFirst("%hours", BString() << hours); 822 } 823 } else if (finishTime > 60) { 824 int64 minutes = finishTime / 60; 825 if (minutes == 1) 826 buffer2 << B_TRANSLATE("Over 1 minute left"); 827 else { 828 buffer2 << B_TRANSLATE("%minutes minutes"); 829 buffer2.ReplaceFirst("%minutes", BString() << minutes); 830 } 831 } else { 832 if (finishTime == 1) 833 buffer2 << B_TRANSLATE("1 second left"); 834 else { 835 buffer2 << B_TRANSLATE("%seconds seconds left"); 836 buffer2.ReplaceFirst("%seconds", BString() << finishTime); 837 } 838 } 839 840 buffer = "("; 841 buffer << buffer1 << " - " << buffer2 << ")"; 842 843 float stringWidth = fInfoView->StringWidth(buffer.String()); 844 if (stringWidth < fInfoView->Bounds().Width()) 845 fInfoView->SetText(buffer.String()); 846 else { 847 // complete string too wide, try with shorter version 848 buffer = "("; 849 buffer << buffer1 << ")"; 850 stringWidth = fInfoView->StringWidth(buffer.String()); 851 if (stringWidth < fInfoView->Bounds().Width()) 852 fInfoView->SetText(buffer.String()); 853 } 854 } 855 } 856 857 858 void 859 DownloadProgressView::_StartNodeMonitor(const BEntry& entry) 860 { 861 node_ref nref; 862 if (entry.GetNodeRef(&nref) == B_OK) 863 watch_node(&nref, B_WATCH_ALL, this); 864 } 865 866 867 void 868 DownloadProgressView::_StopNodeMonitor() 869 { 870 stop_watching(this); 871 } 872 873