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