/* * Copyright (C) 2010 Stephan Aßmus * * All rights reserved. Distributed under the terms of the MIT License. */ #include "DownloadProgressView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "BrowserWindow.h" #include "WebDownload.h" #include "WebPage.h" #include "StringForSize.h" #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "Download Window" enum { OPEN_DOWNLOAD = 'opdn', RESTART_DOWNLOAD = 'rsdn', CANCEL_DOWNLOAD = 'cndn', REMOVE_DOWNLOAD = 'rmdn', COPY_URL_TO_CLIPBOARD = 'curl', OPEN_CONTAINING_FOLDER = 'opfd', }; const bigtime_t kMaxUpdateInterval = 100000LL; const bigtime_t kSpeedReferenceInterval = 500000LL; const bigtime_t kShowSpeedInterval = 8000000LL; const bigtime_t kShowEstimatedFinishInterval = 4000000LL; bigtime_t DownloadProgressView::sLastEstimatedFinishSpeedToggleTime = -1; bool DownloadProgressView::sShowSpeed = true; static const time_t kSecondsPerDay = 24 * 60 * 60; static const time_t kSecondsPerHour = 60 * 60; class IconView : public BView { public: IconView(const BEntry& entry) : BView("Download icon", B_WILL_DRAW), fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32), fDimmedIcon(false) { SetDrawingMode(B_OP_OVER); SetTo(entry); } IconView() : BView("Download icon", B_WILL_DRAW), fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32), fDimmedIcon(false) { SetDrawingMode(B_OP_OVER); memset(fIconBitmap.Bits(), 0, fIconBitmap.BitsLength()); } IconView(BMessage* archive) : BView("Download icon", B_WILL_DRAW), fIconBitmap(archive), fDimmedIcon(true) { SetDrawingMode(B_OP_OVER); } void SetTo(const BEntry& entry) { BNode node(&entry); BNodeInfo info(&node); info.GetTrackerIcon(&fIconBitmap, B_LARGE_ICON); Invalidate(); } void SetIconDimmed(bool iconDimmed) { if (fDimmedIcon != iconDimmed) { fDimmedIcon = iconDimmed; Invalidate(); } } bool IsIconDimmed() const { return fDimmedIcon; } status_t SaveSettings(BMessage* archive) { return fIconBitmap.Archive(archive); } virtual void AttachedToWindow() { AdoptParentColors(); } virtual void Draw(BRect updateRect) { if (fDimmedIcon) { SetDrawingMode(B_OP_ALPHA); SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); SetHighColor(0, 0, 0, 100); } DrawBitmapAsync(&fIconBitmap); } virtual BSize MinSize() { return BSize(fIconBitmap.Bounds().Width(), fIconBitmap.Bounds().Height()); } virtual BSize PreferredSize() { return MinSize(); } virtual BSize MaxSize() { return MinSize(); } BBitmap* Bitmap() { return &fIconBitmap; } private: BBitmap fIconBitmap; bool fDimmedIcon; }; class SmallButton : public BButton { public: SmallButton(const char* label, BMessage* message = NULL) : BButton(label, message) { BFont font; GetFont(&font); float size = ceilf(font.Size() * 0.8); font.SetSize(max_c(8, size)); SetFont(&font, B_FONT_SIZE); } }; // #pragma mark - DownloadProgressView DownloadProgressView::DownloadProgressView(BWebDownload* download) : BGroupView(B_HORIZONTAL, 8), fDownload(download), fURL(download->URL()), fPath(download->Path()) { } DownloadProgressView::DownloadProgressView(const BMessage* archive) : BGroupView(B_HORIZONTAL, 8), fDownload(NULL), fURL(), fPath() { const char* string; if (archive->FindString("path", &string) == B_OK) fPath.SetTo(string); if (archive->FindString("url", &string) == B_OK) fURL = string; } bool DownloadProgressView::Init(BMessage* archive) { fCurrentSize = 0; fExpectedSize = 0; fLastUpdateTime = 0; fBytesPerSecond = 0.0; for (size_t i = 0; i < kBytesPerSecondSlots; i++) fBytesPerSecondSlot[i] = 0.0; fCurrentBytesPerSecondSlot = 0; fLastSpeedReferenceSize = 0; fEstimatedFinishReferenceSize = 0; fProcessStartTime = fLastSpeedReferenceTime = fEstimatedFinishReferenceTime = system_time(); SetViewColor(245, 245, 245); SetFlags(Flags() | B_FULL_UPDATE_ON_RESIZE | B_WILL_DRAW); if (archive) { fStatusBar = new BStatusBar("download progress", fPath.Leaf()); float value; if (archive->FindFloat("value", &value) == B_OK) fStatusBar->SetTo(value); } else fStatusBar = new BStatusBar("download progress", "Download"); fStatusBar->SetMaxValue(100); fStatusBar->SetBarHeight(12); // fPath is only valid when constructed from archive (fDownload == NULL) BEntry entry(fPath.Path()); if (archive) { if (!entry.Exists()) fIconView = new IconView(archive); else fIconView = new IconView(entry); } else fIconView = new IconView(); if (!fDownload && (fStatusBar->CurrentValue() < 100 || !entry.Exists())) { fTopButton = new SmallButton(B_TRANSLATE("Restart"), new BMessage(RESTART_DOWNLOAD)); } else { fTopButton = new SmallButton(B_TRANSLATE("Open"), new BMessage(OPEN_DOWNLOAD)); fTopButton->SetEnabled(fDownload == NULL); } if (fDownload) { fBottomButton = new SmallButton(B_TRANSLATE("Cancel"), new BMessage(CANCEL_DOWNLOAD)); } else { fBottomButton = new SmallButton(B_TRANSLATE("Remove"), new BMessage(REMOVE_DOWNLOAD)); fBottomButton->SetEnabled(fDownload == NULL); } fInfoView = new BStringView("info view", ""); fInfoView->SetViewColor(ViewColor()); BSize topButtonSize = fTopButton->PreferredSize(); BSize bottomButtonSize = fBottomButton->PreferredSize(); if (bottomButtonSize.width < topButtonSize.width) fBottomButton->SetExplicitMaxSize(topButtonSize); else fTopButton->SetExplicitMaxSize(bottomButtonSize); BGroupLayout* layout = GroupLayout(); layout->SetInsets(8, 5, 5, 6); layout->AddView(fIconView); BView* verticalGroup = BGroupLayoutBuilder(B_VERTICAL, 3) .Add(fStatusBar) .Add(fInfoView) .TopView() ; verticalGroup->SetViewColor(ViewColor()); layout->AddView(verticalGroup); verticalGroup = BGroupLayoutBuilder(B_VERTICAL, 3) .Add(fTopButton) .Add(fBottomButton) .TopView() ; verticalGroup->SetViewColor(ViewColor()); layout->AddView(verticalGroup); BFont font; fInfoView->GetFont(&font); float fontSize = font.Size() * 0.8f; font.SetSize(max_c(8.0f, fontSize)); fInfoView->SetFont(&font, B_FONT_SIZE); fInfoView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); return true; } status_t DownloadProgressView::SaveSettings(BMessage* archive) { if (!archive) return B_BAD_VALUE; status_t ret = archive->AddString("path", fPath.Path()); if (ret == B_OK) ret = archive->AddString("url", fURL.String()); if (ret == B_OK) ret = archive->AddFloat("value", fStatusBar->CurrentValue()); if (ret == B_OK) ret = fIconView->SaveSettings(archive); return ret; } void DownloadProgressView::AttachedToWindow() { if (fDownload) { fDownload->SetProgressListener(BMessenger(this)); // Will start node monitor upon receiving the B_DOWNLOAD_STARTED // message. } else { BEntry entry(fPath.Path()); if (entry.Exists()) _StartNodeMonitor(entry); } fTopButton->SetTarget(this); fBottomButton->SetTarget(this); } void DownloadProgressView::DetachedFromWindow() { _StopNodeMonitor(); } void DownloadProgressView::AllAttached() { fStatusBar->SetLowColor(ViewColor()); fInfoView->SetLowColor(ViewColor()); fInfoView->SetHighColor(0, 0, 0, 255); SetViewColor(B_TRANSPARENT_COLOR); SetLowColor(245, 245, 245); SetHighColor(tint_color(LowColor(), B_DARKEN_1_TINT)); } void DownloadProgressView::Draw(BRect updateRect) { BRect bounds(Bounds()); bounds.bottom--; FillRect(bounds, B_SOLID_LOW); bounds.bottom++; StrokeLine(bounds.LeftBottom(), bounds.RightBottom()); } void DownloadProgressView::MessageReceived(BMessage* message) { switch (message->what) { case B_DOWNLOAD_STARTED: { BString path; if (message->FindString("path", &path) != B_OK) break; fPath.SetTo(path); BEntry entry(fPath.Path()); fIconView->SetTo(entry); fStatusBar->Reset(fPath.Leaf()); _StartNodeMonitor(entry); // Immediately switch to speed display whenever a new download // starts. sShowSpeed = true; sLastEstimatedFinishSpeedToggleTime = fProcessStartTime = fLastSpeedReferenceTime = fEstimatedFinishReferenceTime = system_time(); break; } case B_DOWNLOAD_PROGRESS: { int64 currentSize; int64 expectedSize; if (message->FindInt64("current size", ¤tSize) == B_OK && message->FindInt64("expected size", &expectedSize) == B_OK) { _UpdateStatus(currentSize, expectedSize); } break; } case B_DOWNLOAD_REMOVED: // TODO: This is a bit asymetric. The removed notification // arrives here, but it would be nicer if it arrived // at the window... Window()->PostMessage(message); break; case OPEN_DOWNLOAD: { // TODO: In case of executable files, ask the user first! entry_ref ref; status_t status = get_ref_for_path(fPath.Path(), &ref); if (status == B_OK) status = be_roster->Launch(&ref); if (status != B_OK && status != B_ALREADY_RUNNING) { BAlert* alert = new BAlert(B_TRANSLATE("Open download error"), B_TRANSLATE("The download could not be opened."), B_TRANSLATE("OK")); alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); alert->Go(NULL); } break; } case RESTART_DOWNLOAD: { // We can't create a download without a full web context (mainly // because it needs to access the cookie jar), and when we get here // the original context is long gone (possibly the browser was // restarted). So we create a new window to restart the download // in a fresh context. // FIXME this has of course the huge downside of leaving the new // window open with a blank page. I can't think of a better // solution right now... BMessage* request = new BMessage(NEW_WINDOW); request->AddString("url", fURL); be_app->PostMessage(request); break; } case CANCEL_DOWNLOAD: CancelDownload(); break; case REMOVE_DOWNLOAD: { Window()->PostMessage(SAVE_SETTINGS); RemoveSelf(); delete this; // TOAST! return; } case B_NODE_MONITOR: { int32 opCode; if (message->FindInt32("opcode", &opCode) != B_OK) break; switch (opCode) { case B_ENTRY_REMOVED: fIconView->SetIconDimmed(true); CancelDownload(); break; case B_ENTRY_MOVED: { // Follow the entry to the new location dev_t device; ino_t directory; const char* name; if (message->FindInt32("device", reinterpret_cast(&device)) != B_OK || message->FindInt64("to directory", reinterpret_cast(&directory)) != B_OK || message->FindString("name", &name) != B_OK || strlen(name) == 0) { break; } // Construct the BEntry and update fPath entry_ref ref(device, directory, name); BEntry entry(&ref); if (entry.GetPath(&fPath) != B_OK) break; // Find out if the directory is the Trash for this // volume char trashPath[B_PATH_NAME_LENGTH]; if (find_directory(B_TRASH_DIRECTORY, device, false, trashPath, B_PATH_NAME_LENGTH) == B_OK) { BPath trashDirectory(trashPath); BPath parentDirectory; fPath.GetParent(&parentDirectory); if (parentDirectory == trashDirectory) { // The entry was moved into the Trash. // If the download is still in progress, // cancel it. fIconView->SetIconDimmed(true); CancelDownload(); break; } else if (fIconView->IsIconDimmed()) { // Maybe it was moved out of the trash. fIconView->SetIconDimmed(false); } } // Inform download of the new path if (fDownload) fDownload->HasMovedTo(fPath); float value = fStatusBar->CurrentValue(); fStatusBar->Reset(name); fStatusBar->SetTo(value); Window()->PostMessage(SAVE_SETTINGS); break; } case B_ATTR_CHANGED: { BEntry entry(fPath.Path()); fIconView->SetIconDimmed(false); fIconView->SetTo(entry); break; } } break; } // Context menu messages case COPY_URL_TO_CLIPBOARD: if (be_clipboard->Lock()) { BMessage* data = be_clipboard->Data(); if (data != NULL) { be_clipboard->Clear(); data->AddData("text/plain", B_MIME_TYPE, fURL.String(), fURL.Length()); } be_clipboard->Commit(); be_clipboard->Unlock(); } break; case OPEN_CONTAINING_FOLDER: if (fPath.InitCheck() == B_OK) { BEntry selected(fPath.Path()); if (!selected.Exists()) break; BPath containingFolder; if (fPath.GetParent(&containingFolder) != B_OK) break; entry_ref ref; if (get_ref_for_path(containingFolder.Path(), &ref) != B_OK) break; // Ask Tracker to open the containing folder and select the // file inside it. BMessenger trackerMessenger("application/x-vnd.Be-TRAK"); if (trackerMessenger.IsValid()) { BMessage selectionCommand(B_REFS_RECEIVED); selectionCommand.AddRef("refs", &ref); node_ref selectedRef; if (selected.GetNodeRef(&selectedRef) == B_OK) { selectionCommand.AddData("nodeRefToSelect", B_RAW_TYPE, (void*)&selectedRef, sizeof(node_ref)); } trackerMessenger.SendMessage(&selectionCommand); } } break; default: BGroupView::MessageReceived(message); } } void DownloadProgressView::ShowContextMenu(BPoint screenWhere) { screenWhere += BPoint(2, 2); BPopUpMenu* contextMenu = new BPopUpMenu("download context"); BMenuItem* copyURL = new BMenuItem(B_TRANSLATE("Copy URL to clipboard"), new BMessage(COPY_URL_TO_CLIPBOARD)); copyURL->SetEnabled(fURL.Length() > 0); contextMenu->AddItem(copyURL); BMenuItem* openFolder = new BMenuItem(B_TRANSLATE("Open containing folder"), new BMessage(OPEN_CONTAINING_FOLDER)); contextMenu->AddItem(openFolder); contextMenu->SetTargetForItems(this); contextMenu->Go(screenWhere, true, true, true); } BWebDownload* DownloadProgressView::Download() const { return fDownload; } const BString& DownloadProgressView::URL() const { return fURL; } bool DownloadProgressView::IsMissing() const { return fIconView->IsIconDimmed(); } bool DownloadProgressView::IsFinished() const { return !fDownload && fStatusBar->CurrentValue() == 100; } void DownloadProgressView::DownloadFinished() { fDownload = NULL; if (fExpectedSize == -1) { fStatusBar->SetTo(100.0); fExpectedSize = fCurrentSize; } fTopButton->SetEnabled(true); fBottomButton->SetLabel(B_TRANSLATE("Remove")); fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD)); fBottomButton->SetEnabled(true); fInfoView->SetText(""); fStatusBar->SetBarColor(ui_color(B_SUCCESS_COLOR)); BNotification success(B_INFORMATION_NOTIFICATION); success.SetGroup(B_TRANSLATE("WebPositive")); success.SetTitle(B_TRANSLATE("Download finished")); success.SetContent(fPath.Leaf()); BEntry entry(fPath.Path()); entry_ref ref; entry.GetRef(&ref); success.SetOnClickFile(&ref); success.SetIcon(fIconView->Bitmap()); success.Send(); } void DownloadProgressView::CancelDownload() { // Show the cancel notification, and set the progress bar red, only if the // download was still running. In cases where the file is deleted after // the download was finished, we don't want these things to happen. if (fDownload) { // Also cancel the download fDownload->Cancel(); BNotification success(B_ERROR_NOTIFICATION); success.SetGroup(B_TRANSLATE("WebPositive")); success.SetTitle(B_TRANSLATE("Download aborted")); success.SetContent(fPath.Leaf()); // Don't make a click on the notification open the file: it is not // complete success.SetIcon(fIconView->Bitmap()); success.Send(); fStatusBar->SetBarColor(ui_color(B_FAILURE_COLOR)); } fDownload = NULL; fTopButton->SetLabel(B_TRANSLATE("Restart")); fTopButton->SetMessage(new BMessage(RESTART_DOWNLOAD)); fTopButton->SetEnabled(true); fBottomButton->SetLabel(B_TRANSLATE("Remove")); fBottomButton->SetMessage(new BMessage(REMOVE_DOWNLOAD)); fBottomButton->SetEnabled(true); fInfoView->SetText(""); fPath.Unset(); } /*static*/ void DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse() { bigtime_t now = system_time(); if (sShowSpeed && sLastEstimatedFinishSpeedToggleTime + kShowSpeedInterval <= now) { sShowSpeed = false; sLastEstimatedFinishSpeedToggleTime = now; } else if (!sShowSpeed && sLastEstimatedFinishSpeedToggleTime + kShowEstimatedFinishInterval <= now) { sShowSpeed = true; sLastEstimatedFinishSpeedToggleTime = now; } } // #pragma mark - private void DownloadProgressView::_UpdateStatus(off_t currentSize, off_t expectedSize) { fCurrentSize = currentSize; fExpectedSize = expectedSize; fStatusBar->SetTo(100.0 * currentSize / expectedSize); bigtime_t currentTime = system_time(); if ((currentTime - fLastUpdateTime) > kMaxUpdateInterval) { fLastUpdateTime = currentTime; if (currentTime >= fLastSpeedReferenceTime + kSpeedReferenceInterval) { // update current speed every kSpeedReferenceInterval fCurrentBytesPerSecondSlot = (fCurrentBytesPerSecondSlot + 1) % kBytesPerSecondSlots; fBytesPerSecondSlot[fCurrentBytesPerSecondSlot] = (double)(currentSize - fLastSpeedReferenceSize) * 1000000LL / (currentTime - fLastSpeedReferenceTime); fLastSpeedReferenceSize = currentSize; fLastSpeedReferenceTime = currentTime; fBytesPerSecond = 0.0; size_t count = 0; for (size_t i = 0; i < kBytesPerSecondSlots; i++) { if (fBytesPerSecondSlot[i] != 0.0) { fBytesPerSecond += fBytesPerSecondSlot[i]; count++; } } if (count > 0) fBytesPerSecond /= count; } _UpdateStatusText(); } } void DownloadProgressView::_UpdateStatusText() { fInfoView->SetText(""); BString buffer; if (sShowSpeed && fBytesPerSecond != 0.0) { // Draw speed info char sizeBuffer[128]; // Get strings for current and expected size and remove the unit // from the current size string if it's the same as the expected // size unit. BString currentSize = string_for_size((double)fCurrentSize, sizeBuffer, sizeof(sizeBuffer)); BString expectedSize = string_for_size((double)fExpectedSize, sizeBuffer, sizeof(sizeBuffer)); int currentSizeUnitPos = currentSize.FindLast(' '); int expectedSizeUnitPos = expectedSize.FindLast(' '); if (currentSizeUnitPos >= 0 && expectedSizeUnitPos >= 0 && strcmp(currentSize.String() + currentSizeUnitPos, expectedSize.String() + expectedSizeUnitPos) == 0) { currentSize.Truncate(currentSizeUnitPos); } buffer = B_TRANSLATE("(%currentSize% of %expectedSize%, %rate%/s)"); buffer.ReplaceFirst("%currentSize%", currentSize); buffer.ReplaceFirst("%expectedSize%", expectedSize); buffer.ReplaceFirst("%rate%", string_for_size(fBytesPerSecond, sizeBuffer, sizeof(sizeBuffer))); float stringWidth = fInfoView->StringWidth(buffer.String()); if (stringWidth < fInfoView->Bounds().Width()) fInfoView->SetText(buffer.String()); else { // complete string too wide, try with shorter version buffer = string_for_size(fBytesPerSecond, sizeBuffer, sizeof(sizeBuffer)); buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'"); stringWidth = fInfoView->StringWidth(buffer.String()); if (stringWidth < fInfoView->Bounds().Width()) fInfoView->SetText(buffer.String()); } } else if (!sShowSpeed && fCurrentSize < fExpectedSize) { double totalBytesPerSecond = (double)(fCurrentSize - fEstimatedFinishReferenceSize) * 1000000LL / (system_time() - fEstimatedFinishReferenceTime); double secondsRemaining = (fExpectedSize - fCurrentSize) / totalBytesPerSecond; time_t now = (time_t)real_time_clock(); time_t finishTime = (time_t)(now + secondsRemaining); BString timeText; if (finishTime - now > kSecondsPerDay) { BDateTimeFormat().Format(timeText, finishTime, B_MEDIUM_DATE_FORMAT, B_MEDIUM_TIME_FORMAT); } else { BTimeFormat().Format(timeText, finishTime, B_MEDIUM_TIME_FORMAT); } BString statusString; BDurationFormat formatter; BString finishString; if (finishTime - now > kSecondsPerHour) { statusString.SetTo(B_TRANSLATE("(Finish: %date - Over %duration left)")); formatter.Format(finishString, now * 1000000LL, finishTime * 1000000LL); } else { statusString.SetTo(B_TRANSLATE("(Finish: %date - %duration left)")); formatter.Format(finishString, now * 1000000LL, finishTime * 1000000LL); } statusString.ReplaceFirst("%date", timeText); statusString.ReplaceFirst("%duration", finishString); float stringWidth = fInfoView->StringWidth(statusString.String()); if (stringWidth < fInfoView->Bounds().Width()) fInfoView->SetText(statusString.String()); else { // complete string too wide, try with shorter version statusString.SetTo(B_TRANSLATE("(Finish: %date)")); statusString.ReplaceFirst("%date", timeText); stringWidth = fInfoView->StringWidth(statusString.String()); if (stringWidth < fInfoView->Bounds().Width()) fInfoView->SetText(statusString.String()); } } } void DownloadProgressView::_StartNodeMonitor(const BEntry& entry) { node_ref nref; if (entry.GetNodeRef(&nref) == B_OK) watch_node(&nref, B_WATCH_ALL, this); } void DownloadProgressView::_StopNodeMonitor() { stop_watching(this); }