1/*
2 * Copyright 2007-2014, Haiku, Inc.
3 * Distributed under the terms of the MIT license.
4 *
5 * Author:
6 *		��ukasz 'Sil2100' Zemczak <sil2100@vexillium.org>
7 *		Stephan A��mus <superstippi@gmx.de>
8 */
9
10
11#include "InstalledPackageInfo.h"
12#include "PackageImageViewer.h"
13#include "PackageTextViewer.h"
14#include "PackageView.h"
15
16#include <Alert.h>
17#include <Box.h>
18#include <Button.h>
19#include <Catalog.h>
20#include <Directory.h>
21#include <FilePanel.h>
22#include <FindDirectory.h>
23#include <Locale.h>
24#include <LayoutBuilder.h>
25#include <MenuField.h>
26#include <MenuItem.h>
27#include <Path.h>
28#include <PopUpMenu.h>
29#include <ScrollView.h>
30#include <StringForSize.h>
31#include <TextView.h>
32#include <Volume.h>
33#include <VolumeRoster.h>
34#include <Window.h>
35
36#include <GroupLayout.h>
37#include <GroupLayoutBuilder.h>
38#include <GroupView.h>
39
40#include <fs_info.h>
41#include <stdio.h> // For debugging
42
43
44#undef B_TRANSLATION_CONTEXT
45#define B_TRANSLATION_CONTEXT "PackageView"
46
47const float kMaxDescHeight = 125.0f;
48const uint32 kSeparatorIndex = 3;
49
50
51// #pragma mark -
52
53
54PackageView::PackageView(const entry_ref* ref)
55	:
56	BView("package_view", 0),
57	fOpenPanel(new BFilePanel(B_OPEN_PANEL, NULL, NULL, B_DIRECTORY_NODE,
58		false)),
59	fExpectingOpenPanelResult(false),
60	fInfo(ref),
61	fInstallProcess(this)
62{
63	_InitView();
64
65	// Check whether the package has been successfuly parsed
66	status_t ret = fInfo.InitCheck();
67	if (ret == B_OK)
68		_InitProfiles();
69}
70
71
72PackageView::~PackageView()
73{
74	delete fOpenPanel;
75}
76
77
78void
79PackageView::AttachedToWindow()
80{
81	status_t ret = fInfo.InitCheck();
82	if (ret != B_OK && ret != B_NO_INIT) {
83		BAlert* warning = new BAlert("parsing_failed",
84				B_TRANSLATE("The package file is not readable.\nOne of the "
85				"possible reasons for this might be that the requested file "
86				"is not a valid BeOS .pkg package."), B_TRANSLATE("OK"),
87				NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
88		warning->SetFlags(warning->Flags() | B_CLOSE_ON_ESCAPE);
89		warning->Go();
90
91		Window()->PostMessage(B_QUIT_REQUESTED);
92		return;
93	}
94
95	// Set the window title
96	BWindow* parent = Window();
97	BString title;
98	BString name = fInfo.GetName();
99	if (name.CountChars() == 0) {
100		title = B_TRANSLATE("Package installer");
101	} else {
102		title = B_TRANSLATE("Install %name%");
103		title.ReplaceAll("%name%", name);
104	}
105	parent->SetTitle(title.String());
106	fBeginButton->SetTarget(this);
107
108	fOpenPanel->SetTarget(BMessenger(this));
109	fInstallTypes->SetTargetForItems(this);
110
111	if (ret != B_OK)
112		return;
113
114	// If the package is valid, we can set up the default group and all
115	// other things. If not, then the application will close just after
116	// attaching the view to the window
117	_InstallTypeChanged(0);
118
119	fStatusWindow = new PackageStatus(B_TRANSLATE("Installation progress"),
120		NULL, NULL, this);
121
122	// Show the splash screen, if present
123	BMallocIO* image = fInfo.GetSplashScreen();
124	if (image != NULL) {
125		PackageImageViewer* imageViewer = new PackageImageViewer(image);
126		imageViewer->Go();
127	}
128
129	// Show the disclaimer/info text popup, if present
130	BString disclaimer = fInfo.GetDisclaimer();
131	if (disclaimer.Length() != 0) {
132		PackageTextViewer* text = new PackageTextViewer(
133			disclaimer.String());
134		int32 selection = text->Go();
135		// The user didn't accept our disclaimer, this means we cannot
136		// continue.
137		if (selection == 0)
138			parent->Quit();
139	}
140}
141
142
143void
144PackageView::MessageReceived(BMessage* message)
145{
146	switch (message->what) {
147		case P_MSG_INSTALL:
148		{
149			fBeginButton->SetEnabled(false);
150			fInstallTypes->SetEnabled(false);
151			fDestination->SetEnabled(false);
152			fStatusWindow->Show();
153
154			fInstallProcess.Start();
155			break;
156		}
157
158		case P_MSG_PATH_CHANGED:
159		{
160			BString path;
161			if (message->FindString("path", &path) == B_OK)
162				fCurrentPath.SetTo(path.String());
163			break;
164		}
165
166		case P_MSG_OPEN_PANEL:
167			fExpectingOpenPanelResult = true;
168			fOpenPanel->Show();
169			break;
170
171		case P_MSG_INSTALL_TYPE_CHANGED:
172		{
173			int32 index;
174			if (message->FindInt32("index", &index) == B_OK)
175				_InstallTypeChanged(index);
176			break;
177		}
178
179		case P_MSG_I_FINISHED:
180		{
181			BAlert* notify = new BAlert("installation_success",
182				B_TRANSLATE("The package you requested has been successfully "
183					"installed on your system."),
184				B_TRANSLATE("OK"));
185			notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE);
186
187			notify->Go();
188			fStatusWindow->Hide();
189			fBeginButton->SetEnabled(true);
190			fInstallTypes->SetEnabled(true);
191			fDestination->SetEnabled(true);
192			fInstallProcess.Stop();
193
194			BWindow *parent = Window();
195			if (parent && parent->Lock())
196				parent->Quit();
197			break;
198		}
199
200		case P_MSG_I_ABORT:
201		{
202			BAlert* notify = new BAlert("installation_aborted",
203				B_TRANSLATE(
204					"The installation of the package has been aborted."),
205				B_TRANSLATE("OK"));
206			notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE);
207			notify->Go();
208			fStatusWindow->Hide();
209			fBeginButton->SetEnabled(true);
210			fInstallTypes->SetEnabled(true);
211			fDestination->SetEnabled(true);
212			fInstallProcess.Stop();
213			break;
214		}
215
216		case P_MSG_I_ERROR:
217		{
218			// TODO: Review this
219			BAlert* notify = new BAlert("installation_failed",
220				B_TRANSLATE("The requested package failed to install on your "
221					"system. This might be a problem with the target package "
222					"file. Please consult this issue with the package "
223					"distributor."),
224				B_TRANSLATE("OK"),
225				NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
226			fputs(B_TRANSLATE("Error while installing the package\n"),
227				stderr);
228			notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE);
229			notify->Go();
230			fStatusWindow->Hide();
231			fBeginButton->SetEnabled(true);
232			fInstallTypes->SetEnabled(true);
233			fDestination->SetEnabled(true);
234			fInstallProcess.Stop();
235			break;
236		}
237
238		case P_MSG_STOP:
239		{
240			// This message is sent to us by the PackageStatus window, informing
241			// user interruptions.
242			// We actually use this message only when a post installation script
243			// is running and we want to kill it while it's still running
244			fStatusWindow->Hide();
245			fBeginButton->SetEnabled(true);
246			fInstallTypes->SetEnabled(true);
247			fDestination->SetEnabled(true);
248			fInstallProcess.Stop();
249			break;
250		}
251
252		case B_REFS_RECEIVED:
253		{
254			if (!_ValidateFilePanelMessage(message))
255				break;
256
257			entry_ref ref;
258			if (message->FindRef("refs", &ref) == B_OK) {
259				BPath path(&ref);
260				if (path.InitCheck() != B_OK)
261					break;
262
263				dev_t device = dev_for_path(path.Path());
264				BVolume volume(device);
265				if (volume.InitCheck() != B_OK)
266					break;
267
268				BMenuItem* item = fDestField->MenuItem();
269
270				BString name = _NamePlusSizeString(path.Path(),
271					volume.FreeBytes(), B_TRANSLATE("%name% (%size% free)"));
272
273				item->SetLabel(name.String());
274				fCurrentPath.SetTo(path.Path());
275			}
276			break;
277		}
278
279		case B_CANCEL:
280		{
281			if (!_ValidateFilePanelMessage(message))
282				break;
283
284			// file panel aborted, select first suitable item
285			for (int32 i = 0; i < fDestination->CountItems(); i++) {
286				BMenuItem* item = fDestination->ItemAt(i);
287				BMessage* message = item->Message();
288				if (message == NULL)
289					continue;
290				BString path;
291				if (message->FindString("path", &path) == B_OK) {
292					fCurrentPath.SetTo(path.String());
293					item->SetMarked(true);
294					break;
295				}
296			}
297			break;
298		}
299
300		case B_SIMPLE_DATA:
301			if (message->WasDropped()) {
302				uint32 type;
303				int32 count;
304				status_t ret = message->GetInfo("refs", &type, &count);
305				// check whether the message means someone dropped a file
306				// to our view
307				if (ret == B_OK && type == B_REF_TYPE) {
308					// if it is, send it along with the refs to the application
309					message->what = B_REFS_RECEIVED;
310					be_app->PostMessage(message);
311				}
312			}
313			// fall-through
314		default:
315			BView::MessageReceived(message);
316			break;
317	}
318}
319
320
321int32
322PackageView::ItemExists(PackageItem& item, BPath& path, int32& policy)
323{
324	int32 choice = P_EXISTS_NONE;
325
326	switch (policy) {
327		case P_EXISTS_OVERWRITE:
328			choice = P_EXISTS_OVERWRITE;
329			break;
330
331		case P_EXISTS_SKIP:
332			choice = P_EXISTS_SKIP;
333			break;
334
335		case P_EXISTS_ASK:
336		case P_EXISTS_NONE:
337		{
338			const char* formatString;
339			switch (item.ItemKind()) {
340				case P_KIND_SCRIPT:
341					formatString = B_TRANSLATE("The script named \'%s\' "
342						"already exists in the given path.\nReplace the script "
343						"with the one from this package or skip it?");
344					break;
345				case P_KIND_FILE:
346					formatString = B_TRANSLATE("The file named \'%s\' already "
347						"exists in the given path.\nReplace the file with the "
348						"one from this package or skip it?");
349					break;
350				case P_KIND_DIRECTORY:
351					formatString = B_TRANSLATE("The directory named \'%s\' "
352						"already exists in the given path.\nReplace the "
353						"directory with one from this package or skip it?");
354					break;
355				case P_KIND_SYM_LINK:
356					formatString = B_TRANSLATE("The symbolic link named \'%s\' "
357						"already exists in the given path.\nReplace the link "
358						"with the one from this package or skip it?");
359					break;
360				default:
361					formatString = B_TRANSLATE("The item named \'%s\' already "
362						"exists in the given path.\nReplace the item with the "
363						"one from this package or skip it?");
364					break;
365			}
366			char buffer[512];
367			snprintf(buffer, sizeof(buffer), formatString, path.Leaf());
368
369			BString alertString = buffer;
370
371			BAlert* alert = new BAlert("file_exists", alertString.String(),
372				B_TRANSLATE("Replace"),
373				B_TRANSLATE("Skip"),
374				B_TRANSLATE("Abort"));
375			alert->SetShortcut(2, B_ESCAPE);
376
377			choice = alert->Go();
378			switch (choice) {
379				case 0:
380					choice = P_EXISTS_OVERWRITE;
381					break;
382				case 1:
383					choice = P_EXISTS_SKIP;
384					break;
385				default:
386					return P_EXISTS_ABORT;
387			}
388
389			if (policy == P_EXISTS_NONE) {
390				// TODO: Maybe add 'No, but ask again' type of choice as well?
391				alertString = B_TRANSLATE("Do you want to remember this "
392					"decision for the rest of this installation?\n");
393
394				BString actionString;
395				if (choice == P_EXISTS_OVERWRITE) {
396					alertString << B_TRANSLATE(
397						"All existing files will be replaced?");
398					actionString = B_TRANSLATE("Replace all");
399				} else {
400					alertString << B_TRANSLATE(
401						"All existing files will be skipped?");
402					actionString = B_TRANSLATE("Skip all");
403				}
404				alert = new BAlert("policy_decision", alertString.String(),
405					actionString.String(), B_TRANSLATE("Ask again"));
406
407				int32 decision = alert->Go();
408				if (decision == 0)
409					policy = choice;
410				else
411					policy = P_EXISTS_ASK;
412			}
413			break;
414		}
415	}
416
417	return choice;
418}
419
420
421// #pragma mark -
422
423
424class DescriptionTextView : public BTextView {
425public:
426	DescriptionTextView(const char* name, float minHeight)
427		:
428		BTextView(name)
429	{
430		SetExplicitMinSize(BSize(B_SIZE_UNSET, minHeight));
431	}
432
433	virtual void AttachedToWindow()
434	{
435		BTextView::AttachedToWindow();
436		_UpdateScrollBarVisibility();
437	}
438
439	virtual void FrameResized(float width, float height)
440	{
441		BTextView::FrameResized(width, height);
442		_UpdateScrollBarVisibility();
443	}
444
445	virtual void Draw(BRect updateRect)
446	{
447		BTextView::Draw(updateRect);
448		_UpdateScrollBarVisibility();
449	}
450
451private:
452	void _UpdateScrollBarVisibility()
453	{
454		BScrollBar* verticalBar = ScrollBar(B_VERTICAL);
455		if (verticalBar != NULL) {
456			float min;
457			float max;
458			verticalBar->GetRange(&min, &max);
459			if (min == max) {
460				if (!verticalBar->IsHidden(verticalBar))
461					verticalBar->Hide();
462			} else {
463				if (verticalBar->IsHidden(verticalBar))
464					verticalBar->Show();
465			}
466		}
467	}
468};
469
470
471void
472PackageView::_InitView()
473{
474	SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
475
476	float fontHeight = be_plain_font->Size();
477	rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
478
479	BTextView* packageDescriptionView = new DescriptionTextView(
480		"package description", fontHeight * 13);
481	packageDescriptionView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
482	packageDescriptionView->SetText(fInfo.GetDescription());
483	packageDescriptionView->MakeEditable(false);
484	packageDescriptionView->MakeSelectable(false);
485	packageDescriptionView->SetFontAndColor(be_plain_font, B_FONT_ALL,
486		&textColor);
487
488	BScrollView* descriptionScrollView = new BScrollView(
489		"package description scroll view", packageDescriptionView,
490		0, false, true, B_NO_BORDER);
491
492	// Install type menu field
493	fInstallTypes = new BPopUpMenu(B_TRANSLATE("none"));
494	BMenuField* installType = new BMenuField("install_type",
495		B_TRANSLATE("Installation type:"), fInstallTypes);
496
497	// Install type description text view
498	fInstallTypeDescriptionView = new DescriptionTextView(
499		"install type description", fontHeight * 4);
500	fInstallTypeDescriptionView->MakeEditable(false);
501	fInstallTypeDescriptionView->MakeSelectable(false);
502	fInstallTypeDescriptionView->SetInsets(8, 0, 0, 0);
503		// Left inset needs to match BMenuField text offset
504	fInstallTypeDescriptionView->SetText(
505		B_TRANSLATE("No installation type selected"));
506	fInstallTypeDescriptionView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
507	BFont font(be_plain_font);
508	font.SetSize(ceilf(font.Size() * 0.85));
509	fInstallTypeDescriptionView->SetFontAndColor(&font, B_FONT_ALL,
510		&textColor);
511
512	BScrollView* installTypeScrollView = new BScrollView(
513		"install type description scroll view", fInstallTypeDescriptionView,
514		 0, false, true, B_NO_BORDER);
515
516	// Destination menu field
517	fDestination = new BPopUpMenu(B_TRANSLATE("none"));
518	fDestField = new BMenuField("install_to", B_TRANSLATE("Install to:"),
519		fDestination);
520
521	fBeginButton = new BButton("begin_button", B_TRANSLATE("Begin"),
522		new BMessage(P_MSG_INSTALL));
523
524	BLayoutItem* typeLabelItem = installType->CreateLabelLayoutItem();
525	BLayoutItem* typeMenuItem = installType->CreateMenuBarLayoutItem();
526
527	BLayoutItem* destFieldLabelItem = fDestField->CreateLabelLayoutItem();
528	BLayoutItem* destFieldMenuItem = fDestField->CreateMenuBarLayoutItem();
529
530	float forcedMinWidth = be_plain_font->StringWidth("XXX") * 5;
531	destFieldMenuItem->SetExplicitMinSize(BSize(forcedMinWidth, B_SIZE_UNSET));
532	typeMenuItem->SetExplicitMinSize(BSize(forcedMinWidth, B_SIZE_UNSET));
533
534	BAlignment labelAlignment(B_ALIGN_RIGHT, B_ALIGN_VERTICAL_UNSET);
535	typeLabelItem->SetExplicitAlignment(labelAlignment);
536	destFieldLabelItem->SetExplicitAlignment(labelAlignment);
537
538	// Build the layout
539	BLayoutBuilder::Group<>(this, B_VERTICAL)
540		.Add(descriptionScrollView)
541		.AddGrid(B_USE_SMALL_SPACING, B_USE_DEFAULT_SPACING)
542			.Add(typeLabelItem, 0, 0)
543			.Add(typeMenuItem, 1, 0)
544			.Add(installTypeScrollView, 1, 1)
545			.Add(destFieldLabelItem, 0, 2)
546			.Add(destFieldMenuItem, 1, 2)
547		.End()
548		.AddGroup(B_HORIZONTAL)
549			.AddGlue()
550			.Add(fBeginButton)
551		.End()
552		.SetInsets(B_USE_DEFAULT_SPACING)
553	;
554
555	fBeginButton->MakeDefault(true);
556}
557
558
559void
560PackageView::_InitProfiles()
561{
562	int count = fInfo.GetProfileCount();
563
564	if (count > 0) {
565		// Add the default item
566		pkg_profile* profile = fInfo.GetProfile(0);
567		BMenuItem* item = _AddInstallTypeMenuItem(profile->name,
568			profile->space_needed, 0);
569		item->SetMarked(true);
570		fCurrentType = 0;
571	}
572
573	for (int i = 1; i < count; i++) {
574		pkg_profile* profile = fInfo.GetProfile(i);
575
576		if (profile != NULL)
577			_AddInstallTypeMenuItem(profile->name, profile->space_needed, i);
578		else
579			fInstallTypes->AddSeparatorItem();
580	}
581}
582
583
584status_t
585PackageView::_InstallTypeChanged(int32 index)
586{
587	if (index < 0)
588		return B_ERROR;
589
590	// Clear the choice list
591	for (int32 i = fDestination->CountItems() - 1; i >= 0; i--) {
592		BMenuItem* item = fDestination->RemoveItem(i);
593		delete item;
594	}
595
596	fCurrentType = index;
597	pkg_profile* profile = fInfo.GetProfile(index);
598
599	if (profile == NULL)
600		return B_ERROR;
601
602	BString typeDescription = profile->description;
603	if (typeDescription.IsEmpty())
604		typeDescription = profile->name;
605
606	fInstallTypeDescriptionView->SetText(typeDescription.String());
607
608	BPath path;
609	BVolume volume;
610
611	if (profile->path_type == P_INSTALL_PATH) {
612		BMenuItem* item = NULL;
613		if (find_directory(B_SYSTEM_NONPACKAGED_DIRECTORY, &path) == B_OK) {
614			dev_t device = dev_for_path(path.Path());
615			if (volume.SetTo(device) == B_OK && !volume.IsReadOnly()
616				&& path.Append("apps") == B_OK) {
617				item = _AddDestinationMenuItem(path.Path(), volume.FreeBytes(),
618					path.Path());
619			}
620		}
621
622		if (item != NULL) {
623			item->SetMarked(true);
624			fCurrentPath.SetTo(path.Path());
625			fDestination->AddSeparatorItem();
626		}
627
628		_AddMenuItem(B_TRANSLATE("Other" B_UTF8_ELLIPSIS),
629			new BMessage(P_MSG_OPEN_PANEL), fDestination);
630
631		fDestField->SetEnabled(true);
632	} else if (profile->path_type == P_USER_PATH) {
633		bool defaultPathSet = false;
634		BVolumeRoster roster;
635
636		while (roster.GetNextVolume(&volume) != B_BAD_VALUE) {
637			BDirectory mountPoint;
638			if (volume.IsReadOnly() || !volume.IsPersistent()
639				|| volume.GetRootDirectory(&mountPoint) != B_OK) {
640				continue;
641			}
642
643			if (path.SetTo(&mountPoint, NULL) != B_OK)
644				continue;
645
646			char volumeName[B_FILE_NAME_LENGTH];
647			volume.GetName(volumeName);
648
649			BMenuItem* item = _AddDestinationMenuItem(volumeName,
650				volume.FreeBytes(), path.Path());
651
652			// The first volume becomes the default element
653			if (!defaultPathSet) {
654				item->SetMarked(true);
655				fCurrentPath.SetTo(path.Path());
656				defaultPathSet = true;
657			}
658		}
659
660		fDestField->SetEnabled(true);
661	} else
662		fDestField->SetEnabled(false);
663
664	return B_OK;
665}
666
667
668BString
669PackageView::_NamePlusSizeString(BString baseName, size_t size,
670	const char* format) const
671{
672	char sizeString[48];
673	string_for_size(size, sizeString, sizeof(sizeString));
674
675	BString name(format);
676	name.ReplaceAll("%name%", baseName);
677	name.ReplaceAll("%size%", sizeString);
678
679	return name;
680}
681
682
683BMenuItem*
684PackageView::_AddInstallTypeMenuItem(BString baseName, size_t size,
685	int32 index) const
686{
687	BString name = _NamePlusSizeString(baseName, size,
688		B_TRANSLATE("%name% (%size%)"));
689
690	BMessage* message = new BMessage(P_MSG_INSTALL_TYPE_CHANGED);
691	message->AddInt32("index", index);
692
693	return _AddMenuItem(name, message, fInstallTypes);
694}
695
696
697BMenuItem*
698PackageView::_AddDestinationMenuItem(BString baseName, size_t size,
699	const char* path) const
700{
701	BString name = _NamePlusSizeString(baseName, size,
702		B_TRANSLATE("%name% (%size% free)"));
703
704	BMessage* message = new BMessage(P_MSG_PATH_CHANGED);
705	message->AddString("path", path);
706
707	return _AddMenuItem(name, message, fDestination);
708}
709
710
711BMenuItem*
712PackageView::_AddMenuItem(const char* name, BMessage* message,
713	BMenu* menu) const
714{
715	BMenuItem* item = new BMenuItem(name, message);
716	item->SetTarget(this);
717	menu->AddItem(item);
718	return item;
719}
720
721
722bool
723PackageView::_ValidateFilePanelMessage(BMessage* message)
724{
725	if (!fExpectingOpenPanelResult)
726		return false;
727
728	fExpectingOpenPanelResult = false;
729	return true;
730}
731