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