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