xref: /haiku/src/apps/webpositive/DownloadProgressView.cpp (revision 204dee708a999d5a71d0cb9497650ee7cef85d0a)
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 
59 enum {
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 
68 const bigtime_t kMaxUpdateInterval = 100000LL;
69 const bigtime_t kSpeedReferenceInterval = 500000LL;
70 const bigtime_t kShowSpeedInterval = 8000000LL;
71 const bigtime_t kShowEstimatedFinishInterval = 4000000LL;
72 
73 bigtime_t DownloadProgressView::sLastEstimatedFinishSpeedToggleTime = -1;
74 bool DownloadProgressView::sShowSpeed = true;
75 
76 
77 class IconView : public BView {
78 public:
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 
165 private:
166 	BBitmap	fIconBitmap;
167 	bool	fDimmedIcon;
168 };
169 
170 
171 class SmallButton : public BButton {
172 public:
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 
189 DownloadProgressView::DownloadProgressView(BWebDownload* download)
190 	:
191 	BGroupView(B_HORIZONTAL, 8),
192 	fDownload(download),
193 	fURL(download->URL()),
194 	fPath(download->Path())
195 {
196 }
197 
198 
199 DownloadProgressView::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 
214 bool
215 DownloadProgressView::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 
304 status_t
305 DownloadProgressView::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 
320 void
321 DownloadProgressView::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 
338 void
339 DownloadProgressView::DetachedFromWindow()
340 {
341 	_StopNodeMonitor();
342 }
343 
344 
345 void
346 DownloadProgressView::AllAttached()
347 {
348 	SetViewColor(B_TRANSPARENT_COLOR);
349 	SetLowColor(245, 245, 245);
350 	SetHighColor(tint_color(LowColor(), B_DARKEN_1_TINT));
351 }
352 
353 
354 void
355 DownloadProgressView::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 
365 void
366 DownloadProgressView::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 
581 void
582 DownloadProgressView::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 
600 BWebDownload*
601 DownloadProgressView::Download() const
602 {
603 	return fDownload;
604 }
605 
606 
607 const BString&
608 DownloadProgressView::URL() const
609 {
610 	return fURL;
611 }
612 
613 
614 bool
615 DownloadProgressView::IsMissing() const
616 {
617 	return fIconView->IsIconDimmed();
618 }
619 
620 
621 bool
622 DownloadProgressView::IsFinished() const
623 {
624 	return !fDownload && fStatusBar->CurrentValue() == 100;
625 }
626 
627 
628 void
629 DownloadProgressView::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 
644 void
645 DownloadProgressView::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
660 DownloadProgressView::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 
680 void
681 DownloadProgressView::_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 
717 void
718 DownloadProgressView::_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 
844 void
845 DownloadProgressView::_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 
853 void
854 DownloadProgressView::_StopNodeMonitor()
855 {
856 	stop_watching(this);
857 }
858 
859