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