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