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