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