xref: /haiku/src/apps/webpositive/DownloadWindow.cpp (revision 850f2d1e58cc443f77353c7fc0ce0c158c1fd328)
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 
41 enum {
42 	INIT = 'init',
43 	OPEN_DOWNLOADS_FOLDER = 'odnf',
44 	REMOVE_FINISHED_DOWNLOADS = 'rmfd',
45 	REMOVE_MISSING_DOWNLOADS = 'rmmd'
46 };
47 
48 
49 class DownloadsContainerView : public BGroupView {
50 public:
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 
71 protected:
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 
89 class DownloadContainerScrollView : public BScrollView {
90 public:
91 	DownloadContainerScrollView(BView* target)
92 		:
93 		BScrollView("Downloads scroll view", target, 0, false, true,
94 			B_NO_BORDER)
95 	{
96 	}
97 
98 protected:
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 
119 DownloadWindow::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("Hide"),
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 }
186 
187 
188 DownloadWindow::~DownloadWindow()
189 {
190 	// Only necessary to save the current progress of unfinished downloads:
191 	_SaveSettings();
192 }
193 
194 
195 void
196 DownloadWindow::DispatchMessage(BMessage* message, BHandler* target)
197 {
198 	// We need to intercept mouse down events inside the area of download
199 	// progress views (regardless of whether they have children at the click),
200 	// so that they may display a context menu.
201 	BPoint where;
202 	int32 buttons;
203 	if (message->what == B_MOUSE_DOWN
204 		&& message->FindPoint("screen_where", &where) == B_OK
205 		&& message->FindInt32("buttons", &buttons) == B_OK
206 		&& (buttons & B_SECONDARY_MOUSE_BUTTON) != 0) {
207 		for (int32 i = fDownloadViewsLayout->CountItems() - 1;
208 				BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
209 			DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
210 				item->View());
211 			if (!view)
212 				continue;
213 			BPoint viewWhere(where);
214 			view->ConvertFromScreen(&viewWhere);
215 			if (view->Bounds().Contains(viewWhere)) {
216 				view->ShowContextMenu(where);
217 				return;
218 			}
219 		}
220 	}
221 	BWindow::DispatchMessage(message, target);
222 }
223 
224 
225 void
226 DownloadWindow::MessageReceived(BMessage* message)
227 {
228 	switch (message->what) {
229 		case INIT:
230 		{
231 			_LoadSettings();
232 			// Small trick to get the correct enabled status of the Remove
233 			// finished button
234 			_DownloadFinished(NULL);
235 			break;
236 		}
237 		case B_DOWNLOAD_ADDED:
238 		{
239 			BWebDownload* download;
240 			if (message->FindPointer("download", reinterpret_cast<void**>(
241 					&download)) == B_OK) {
242 				_DownloadStarted(download);
243 			}
244 			break;
245 		}
246 		case B_DOWNLOAD_REMOVED:
247 		{
248 			BWebDownload* download;
249 			if (message->FindPointer("download", reinterpret_cast<void**>(
250 					&download)) == B_OK) {
251 				_DownloadFinished(download);
252 			}
253 			break;
254 		}
255 		case OPEN_DOWNLOADS_FOLDER:
256 		{
257 			entry_ref ref;
258 			status_t status = get_ref_for_path(fDownloadPath.String(), &ref);
259 			if (status == B_OK)
260 				status = be_roster->Launch(&ref);
261 			if (status != B_OK && status != B_ALREADY_RUNNING) {
262 				BString errorString(B_TRANSLATE_COMMENT("The downloads folder could "
263 					"not be opened.\n\nError: %error", "Don't translate "
264 					"variable %error"));
265 				errorString.ReplaceFirst("%error", strerror(status));
266 				BAlert* alert = new BAlert(B_TRANSLATE("Error opening downloads "
267 					"folder"), errorString.String(), B_TRANSLATE("OK"));
268 				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
269 				alert->Go(NULL);
270 			}
271 			break;
272 		}
273 		case REMOVE_FINISHED_DOWNLOADS:
274 			_RemoveFinishedDownloads();
275 			break;
276 		case REMOVE_MISSING_DOWNLOADS:
277 			_RemoveMissingDownloads();
278 			break;
279 		case SAVE_SETTINGS:
280 			_ValidateButtonStatus();
281 			_SaveSettings();
282 			break;
283 
284 		case SETTINGS_VALUE_CHANGED:
285 		{
286 			BString string;
287 			if (message->FindString("name", &string) == B_OK
288 				&& string == kSettingsKeyDownloadPath
289 				&& message->FindString("value", &string) == B_OK) {
290 				fDownloadPath = string;
291 			}
292 			break;
293 		}
294 		default:
295 			BWindow::MessageReceived(message);
296 			break;
297 	}
298 }
299 
300 
301 bool
302 DownloadWindow::QuitRequested()
303 {
304 	if (fMinimizeOnClose) {
305 		if (!IsMinimized())
306 			Minimize(true);
307 	} else {
308 		if (!IsHidden())
309 			Hide();
310 	}
311 	return false;
312 }
313 
314 
315 bool
316 DownloadWindow::DownloadsInProgress()
317 {
318 	bool downloadsInProgress = false;
319 	if (!Lock())
320 		return downloadsInProgress;
321 
322 	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
323 			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
324 		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
325 			item->View());
326 		if (!view)
327 			continue;
328 		if (view->Download() != NULL) {
329 			downloadsInProgress = true;
330 			break;
331 		}
332 	}
333 
334 	Unlock();
335 
336 	return downloadsInProgress;
337 }
338 
339 
340 void
341 DownloadWindow::SetMinimizeOnClose(bool minimize)
342 {
343 	if (Lock()) {
344 		fMinimizeOnClose = minimize;
345 		Unlock();
346 	}
347 }
348 
349 
350 // #pragma mark - private
351 
352 
353 void
354 DownloadWindow::_DownloadStarted(BWebDownload* download)
355 {
356 	download->Start(BPath(fDownloadPath.String()));
357 
358 	int32 finishedCount = 0;
359 	int32 missingCount = 0;
360 	int32 index = 0;
361 	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
362 			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
363 		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
364 			item->View());
365 		if (!view)
366 			continue;
367 		if (view->URL() == download->URL()) {
368 			index = i;
369 			view->RemoveSelf();
370 			delete view;
371 			continue;
372 		}
373 		if (view->IsFinished())
374 			finishedCount++;
375 		if (view->IsMissing())
376 			missingCount++;
377 	}
378 	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
379 	fRemoveMissingButton->SetEnabled(missingCount > 0);
380 	DownloadProgressView* view = new DownloadProgressView(download);
381 	if (!view->Init()) {
382 		delete view;
383 		return;
384 	}
385 	fDownloadViewsLayout->AddView(index, view);
386 
387 	// Scroll new download into view
388 	if (BScrollBar* scrollBar = fDownloadsScrollView->ScrollBar(B_VERTICAL)) {
389 		float min;
390 		float max;
391 		scrollBar->GetRange(&min, &max);
392 		float viewHeight = view->MinSize().height + 1;
393 		float scrollOffset = min + index * viewHeight;
394 		float scrollBarHeight = scrollBar->Bounds().Height() - 1;
395 		float value = scrollBar->Value();
396 		if (scrollOffset < value)
397 			scrollBar->SetValue(scrollOffset);
398 		else if (scrollOffset + viewHeight > value + scrollBarHeight) {
399 			float diff = scrollOffset + viewHeight - (value + scrollBarHeight);
400 			scrollBar->SetValue(value + diff);
401 		}
402 	}
403 
404 	_SaveSettings();
405 
406 	SetWorkspaces(B_CURRENT_WORKSPACE);
407 	if (IsHidden())
408 		Show();
409 
410 	Activate(true);
411 }
412 
413 
414 void
415 DownloadWindow::_DownloadFinished(BWebDownload* download)
416 {
417 	int32 finishedCount = 0;
418 	int32 missingCount = 0;
419 	for (int32 i = 0;
420 			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i++) {
421 		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
422 			item->View());
423 		if (!view)
424 			continue;
425 		if (download && view->Download() == download) {
426 			view->DownloadFinished();
427 			finishedCount++;
428 			continue;
429 		}
430 		if (view->IsFinished())
431 			finishedCount++;
432 		if (view->IsMissing())
433 			missingCount++;
434 	}
435 	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
436 	fRemoveMissingButton->SetEnabled(missingCount > 0);
437 	if (download)
438 		_SaveSettings();
439 }
440 
441 
442 void
443 DownloadWindow::_RemoveFinishedDownloads()
444 {
445 	int32 missingCount = 0;
446 	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
447 			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
448 		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
449 			item->View());
450 		if (!view)
451 			continue;
452 		if (view->IsFinished()) {
453 			view->RemoveSelf();
454 			delete view;
455 		} else if (view->IsMissing())
456 			missingCount++;
457 	}
458 	fRemoveFinishedButton->SetEnabled(false);
459 	fRemoveMissingButton->SetEnabled(missingCount > 0);
460 	_SaveSettings();
461 }
462 
463 
464 void
465 DownloadWindow::_RemoveMissingDownloads()
466 {
467 	int32 finishedCount = 0;
468 	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
469 			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
470 		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
471 			item->View());
472 		if (!view)
473 			continue;
474 		if (view->IsMissing()) {
475 			view->RemoveSelf();
476 			delete view;
477 		} else if (view->IsFinished())
478 			finishedCount++;
479 	}
480 	fRemoveMissingButton->SetEnabled(false);
481 	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
482 	_SaveSettings();
483 }
484 
485 
486 void
487 DownloadWindow::_ValidateButtonStatus()
488 {
489 	int32 finishedCount = 0;
490 	int32 missingCount = 0;
491 	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
492 			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
493 		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
494 			item->View());
495 		if (!view)
496 			continue;
497 		if (view->IsFinished())
498 			finishedCount++;
499 		if (view->IsMissing())
500 			missingCount++;
501 	}
502 	fRemoveFinishedButton->SetEnabled(finishedCount > 0);
503 	fRemoveMissingButton->SetEnabled(missingCount > 0);
504 }
505 
506 
507 void
508 DownloadWindow::_SaveSettings()
509 {
510 	BFile file;
511 	if (!_OpenSettingsFile(file, B_ERASE_FILE | B_CREATE_FILE | B_WRITE_ONLY))
512 		return;
513 	BMessage message;
514 	for (int32 i = fDownloadViewsLayout->CountItems() - 1;
515 			BLayoutItem* item = fDownloadViewsLayout->ItemAt(i); i--) {
516 		DownloadProgressView* view = dynamic_cast<DownloadProgressView*>(
517 			item->View());
518 		if (!view)
519 			continue;
520 
521 	BMessage downloadArchive;
522 		if (view->SaveSettings(&downloadArchive) == B_OK)
523 			message.AddMessage("download", &downloadArchive);
524 	}
525 	message.Flatten(&file);
526 }
527 
528 
529 void
530 DownloadWindow::_LoadSettings()
531 {
532 	BFile file;
533 	if (!_OpenSettingsFile(file, B_READ_ONLY))
534 		return;
535 	BMessage message;
536 	if (message.Unflatten(&file) != B_OK)
537 		return;
538 	BMessage downloadArchive;
539 	for (int32 i = 0;
540 			message.FindMessage("download", i, &downloadArchive) == B_OK;
541 			i++) {
542 		DownloadProgressView* view = new DownloadProgressView(
543 			&downloadArchive);
544 		if (!view->Init(&downloadArchive))
545 			continue;
546 		fDownloadViewsLayout->AddView(0, view);
547 	}
548 }
549 
550 
551 bool
552 DownloadWindow::_OpenSettingsFile(BFile& file, uint32 mode)
553 {
554 	BPath path;
555 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
556 		|| path.Append(kApplicationName) != B_OK
557 		|| path.Append("Downloads") != B_OK) {
558 		return false;
559 	}
560 	return file.SetTo(path.Path(), mode) == B_OK;
561 }
562 
563 
564