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
44enum {
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
53const bigtime_t kMaxUpdateInterval = 100000LL;
54const bigtime_t kSpeedReferenceInterval = 500000LL;
55const bigtime_t kShowSpeedInterval = 8000000LL;
56const bigtime_t kShowEstimatedFinishInterval = 4000000LL;
57
58bigtime_t DownloadProgressView::sLastEstimatedFinishSpeedToggleTime = -1;
59bool DownloadProgressView::sShowSpeed = true;
60static const time_t kSecondsPerDay = 24 * 60 * 60;
61static const time_t kSecondsPerHour = 60 * 60;
62
63
64class IconView : public BView {
65public:
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
157private:
158	BBitmap	fIconBitmap;
159	bool	fDimmedIcon;
160};
161
162
163class SmallButton : public BButton {
164public:
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
181DownloadProgressView::DownloadProgressView(BWebDownload* download)
182	:
183	BGroupView(B_HORIZONTAL, 8),
184	fDownload(download),
185	fURL(download->URL()),
186	fPath(download->Path())
187{
188}
189
190
191DownloadProgressView::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
206bool
207DownloadProgressView::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	SetViewColor(245, 245, 245);
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
303status_t
304DownloadProgressView::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
319void
320DownloadProgressView::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
337void
338DownloadProgressView::DetachedFromWindow()
339{
340	_StopNodeMonitor();
341}
342
343
344void
345DownloadProgressView::AllAttached()
346{
347	fStatusBar->SetLowColor(ViewColor());
348	fInfoView->SetLowColor(ViewColor());
349	fInfoView->SetHighColor(0, 0, 0, 255);
350
351	SetViewColor(B_TRANSPARENT_COLOR);
352	SetLowColor(245, 245, 245);
353	SetHighColor(tint_color(LowColor(), B_DARKEN_1_TINT));
354}
355
356
357void
358DownloadProgressView::Draw(BRect updateRect)
359{
360	BRect bounds(Bounds());
361	bounds.bottom--;
362	FillRect(bounds, B_SOLID_LOW);
363	bounds.bottom++;
364	StrokeLine(bounds.LeftBottom(), bounds.RightBottom());
365}
366
367
368void
369DownloadProgressView::MessageReceived(BMessage* message)
370{
371	switch (message->what) {
372		case B_DOWNLOAD_STARTED:
373		{
374			BString path;
375			if (message->FindString("path", &path) != B_OK)
376				break;
377			fPath.SetTo(path);
378			BEntry entry(fPath.Path());
379			fIconView->SetTo(entry);
380			fStatusBar->Reset(fPath.Leaf());
381			_StartNodeMonitor(entry);
382
383			// Immediately switch to speed display whenever a new download
384			// starts.
385			sShowSpeed = true;
386			sLastEstimatedFinishSpeedToggleTime
387				= fProcessStartTime = fLastSpeedReferenceTime
388				= fEstimatedFinishReferenceTime = system_time();
389			break;
390		}
391		case B_DOWNLOAD_PROGRESS:
392		{
393			int64 currentSize;
394			int64 expectedSize;
395			if (message->FindInt64("current size", &currentSize) == B_OK
396				&& message->FindInt64("expected size", &expectedSize) == B_OK) {
397				_UpdateStatus(currentSize, expectedSize);
398			}
399			break;
400		}
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
404			// at the window...
405			Window()->PostMessage(message);
406			break;
407		case OPEN_DOWNLOAD:
408		{
409			// TODO: In case of executable files, ask the user first!
410			entry_ref ref;
411			status_t status = get_ref_for_path(fPath.Path(), &ref);
412			if (status == B_OK)
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."),
417					B_TRANSLATE("OK"));
418				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
419				alert->Go(NULL);
420			}
421			break;
422		}
423		case RESTART_DOWNLOAD:
424		{
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);
436			break;
437		}
438
439		case CANCEL_DOWNLOAD:
440			CancelDownload();
441			break;
442
443		case REMOVE_DOWNLOAD:
444		{
445			Window()->PostMessage(SAVE_SETTINGS);
446			RemoveSelf();
447			delete this;
448			// TOAST!
449			return;
450		}
451		case B_NODE_MONITOR:
452		{
453			int32 opCode;
454			if (message->FindInt32("opcode", &opCode) != B_OK)
455				break;
456			switch (opCode) {
457				case B_ENTRY_REMOVED:
458					fIconView->SetIconDimmed(true);
459					CancelDownload();
460					break;
461				case B_ENTRY_MOVED:
462				{
463					// Follow the entry to the new location
464					dev_t device;
465					ino_t directory;
466					const char* name;
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) {
473						break;
474					}
475					// Construct the BEntry and update fPath
476					entry_ref ref(device, directory, name);
477					BEntry entry(&ref);
478					if (entry.GetPath(&fPath) != B_OK)
479						break;
480
481					// Find out if the directory is the Trash for this
482					// volume
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,
492							// cancel it.
493							fIconView->SetIconDimmed(true);
494							CancelDownload();
495							break;
496						} else if (fIconView->IsIconDimmed()) {
497							// Maybe it was moved out of the trash.
498							fIconView->SetIconDimmed(false);
499						}
500					}
501
502					// Inform download of the new path
503					if (fDownload)
504						fDownload->HasMovedTo(fPath);
505
506					float value = fStatusBar->CurrentValue();
507					fStatusBar->Reset(name);
508					fStatusBar->SetTo(value);
509					Window()->PostMessage(SAVE_SETTINGS);
510					break;
511				}
512				case B_ATTR_CHANGED:
513				{
514					BEntry entry(fPath.Path());
515					fIconView->SetIconDimmed(false);
516					fIconView->SetTo(entry);
517					break;
518				}
519			}
520			break;
521		}
522
523		// Context menu messages
524		case COPY_URL_TO_CLIPBOARD:
525			if (be_clipboard->Lock()) {
526				BMessage* data = be_clipboard->Data();
527				if (data != NULL) {
528					be_clipboard->Clear();
529					data->AddData("text/plain", B_MIME_TYPE, fURL.String(),
530						fURL.Length());
531				}
532				be_clipboard->Commit();
533				be_clipboard->Unlock();
534			}
535			break;
536		case OPEN_CONTAINING_FOLDER:
537			if (fPath.InitCheck() == B_OK) {
538				BEntry selected(fPath.Path());
539				if (!selected.Exists())
540					break;
541
542				BPath containingFolder;
543				if (fPath.GetParent(&containingFolder) != B_OK)
544					break;
545				entry_ref ref;
546				if (get_ref_for_path(containingFolder.Path(), &ref) != B_OK)
547					break;
548
549				// Ask Tracker to open the containing folder and select the
550				// file inside it.
551				BMessenger trackerMessenger("application/x-vnd.Be-TRAK");
552
553				if (trackerMessenger.IsValid()) {
554					BMessage selectionCommand(B_REFS_RECEIVED);
555					selectionCommand.AddRef("refs", &ref);
556
557					node_ref selectedRef;
558					if (selected.GetNodeRef(&selectedRef) == B_OK) {
559						selectionCommand.AddData("nodeRefToSelect", B_RAW_TYPE,
560							(void*)&selectedRef, sizeof(node_ref));
561					}
562
563					trackerMessenger.SendMessage(&selectionCommand);
564				}
565			}
566			break;
567
568		default:
569			BGroupView::MessageReceived(message);
570	}
571}
572
573
574void
575DownloadProgressView::ShowContextMenu(BPoint screenWhere)
576{
577	screenWhere += BPoint(2, 2);
578
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);
587
588	contextMenu->SetTargetForItems(this);
589	contextMenu->Go(screenWhere, true, true, true);
590}
591
592
593BWebDownload*
594DownloadProgressView::Download() const
595{
596	return fDownload;
597}
598
599
600const BString&
601DownloadProgressView::URL() const
602{
603	return fURL;
604}
605
606
607bool
608DownloadProgressView::IsMissing() const
609{
610	return fIconView->IsIconDimmed();
611}
612
613
614bool
615DownloadProgressView::IsFinished() const
616{
617	return !fDownload && fStatusBar->CurrentValue() == 100;
618}
619
620
621void
622DownloadProgressView::DownloadFinished()
623{
624	fDownload = NULL;
625	if (fExpectedSize == -1) {
626		fStatusBar->SetTo(100.0);
627		fExpectedSize = fCurrentSize;
628	}
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));
635
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());
641	entry_ref ref;
642	entry.GetRef(&ref);
643	success.SetOnClickFile(&ref);
644	success.SetIcon(fIconView->Bitmap());
645	success.Send();
646
647}
648
649
650void
651DownloadProgressView::CancelDownload()
652{
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.
656	if (fDownload) {
657		// Also cancel the download
658		fDownload->Cancel();
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
664		// complete
665		success.SetIcon(fIconView->Bitmap());
666		success.Send();
667
668		fStatusBar->SetBarColor(ui_color(B_FAILURE_COLOR));
669	}
670
671	fDownload = NULL;
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("");
679
680	fPath.Unset();
681}
682
683
684/*static*/ void
685DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse()
686{
687	bigtime_t now = system_time();
688	if (sShowSpeed
689		&& sLastEstimatedFinishSpeedToggleTime + kShowSpeedInterval
690			<= now) {
691		sShowSpeed = false;
692		sLastEstimatedFinishSpeedToggleTime = now;
693	} else if (!sShowSpeed
694		&& sLastEstimatedFinishSpeedToggleTime
695			+ kShowEstimatedFinishInterval <= now) {
696		sShowSpeed = true;
697		sLastEstimatedFinishSpeedToggleTime = now;
698	}
699}
700
701
702// #pragma mark - private
703
704
705void
706DownloadProgressView::_UpdateStatus(off_t currentSize, off_t expectedSize)
707{
708	fCurrentSize = currentSize;
709	fExpectedSize = expectedSize;
710
711	fStatusBar->SetTo(100.0 * currentSize / expectedSize);
712
713	bigtime_t currentTime = system_time();
714	if ((currentTime - fLastUpdateTime) > kMaxUpdateInterval) {
715		fLastUpdateTime = currentTime;
716
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;
727			size_t count = 0;
728			for (size_t i = 0; i < kBytesPerSecondSlots; i++) {
729				if (fBytesPerSecondSlot[i] != 0.0) {
730					fBytesPerSecond += fBytesPerSecondSlot[i];
731					count++;
732				}
733			}
734			if (count > 0)
735				fBytesPerSecond /= count;
736		}
737		_UpdateStatusText();
738	}
739}
740
741
742void
743DownloadProgressView::_UpdateStatusText()
744{
745	fInfoView->SetText("");
746	BString buffer;
747	if (sShowSpeed && fBytesPerSecond != 0.0) {
748		// Draw speed info
749		char sizeBuffer[128];
750		// Get strings for current and expected size and remove the unit
751		// from the current size string if it's the same as the expected
752		// size unit.
753		BString currentSize = string_for_size((double)fCurrentSize, sizeBuffer,
754			sizeof(sizeBuffer));
755		BString expectedSize = string_for_size((double)fExpectedSize, sizeBuffer,
756			sizeof(sizeBuffer));
757		int currentSizeUnitPos = currentSize.FindLast(' ');
758		int expectedSizeUnitPos = expectedSize.FindLast(' ');
759		if (currentSizeUnitPos >= 0 && expectedSizeUnitPos >= 0
760			&& strcmp(currentSize.String() + currentSizeUnitPos,
761				expectedSize.String() + expectedSizeUnitPos) == 0) {
762			currentSize.Truncate(currentSizeUnitPos);
763		}
764
765		buffer = B_TRANSLATE("(%currentSize% of %expectedSize%, %rate%/s)");
766		buffer.ReplaceFirst("%currentSize%", currentSize);
767		buffer.ReplaceFirst("%expectedSize%", expectedSize);
768		buffer.ReplaceFirst("%rate%", string_for_size(fBytesPerSecond,
769				sizeBuffer, sizeof(sizeBuffer)));
770
771		float stringWidth = fInfoView->StringWidth(buffer.String());
772		if (stringWidth < fInfoView->Bounds().Width())
773			fInfoView->SetText(buffer.String());
774		else {
775			// complete string too wide, try with shorter version
776			buffer = string_for_size(fBytesPerSecond, sizeBuffer,
777				sizeof(sizeBuffer));
778			buffer << B_TRANSLATE_COMMENT("/s)", "...as in 'per second'");
779			stringWidth = fInfoView->StringWidth(buffer.String());
780			if (stringWidth < fInfoView->Bounds().Width())
781				fInfoView->SetText(buffer.String());
782		}
783	} else if (!sShowSpeed && fCurrentSize < fExpectedSize) {
784		double totalBytesPerSecond = (double)(fCurrentSize
785				- fEstimatedFinishReferenceSize)
786			* 1000000LL / (system_time() - fEstimatedFinishReferenceTime);
787		double secondsRemaining = (fExpectedSize - fCurrentSize)
788			/ totalBytesPerSecond;
789		time_t now = (time_t)real_time_clock();
790		time_t finishTime = (time_t)(now + secondsRemaining);
791
792		BString timeText;
793		if (finishTime - now > kSecondsPerDay) {
794			BDateTimeFormat().Format(timeText, finishTime,
795				B_MEDIUM_DATE_FORMAT, B_MEDIUM_TIME_FORMAT);
796		} else {
797			BTimeFormat().Format(timeText, finishTime,
798				B_MEDIUM_TIME_FORMAT);
799		}
800
801		BString statusString;
802		BDurationFormat formatter;
803		BString finishString;
804		if (finishTime - now > kSecondsPerHour) {
805			statusString.SetTo(B_TRANSLATE("(Finish: %date - Over %duration left)"));
806			formatter.Format(finishString, now * 1000000LL, finishTime * 1000000LL);
807		} else {
808			statusString.SetTo(B_TRANSLATE("(Finish: %date - %duration left)"));
809			formatter.Format(finishString, now * 1000000LL, finishTime * 1000000LL);
810		}
811
812		statusString.ReplaceFirst("%date", timeText);
813		statusString.ReplaceFirst("%duration", finishString);
814
815		float stringWidth = fInfoView->StringWidth(statusString.String());
816		if (stringWidth < fInfoView->Bounds().Width())
817			fInfoView->SetText(statusString.String());
818		else {
819			// complete string too wide, try with shorter version
820			statusString.SetTo(B_TRANSLATE("(Finish: %date)"));
821			statusString.ReplaceFirst("%date", timeText);
822			stringWidth = fInfoView->StringWidth(statusString.String());
823			if (stringWidth < fInfoView->Bounds().Width())
824				fInfoView->SetText(statusString.String());
825		}
826	}
827}
828
829
830void
831DownloadProgressView::_StartNodeMonitor(const BEntry& entry)
832{
833	node_ref nref;
834	if (entry.GetNodeRef(&nref) == B_OK)
835		watch_node(&nref, B_WATCH_ALL, this);
836}
837
838
839void
840DownloadProgressView::_StopNodeMonitor()
841{
842	stop_watching(this);
843}
844
845