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