Previously, the target folder was always unset, which meant one needed to select a target folder manually. When running from a read-only media, this would still be a problem (a TODO), so that instead of failing to install the package, the user is simply prompted for a path. The change means that together with rewriting old paths to packaged locations to the non-packaged equivalents, just double clicking a PKG and hitting "Install" will now work. Tested with Moho installer and Gobe Productive.
673 lines
16 KiB
C++
673 lines
16 KiB
C++
/*
|
|
* Copyright 2007-2014, Haiku, Inc.
|
|
* Distributed under the terms of the MIT license.
|
|
*
|
|
* Author:
|
|
* Łukasz 'Sil2100' Zemczak <[email protected]>
|
|
* Stephan Aßmus <[email protected]>
|
|
*/
|
|
|
|
|
|
#include "InstalledPackageInfo.h"
|
|
#include "PackageImageViewer.h"
|
|
#include "PackageTextViewer.h"
|
|
#include "PackageView.h"
|
|
|
|
#include <Alert.h>
|
|
#include <Box.h>
|
|
#include <Button.h>
|
|
#include <Catalog.h>
|
|
#include <Directory.h>
|
|
#include <FilePanel.h>
|
|
#include <FindDirectory.h>
|
|
#include <Locale.h>
|
|
#include <LayoutBuilder.h>
|
|
#include <MenuField.h>
|
|
#include <MenuItem.h>
|
|
#include <Path.h>
|
|
#include <PopUpMenu.h>
|
|
#include <ScrollView.h>
|
|
#include <TextView.h>
|
|
#include <Volume.h>
|
|
#include <VolumeRoster.h>
|
|
#include <Window.h>
|
|
|
|
#include <GroupLayout.h>
|
|
#include <GroupLayoutBuilder.h>
|
|
#include <GroupView.h>
|
|
|
|
#include <fs_info.h>
|
|
#include <stdio.h> // For debugging
|
|
|
|
|
|
#undef B_TRANSLATION_CONTEXT
|
|
#define B_TRANSLATION_CONTEXT "PackageView"
|
|
|
|
const float kMaxDescHeight = 125.0f;
|
|
const uint32 kSeparatorIndex = 3;
|
|
|
|
|
|
static void
|
|
convert_size(uint64 size, char *buffer, uint32 n)
|
|
{
|
|
if (size < 1024)
|
|
snprintf(buffer, n, B_TRANSLATE("%llu bytes"), size);
|
|
else if (size < 1024 * 1024)
|
|
snprintf(buffer, n, B_TRANSLATE("%.1f KiB"), size / 1024.0f);
|
|
else if (size < 1024 * 1024 * 1024)
|
|
snprintf(buffer, n, B_TRANSLATE("%.1f MiB"),
|
|
size / (1024.0f * 1024.0f));
|
|
else {
|
|
snprintf(buffer, n, B_TRANSLATE("%.1f GiB"),
|
|
size / (1024.0f * 1024.0f * 1024.0f));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// #pragma mark -
|
|
|
|
|
|
PackageView::PackageView(const entry_ref* ref)
|
|
:
|
|
BView("package_view", 0),
|
|
fOpenPanel(new BFilePanel(B_OPEN_PANEL, NULL, NULL, B_DIRECTORY_NODE,
|
|
false)),
|
|
fInfo(ref),
|
|
fInstallProcess(this)
|
|
{
|
|
_InitView();
|
|
|
|
// Check whether the package has been successfuly parsed
|
|
status_t ret = fInfo.InitCheck();
|
|
if (ret == B_OK)
|
|
_InitProfiles();
|
|
}
|
|
|
|
|
|
PackageView::~PackageView()
|
|
{
|
|
delete fOpenPanel;
|
|
}
|
|
|
|
|
|
void
|
|
PackageView::AttachedToWindow()
|
|
{
|
|
status_t ret = fInfo.InitCheck();
|
|
if (ret != B_OK && ret != B_NO_INIT) {
|
|
BAlert* warning = new BAlert("parsing_failed",
|
|
B_TRANSLATE("The package file is not readable.\nOne of the "
|
|
"possible reasons for this might be that the requested file "
|
|
"is not a valid BeOS .pkg package."), B_TRANSLATE("OK"),
|
|
NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
|
|
warning->SetFlags(warning->Flags() | B_CLOSE_ON_ESCAPE);
|
|
warning->Go();
|
|
|
|
Window()->PostMessage(B_QUIT_REQUESTED);
|
|
return;
|
|
}
|
|
|
|
// Set the window title
|
|
BWindow* parent = Window();
|
|
BString title;
|
|
BString name = fInfo.GetName();
|
|
if (name.CountChars() == 0) {
|
|
title = B_TRANSLATE("Package installer");
|
|
} else {
|
|
title = B_TRANSLATE("Install %name%");
|
|
title.ReplaceAll("%name%", name);
|
|
}
|
|
parent->SetTitle(title.String());
|
|
fInstall->SetTarget(this);
|
|
|
|
fOpenPanel->SetTarget(BMessenger(this));
|
|
fInstallTypes->SetTargetForItems(this);
|
|
|
|
if (ret != B_OK)
|
|
return;
|
|
|
|
// If the package is valid, we can set up the default group and all
|
|
// other things. If not, then the application will close just after
|
|
// attaching the view to the window
|
|
_InstallTypeChanged(0);
|
|
|
|
fStatusWindow = new PackageStatus(B_TRANSLATE("Installation progress"),
|
|
NULL, NULL, this);
|
|
|
|
// Show the splash screen, if present
|
|
BMallocIO* image = fInfo.GetSplashScreen();
|
|
if (image != NULL) {
|
|
PackageImageViewer* imageViewer = new PackageImageViewer(image);
|
|
imageViewer->Go();
|
|
}
|
|
|
|
// Show the disclaimer/info text popup, if present
|
|
BString disclaimer = fInfo.GetDisclaimer();
|
|
if (disclaimer.Length() != 0) {
|
|
PackageTextViewer* text = new PackageTextViewer(
|
|
disclaimer.String());
|
|
int32 selection = text->Go();
|
|
// The user didn't accept our disclaimer, this means we cannot
|
|
// continue.
|
|
if (selection == 0)
|
|
parent->Quit();
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
PackageView::MessageReceived(BMessage* message)
|
|
{
|
|
switch (message->what) {
|
|
case P_MSG_INSTALL:
|
|
{
|
|
fInstall->SetEnabled(false);
|
|
fInstallTypes->SetEnabled(false);
|
|
fDestination->SetEnabled(false);
|
|
fStatusWindow->Show();
|
|
|
|
fInstallProcess.Start();
|
|
break;
|
|
}
|
|
case P_MSG_PATH_CHANGED:
|
|
{
|
|
BString path;
|
|
if (message->FindString("path", &path) == B_OK)
|
|
fCurrentPath.SetTo(path.String());
|
|
break;
|
|
}
|
|
case P_MSG_OPEN_PANEL:
|
|
fOpenPanel->Show();
|
|
break;
|
|
case P_MSG_INSTALL_TYPE_CHANGED:
|
|
{
|
|
int32 index;
|
|
if (message->FindInt32("index", &index) == B_OK)
|
|
_InstallTypeChanged(index);
|
|
break;
|
|
}
|
|
case P_MSG_I_FINISHED:
|
|
{
|
|
BAlert* notify = new BAlert("installation_success",
|
|
B_TRANSLATE("The package you requested has been successfully "
|
|
"installed on your system."),
|
|
B_TRANSLATE("OK"));
|
|
notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE);
|
|
|
|
notify->Go();
|
|
fStatusWindow->Hide();
|
|
fInstall->SetEnabled(true);
|
|
fInstallTypes->SetEnabled(true);
|
|
fDestination->SetEnabled(true);
|
|
fInstallProcess.Stop();
|
|
|
|
BWindow *parent = Window();
|
|
if (parent && parent->Lock())
|
|
parent->Quit();
|
|
break;
|
|
}
|
|
case P_MSG_I_ABORT:
|
|
{
|
|
BAlert* notify = new BAlert("installation_aborted",
|
|
B_TRANSLATE(
|
|
"The installation of the package has been aborted."),
|
|
B_TRANSLATE("OK"));
|
|
notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE);
|
|
notify->Go();
|
|
fStatusWindow->Hide();
|
|
fInstall->SetEnabled(true);
|
|
fInstallTypes->SetEnabled(true);
|
|
fDestination->SetEnabled(true);
|
|
fInstallProcess.Stop();
|
|
break;
|
|
}
|
|
case P_MSG_I_ERROR:
|
|
{
|
|
// TODO: Review this
|
|
BAlert* notify = new BAlert("installation_failed",
|
|
B_TRANSLATE("The requested package failed to install on your "
|
|
"system. This might be a problem with the target package "
|
|
"file. Please consult this issue with the package "
|
|
"distributor."),
|
|
B_TRANSLATE("OK"),
|
|
NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
|
|
fprintf(stderr,
|
|
B_TRANSLATE("Error while installing the package\n"));
|
|
notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE);
|
|
notify->Go();
|
|
fStatusWindow->Hide();
|
|
fInstall->SetEnabled(true);
|
|
fInstallTypes->SetEnabled(true);
|
|
fDestination->SetEnabled(true);
|
|
fInstallProcess.Stop();
|
|
break;
|
|
}
|
|
case P_MSG_STOP:
|
|
{
|
|
// This message is sent to us by the PackageStatus window, informing
|
|
// user interruptions.
|
|
// We actually use this message only when a post installation script
|
|
// is running and we want to kill it while it's still running
|
|
fStatusWindow->Hide();
|
|
fInstall->SetEnabled(true);
|
|
fInstallTypes->SetEnabled(true);
|
|
fDestination->SetEnabled(true);
|
|
fInstallProcess.Stop();
|
|
break;
|
|
}
|
|
case B_REFS_RECEIVED:
|
|
{
|
|
entry_ref ref;
|
|
if (message->FindRef("refs", &ref) == B_OK) {
|
|
BPath path(&ref);
|
|
if (path.InitCheck() != B_OK)
|
|
break;
|
|
|
|
dev_t device = dev_for_path(path.Path());
|
|
BVolume volume(device);
|
|
if (volume.InitCheck() != B_OK)
|
|
break;
|
|
|
|
BMenuItem* item = fDestField->MenuItem();
|
|
|
|
BString name = _NamePlusSizeString(path.Path(),
|
|
volume.FreeBytes(), B_TRANSLATE("%name% (%size% free)"));
|
|
|
|
item->SetLabel(name.String());
|
|
fCurrentPath.SetTo(path.Path());
|
|
}
|
|
break;
|
|
}
|
|
case B_SIMPLE_DATA:
|
|
if (message->WasDropped()) {
|
|
uint32 type;
|
|
int32 count;
|
|
status_t ret = message->GetInfo("refs", &type, &count);
|
|
// Check whether the message means someone dropped a file
|
|
// to our view
|
|
if (ret == B_OK && type == B_REF_TYPE) {
|
|
// If it is, send it along with the refs to the application
|
|
message->what = B_REFS_RECEIVED;
|
|
be_app->PostMessage(message);
|
|
}
|
|
}
|
|
default:
|
|
BView::MessageReceived(message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
int32
|
|
PackageView::ItemExists(PackageItem& item, BPath& path, int32& policy)
|
|
{
|
|
int32 choice = P_EXISTS_NONE;
|
|
|
|
switch (policy) {
|
|
case P_EXISTS_OVERWRITE:
|
|
choice = P_EXISTS_OVERWRITE;
|
|
break;
|
|
|
|
case P_EXISTS_SKIP:
|
|
choice = P_EXISTS_SKIP;
|
|
break;
|
|
|
|
case P_EXISTS_ASK:
|
|
case P_EXISTS_NONE:
|
|
{
|
|
const char* formatString;
|
|
switch (item.ItemKind()){
|
|
case P_KIND_SCRIPT:
|
|
formatString = B_TRANSLATE("The script named \'%s\' "
|
|
"already exits in the given path.\nReplace the script "
|
|
"with the one from this package or skip it?");
|
|
break;
|
|
case P_KIND_FILE:
|
|
formatString = B_TRANSLATE("The file named \'%s\' already "
|
|
"exits in the given path.\nReplace the file with the "
|
|
"one from this package or skip it?");
|
|
break;
|
|
case P_KIND_DIRECTORY:
|
|
formatString = B_TRANSLATE("The directory named \'%s\' "
|
|
"already exits in the given path.\nReplace the "
|
|
"directory with one from this package or skip it?");
|
|
break;
|
|
case P_KIND_SYM_LINK:
|
|
formatString = B_TRANSLATE("The symbolic link named \'%s\' "
|
|
"already exists in the give path.\nReplace the link "
|
|
"with the one from this package or skip it?");
|
|
break;
|
|
default:
|
|
formatString = B_TRANSLATE("The item named \'%s\' already "
|
|
"exits in the given path.\nReplace the item with the "
|
|
"one from this package or skip it?");
|
|
break;
|
|
}
|
|
char buffer[512];
|
|
snprintf(buffer, sizeof(buffer), formatString, path.Leaf());
|
|
|
|
BString alertString = buffer;
|
|
|
|
BAlert* alert = new BAlert("file_exists", alertString.String(),
|
|
B_TRANSLATE("Replace"),
|
|
B_TRANSLATE("Skip"),
|
|
B_TRANSLATE("Abort"));
|
|
alert->SetShortcut(2, B_ESCAPE);
|
|
|
|
choice = alert->Go();
|
|
switch (choice) {
|
|
case 0:
|
|
choice = P_EXISTS_OVERWRITE;
|
|
break;
|
|
case 1:
|
|
choice = P_EXISTS_SKIP;
|
|
break;
|
|
default:
|
|
return P_EXISTS_ABORT;
|
|
}
|
|
|
|
if (policy == P_EXISTS_NONE) {
|
|
// TODO: Maybe add 'No, but ask again' type of choice as well?
|
|
alertString = B_TRANSLATE("Do you want to remember this "
|
|
"decision for the rest of this installation?\n");
|
|
|
|
BString actionString;
|
|
if (choice == P_EXISTS_OVERWRITE) {
|
|
alertString << B_TRANSLATE(
|
|
"All existing files will be replaced?");
|
|
actionString = B_TRANSLATE("Replace all");
|
|
} else {
|
|
alertString << B_TRANSLATE(
|
|
"All existing files will be skipped?");
|
|
actionString = B_TRANSLATE("Skip all");
|
|
}
|
|
alert = new BAlert("policy_decision", alertString.String(),
|
|
actionString.String(), B_TRANSLATE("Ask again"));
|
|
|
|
int32 decision = alert->Go();
|
|
if (decision == 0)
|
|
policy = choice;
|
|
else
|
|
policy = P_EXISTS_ASK;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return choice;
|
|
}
|
|
|
|
|
|
// #pragma mark -
|
|
|
|
|
|
class DescriptionTextView : public BTextView {
|
|
public:
|
|
DescriptionTextView(const char* name, float minHeight)
|
|
:
|
|
BTextView(name)
|
|
{
|
|
SetExplicitMinSize(BSize(B_SIZE_UNSET, minHeight));
|
|
}
|
|
|
|
virtual void AttachedToWindow()
|
|
{
|
|
BTextView::AttachedToWindow();
|
|
_UpdateScrollBarVisibility();
|
|
}
|
|
|
|
virtual void FrameResized(float width, float height)
|
|
{
|
|
BTextView::FrameResized(width, height);
|
|
_UpdateScrollBarVisibility();
|
|
}
|
|
|
|
virtual void Draw(BRect updateRect)
|
|
{
|
|
BTextView::Draw(updateRect);
|
|
_UpdateScrollBarVisibility();
|
|
}
|
|
|
|
private:
|
|
void _UpdateScrollBarVisibility()
|
|
{
|
|
BScrollBar* verticalBar = ScrollBar(B_VERTICAL);
|
|
if (verticalBar != NULL) {
|
|
float min;
|
|
float max;
|
|
verticalBar->GetRange(&min, &max);
|
|
if (min == max) {
|
|
if (!verticalBar->IsHidden(verticalBar))
|
|
verticalBar->Hide();
|
|
} else {
|
|
if (verticalBar->IsHidden(verticalBar))
|
|
verticalBar->Show();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
void
|
|
PackageView::_InitView()
|
|
{
|
|
SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
|
|
|
|
float fontHeight = be_plain_font->Size();
|
|
|
|
BTextView* packageDescriptionView = new DescriptionTextView(
|
|
"package description", fontHeight * 13);
|
|
packageDescriptionView->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
|
|
packageDescriptionView->SetText(fInfo.GetDescription());
|
|
packageDescriptionView->MakeEditable(false);
|
|
packageDescriptionView->MakeSelectable(false);
|
|
|
|
BScrollView* descriptionScrollView = new BScrollView(
|
|
"package description scroll view", packageDescriptionView,
|
|
0, false, true, B_NO_BORDER);
|
|
|
|
// Install type menu field
|
|
fInstallTypes = new BPopUpMenu(B_TRANSLATE("none"));
|
|
BMenuField* installType = new BMenuField("install_type",
|
|
B_TRANSLATE("Installation type:"), fInstallTypes);
|
|
|
|
// Install type description text view
|
|
fInstallTypeDescriptionView = new DescriptionTextView(
|
|
"install type description", fontHeight * 3);
|
|
fInstallTypeDescriptionView->MakeEditable(false);
|
|
fInstallTypeDescriptionView->MakeSelectable(false);
|
|
fInstallTypeDescriptionView->SetText(
|
|
B_TRANSLATE("No installation type selected"));
|
|
fInstallTypeDescriptionView->SetViewColor(
|
|
ui_color(B_PANEL_BACKGROUND_COLOR));
|
|
|
|
BScrollView* installTypeScrollView = new BScrollView(
|
|
"install type description scroll view", fInstallTypeDescriptionView,
|
|
0, false, true, B_NO_BORDER);
|
|
|
|
// Destination menu field
|
|
fDestination = new BPopUpMenu(B_TRANSLATE("none"));
|
|
fDestField = new BMenuField("install_to", B_TRANSLATE("Install to:"),
|
|
fDestination);
|
|
|
|
fInstall = new BButton("install_button", B_TRANSLATE("Install"),
|
|
new BMessage(P_MSG_INSTALL));
|
|
|
|
// Build the layout
|
|
BLayoutBuilder::Group<>(this, B_VERTICAL)
|
|
.Add(descriptionScrollView)
|
|
.AddGrid()
|
|
.AddMenuField(fDestField, 0, 0, B_ALIGN_RIGHT)
|
|
.AddMenuField(installType, 0, 1, B_ALIGN_RIGHT)
|
|
.Add(installTypeScrollView, 0, 2, 2)
|
|
.End()
|
|
.AddGroup(B_HORIZONTAL)
|
|
.AddGlue()
|
|
.Add(fInstall)
|
|
.End()
|
|
.SetInsets(B_USE_DEFAULT_SPACING)
|
|
;
|
|
|
|
fInstall->MakeDefault(true);
|
|
}
|
|
|
|
|
|
void
|
|
PackageView::_InitProfiles()
|
|
{
|
|
int count = fInfo.GetProfileCount();
|
|
|
|
if (count > 0) {
|
|
// Add the default item
|
|
pkg_profile* profile = fInfo.GetProfile(0);
|
|
BMenuItem* item = _AddInstallTypeMenuItem(profile->name,
|
|
profile->space_needed, 0);
|
|
item->SetMarked(true);
|
|
fCurrentType = 0;
|
|
}
|
|
|
|
for (int i = 1; i < count; i++) {
|
|
pkg_profile* profile = fInfo.GetProfile(i);
|
|
|
|
if (profile != NULL)
|
|
_AddInstallTypeMenuItem(profile->name, profile->space_needed, i);
|
|
else
|
|
fInstallTypes->AddSeparatorItem();
|
|
}
|
|
}
|
|
|
|
|
|
status_t
|
|
PackageView::_InstallTypeChanged(int32 index)
|
|
{
|
|
if (index < 0)
|
|
return B_ERROR;
|
|
|
|
// Clear the choice list
|
|
for (int32 i = fDestination->CountItems() - 1; i >= 0; i--) {
|
|
BMenuItem* item = fDestination->RemoveItem(i);
|
|
delete item;
|
|
}
|
|
|
|
fCurrentType = index;
|
|
pkg_profile* profile = fInfo.GetProfile(index);
|
|
|
|
if (profile == NULL)
|
|
return B_ERROR;
|
|
|
|
fInstallTypeDescriptionView->SetText(profile->description.String());
|
|
|
|
BPath path;
|
|
BVolume volume;
|
|
|
|
if (profile->path_type == P_INSTALL_PATH) {
|
|
BMenuItem* item = NULL;
|
|
if (find_directory(B_SYSTEM_NONPACKAGED_DIRECTORY, &path) == B_OK) {
|
|
dev_t device = dev_for_path(path.Path());
|
|
if (volume.SetTo(device) == B_OK && !volume.IsReadOnly()
|
|
&& path.Append("apps") == B_OK) {
|
|
item = _AddDestinationMenuItem(path.Path(), volume.FreeBytes(),
|
|
path.Path());
|
|
}
|
|
}
|
|
|
|
if (item != NULL) {
|
|
item->SetMarked(true);
|
|
fCurrentPath.SetTo(path.Path());
|
|
fDestination->AddSeparatorItem();
|
|
}
|
|
|
|
_AddMenuItem(B_TRANSLATE("Other" B_UTF8_ELLIPSIS),
|
|
new BMessage(P_MSG_OPEN_PANEL), fDestination);
|
|
|
|
fDestField->SetEnabled(true);
|
|
} else if (profile->path_type == P_USER_PATH) {
|
|
bool defaultPathSet = false;
|
|
BVolumeRoster roster;
|
|
|
|
while (roster.GetNextVolume(&volume) != B_BAD_VALUE) {
|
|
BDirectory mountPoint;
|
|
if (volume.IsReadOnly() || !volume.IsPersistent()
|
|
|| volume.GetRootDirectory(&mountPoint) != B_OK) {
|
|
continue;
|
|
}
|
|
|
|
if (path.SetTo(&mountPoint, NULL) != B_OK)
|
|
continue;
|
|
|
|
char volumeName[B_FILE_NAME_LENGTH];
|
|
volume.GetName(volumeName);
|
|
|
|
BMenuItem* item = _AddDestinationMenuItem(volumeName,
|
|
volume.FreeBytes(), path.Path());
|
|
|
|
// The first volume becomes the default element
|
|
if (!defaultPathSet) {
|
|
item->SetMarked(true);
|
|
fCurrentPath.SetTo(path.Path());
|
|
defaultPathSet = true;
|
|
}
|
|
}
|
|
|
|
fDestField->SetEnabled(true);
|
|
} else
|
|
fDestField->SetEnabled(false);
|
|
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
BString
|
|
PackageView::_NamePlusSizeString(BString baseName, size_t size,
|
|
const char* format) const
|
|
{
|
|
char sizeString[48];
|
|
convert_size(size, sizeString, sizeof(sizeString));
|
|
|
|
BString name(format);
|
|
name.ReplaceAll("%name%", baseName);
|
|
name.ReplaceAll("%size%", sizeString);
|
|
|
|
return name;
|
|
}
|
|
|
|
|
|
BMenuItem*
|
|
PackageView::_AddInstallTypeMenuItem(BString baseName, size_t size,
|
|
int32 index) const
|
|
{
|
|
BString name = _NamePlusSizeString(baseName, size,
|
|
B_TRANSLATE("%name% (%size%)"));
|
|
|
|
BMessage* message = new BMessage(P_MSG_INSTALL_TYPE_CHANGED);
|
|
message->AddInt32("index", index);
|
|
|
|
return _AddMenuItem(name, message, fInstallTypes);
|
|
}
|
|
|
|
|
|
BMenuItem*
|
|
PackageView::_AddDestinationMenuItem(BString baseName, size_t size,
|
|
const char* path) const
|
|
{
|
|
BString name = _NamePlusSizeString(baseName, size,
|
|
B_TRANSLATE("%name% (%size% free)"));
|
|
|
|
BMessage* message = new BMessage(P_MSG_PATH_CHANGED);
|
|
message->AddString("path", path);
|
|
|
|
return _AddMenuItem(name, message, fDestination);
|
|
}
|
|
|
|
|
|
BMenuItem*
|
|
PackageView::_AddMenuItem(const char* name, BMessage* message,
|
|
BMenu* menu) const
|
|
{
|
|
BMenuItem* item = new BMenuItem(name, message);
|
|
item->SetTarget(this);
|
|
menu->AddItem(item);
|
|
return item;
|
|
}
|