xref: /haiku/src/apps/webpositive/DownloadWindow.cpp (revision 1026b0a1a76dc88927bb8175c470f638dc5464ee)
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 
62 enum {
63 	INIT = 'init',
64 	OPEN_DOWNLOADS_FOLDER = 'odnf',
65 	REMOVE_FINISHED_DOWNLOADS = 'rmfd',
66 	REMOVE_MISSING_DOWNLOADS = 'rmmd'
67 };
68 
69 
70 class DownloadsContainerView : public BGroupView {
71 public:
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 
92 protected:
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 
110 class DownloadContainerScrollView : public BScrollView {
111 public:
112 	DownloadContainerScrollView(BView* target)
113 		:
114 		BScrollView("Downloads scroll view", target, 0, false, true,
115 			B_NO_BORDER)
116 	{
117 	}
118 
119 protected:
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 
140 DownloadWindow::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 
209 DownloadWindow::~DownloadWindow()
210 {
211 	// Only necessary to save the current progress of unfinished downloads:
212 	_SaveSettings();
213 }
214 
215 
216 void
217 DownloadWindow::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 
246 void
247 DownloadWindow::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 
322 bool
323 DownloadWindow::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 
336 bool
337 DownloadWindow::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 
361 void
362 DownloadWindow::SetMinimizeOnClose(bool minimize)
363 {
364 	if (Lock()) {
365 		fMinimizeOnClose = minimize;
366 		Unlock();
367 	}
368 }
369 
370 
371 // #pragma mark - private
372 
373 
374 void
375 DownloadWindow::_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 
433 void
434 DownloadWindow::_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 
461 void
462 DownloadWindow::_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 
483 void
484 DownloadWindow::_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 
505 void
506 DownloadWindow::_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 
526 void
527 DownloadWindow::_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 
548 void
549 DownloadWindow::_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 
570 bool
571 DownloadWindow::_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