2 * Copyright (C) 2010 Stephan Aßmus <superstippi@gmx.de>
4 * All rights reserved. Distributed under the terms of the MIT License.
7 #include "DownloadProgressView.h"
12 #include <Application.h>
16 #include <Clipboard.h>
17 #include <Directory.h>
18 #include <DateTimeFormat.h>
19 #include <DurationFormat.h>
21 #include <FindDirectory.h>
22 #include <GroupLayoutBuilder.h>
26 #include <NodeMonitor.h>
27 #include <Notification.h>
28 #include <PopUpMenu.h>
30 #include <SpaceLayoutItem.h>
31 #include <StatusBar.h>
32 #include <StringView.h>
33 #include <TimeFormat.h>
35 #include "BrowserWindow.h"
36 #include "WebDownload.h"
38 #include "StringForSize.h"
41 #undef B_TRANSLATION_CONTEXT
42 #define B_TRANSLATION_CONTEXT "Download Window"
45 OPEN_DOWNLOAD
= 'opdn',
46 RESTART_DOWNLOAD
= 'rsdn',
47 CANCEL_DOWNLOAD
= 'cndn',
48 REMOVE_DOWNLOAD
= 'rmdn',
49 COPY_URL_TO_CLIPBOARD
= 'curl',
50 OPEN_CONTAINING_FOLDER
= 'opfd',
53 const bigtime_t kMaxUpdateInterval
= 100000LL;
54 const bigtime_t kSpeedReferenceInterval
= 500000LL;
55 const bigtime_t kShowSpeedInterval
= 8000000LL;
56 const bigtime_t kShowEstimatedFinishInterval
= 4000000LL;
58 bigtime_t
DownloadProgressView::sLastEstimatedFinishSpeedToggleTime
= -1;
59 bool DownloadProgressView::sShowSpeed
= true;
60 static const time_t kSecondsPerDay
= 24 * 60 * 60;
61 static const time_t kSecondsPerHour
= 60 * 60;
64 class IconView
: public BView
{
66 IconView(const BEntry
& entry
)
68 BView("Download icon", B_WILL_DRAW
),
69 fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32
),
72 SetDrawingMode(B_OP_OVER
);
78 BView("Download icon", B_WILL_DRAW
),
79 fIconBitmap(BRect(0, 0, 31, 31), 0, B_RGBA32
),
82 SetDrawingMode(B_OP_OVER
);
83 memset(fIconBitmap
.Bits(), 0, fIconBitmap
.BitsLength());
86 IconView(BMessage
* archive
)
88 BView("Download icon", B_WILL_DRAW
),
92 SetDrawingMode(B_OP_OVER
);
95 void SetTo(const BEntry
& entry
)
98 BNodeInfo
info(&node
);
99 info
.GetTrackerIcon(&fIconBitmap
, B_LARGE_ICON
);
103 void SetIconDimmed(bool iconDimmed
)
105 if (fDimmedIcon
!= iconDimmed
) {
106 fDimmedIcon
= iconDimmed
;
111 bool IsIconDimmed() const
116 status_t
SaveSettings(BMessage
* archive
)
118 return fIconBitmap
.Archive(archive
);
121 virtual void AttachedToWindow()
126 virtual void Draw(BRect updateRect
)
129 SetDrawingMode(B_OP_ALPHA
);
130 SetBlendingMode(B_CONSTANT_ALPHA
, B_ALPHA_OVERLAY
);
131 SetHighColor(0, 0, 0, 100);
133 DrawBitmapAsync(&fIconBitmap
);
136 virtual BSize
MinSize()
138 return BSize(fIconBitmap
.Bounds().Width(),
139 fIconBitmap
.Bounds().Height());
142 virtual BSize
PreferredSize()
147 virtual BSize
MaxSize()
163 class SmallButton
: public BButton
{
165 SmallButton(const char* label
, BMessage
* message
= NULL
)
167 BButton(label
, message
)
171 float size
= ceilf(font
.Size() * 0.8);
172 font
.SetSize(max_c(8, size
));
173 SetFont(&font
, B_FONT_SIZE
);
178 // #pragma mark - DownloadProgressView
181 DownloadProgressView::DownloadProgressView(BWebDownload
* download
)
183 BGroupView(B_HORIZONTAL
, 8),
185 fURL(download
->URL()),
186 fPath(download
->Path())
191 DownloadProgressView::DownloadProgressView(const BMessage
* archive
)
193 BGroupView(B_HORIZONTAL
, 8),
199 if (archive
->FindString("path", &string
) == B_OK
)
201 if (archive
->FindString("url", &string
) == B_OK
)
207 DownloadProgressView::Init(BMessage
* archive
)
212 fBytesPerSecond
= 0.0;
213 for (size_t i
= 0; i
< kBytesPerSecondSlots
; i
++)
214 fBytesPerSecondSlot
[i
] = 0.0;
215 fCurrentBytesPerSecondSlot
= 0;
216 fLastSpeedReferenceSize
= 0;
217 fEstimatedFinishReferenceSize
= 0;
219 fProcessStartTime
= fLastSpeedReferenceTime
220 = fEstimatedFinishReferenceTime
= system_time();
222 SetViewColor(245, 245, 245);
223 SetFlags(Flags() | B_FULL_UPDATE_ON_RESIZE
| B_WILL_DRAW
);
226 fStatusBar
= new BStatusBar("download progress", fPath
.Leaf());
228 if (archive
->FindFloat("value", &value
) == B_OK
)
229 fStatusBar
->SetTo(value
);
231 fStatusBar
= new BStatusBar("download progress", "Download");
232 fStatusBar
->SetMaxValue(100);
233 fStatusBar
->SetBarHeight(12);
235 // fPath is only valid when constructed from archive (fDownload == NULL)
236 BEntry
entry(fPath
.Path());
240 fIconView
= new IconView(archive
);
242 fIconView
= new IconView(entry
);
244 fIconView
= new IconView();
246 if (!fDownload
&& (fStatusBar
->CurrentValue() < 100 || !entry
.Exists())) {
247 fTopButton
= new SmallButton(B_TRANSLATE("Restart"),
248 new BMessage(RESTART_DOWNLOAD
));
250 fTopButton
= new SmallButton(B_TRANSLATE("Open"),
251 new BMessage(OPEN_DOWNLOAD
));
252 fTopButton
->SetEnabled(fDownload
== NULL
);
255 fBottomButton
= new SmallButton(B_TRANSLATE("Cancel"),
256 new BMessage(CANCEL_DOWNLOAD
));
258 fBottomButton
= new SmallButton(B_TRANSLATE("Remove"),
259 new BMessage(REMOVE_DOWNLOAD
));
260 fBottomButton
->SetEnabled(fDownload
== NULL
);
263 fInfoView
= new BStringView("info view", "");
264 fInfoView
->SetViewColor(ViewColor());
266 BSize topButtonSize
= fTopButton
->PreferredSize();
267 BSize bottomButtonSize
= fBottomButton
->PreferredSize();
268 if (bottomButtonSize
.width
< topButtonSize
.width
)
269 fBottomButton
->SetExplicitMaxSize(topButtonSize
);
271 fTopButton
->SetExplicitMaxSize(bottomButtonSize
);
273 BGroupLayout
* layout
= GroupLayout();
274 layout
->SetInsets(8, 5, 5, 6);
275 layout
->AddView(fIconView
);
276 BView
* verticalGroup
= BGroupLayoutBuilder(B_VERTICAL
, 3)
281 verticalGroup
->SetViewColor(ViewColor());
282 layout
->AddView(verticalGroup
);
284 verticalGroup
= BGroupLayoutBuilder(B_VERTICAL
, 3)
289 verticalGroup
->SetViewColor(ViewColor());
290 layout
->AddView(verticalGroup
);
293 fInfoView
->GetFont(&font
);
294 float fontSize
= font
.Size() * 0.8f
;
295 font
.SetSize(max_c(8.0f
, fontSize
));
296 fInfoView
->SetFont(&font
, B_FONT_SIZE
);
297 fInfoView
->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED
, B_SIZE_UNSET
));
304 DownloadProgressView::SaveSettings(BMessage
* archive
)
308 status_t ret
= archive
->AddString("path", fPath
.Path());
310 ret
= archive
->AddString("url", fURL
.String());
312 ret
= archive
->AddFloat("value", fStatusBar
->CurrentValue());
314 ret
= fIconView
->SaveSettings(archive
);
320 DownloadProgressView::AttachedToWindow()
323 fDownload
->SetProgressListener(BMessenger(this));
324 // Will start node monitor upon receiving the B_DOWNLOAD_STARTED
327 BEntry
entry(fPath
.Path());
329 _StartNodeMonitor(entry
);
332 fTopButton
->SetTarget(this);
333 fBottomButton
->SetTarget(this);
338 DownloadProgressView::DetachedFromWindow()
345 DownloadProgressView::AllAttached()
347 fStatusBar
->SetLowColor(ViewColor());
348 fInfoView
->SetLowColor(ViewColor());
349 fInfoView
->SetHighColor(0, 0, 0, 255);
351 SetViewColor(B_TRANSPARENT_COLOR
);
352 SetLowColor(245, 245, 245);
353 SetHighColor(tint_color(LowColor(), B_DARKEN_1_TINT
));
358 DownloadProgressView::Draw(BRect updateRect
)
360 BRect
bounds(Bounds());
362 FillRect(bounds
, B_SOLID_LOW
);
364 StrokeLine(bounds
.LeftBottom(), bounds
.RightBottom());
369 DownloadProgressView::MessageReceived(BMessage
* message
)
371 switch (message
->what
) {
372 case B_DOWNLOAD_STARTED
:
375 if (message
->FindString("path", &path
) != B_OK
)
378 BEntry
entry(fPath
.Path());
379 fIconView
->SetTo(entry
);
380 fStatusBar
->Reset(fPath
.Leaf());
381 _StartNodeMonitor(entry
);
383 // Immediately switch to speed display whenever a new download
386 sLastEstimatedFinishSpeedToggleTime
387 = fProcessStartTime
= fLastSpeedReferenceTime
388 = fEstimatedFinishReferenceTime
= system_time();
391 case B_DOWNLOAD_PROGRESS
:
395 if (message
->FindInt64("current size", ¤tSize
) == B_OK
396 && message
->FindInt64("expected size", &expectedSize
) == B_OK
) {
397 _UpdateStatus(currentSize
, expectedSize
);
401 case B_DOWNLOAD_REMOVED
:
402 // TODO: This is a bit asymetric. The removed notification
403 // arrives here, but it would be nicer if it arrived
405 Window()->PostMessage(message
);
409 // TODO: In case of executable files, ask the user first!
411 status_t status
= get_ref_for_path(fPath
.Path(), &ref
);
413 status
= be_roster
->Launch(&ref
);
414 if (status
!= B_OK
&& status
!= B_ALREADY_RUNNING
) {
415 BAlert
* alert
= new BAlert(B_TRANSLATE("Open download error"),
416 B_TRANSLATE("The download could not be opened."),
418 alert
->SetFlags(alert
->Flags() | B_CLOSE_ON_ESCAPE
);
423 case RESTART_DOWNLOAD
:
425 // We can't create a download without a full web context (mainly
426 // because it needs to access the cookie jar), and when we get here
427 // the original context is long gone (possibly the browser was
428 // restarted). So we create a new window to restart the download
429 // in a fresh context.
430 // FIXME this has of course the huge downside of leaving the new
431 // window open with a blank page. I can't think of a better
432 // solution right now...
433 BMessage
* request
= new BMessage(NEW_WINDOW
);
434 request
->AddString("url", fURL
);
435 be_app
->PostMessage(request
);
439 case CANCEL_DOWNLOAD
:
443 case REMOVE_DOWNLOAD
:
445 Window()->PostMessage(SAVE_SETTINGS
);
454 if (message
->FindInt32("opcode", &opCode
) != B_OK
)
457 case B_ENTRY_REMOVED
:
458 fIconView
->SetIconDimmed(true);
463 // Follow the entry to the new location
467 if (message
->FindInt32("device",
468 reinterpret_cast<int32
*>(&device
)) != B_OK
469 || message
->FindInt64("to directory",
470 reinterpret_cast<int64
*>(&directory
)) != B_OK
471 || message
->FindString("name", &name
) != B_OK
472 || strlen(name
) == 0) {
475 // Construct the BEntry and update fPath
476 entry_ref
ref(device
, directory
, name
);
478 if (entry
.GetPath(&fPath
) != B_OK
)
481 // Find out if the directory is the Trash for this
483 char trashPath
[B_PATH_NAME_LENGTH
];
484 if (find_directory(B_TRASH_DIRECTORY
, device
, false,
485 trashPath
, B_PATH_NAME_LENGTH
) == B_OK
) {
486 BPath
trashDirectory(trashPath
);
487 BPath parentDirectory
;
488 fPath
.GetParent(&parentDirectory
);
489 if (parentDirectory
== trashDirectory
) {
490 // The entry was moved into the Trash.
491 // If the download is still in progress,
493 fIconView
->SetIconDimmed(true);
496 } else if (fIconView
->IsIconDimmed()) {
497 // Maybe it was moved out of the trash.
498 fIconView
->SetIconDimmed(false);
502 // Inform download of the new path
504 fDownload
->HasMovedTo(fPath
);
506 float value
= fStatusBar
->CurrentValue();
507 fStatusBar
->Reset(name
);
508 fStatusBar
->SetTo(value
);
509 Window()->PostMessage(SAVE_SETTINGS
);
514 BEntry
entry(fPath
.Path());
515 fIconView
->SetIconDimmed(false);
516 fIconView
->SetTo(entry
);
523 // Context menu messages
524 case COPY_URL_TO_CLIPBOARD
:
525 if (be_clipboard
->Lock()) {
526 BMessage
* data
= be_clipboard
->Data();
528 be_clipboard
->Clear();
529 data
->AddData("text/plain", B_MIME_TYPE
, fURL
.String(),
532 be_clipboard
->Commit();
533 be_clipboard
->Unlock();
536 case OPEN_CONTAINING_FOLDER
:
537 if (fPath
.InitCheck() == B_OK
) {
538 BEntry
selected(fPath
.Path());
539 if (!selected
.Exists())
542 BPath containingFolder
;
543 if (fPath
.GetParent(&containingFolder
) != B_OK
)
546 if (get_ref_for_path(containingFolder
.Path(), &ref
) != B_OK
)
549 // Ask Tracker to open the containing folder and select the
551 BMessenger
trackerMessenger("application/x-vnd.Be-TRAK");
553 if (trackerMessenger
.IsValid()) {
554 BMessage
selectionCommand(B_REFS_RECEIVED
);
555 selectionCommand
.AddRef("refs", &ref
);
557 node_ref selectedRef
;
558 if (selected
.GetNodeRef(&selectedRef
) == B_OK
) {
559 selectionCommand
.AddData("nodeRefToSelect", B_RAW_TYPE
,
560 (void*)&selectedRef
, sizeof(node_ref
));
563 trackerMessenger
.SendMessage(&selectionCommand
);
569 BGroupView::MessageReceived(message
);
575 DownloadProgressView::ShowContextMenu(BPoint screenWhere
)
577 screenWhere
+= BPoint(2, 2);
579 BPopUpMenu
* contextMenu
= new BPopUpMenu("download context");
580 BMenuItem
* copyURL
= new BMenuItem(B_TRANSLATE("Copy URL to clipboard"),
581 new BMessage(COPY_URL_TO_CLIPBOARD
));
582 copyURL
->SetEnabled(fURL
.Length() > 0);
583 contextMenu
->AddItem(copyURL
);
584 BMenuItem
* openFolder
= new BMenuItem(B_TRANSLATE("Open containing folder"),
585 new BMessage(OPEN_CONTAINING_FOLDER
));
586 contextMenu
->AddItem(openFolder
);
588 contextMenu
->SetTargetForItems(this);
589 contextMenu
->Go(screenWhere
, true, true, true);
594 DownloadProgressView::Download() const
601 DownloadProgressView::URL() const
608 DownloadProgressView::IsMissing() const
610 return fIconView
->IsIconDimmed();
615 DownloadProgressView::IsFinished() const
617 return !fDownload
&& fStatusBar
->CurrentValue() == 100;
622 DownloadProgressView::DownloadFinished()
625 if (fExpectedSize
== -1) {
626 fStatusBar
->SetTo(100.0);
627 fExpectedSize
= fCurrentSize
;
629 fTopButton
->SetEnabled(true);
630 fBottomButton
->SetLabel(B_TRANSLATE("Remove"));
631 fBottomButton
->SetMessage(new BMessage(REMOVE_DOWNLOAD
));
632 fBottomButton
->SetEnabled(true);
633 fInfoView
->SetText("");
634 fStatusBar
->SetBarColor(ui_color(B_SUCCESS_COLOR
));
636 BNotification
success(B_INFORMATION_NOTIFICATION
);
637 success
.SetGroup(B_TRANSLATE("WebPositive"));
638 success
.SetTitle(B_TRANSLATE("Download finished"));
639 success
.SetContent(fPath
.Leaf());
640 BEntry
entry(fPath
.Path());
643 success
.SetOnClickFile(&ref
);
644 success
.SetIcon(fIconView
->Bitmap());
651 DownloadProgressView::CancelDownload()
653 // Show the cancel notification, and set the progress bar red, only if the
654 // download was still running. In cases where the file is deleted after
655 // the download was finished, we don't want these things to happen.
657 // Also cancel the download
659 BNotification
success(B_ERROR_NOTIFICATION
);
660 success
.SetGroup(B_TRANSLATE("WebPositive"));
661 success
.SetTitle(B_TRANSLATE("Download aborted"));
662 success
.SetContent(fPath
.Leaf());
663 // Don't make a click on the notification open the file: it is not
665 success
.SetIcon(fIconView
->Bitmap());
668 fStatusBar
->SetBarColor(ui_color(B_FAILURE_COLOR
));
672 fTopButton
->SetLabel(B_TRANSLATE("Restart"));
673 fTopButton
->SetMessage(new BMessage(RESTART_DOWNLOAD
));
674 fTopButton
->SetEnabled(true);
675 fBottomButton
->SetLabel(B_TRANSLATE("Remove"));
676 fBottomButton
->SetMessage(new BMessage(REMOVE_DOWNLOAD
));
677 fBottomButton
->SetEnabled(true);
678 fInfoView
->SetText("");
685 DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse()
687 bigtime_t now
= system_time();
689 && sLastEstimatedFinishSpeedToggleTime
+ kShowSpeedInterval
692 sLastEstimatedFinishSpeedToggleTime
= now
;
693 } else if (!sShowSpeed
694 && sLastEstimatedFinishSpeedToggleTime
695 + kShowEstimatedFinishInterval
<= now
) {
697 sLastEstimatedFinishSpeedToggleTime
= now
;
702 // #pragma mark - private
706 DownloadProgressView::_UpdateStatus(off_t currentSize
, off_t expectedSize
)
708 fCurrentSize
= currentSize
;
709 fExpectedSize
= expectedSize
;
711 fStatusBar
->SetTo(100.0 * currentSize
/ expectedSize
);
713 bigtime_t currentTime
= system_time();
714 if ((currentTime
- fLastUpdateTime
) > kMaxUpdateInterval
) {
715 fLastUpdateTime
= currentTime
;
717 if (currentTime
>= fLastSpeedReferenceTime
+ kSpeedReferenceInterval
) {
718 // update current speed every kSpeedReferenceInterval
719 fCurrentBytesPerSecondSlot
720 = (fCurrentBytesPerSecondSlot
+ 1) % kBytesPerSecondSlots
;
721 fBytesPerSecondSlot
[fCurrentBytesPerSecondSlot
]
722 = (double)(currentSize
- fLastSpeedReferenceSize
)
723 * 1000000LL / (currentTime
- fLastSpeedReferenceTime
);
724 fLastSpeedReferenceSize
= currentSize
;
725 fLastSpeedReferenceTime
= currentTime
;
726 fBytesPerSecond
= 0.0;
728 for (size_t i
= 0; i
< kBytesPerSecondSlots
; i
++) {
729 if (fBytesPerSecondSlot
[i
] != 0.0) {
730 fBytesPerSecond
+= fBytesPerSecondSlot
[i
];
735 fBytesPerSecond
/= count
;
743 DownloadProgressView::_UpdateStatusText()
745 fInfoView
->SetText("");
747 if (sShowSpeed
&& fBytesPerSecond
!= 0.0) {
749 char sizeBuffer
[128];
751 // Get strings for current and expected size and remove the unit
752 // from the current size string if it's the same as the expected
754 BString currentSize
= string_for_size((double)fCurrentSize
, sizeBuffer
,
756 BString expectedSize
= string_for_size((double)fExpectedSize
, sizeBuffer
,
758 int currentSizeUnitPos
= currentSize
.FindLast(' ');
759 int expectedSizeUnitPos
= expectedSize
.FindLast(' ');
760 if (currentSizeUnitPos
>= 0 && expectedSizeUnitPos
>= 0
761 && strcmp(currentSize
.String() + currentSizeUnitPos
,
762 expectedSize
.String() + expectedSizeUnitPos
) == 0) {
763 currentSize
.Truncate(currentSizeUnitPos
);
765 buffer
<< currentSize
;
767 buffer
<< B_TRANSLATE_COMMENT("of", "...as in '12kB of 256kB'");
769 buffer
<< expectedSize
;
771 buffer
<< string_for_size(fBytesPerSecond
, sizeBuffer
,
773 buffer
<< B_TRANSLATE_COMMENT("/s)", "...as in 'per second'");
774 float stringWidth
= fInfoView
->StringWidth(buffer
.String());
775 if (stringWidth
< fInfoView
->Bounds().Width())
776 fInfoView
->SetText(buffer
.String());
778 // complete string too wide, try with shorter version
779 buffer
<< string_for_size(fBytesPerSecond
, sizeBuffer
,
781 buffer
<< B_TRANSLATE_COMMENT("/s)", "...as in 'per second'");
782 stringWidth
= fInfoView
->StringWidth(buffer
.String());
783 if (stringWidth
< fInfoView
->Bounds().Width())
784 fInfoView
->SetText(buffer
.String());
786 } else if (!sShowSpeed
&& fCurrentSize
< fExpectedSize
) {
787 double totalBytesPerSecond
= (double)(fCurrentSize
788 - fEstimatedFinishReferenceSize
)
789 * 1000000LL / (system_time() - fEstimatedFinishReferenceTime
);
790 double secondsRemaining
= (fExpectedSize
- fCurrentSize
)
791 / totalBytesPerSecond
;
792 time_t now
= (time_t)real_time_clock();
793 time_t finishTime
= (time_t)(now
+ secondsRemaining
);
796 if (finishTime
- now
> kSecondsPerDay
) {
797 BDateTimeFormat().Format(timeText
, finishTime
,
798 B_MEDIUM_DATE_FORMAT
, B_MEDIUM_TIME_FORMAT
);
800 BTimeFormat().Format(timeText
, finishTime
,
801 B_MEDIUM_TIME_FORMAT
);
804 BString statusString
;
805 BDurationFormat formatter
;
806 BString finishString
;
807 if (finishTime
- now
> kSecondsPerHour
) {
808 statusString
.SetTo(B_TRANSLATE("(Finish: %date - Over %duration left)"));
809 formatter
.Format(finishString
, now
* 1000000LL, finishTime
* 1000000LL);
811 statusString
.SetTo(B_TRANSLATE("(Finish: %date - %duration left)"));
812 formatter
.Format(finishString
, now
* 1000000LL, finishTime
* 1000000LL);
815 statusString
.ReplaceFirst("%date", timeText
);
816 statusString
.ReplaceFirst("%duration", finishString
);
818 float stringWidth
= fInfoView
->StringWidth(statusString
.String());
819 if (stringWidth
< fInfoView
->Bounds().Width())
820 fInfoView
->SetText(statusString
.String());
822 // complete string too wide, try with shorter version
823 statusString
.SetTo(B_TRANSLATE("(Finish: %date)"));
824 statusString
.ReplaceFirst("%date", timeText
);
825 stringWidth
= fInfoView
->StringWidth(statusString
.String());
826 if (stringWidth
< fInfoView
->Bounds().Width())
827 fInfoView
->SetText(statusString
.String());
834 DownloadProgressView::_StartNodeMonitor(const BEntry
& entry
)
837 if (entry
.GetNodeRef(&nref
) == B_OK
)
838 watch_node(&nref
, B_WATCH_ALL
, this);
843 DownloadProgressView::_StopNodeMonitor()