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 "DownloadWindow.h"
8
9#include <stdio.h>
10
11#include <Alert.h>
12#include <Button.h>
13#include <Catalog.h>
14#include <ControlLook.h>
15#include <Entry.h>
16#include <File.h>
17#include <FindDirectory.h>
18#include <GroupLayout.h>
19#include <GroupLayoutBuilder.h>
20#include <Locale.h>
21#include <MenuBar.h>
22#include <MenuItem.h>
23#include <Path.h>
24#include <Roster.h>
25#include <ScrollView.h>
26#include <SeparatorView.h>
27#include <SpaceLayoutItem.h>
28
29#include "BrowserApp.h"
30#include "BrowserWindow.h"
31#include "DownloadProgressView.h"
32#include "SettingsKeys.h"
33#include "SettingsMessage.h"
34#include "WebDownload.h"
35#include "WebPage.h"
36
37
38#undef B_TRANSLATION_CONTEXT
39#define B_TRANSLATION_CONTEXT "Download Window"
40
41enum {
42	INIT = 'init',
43	OPEN_DOWNLOADS_FOLDER = 'odnf',
44	REMOVE_FINISHED_DOWNLOADS = 'rmfd',
45	REMOVE_MISSING_DOWNLOADS = 'rmmd'
46};
47
48
49class DownloadsContainerView : public BGroupView {
50public:
51	DownloadsContainerView()
52		:
53		BGroupView(B_VERTICAL, 0.0)
54	{
55		SetFlags(Flags() | B_PULSE_NEEDED);
56		SetViewColor(245, 245, 245);
57		AddChild(BSpaceLayoutItem::CreateGlue());
58	}
59
60	virtual BSize MinSize()
61	{
62		BSize minSize = BGroupView::MinSize();
63		return BSize(minSize.width, 80);
64	}
65
66	virtual void Pulse()
67	{
68		DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse();
69	}
70
71protected:
72	virtual void DoLayout()
73	{
74		BGroupView::DoLayout();
75		if (BScrollBar* scrollBar = ScrollBar(B_VERTICAL)) {
76			BSize minSize = BGroupView::MinSize();
77			float height = Bounds().Height();
78			float max = minSize.height - height;
79			scrollBar->SetRange(0, max);
80			if (minSize.height > 0)
81				scrollBar->SetProportion(height / minSize.height);
82			else
83				scrollBar->SetProportion(1);
84		}
85	}
86};
87
88
89class DownloadContainerScrollView : public BScrollView {
90public:
91	DownloadContainerScrollView(BView* target)
92		:
93		BScrollView("Downloads scroll view", target, 0, false, true,
94			B_NO_BORDER)
95	{
96	}
97
98protected:
99	virtual void DoLayout()
100	{
101		BScrollView::DoLayout();
102		// Tweak scroll bar layout to hide part of the frame for better looks.
103		BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
104		scrollBar->MoveBy(1, -1);
105		scrollBar->ResizeBy(0, 2);
106		Target()->ResizeBy(1, 0);
107		// Set the scroll steps
108		if (BView* item = Target()->ChildAt(0)) {
109			scrollBar->SetSteps(item->MinSize().height + 1,
110				item->MinSize().height + 1);
111		}
112	}
113};
114
115
116// #pragma mark -
117
118
119DownloadWindow::DownloadWindow(BRect frame, bool visible,
120		SettingsMessage* settings)
121	: BWindow(frame, B_TRANSLATE("Downloads"),
122		B_TITLED_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
123		B_AUTO_UPDATE_SIZE_LIMITS | B_ASYNCHRONOUS_CONTROLS | B_NOT_ZOOMABLE),
124	fMinimizeOnClose(false)
125{
126	SetPulseRate(1000000);
127
128	settings->AddListener(BMessenger(this));
129	BPath downloadPath;
130	if (find_directory(B_DESKTOP_DIRECTORY, &downloadPath) != B_OK)
131		downloadPath.SetTo("/boot/home/Desktop");
132	fDownloadPath = settings->GetValue(kSettingsKeyDownloadPath,
133		downloadPath.Path());
134	settings->SetValue(kSettingsKeyDownloadPath, fDownloadPath);
135
136	SetLayout(new BGroupLayout(B_VERTICAL, 0.0));
137
138	DownloadsContainerView* downloadsGroupView = new DownloadsContainerView();
139	fDownloadViewsLayout = downloadsGroupView->GroupLayout();
140
141	BMenuBar* menuBar = new BMenuBar("Menu bar");
142	BMenu* menu = new BMenu(B_TRANSLATE("Downloads"));
143	menu->AddItem(new BMenuItem(B_TRANSLATE("Open downloads folder"),
144		new BMessage(OPEN_DOWNLOADS_FOLDER)));
145	BMessage* newWindowMessage = new BMessage(NEW_WINDOW);
146	newWindowMessage->AddString("url", "");
147	BMenuItem* newWindowItem = new BMenuItem(B_TRANSLATE("New browser window"),
148		newWindowMessage, 'N');
149	menu->AddItem(newWindowItem);
150	newWindowItem->SetTarget(be_app);
151	menu->AddSeparatorItem();
152	menu->AddItem(new BMenuItem(B_TRANSLATE("Close"),
153		new BMessage(B_QUIT_REQUESTED), 'D'));
154	menuBar->AddItem(menu);
155
156	fDownloadsScrollView = new DownloadContainerScrollView(downloadsGroupView);
157
158	fRemoveFinishedButton = new BButton(B_TRANSLATE("Remove finished"),
159		new BMessage(REMOVE_FINISHED_DOWNLOADS));
160	fRemoveFinishedButton->SetEnabled(false);
161
162	fRemoveMissingButton = new BButton(B_TRANSLATE("Remove missing"),
163		new BMessage(REMOVE_MISSING_DOWNLOADS));
164	fRemoveMissingButton->SetEnabled(false);
165
166	const float spacing = be_control_look->DefaultItemSpacing();
167
168	AddChild(BGroupLayoutBuilder(B_VERTICAL, 0.0)
169		.Add(menuBar)
170		.Add(fDownloadsScrollView)
171		.Add(new BSeparatorView(B_HORIZONTAL, B_PLAIN_BORDER))
172		.Add(BGroupLayoutBuilder(B_HORIZONTAL, spacing)
173			.AddGlue()
174			.Add(fRemoveMissingButton)
175			.Add(fRemoveFinishedButton)
176			.SetInsets(12, 5, 12, 5)
177		)
178	);
179
180	PostMessage(INIT);
181
182	if (!visible)
183		Hide();
184	Show();
185	MoveOnScreen(B_MOVE_IF_PARTIALLY_OFFSCREEN);
186}
187
188
189DownloadWindow::~DownloadWindow()
190{
191	// Only necessary to save the current progress of unfinished downloads:
192	_SaveSettings();
193}
194
195
196void
197DownloadWindow::DispatchMessage(BMessage* message, BHandler* target)
198{
199	// We need to intercept mouse down events inside the area of download
200	// progress views (regardless of whether they have children at the click),
201	// so that they may display a context menu.
202	BPoint where;
203	int32 buttons;
204	if (message->what == B_MOUSE_DOWN
205		&& message->FindPoint("screen_where", &where) == B_OK
206		&& message->FindInt32("buttons", &buttons) == B_OK
207		&& (buttons & B_SECONDARY_MOUSE_BUTTON) != 0) {
208		for (int32 i = fDownloadViewsLayout->CountItems() - 1;
209				BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
210			DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
211				item->View());
212			if (!view)
213				continue;
214			BPoint viewWhere(where);
215			view->ConvertFromScreen(&viewWhere);
216			if (view->Bounds().Contains(viewWhere)) {
217				view->ShowContextMenu(where);
218				return;
219			}
220		}
221	}
222	BWindow::DispatchMessage(message, target);
223}
224
225
226void
227DownloadWindow::FrameResized(float newWidth, float newHeight)
228{
229	MoveOnScreen(B_DO_NOT_RESIZE_TO_FIT | B_MOVE_IF_PARTIALLY_OFFSCREEN);
230}
231
232
233void
234DownloadWindow::MessageReceived(BMessage* message)
235{
236	switch (message->what) {
237		case INIT:
238		{
239			_LoadSettings();
240			// Small trick to get the correct enabled status of the Remove
241			// finished button
242			_DownloadFinished(NULL);
243			break;
244		}
245		case B_DOWNLOAD_ADDED:
246		{
247			BWebDownload* download;
248			if (message->FindPointer("download", reinterpret_cast<void**>(
249					&download)) == B_OK) {
250				_DownloadStarted(download);
251			}
252			break;
253		}
254		case B_DOWNLOAD_REMOVED:
255		{
256			BWebDownload* download;
257			if (message->FindPointer("download", reinterpret_cast<void**>(
258					&download)) == B_OK) {
259				_DownloadFinished(download);
260			}
261			break;
262		}
263		case OPEN_DOWNLOADS_FOLDER:
264		{
265			entry_ref ref;
266			status_t status = get_ref_for_path(fDownloadPath.String(), &ref);
267			if (status == B_OK)
268				status = be_roster->Launch(&ref);
269			if (status != B_OK && status != B_ALREADY_RUNNING) {
270				BString errorString(B_TRANSLATE_COMMENT("The downloads folder could "
271					"not be opened.\n\nError: %error", "Don't translate "
272					"variable %error"));
273				errorString.ReplaceFirst("%error", strerror(status));
274				BAlert* alert = new BAlert(B_TRANSLATE("Error opening downloads "
275					"folder"), errorString.String(), B_TRANSLATE("OK"));
276				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
277				alert->Go(NULL);
278			}
279			break;
280		}
281		case REMOVE_FINISHED_DOWNLOADS:
282			_RemoveFinishedDownloads();
283			break;
284		case REMOVE_MISSING_DOWNLOADS:
285			_RemoveMissingDownloads();
286			break;
287		case SAVE_SETTINGS:
288			_ValidateButtonStatus();
289			_SaveSettings();
290			break;
291
292		case SETTINGS_VALUE_CHANGED:
293		{
294			BString string;
295			if (message->FindString("name", &string) == B_OK
296				&& string == kSettingsKeyDownloadPath
297				&& message->FindString("value", &string) == B_OK) {
298				fDownloadPath = string;
299			}
300			break;
301		}
302		default:
303			BWindow::MessageReceived(message);
304			break;
305	}
306}
307
308
309bool
310DownloadWindow::QuitRequested()
311{
312	if (fMinimizeOnClose) {
313		if (!IsMinimized())
314			Minimize(true);
315	} else {
316		if (!IsHidden())
317			Hide();
318	}
319	return false;
320}
321
322
323bool
324DownloadWindow::DownloadsInProgress()
325{
326	bool downloadsInProgress = false;
327	if (!Lock())
328		return downloadsInProgress;
329
330	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
331			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
332		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
333			item->View());
334		if (!view)
335			continue;
336		if (view->Download() != NULL) {
337			downloadsInProgress = true;
338			break;
339		}
340	}
341
342	Unlock();
343
344	return downloadsInProgress;
345}
346
347
348void
349DownloadWindow::SetMinimizeOnClose(bool minimize)
350{
351	if (Lock()) {
352		fMinimizeOnClose = minimize;
353		Unlock();
354	}
355}
356
357
358// #pragma mark - private
359
360
361void
362DownloadWindow::_DownloadStarted(BWebDownload* download)
363{
364	download->Start(BPath(fDownloadPath.String()));
365
366	int32 finishedCount = 0;
367	int32 missingCount = 0;
368	int32 index = 0;
369	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
370			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
371		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
372			item->View());
373		if (!view)
374			continue;
375		if (view->URL() == download->URL()) {
376			index = i;
377			view->RemoveSelf();
378			delete view;
379			continue;
380		}
381		if (view->IsFinished())
382			finishedCount++;
383		if (view->IsMissing())
384			missingCount++;
385	}
386	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
387	fRemoveMissingButton->SetEnabled(missingCount > 0);
388	DownloadProgressView* view = new DownloadProgressView(download);
389	if (!view->Init()) {
390		delete view;
391		return;
392	}
393	fDownloadViewsLayout->AddView(index, view);
394
395	// Scroll new download into view
396	if (BScrollBar* scrollBar = fDownloadsScrollView->ScrollBar(B_VERTICAL)) {
397		float min;
398		float max;
399		scrollBar->GetRange(&min, &max);
400		float viewHeight = view->MinSize().height + 1;
401		float scrollOffset = min + index * viewHeight;
402		float scrollBarHeight = scrollBar->Bounds().Height() - 1;
403		float value = scrollBar->Value();
404		if (scrollOffset < value)
405			scrollBar->SetValue(scrollOffset);
406		else if (scrollOffset + viewHeight > value + scrollBarHeight) {
407			float diff = scrollOffset + viewHeight - (value + scrollBarHeight);
408			scrollBar->SetValue(value + diff);
409		}
410	}
411
412	_SaveSettings();
413
414	SetWorkspaces(B_CURRENT_WORKSPACE);
415	if (IsHidden())
416		Show();
417
418	Activate(true);
419}
420
421
422void
423DownloadWindow::_DownloadFinished(BWebDownload* download)
424{
425	int32 finishedCount = 0;
426	int32 missingCount = 0;
427	for (int32 i = 0;
428			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i++) {
429		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
430			item->View());
431		if (!view)
432			continue;
433		if (download && view->Download() == download) {
434			view->DownloadFinished();
435			finishedCount++;
436			continue;
437		}
438		if (view->IsFinished())
439			finishedCount++;
440		if (view->IsMissing())
441			missingCount++;
442	}
443	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
444	fRemoveMissingButton->SetEnabled(missingCount > 0);
445	if (download)
446		_SaveSettings();
447}
448
449
450void
451DownloadWindow::_RemoveFinishedDownloads()
452{
453	int32 missingCount = 0;
454	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
455			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
456		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
457			item->View());
458		if (!view)
459			continue;
460		if (view->IsFinished()) {
461			view->RemoveSelf();
462			delete view;
463		} else if (view->IsMissing())
464			missingCount++;
465	}
466	fRemoveFinishedButton->SetEnabled(false);
467	fRemoveMissingButton->SetEnabled(missingCount > 0);
468	_SaveSettings();
469}
470
471
472void
473DownloadWindow::_RemoveMissingDownloads()
474{
475	int32 finishedCount = 0;
476	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
477			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
478		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
479			item->View());
480		if (!view)
481			continue;
482		if (view->IsMissing()) {
483			view->RemoveSelf();
484			delete view;
485		} else if (view->IsFinished())
486			finishedCount++;
487	}
488	fRemoveMissingButton->SetEnabled(false);
489	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
490	_SaveSettings();
491}
492
493
494void
495DownloadWindow::_ValidateButtonStatus()
496{
497	int32 finishedCount = 0;
498	int32 missingCount = 0;
499	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
500			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
501		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
502			item->View());
503		if (!view)
504			continue;
505		if (view->IsFinished())
506			finishedCount++;
507		if (view->IsMissing())
508			missingCount++;
509	}
510	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
511	fRemoveMissingButton->SetEnabled(missingCount > 0);
512}
513
514
515void
516DownloadWindow::_SaveSettings()
517{
518	BFile file;
519	if (!_OpenSettingsFile(file, B_ERASE_FILE | B_CREATE_FILE | B_WRITE_ONLY))
520		return;
521	BMessage message;
522	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
523			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
524		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
525			item->View());
526		if (!view)
527			continue;
528
529	BMessage downloadArchive;
530		if (view->SaveSettings(&downloadArchive) == B_OK)
531			message.AddMessage("download", &downloadArchive);
532	}
533	message.Flatten(&file);
534}
535
536
537void
538DownloadWindow::_LoadSettings()
539{
540	BFile file;
541	if (!_OpenSettingsFile(file, B_READ_ONLY))
542		return;
543	BMessage message;
544	if (message.Unflatten(&file) != B_OK)
545		return;
546	BMessage downloadArchive;
547	for (int32 i = 0;
548			message.FindMessage("download", i, &downloadArchive) == B_OK;
549			i++) {
550		DownloadProgressView* view = new DownloadProgressView(
551			&downloadArchive);
552		if (!view->Init(&downloadArchive))
553			continue;
554		fDownloadViewsLayout->AddView(0, view);
555	}
556}
557
558
559bool
560DownloadWindow::_OpenSettingsFile(BFile& file, uint32 mode)
561{
562	BPath path;
563	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
564		|| path.Append(kApplicationName) != B_OK
565		|| path.Append("Downloads") != B_OK) {
566		return false;
567	}
568	return file.SetTo(path.Path(), mode) == B_OK;
569}
570
571
572