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:
DownloadsContainerView()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
MinSize()61 virtual BSize MinSize()
62 {
63 BSize minSize = BGroupView::MinSize();
64 return BSize(minSize.width, 80);
65 }
66
Pulse()67 virtual void Pulse()
68 {
69 DownloadProgressView::SpeedVersusEstimatedFinishTogglePulse();
70 }
71
72 protected:
DoLayout()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:
DownloadContainerScrollView(BView * target)92 DownloadContainerScrollView(BView* target)
93 :
94 BScrollView("Downloads scroll view", target, 0, false, true,
95 B_NO_BORDER)
96 {
97 }
98
99 protected:
DoLayout()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
DownloadWindow(BRect frame,bool visible,SettingsMessage * settings)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
~DownloadWindow()190 DownloadWindow::~DownloadWindow()
191 {
192 // Only necessary to save the current progress of unfinished downloads:
193 _SaveSettings();
194 }
195
196
197 void
DispatchMessage(BMessage * message,BHandler * target)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
FrameResized(float newWidth,float newHeight)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
MessageReceived(BMessage * message)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
QuitRequested()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
DownloadsInProgress()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
SetMinimizeOnClose(bool minimize)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
_DownloadStarted(BWebDownload * download)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
_DownloadFinished(BWebDownload * download)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
_RemoveFinishedDownloads()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
_RemoveMissingDownloads()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
_ValidateButtonStatus()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
_SaveSettings()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
_LoadSettings()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
_OpenSettingsFile(BFile & file,uint32 mode)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