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 DownloadCanceled(); 431 break; 432 case B_ENTRY_MOVED: 433 { 434 // Follow the entry to the new location 435 dev_t device; 436 ino_t directory; 437 const char* name; 438 if (message->FindInt32("device", 439 reinterpret_cast<int32*>(&device)) != B_OK 440 || message->FindInt64("to directory", 441 reinterpret_cast<int64*>(&directory)) != B_OK 442 || message->FindString("name", &name) != B_OK 443 || strlen(name) == 0) { 444 break; 445 } 446 // Construct the BEntry and update fPath 447 entry_ref ref(device, directory, name); 448 BEntry entry(&ref); 449 if (entry.GetPath(&fPath) != B_OK) 450 break; 451 452 // Find out if the directory is the Trash for this 453 // volume 454 char trashPath[B_PATH_NAME_LENGTH]; 455 if (find_directory(B_TRASH_DIRECTORY, device, false, 456 trashPath, B_PATH_NAME_LENGTH) == B_OK) { 457 BPath trashDirectory(trashPath); 458 BPath parentDirectory; 459 fPath.GetParent(&parentDirectory); 460 if (parentDirectory == trashDirectory) { 461 // The entry was moved into the Trash. 462 // If the download is still in progress, 463 // cancel it. 464 if (fDownload) 465 fDownload->Cancel(); 466 fIconView->SetIconDimmed(true); 467 DownloadCanceled(); 468 break; 469 } else if (fIconView->IsIconDimmed()) { 470 // Maybe it was moved out of the trash. 471 fIconView->SetIconDimmed(false); 472 } 473 } 474 475 // Inform download of the new path 476 if (fDownload) 477 fDownload->HasMovedTo(fPath); 478 479 float value = fStatusBar->CurrentValue(); 480 fStatusBar->Reset(name); 481 fStatusBar->SetTo(value); 482 Window()->PostMessage(SAVE_SETTINGS); 483 break; 484 } 485 case B_ATTR_CHANGED: 486 { 487 BEntry entry(fPath.Path()); 488 fIconView->SetIconDimmed(false); 489 fIconView->SetTo(entry); 490 break; 491 } 492 } 493 break; 494 } 495 496 // Context menu messages 497 case COPY_URL_TO_CLIPBOARD: 498 if (be_clipboard->Lock()) { 499 BMessage* data = be_clipboard->Data(); 500 if (data != NULL) { 501 be_clipboard->Clear(); 502 data->AddData("text/plain", B_MIME_TYPE, fURL.String(), 503 fURL.Length()); 504 } 505 be_clipboard->Commit(); 506 be_clipboard->Unlock(); 507 } 508 break; 509 case OPEN_CONTAINING_FOLDER: 510 if (fPath.InitCheck() == B_OK) { 511 BPath containingFolder; 512 if (fPath.GetParent(&containingFolder) != B_OK) 513 break; 514 BEntry entry(containingFolder.Path()); 515 if (!entry.Exists()) 516 break; 517 entry_ref ref; 518 if (entry.GetRef(&ref) != B_OK) 519 break; 520 be_roster->Launch(&ref); 521 522 // Use Tracker scripting and select the download pose 523 // in the window. 524 // TODO: We should somehow get the window that just openend. 525 // Using the name like this is broken when there are multiple 526 // windows open with this name. Also Tracker does not scroll 527 // to this entry. 528 BString windowName = ref.name; 529 BString fullWindowName = containingFolder.Path(); 530 531 BMessenger trackerMessenger("application/x-vnd.Be-TRAK"); 532 if (trackerMessenger.IsValid() 533 && get_ref_for_path(fPath.Path(), &ref) == B_OK) { 534 // We need to wait a bit until the folder is open. 535 // TODO: This is also too fragile... we should be able 536 // to wait for the roster message. 537 snooze(250000); 538 int32 tries = 2; 539 while (tries > 0) { 540 BMessage selectionCommand(B_SET_PROPERTY); 541 selectionCommand.AddSpecifier("Selection"); 542 selectionCommand.AddSpecifier("Poses"); 543 selectionCommand.AddSpecifier("Window", 544 windowName.String()); 545 selectionCommand.AddRef("data", &ref); 546 BMessage reply; 547 trackerMessenger.SendMessage(&selectionCommand, &reply); 548 int32 error; 549 if (reply.FindInt32("error", &error) != B_OK 550 || error == B_OK) { 551 break; 552 } 553 windowName = fullWindowName; 554 tries--; 555 } 556 } 557 } 558 break; 559 560 default: 561 BGroupView::MessageReceived(message); 562 } 563 } 564 565 566 void 567 DownloadProgressView::ShowContextMenu(BPoint screenWhere) 568 { 569 screenWhere += BPoint(2, 2); 570 571 BPopUpMenu* contextMenu = new BPopUpMenu("download context"); 572 BMenuItem* copyURL = new BMenuItem(B_TRANSLATE("Copy URL to clipboard"), 573 new BMessage(COPY_URL_TO_CLIPBOARD)); 574 copyURL->SetEnabled(fURL.Length() > 0); 575 contextMenu->AddItem(copyURL); 576 BMenuItem* openFolder = new BMenuItem(B_TRANSLATE("Open containing folder"), 577 new BMessage(OPEN_CONTAINING_FOLDER)); 578 contextMenu->AddItem(openFolder); 579 580 contextMenu->SetTargetForItems(this); 581 contextMenu->Go(screenWhere, true, true, true); 582 } 583 584 585 BWebDownload* 586 DownloadProgressView::Download() const 587 { 588 return fDownload; 589 } 590 591 592 const BString& 593 DownloadProgressView::URL() const 594 { 595 return fURL; 596 } 597 598 599 bool 600 DownloadProgressView::IsMissing() const 601 { 602 return fIconView->IsIconDimmed(); 603 } 604 605 606 bool 607 DownloadProgressView::IsFinished() const 608 { 609 return !fDownload && fStatusBar->CurrentValue() == 100; 610 } 611 612 613 void 614 DownloadProgressView::DownloadFinished() 615 { 616 fDownload = NULL; 617 if (fExpectedSize == -1) { 618 fStatusBar->SetTo(100.0); 619 fExpectedSize = fCurrentSize; 620 } 621 fTopButton->SetEnabled(true); 622 fBottomButton->SetLabel(B_TRANSLATE("Remove")); 623 fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD)); 624 fBottomButton->SetEnabled(true); 625 fInfoView->SetText(""); 626 fStatusBar->SetBarColor(ui_color(B_SUCCESS_COLOR)); 627 628 BNotification success(B_INFORMATION_NOTIFICATION); 629 success.SetTitle(B_TRANSLATE("Download finished")); 630 success.SetContent(fPath.Leaf()); 631 BEntry entry(fPath.Path()); 632 entry_ref ref; 633 entry.GetRef(&ref); 634 success.SetOnClickFile(&ref); 635 success.SetIcon(fIconView->Bitmap()); 636 success.Send(); 637 638 } 639 640 641 void 642 DownloadProgressView::DownloadCanceled() 643 { 644 fDownload = NULL; 645 fTopButton->SetLabel(B_TRANSLATE("Restart")); 646 fTopButton->SetMessage(new BMessage(RESTART_DOWNLOAD)); 647 fTopButton->SetEnabled(true); 648 fBottomButton->SetLabel(B_TRANSLATE("Remove")); 649 fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD)); 650 fBottomButton->SetEnabled(true); 651 fInfoView->SetText(""); 652 fStatusBar->SetBarColor(ui_color(B_FAILURE_COLOR)); 653 654 BNotification success(B_ERROR_NOTIFICATION); 655 success.SetTitle(B_TRANSLATE("Download aborted")); 656 success.SetContent(fPath.Leaf()); 657 // Don't make a click on the notification open the file: it is not complete 658 success.SetIcon(fIconView->Bitmap()); 659 success.Send(); 660 661 fPath.Unset(); 662 } 663 664 665 /*static*/ void 666 DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse() 667 { 668 bigtime_t now = system_time(); 669 if (sShowSpeed 670 && sLastEstimatedFinishSpeedToggleTime + kShowSpeedInterval 671 <= now) { 672 sShowSpeed = false; 673 sLastEstimatedFinishSpeedToggleTime = now; 674 } else if (!sShowSpeed 675 && sLastEstimatedFinishSpeedToggleTime 676 + kShowEstimatedFinishInterval <= now) { 677 sShowSpeed = true; 678 sLastEstimatedFinishSpeedToggleTime = now; 679 } 680 } 681 682 683 // #pragma mark - private 684 685 686 void 687 DownloadProgressView::_UpdateStatus(off_t currentSize, off_t expectedSize) 688 { 689 fCurrentSize = currentSize; 690 fExpectedSize = expectedSize; 691 692 fStatusBar->SetTo(100.0 * currentSize / expectedSize); 693 694 bigtime_t currentTime = system_time(); 695 if ((currentTime - fLastUpdateTime) > kMaxUpdateInterval) { 696 fLastUpdateTime = currentTime; 697 698 if (currentTime >= fLastSpeedReferenceTime + kSpeedReferenceInterval) { 699 // update current speed every kSpeedReferenceInterval 700 fCurrentBytesPerSecondSlot 701 = (fCurrentBytesPerSecondSlot + 1) % kBytesPerSecondSlots; 702 fBytesPerSecondSlot[fCurrentBytesPerSecondSlot] 703 = (double)(currentSize - fLastSpeedReferenceSize) 704 * 1000000LL / (currentTime - fLastSpeedReferenceTime); 705 fLastSpeedReferenceSize = currentSize; 706 fLastSpeedReferenceTime = currentTime; 707 fBytesPerSecond = 0.0; 708 size_t count = 0; 709 for (size_t i = 0; i < kBytesPerSecondSlots; i++) { 710 if (fBytesPerSecondSlot[i] != 0.0) { 711 fBytesPerSecond += fBytesPerSecondSlot[i]; 712 count++; 713 } 714 } 715 if (count > 0) 716 fBytesPerSecond /= count; 717 } 718 _UpdateStatusText(); 719 } 720 } 721 722 723 void 724 DownloadProgressView::_UpdateStatusText() 725 { 726 fInfoView->SetText(""); 727 BString buffer; 728 if (sShowSpeed && fBytesPerSecond != 0.0) { 729 // Draw speed info 730 char sizeBuffer[128]; 731 buffer = "("; 732 // Get strings for current and expected size and remove the unit 733 // from the current size string if it's the same as the expected 734 // size unit. 735 BString currentSize = string_for_size((double)fCurrentSize, sizeBuffer, 736 sizeof(sizeBuffer)); 737 BString expectedSize = string_for_size((double)fExpectedSize, sizeBuffer, 738 sizeof(sizeBuffer)); 739 int currentSizeUnitPos = currentSize.FindLast(' '); 740 int expectedSizeUnitPos = expectedSize.FindLast(' '); 741 if (currentSizeUnitPos >= 0 && expectedSizeUnitPos >= 0 742 && strcmp(currentSize.String() + currentSizeUnitPos, 743 expectedSize.String() + expectedSizeUnitPos) == 0) { 744 currentSize.Truncate(currentSizeUnitPos); 745 } 746 buffer << currentSize; 747 buffer << " "; 748 buffer << B_TRANSLATE_COMMENT("of", "...as in '12kB of 256kB'"); 749 buffer << " "; 750 buffer << expectedSize; 751 buffer << ", "; 752 buffer << string_for_size(fBytesPerSecond, sizeBuffer, 753 sizeof(sizeBuffer)); 754 buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'"); 755 float stringWidth = fInfoView->StringWidth(buffer.String()); 756 if (stringWidth < fInfoView->Bounds().Width()) 757 fInfoView->SetText(buffer.String()); 758 else { 759 // complete string too wide, try with shorter version 760 buffer << string_for_size(fBytesPerSecond, sizeBuffer, 761 sizeof(sizeBuffer)); 762 buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'"); 763 stringWidth = fInfoView->StringWidth(buffer.String()); 764 if (stringWidth < fInfoView->Bounds().Width()) 765 fInfoView->SetText(buffer.String()); 766 } 767 } else if (!sShowSpeed && fCurrentSize < fExpectedSize) { 768 double totalBytesPerSecond = (double)(fCurrentSize 769 - fEstimatedFinishReferenceSize) 770 * 1000000LL / (system_time() - fEstimatedFinishReferenceTime); 771 double secondsRemaining = (fExpectedSize - fCurrentSize) 772 / totalBytesPerSecond; 773 time_t now = (time_t)real_time_clock(); 774 time_t finishTime = (time_t)(now + secondsRemaining); 775 776 tm _time; 777 tm* time = localtime_r(&finishTime, &_time); 778 int32 year = time->tm_year + 1900; 779 780 char timeText[32]; 781 time_t secondsPerDay = 24 * 60 * 60; 782 // TODO: Localization of time string... 783 if (now < finishTime - secondsPerDay) { 784 // process is going to take more than a day! 785 sprintf(timeText, "%0*d:%0*d %0*d/%0*d/%" B_PRId32, 786 2, time->tm_hour, 2, time->tm_min, 787 2, time->tm_mon + 1, 2, time->tm_mday, year); 788 } else { 789 sprintf(timeText, "%0*d:%0*d", 790 2, time->tm_hour, 2, time->tm_min); 791 } 792 793 BString buffer1(B_TRANSLATE_COMMENT("Finish: ", "Finishing time")); 794 buffer1 << timeText; 795 finishTime -= now; 796 time = gmtime(&finishTime); 797 798 BString buffer2; 799 if (finishTime > secondsPerDay) { 800 int64 days = finishTime / secondsPerDay; 801 if (days == 1) 802 buffer2 << B_TRANSLATE("Over 1 day left"); 803 else { 804 buffer2 << B_TRANSLATE("Over %days days left"); 805 buffer2.ReplaceFirst("%days", BString() << days); 806 } 807 } else if (finishTime > 60 * 60) { 808 int64 hours = finishTime / (60 * 60); 809 if (hours == 1) 810 buffer2 << B_TRANSLATE("Over 1 hour left"); 811 else { 812 buffer2 << B_TRANSLATE("Over %hours hours left"); 813 buffer2.ReplaceFirst("%hours", BString() << hours); 814 } 815 } else if (finishTime > 60) { 816 int64 minutes = finishTime / 60; 817 if (minutes == 1) 818 buffer2 << B_TRANSLATE("Over 1 minute left"); 819 else { 820 buffer2 << B_TRANSLATE("%minutes minutes"); 821 buffer2.ReplaceFirst("%minutes", BString() << minutes); 822 } 823 } else { 824 if (finishTime == 1) 825 buffer2 << B_TRANSLATE("1 second left"); 826 else { 827 buffer2 << B_TRANSLATE("%seconds seconds left"); 828 buffer2.ReplaceFirst("%seconds", BString() << finishTime); 829 } 830 } 831 832 buffer = "("; 833 buffer << buffer1 << " - " << buffer2 << ")"; 834 835 float stringWidth = fInfoView->StringWidth(buffer.String()); 836 if (stringWidth < fInfoView->Bounds().Width()) 837 fInfoView->SetText(buffer.String()); 838 else { 839 // complete string too wide, try with shorter version 840 buffer = "("; 841 buffer << buffer1 << ")"; 842 stringWidth = fInfoView->StringWidth(buffer.String()); 843 if (stringWidth < fInfoView->Bounds().Width()) 844 fInfoView->SetText(buffer.String()); 845 } 846 } 847 } 848 849 850 void 851 DownloadProgressView::_StartNodeMonitor(const BEntry& entry) 852 { 853 node_ref nref; 854 if (entry.GetNodeRef(&nref) == B_OK) 855 watch_node(&nref, B_WATCH_ALL, this); 856 } 857 858 859 void 860 DownloadProgressView::_StopNodeMonitor() 861 { 862 stop_watching(this); 863 } 864 865