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