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