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