* Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2018-2026, Andrew Lindesay <apl@lindesay.co.nz>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#include "PackageInfoView.h"
#include <algorithm>
#include <Alert.h>
#include <Autolock.h>
#include <Bitmap.h>
#include <Button.h>
#include <CardLayout.h>
#include <Catalog.h>
#include <ColumnListView.h>
#include <ControlLook.h>
#include <Font.h>
#include <GridView.h>
#include <LayoutBuilder.h>
#include <LayoutUtils.h>
#include <LocaleRoster.h>
#include <Message.h>
#include <OutlineListView.h>
#include <ScrollView.h>
#include <SpaceLayoutItem.h>
#include <StatusBar.h>
#include <StringView.h>
#include <TabView.h>
#include <Url.h>
#include <package/hpkg/NoErrorOutput.h>
#include <package/hpkg/PackageContentHandler.h>
#include <package/hpkg/PackageEntry.h>
#include <package/hpkg/PackageReader.h>
#include "BitmapView.h"
#include "GeneralContentScrollView.h"
#include "LinkView.h"
#include "LinkedBitmapView.h"
#include "LocaleUtils.h"
#include "Logger.h"
#include "MarkupTextView.h"
#include "PackageContentsView.h"
#include "PackageInfo.h"
#include "PackageManager.h"
#include "PackageUtils.h"
#include "ProcessCoordinatorFactory.h"
#include "RatingView.h"
#include "ScrollableGroupView.h"
#include "ServerHelper.h"
#include "SharedIcons.h"
#include "TextView.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "PackageInfoView"
enum {
TAB_ABOUT = 0,
TAB_RATINGS = 1,
TAB_CHANGELOG = 2,
TAB_CONTENTS = 3
};
static const float kContentTint = (B_NO_TINT + B_LIGHTEN_1_TINT) / 2.0f;
static const uint32 kScreenshotSize = 320;
class RatingsScrollView : public GeneralContentScrollView {
public:
RatingsScrollView(const char* name, BView* target)
:
GeneralContentScrollView(name, target)
{
}
virtual void DoLayout()
{
GeneralContentScrollView::DoLayout();
BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
BView* target = Target();
if (target != NULL && scrollBar != NULL) {
BView* item = target->ChildAt(0);
if (item != NULL)
scrollBar->SetSteps(item->MinSize().height + 1, item->MinSize().height + 1);
}
}
};
class DiagramBarView : public BView {
public:
DiagramBarView()
:
BView("diagram bar view", B_WILL_DRAW),
fValue(0.0f)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint);
SetHighUIColor(B_CONTROL_MARK_COLOR);
}
virtual ~DiagramBarView()
{
}
virtual void AttachedToWindow()
{
}
virtual void Draw(BRect updateRect)
{
FillRect(updateRect, B_SOLID_LOW);
if (fValue <= 0.0f)
return;
BRect rect(Bounds());
rect.right = ceilf(rect.left + fValue * rect.Width());
FillRect(rect, B_SOLID_HIGH);
}
virtual BSize MinSize()
{
return BSize(64, 10);
}
virtual BSize PreferredSize()
{
return MinSize();
}
virtual BSize MaxSize()
{
return BSize(64, 10);
}
void SetValue(float value)
{
if (fValue != value) {
fValue = value;
Invalidate();
}
}
private:
float fValue;
};
enum {
MSG_MOUSE_ENTERED_RATING = 'menr',
MSG_MOUSE_EXITED_RATING = 'mexr',
};
class TransitReportingButton : public BButton {
public:
TransitReportingButton(const char* name, const char* label, BMessage* message)
:
BButton(name, label, message),
fTransitMessage(NULL)
{
}
virtual ~TransitReportingButton()
{
SetTransitMessage(NULL);
}
virtual void MouseMoved(BPoint point, uint32 transit, const BMessage* dragMessage)
{
BButton::MouseMoved(point, transit, dragMessage);
if (fTransitMessage != NULL && transit == B_EXITED_VIEW)
Invoke(fTransitMessage);
}
void SetTransitMessage(BMessage* message)
{
if (fTransitMessage != message) {
delete fTransitMessage;
fTransitMessage = message;
}
}
private:
BMessage* fTransitMessage;
};
class TransitReportingRatingView : public RatingView, public BInvoker {
public:
TransitReportingRatingView(BMessage* transitMessage)
:
RatingView("package rating view"),
fTransitMessage(transitMessage)
{
}
virtual ~TransitReportingRatingView()
{
delete fTransitMessage;
}
virtual void MouseMoved(BPoint point, uint32 transit,
const BMessage* dragMessage)
{
RatingView::MouseMoved(point, transit, dragMessage);
if (fTransitMessage != NULL && transit == B_ENTERED_VIEW)
Invoke(fTransitMessage);
}
private:
BMessage* fTransitMessage;
};
class TitleView : public BGroupView {
public:
TitleView(Model* model)
:
BGroupView("title view", B_HORIZONTAL),
fModel(model)
{
fIconView = new BitmapView("package icon view");
fTitleView = new BStringView("package title view", "");
fPublisherView = new BStringView("package publisher view", "");
BFont font;
GetFont(&font);
font_family family;
font_style style;
font.SetSize(ceilf(font.Size() * 1.5f));
font.GetFamilyAndStyle(&family, &style);
font.SetFamilyAndStyle(family, "Bold");
fTitleView->SetFont(&font);
GetFont(&font);
font.SetSize(std::max(9.0f, floorf(font.Size() * 0.92f)));
font.SetFamilyAndStyle(family, "Italic");
fPublisherView->SetFont(&font);
fPublisherView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_LIGHTEN_1_TINT);
GetFont(&font);
font.SetSize(ceilf(font.Size() * 1.2f));
fVersionInfo = new BStringView("package version info", "");
fVersionInfo->SetFont(&font);
fVersionInfo->SetHighUIColor(B_PANEL_TEXT_COLOR, B_LIGHTEN_1_TINT);
fRatingView = new TransitReportingRatingView(
new BMessage(MSG_MOUSE_ENTERED_RATING));
fAvgRating = new BStringView("package average rating", "");
fAvgRating->SetFont(&font);
fAvgRating->SetHighUIColor(B_PANEL_TEXT_COLOR, B_LIGHTEN_1_TINT);
fVoteInfo = new BStringView("package vote info", "");
GetFont(&font);
font.SetSize(std::max(9.0f, floorf(font.Size() * 0.85f)));
fVoteInfo->SetFont(&font);
fVoteInfo->SetHighUIColor(B_PANEL_TEXT_COLOR, B_LIGHTEN_1_TINT);
fRateButton = new TransitReportingButton("rate",
B_TRANSLATE("Rate package" B_UTF8_ELLIPSIS), new BMessage(MSG_RATE_PACKAGE));
fRateButton->SetTransitMessage(new BMessage(MSG_MOUSE_EXITED_RATING));
fRateButton->SetExplicitAlignment(BAlignment(B_ALIGN_LEFT, B_ALIGN_VERTICAL_CENTER));
BView* ratingStack = new BView("rating stack", 0);
fRatingLayout = new BCardLayout();
ratingStack->SetLayout(fRatingLayout);
ratingStack->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
ratingStack->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
BGroupView* ratingGroup = new BGroupView(B_HORIZONTAL, B_USE_SMALL_SPACING);
BLayoutBuilder::Group<>(ratingGroup)
.Add(fRatingView)
.Add(fAvgRating)
.Add(fVoteInfo)
;
ratingStack->AddChild(ratingGroup);
ratingStack->AddChild(fRateButton);
fRatingLayout->SetVisibleItem((int32)0);
BLayoutBuilder::Group<>(this)
.Add(fIconView)
.AddGroup(B_VERTICAL, 1.0f, 2.2f)
.Add(fTitleView)
.Add(fPublisherView)
.SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET))
.End()
.AddGlue(0.1f)
.Add(ratingStack, 0.8f)
.AddGlue(0.2f)
.AddGroup(B_HORIZONTAL, B_USE_SMALL_SPACING, 2.0f)
.Add(fVersionInfo)
.AddGlue()
.SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET))
.End();
Clear();
}
virtual ~TitleView()
{
}
virtual void AttachedToWindow()
{
fRateButton->SetTarget(this);
fRatingView->SetTarget(this);
}
virtual void MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_RATE_PACKAGE:
Window()->PostMessage(MSG_RATE_PACKAGE);
break;
case MSG_MOUSE_ENTERED_RATING:
fRatingLayout->SetVisibleItem(1);
break;
case MSG_MOUSE_EXITED_RATING:
fRatingLayout->SetVisibleItem((int32)0);
break;
}
}
void SetIcon(const PackageInfoRef package)
{
if (!package.IsSet()) {
fIconView->UnsetBitmap();
} else {
BitmapHolderRef bitmapHolderRef;
BSize iconSize = BControlLook::ComposeIconSize(32.0);
status_t iconResult = B_OK;
PackageIconRepositoryRef iconRepository = _PackageIconRepository();
if (iconRepository.IsSet()) {
iconResult = iconRepository->GetIcon(package->Name(), iconSize.Width() + 1,
bitmapHolderRef);
} else {
iconResult = B_NOT_INITIALIZED;
}
if (iconResult == B_OK)
fIconView->SetBitmap(bitmapHolderRef);
else
fIconView->UnsetBitmap();
}
}
void SetPackage(const PackageInfoRef package)
{
SetIcon(package);
BString title;
PackageUtils::TitleOrName(package, title);
fTitleView->SetText(title);
BString publisherName = PackageUtils::PublisherName(package);
BString versionString = "???";
PackageCoreInfoRef coreInfo = package->CoreInfo();
if (coreInfo.IsSet()) {
PackageVersionRef version = coreInfo->Version();
if (version.IsSet())
versionString = version->ToString();
}
if (publisherName.CountChars() > 45) {
fPublisherView->SetToolTip(publisherName);
fPublisherView->SetText(publisherName.TruncateChars(45).Append(B_UTF8_ELLIPSIS));
} else {
fPublisherView->SetToolTip("");
fPublisherView->SetText(publisherName);
}
fVersionInfo->SetText(versionString);
PackageUserRatingInfoRef userRatingInfo = package->UserRatingInfo();
UserRatingSummaryRef userRatingSummary;
if (userRatingInfo.IsSet())
userRatingSummary = userRatingInfo->Summary();
bool hasAverageRating = false;
if (userRatingSummary.IsSet()) {
if (userRatingSummary->AverageRating() != RATING_MISSING)
hasAverageRating = true;
}
if (hasAverageRating) {
fRatingView->SetRating(userRatingSummary->AverageRating());
BString avgRating;
avgRating.SetToFormat("%.1f", userRatingSummary->AverageRating());
fAvgRating->SetText(avgRating);
BString votes;
votes.SetToFormat("%d", userRatingSummary->RatingCount());
BString voteInfo(B_TRANSLATE("(%Votes%)"));
voteInfo.ReplaceAll("%Votes%", votes);
fVoteInfo->SetText(voteInfo);
} else {
fRatingView->SetRating(RATING_MISSING);
fAvgRating->SetText("");
fVoteInfo->SetText(B_TRANSLATE("n/a"));
}
fRatingLayout->SetVisible(_PackageCanHaveRatings(package));
InvalidateLayout();
Invalidate();
}
void Clear()
{
fIconView->UnsetBitmap();
fTitleView->SetText("");
fPublisherView->SetText("");
fVersionInfo->SetText("");
fRatingView->SetRating(RATING_MISSING);
fAvgRating->SetText("");
fVoteInfo->SetText("");
}
private:
PackageIconRepositoryRef _PackageIconRepository()
{
return fModel->IconRepository();
}
repository (depot). Otherwise there will be no means to display ratings.
*/
bool _PackageCanHaveRatings(const PackageInfoRef package)
{
BString depotName = PackageUtils::DepotName(package);
if (depotName == SINGLE_PACKAGE_DEPOT_NAME)
return false;
const DepotInfoRef depotInfo = fModel->DepotForName(depotName);
if (depotInfo.IsSet())
return !depotInfo->WebAppRepositoryCode().IsEmpty();
return false;
}
private:
Model* fModel;
BitmapView* fIconView;
BStringView* fTitleView;
BStringView* fPublisherView;
BStringView* fVersionInfo;
BCardLayout* fRatingLayout;
TransitReportingRatingView* fRatingView;
BStringView* fAvgRating;
BStringView* fVoteInfo;
TransitReportingButton* fRateButton;
};
class PackageActionView : public BView {
public:
PackageActionView()
:
BView("about view", B_WILL_DRAW),
fLayout(new BGroupLayout(B_HORIZONTAL)),
fStatusLabel(NULL),
fStatusBar(NULL)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
SetLayout(fLayout);
fLayout->AddItem(BSpaceLayoutItem::CreateGlue());
}
virtual ~PackageActionView()
{
Clear();
}
void SetPackage(const PackageInfoRef package)
{
if (PackageUtils::State(package) == DOWNLOADING)
AdoptDownloadProgress(package);
else
AdoptActions(package);
}
void AdoptActions(const PackageInfoRef package)
{
PackageManager manager(BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME);
std::vector<PackageActionRef> actions;
VectorCollector<PackageActionRef> actionsCollector(actions);
manager.CollectPackageActions(package, actionsCollector);
if (_IsClearNeededToAdoptActions(actions)) {
Clear();
_CreateAllNewButtonsForAdoptActions(actions);
} else {
_UpdateExistingButtonsForAdoptActions(actions);
}
}
void AdoptDownloadProgress(const PackageInfoRef package)
{
if (fButtons.CountItems() > 0)
Clear();
if (fStatusBar == NULL) {
fStatusLabel = new BStringView("progress label", B_TRANSLATE("Downloading:"));
fLayout->AddView(fStatusLabel);
fStatusBar = new BStatusBar("progress");
fStatusBar->SetMaxValue(100.0);
fStatusBar->SetExplicitMinSize(BSize(StringWidth("XXX") * 5, B_SIZE_UNSET));
fLayout->AddView(fStatusBar);
}
fStatusBar->SetTo(PackageUtils::DownloadProgress(package) * 100.0);
}
void Clear()
{
for (int32 i = fButtons.CountItems() - 1; i >= 0; i--) {
BButton* button = (BButton*)fButtons.ItemAtFast(i);
button->RemoveSelf();
delete button;
}
fButtons.MakeEmpty();
if (fStatusBar != NULL) {
fStatusBar->RemoveSelf();
delete fStatusBar;
fStatusBar = NULL;
}
if (fStatusLabel != NULL) {
fStatusLabel->RemoveSelf();
delete fStatusLabel;
fStatusLabel = NULL;
}
}
private:
bool _IsClearNeededToAdoptActions(std::vector<PackageActionRef> actions)
{
if (fStatusBar != NULL)
return true;
if (fButtons.CountItems() != static_cast<int32>(actions.size()))
return true;
return false;
}
void _UpdateExistingButtonsForAdoptActions(std::vector<PackageActionRef> actions)
{
int32 index = 0;
for (int32 i = actions.size() - 1; i >= 0; i--) {
const PackageActionRef& action = actions[i];
BMessage* message = new BMessage(action->Message());
BButton* button = (BButton*)fButtons.ItemAtFast(index++);
button->SetLabel(action->Title());
button->SetMessage(message);
button->SetTarget(Window());
}
}
void _CreateAllNewButtonsForAdoptActions(std::vector<PackageActionRef> actions)
{
for (int32 i = actions.size() - 1; i >= 0; i--) {
const PackageActionRef& action = actions[i];
BMessage* message = new BMessage(action->Message());
BButton* button = new BButton(action->Title(), message);
fLayout->AddView(button);
button->SetTarget(this);
button->SetTarget(Window());
fButtons.AddItem(button);
}
}
bool _MatchesPackageActionMessage(BButton* button, BMessage* message)
{
if (button == NULL)
return false;
BMessage* buttonMessage = button->Message();
if (buttonMessage == NULL)
return false;
return buttonMessage == message;
}
to run it again because this may make no sense. For this
reason, disable the corresponding button.
*/
void _DisableButtonForPackageActionMessage(BMessage* message)
{
for (int32 i = 0; i < fButtons.CountItems(); i++) {
BButton* button = static_cast<BButton*>(fButtons.ItemAt(i));
if (_MatchesPackageActionMessage(button, message))
button->SetEnabled(false);
}
}
private:
BGroupLayout* fLayout;
BList fButtons;
BStringView* fStatusLabel;
BStatusBar* fStatusBar;
};
enum {
MSG_VISIT_PUBLISHER_WEBSITE = 'vpws',
};
class AboutView : public BView {
public:
AboutView()
:
BView("about view", 0)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint);
fDescriptionView = new MarkupTextView("description view");
fDescriptionView->SetViewUIColor(ViewUIColor(), kContentTint);
fDescriptionView->SetInsets(be_plain_font->Size());
BScrollView* scrollView
= new GeneralContentScrollView("description scroll view", fDescriptionView);
BFont smallFont;
GetFont(&smallFont);
smallFont.SetSize(std::max(9.0f, ceilf(smallFont.Size() * 0.85f)));
fScreenshotView
= new LinkedBitmapView("screenshot view", new BMessage(MSG_SHOW_SCREENSHOT));
fScreenshotView->SetExplicitMinSize(BSize(64.0f, 64.0f));
fScreenshotView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
fScreenshotView->SetExplicitAlignment(BAlignment(B_ALIGN_CENTER, B_ALIGN_TOP));
fWebsiteIconView = new BitmapView("website icon view");
fWebsiteLinkView
= new LinkView("website link view", "", new BMessage(MSG_VISIT_PUBLISHER_WEBSITE));
fWebsiteLinkView->SetFont(&smallFont);
BGroupView* leftGroup = new BGroupView(B_VERTICAL, B_USE_DEFAULT_SPACING);
fScreenshotView->SetViewUIColor(ViewUIColor(), kContentTint);
fWebsiteLinkView->SetViewUIColor(ViewUIColor(), kContentTint);
BLayoutBuilder::Group<>(this, B_HORIZONTAL, 0.0f)
.AddGroup(leftGroup, 1.0f)
.Add(fScreenshotView)
.AddGroup(B_HORIZONTAL)
.AddGrid(B_USE_HALF_ITEM_SPACING, B_USE_HALF_ITEM_SPACING)
.Add(fWebsiteIconView, 0, 1)
.Add(fWebsiteLinkView, 1, 1)
.End()
.End()
.SetInsets(B_USE_DEFAULT_SPACING)
.SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET))
.End()
.Add(scrollView, 2.0f)
.SetExplicitMaxSize(BSize(B_SIZE_UNSET, B_SIZE_UNLIMITED))
.SetInsets(0.0f, -1.0f, -1.0f, -1.0f);
}
virtual ~AboutView()
{
Clear();
}
virtual void AttachedToWindow()
{
fScreenshotView->SetTarget(this);
fWebsiteLinkView->SetTarget(this);
}
virtual void AllAttached()
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint);
for (int32 index = 0; index < CountChildren(); ++index)
ChildAt(index)->AdoptParentColors();
}
virtual void MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_SHOW_SCREENSHOT:
{
Window()->PostMessage(message, Window());
break;
}
case MSG_VISIT_PUBLISHER_WEBSITE:
{
BUrl url(fWebsiteLinkView->Text(), true);
url.OpenWithPreferredApplication();
break;
}
default:
BView::MessageReceived(message);
break;
}
}
void SetScreenshotThumbnail(BitmapHolderRef bitmapHolderRef)
{
if (bitmapHolderRef.IsSet()) {
fScreenshotView->SetBitmap(bitmapHolderRef);
fScreenshotView->SetEnabled(true);
} else {
fScreenshotView->UnsetBitmap();
fScreenshotView->SetEnabled(false);
}
}
void SetPackage(const PackageInfoRef package)
{
BString summary = "";
BString description = "";
BString publisherWebsite = "";
if (package.IsSet()) {
PackageLocalizedTextRef localizedText = package->LocalizedText();
if (localizedText.IsSet()) {
summary = localizedText->Summary();
description = localizedText->Description();
}
PackageCoreInfoRef coreInfo = package->CoreInfo();
if (coreInfo.IsSet()) {
PackagePublisherInfoRef publisher = coreInfo->Publisher();
if (publisher.IsSet())
publisherWebsite = publisher->Website();
}
}
fDescriptionView->SetText(summary, description);
fWebsiteIconView->SetBitmap(SharedIcons::IconHTMLPackage16Scaled());
_SetContactInfo(fWebsiteLinkView, publisherWebsite);
}
void Clear()
{
fDescriptionView->SetText("");
fWebsiteIconView->UnsetBitmap();
fWebsiteLinkView->SetText("");
fScreenshotView->UnsetBitmap();
fScreenshotView->SetEnabled(false);
}
private:
void _SetContactInfo(LinkView* view, const BString& string)
{
if (string.Length() > 0) {
view->SetText(string);
view->SetEnabled(true);
} else {
view->SetText(B_TRANSLATE("<no info>"));
view->SetEnabled(false);
}
}
private:
MarkupTextView* fDescriptionView;
LinkedBitmapView* fScreenshotView;
BitmapView* fWebsiteIconView;
LinkView* fWebsiteLinkView;
};
class RatingItemView : public BGroupView {
public:
RatingItemView(const UserRatingRef rating)
:
BGroupView(B_HORIZONTAL, 0.0f)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint);
BGroupLayout* verticalGroup = new BGroupLayout(B_VERTICAL, 0.0f);
GroupLayout()->AddItem(verticalGroup);
{
BStringView* userNicknameView
= new BStringView("user-nickname", rating->User().NickName());
userNicknameView->SetFont(be_bold_font);
verticalGroup->AddView(userNicknameView);
}
BGroupLayout* ratingGroup = new BGroupLayout(B_HORIZONTAL, B_USE_DEFAULT_SPACING);
verticalGroup->AddItem(ratingGroup);
if (rating->Rating() >= 0) {
RatingView* ratingView = new RatingView("package rating view");
ratingView->SetRating(rating->Rating());
ratingGroup->AddView(ratingView);
}
{
BString createTimestampPresentation
= LocaleUtils::TimestampToDateTimeString(rating->CreateTimestamp());
BString ratingContextDescription(B_TRANSLATE("%hd.timestamp% (version %hd.version%)"));
ratingContextDescription.ReplaceAll("%hd.timestamp%", createTimestampPresentation);
ratingContextDescription.ReplaceAll("%hd.version%", rating->PackageVersion());
BStringView* ratingContextView
= new BStringView("rating-context", ratingContextDescription);
BFont versionFont(be_plain_font);
ratingContextView->SetFont(&versionFont);
ratingGroup->AddView(ratingContextView);
}
ratingGroup->AddItem(BSpaceLayoutItem::CreateGlue());
if (rating->Comment() > 0) {
TextView* textView = new TextView("rating-text");
ParagraphStyle paragraphStyle(textView->ParagraphStyle());
paragraphStyle.SetJustify(true);
textView->SetParagraphStyle(paragraphStyle);
textView->SetText(rating->Comment());
verticalGroup->AddItem(BSpaceLayoutItem::CreateVerticalStrut(8.0f));
verticalGroup->AddView(textView);
verticalGroup->AddItem(BSpaceLayoutItem::CreateVerticalStrut(8.0f));
}
verticalGroup->SetInsets(B_USE_DEFAULT_SPACING);
SetFlags(Flags() | B_WILL_DRAW);
}
void AllAttached()
{
for (int32 index = 0; index < CountChildren(); ++index)
ChildAt(index)->AdoptParentColors();
}
void Draw(BRect rect)
{
rgb_color color = mix_color(ViewColor(), ui_color(B_PANEL_TEXT_COLOR), 64);
SetHighColor(color);
StrokeLine(Bounds().LeftBottom(), Bounds().RightBottom());
}
};
class RatingSummaryView : public BGridView {
public:
RatingSummaryView()
:
BGridView("rating summary view", B_USE_HALF_ITEM_SPACING, 0.0f)
{
float tint = kContentTint - 0.1;
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, tint);
BLayoutBuilder::Grid<> layoutBuilder(this);
BFont smallFont;
GetFont(&smallFont);
smallFont.SetSize(std::max(9.0f, floorf(smallFont.Size() * 0.85f)));
for (int32 i = 0; i < 5; i++) {
BString label;
label.SetToFormat("%" B_PRId32, 5 - i);
fLabelViews[i] = new BStringView("", label);
fLabelViews[i]->SetFont(&smallFont);
fLabelViews[i]->SetViewUIColor(ViewUIColor(), tint);
layoutBuilder.Add(fLabelViews[i], 0, i);
fDiagramBarViews[i] = new DiagramBarView();
layoutBuilder.Add(fDiagramBarViews[i], 1, i);
fCountViews[i] = new BStringView("", "");
fCountViews[i]->SetFont(&smallFont);
fCountViews[i]->SetViewUIColor(ViewUIColor(), tint);
fCountViews[i]->SetAlignment(B_ALIGN_RIGHT);
layoutBuilder.Add(fCountViews[i], 2, i);
}
layoutBuilder.SetInsets(5);
}
void SetToSummary(const UserRatingSummaryRef summary)
{
if (!summary.IsSet()) {
Clear();
} else {
for (int32 i = 0; i < 5; i++) {
int32 count = summary->RatingCountByStar(5 - i);
BString label;
label.SetToFormat("%" B_PRId32, count);
fCountViews[i]->SetText(label);
int ratingCount = summary->RatingCount();
if (ratingCount > 0) {
fDiagramBarViews[i]->SetValue(
static_cast<float>(count) / static_cast<float>(ratingCount));
} else {
fDiagramBarViews[i]->SetValue(0.0f);
}
}
}
}
void Clear()
{
for (int32 i = 0; i < 5; i++) {
fCountViews[i]->SetText("");
fDiagramBarViews[i]->SetValue(0.0f);
}
}
private:
BStringView* fLabelViews[5];
DiagramBarView* fDiagramBarViews[5];
BStringView* fCountViews[5];
};
class UserRatingsView : public BGroupView {
public:
UserRatingsView()
:
BGroupView("package ratings view", B_HORIZONTAL)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint);
fRatingSummaryView = new RatingSummaryView();
ScrollableGroupView* ratingsContainerView = new ScrollableGroupView();
ratingsContainerView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR,
kContentTint);
fRatingContainerLayout = ratingsContainerView->GroupLayout();
BScrollView* scrollView
= new RatingsScrollView("ratings scroll view", ratingsContainerView);
scrollView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
BLayoutBuilder::Group<>(this)
.AddGroup(B_VERTICAL)
.Add(fRatingSummaryView, 0.0f)
.AddGlue()
.SetInsets(0.0f, B_USE_DEFAULT_SPACING, 0.0f, 0.0f)
.End()
.AddStrut(64.0)
.Add(scrollView, 1.0f)
.SetInsets(B_USE_DEFAULT_SPACING, -1.0f, -1.0f, -1.0f);
}
virtual ~UserRatingsView()
{
Clear();
}
void SetPackage(const PackageInfoRef package)
{
ClearRatings();
PackageUserRatingInfoRef userRatingInfo = package->UserRatingInfo();
UserRatingSummaryRef userRatingSummary;
if (userRatingInfo.IsSet())
userRatingSummary = userRatingInfo->Summary();
fRatingSummaryView->SetToSummary(userRatingSummary);
int count = 0;
if (userRatingInfo.IsSet())
count = userRatingInfo->CountUserRatings();
if (count == 0) {
BStringView* noRatingsView
= new BStringView("no ratings", B_TRANSLATE("No user ratings available."));
noRatingsView->SetViewUIColor(ViewUIColor(), kContentTint);
noRatingsView->SetAlignment(B_ALIGN_CENTER);
noRatingsView->SetHighColor(disable_color(ui_color(B_PANEL_TEXT_COLOR), ViewColor()));
noRatingsView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
fRatingContainerLayout->AddView(0, noRatingsView);
} else {
for (int i = count - 1; i >= 0; i--) {
UserRatingRef rating = userRatingInfo->UserRatingAtIndex(i);
RatingItemView* view = new RatingItemView(rating);
fRatingContainerLayout->AddView(0, view);
}
}
InvalidateLayout();
}
void Clear()
{
fRatingSummaryView->Clear();
ClearRatings();
}
void ClearRatings()
{
for (int32 i = fRatingContainerLayout->CountItems() - 1;
BLayoutItem* item = fRatingContainerLayout->ItemAt(i); i--) {
BView* view = dynamic_cast<RatingItemView*>(item->View());
if (view == NULL)
view = dynamic_cast<BStringView*>(item->View());
if (view != NULL) {
view->RemoveSelf();
delete view;
}
}
}
private:
BGroupLayout* fRatingContainerLayout;
RatingSummaryView* fRatingSummaryView;
};
class ContentsView : public BGroupView {
public:
ContentsView()
:
BGroupView("package contents view", B_HORIZONTAL)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint);
fPackageContents = new PackageContentsView("contents_list");
AddChild(fPackageContents);
}
virtual ~ContentsView()
{
}
virtual void Draw(BRect updateRect)
{
}
void SetPackage(const PackageInfoRef package)
{
fPackageContents->SetPackage(package);
}
void Clear()
{
fPackageContents->Clear();
}
private:
PackageContentsView* fPackageContents;
};
class ChangelogView : public BGroupView {
public:
ChangelogView()
:
BGroupView("package changelog view", B_HORIZONTAL)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint);
fTextView = new MarkupTextView("changelog view");
fTextView->SetLowUIColor(ViewUIColor());
fTextView->SetInsets(be_plain_font->Size());
BScrollView* scrollView = new GeneralContentScrollView("changelog scroll view", fTextView);
BLayoutBuilder::Group<>(this)
.Add(BSpaceLayoutItem::CreateHorizontalStrut(32.0f))
.Add(scrollView, 1.0f)
.SetInsets(B_USE_DEFAULT_SPACING, -1.0f, -1.0f, -1.0f);
}
virtual ~ChangelogView()
{
}
virtual void Draw(BRect updateRect)
{
}
void SetPackage(const PackageInfoRef package)
{
PackageLocalizedTextRef localizedText = package->LocalizedText();
BString changelog;
if (localizedText.IsSet()) {
if (localizedText->HasChangelog())
changelog = localizedText->Changelog();
}
if (changelog.Length() > 0)
fTextView->SetText(changelog);
else
fTextView->SetDisabledText(B_TRANSLATE("No changelog available."));
}
void Clear()
{
fTextView->SetText("");
}
private:
MarkupTextView* fTextView;
};
class PagesView : public BTabView {
public:
PagesView(Model* model)
:
BTabView("pages view", B_WIDTH_FROM_WIDEST),
fModel(model)
{
SetBorder(B_NO_BORDER);
fAboutView = new AboutView();
fUserRatingsView = new UserRatingsView();
fChangelogView = new ChangelogView();
fContentsView = new ContentsView();
AddTab(fAboutView);
AddTab(fUserRatingsView);
AddTab(fChangelogView);
AddTab(fContentsView);
TabAt(TAB_ABOUT)->SetLabel(B_TRANSLATE("About"));
TabAt(TAB_RATINGS)->SetLabel(B_TRANSLATE("Ratings"));
TabAt(TAB_CHANGELOG)->SetLabel(B_TRANSLATE("Changelog"));
TabAt(TAB_CONTENTS)->SetLabel(B_TRANSLATE("Contents"));
Select(TAB_ABOUT);
}
virtual ~PagesView()
{
Clear();
}
void SetScreenshotThumbnail(BitmapHolderRef bitmap)
{
fAboutView->SetScreenshotThumbnail(bitmap);
}
void SetPackage(const PackageInfoRef package, bool switchToDefaultTab)
{
if (switchToDefaultTab)
Select(TAB_ABOUT);
bool enableUserRatingsTab = false;
bool enableChangelogTab = false;
bool enableContentsTab = false;
if (package.IsSet()) {
PackageLocalizedTextRef localizedText = package->LocalizedText();
if (localizedText.IsSet())
enableChangelogTab = localizedText->HasChangelog();
enableContentsTab = PackageUtils::IsActivatedOrLocalFile(package);
enableUserRatingsTab = _PackageCanHaveRatings(package);
}
TabAt(TAB_CHANGELOG)->SetEnabled(enableChangelogTab);
TabAt(TAB_CONTENTS)->SetEnabled(enableContentsTab);
TabAt(TAB_RATINGS)->SetEnabled(enableUserRatingsTab);
Invalidate(TabFrame(TAB_CHANGELOG));
Invalidate(TabFrame(TAB_CONTENTS));
fAboutView->SetPackage(package);
fUserRatingsView->SetPackage(package);
fChangelogView->SetPackage(package);
fContentsView->SetPackage(package);
}
void Clear()
{
fAboutView->Clear();
fUserRatingsView->Clear();
fChangelogView->Clear();
fContentsView->Clear();
}
private:
repository (depot). Otherwise there will be no means to display ratings.
*/
bool _PackageCanHaveRatings(const PackageInfoRef package)
{
BString depotName = PackageUtils::DepotName(package);
if (depotName == SINGLE_PACKAGE_DEPOT_NAME)
return false;
const DepotInfoRef depotInfo = fModel->DepotForName(depotName);
if (depotInfo.IsSet())
return !depotInfo->WebAppRepositoryCode().IsEmpty();
return false;
}
private:
Model* fModel;
AboutView* fAboutView;
UserRatingsView* fUserRatingsView;
ChangelogView* fChangelogView;
ContentsView* fContentsView;
};
PackageInfoView::PackageInfoView(Model* model,
ProcessCoordinatorConsumer* processCoordinatorConsumer)
:
BView("package info view", 0),
fModel(model),
fProcessCoordinatorConsumer(processCoordinatorConsumer)
{
fCardLayout = new BCardLayout();
SetLayout(fCardLayout);
BGroupView* noPackageCard = new BGroupView("no package card", B_VERTICAL);
AddChild(noPackageCard);
BStringView* noPackageView
= new BStringView("no package view", B_TRANSLATE("Click a package to view information"));
noPackageView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_LIGHTEN_1_TINT);
noPackageView->SetExplicitAlignment(
BAlignment(B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER));
BLayoutBuilder::Group<>(noPackageCard)
.Add(noPackageView)
.SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
BGroupView* packageCard = new BGroupView("package card", B_VERTICAL);
AddChild(packageCard);
fCardLayout->SetVisibleItem((int32)0);
fTitleView = new TitleView(fModel);
fPackageActionView = new PackageActionView();
fPackageActionView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
fPagesView = new PagesView(model);
BLayoutBuilder::Group<>(packageCard)
.AddGroup(B_HORIZONTAL, 0.0f)
.Add(fTitleView, 6.0f)
.Add(fPackageActionView, 1.0f)
.SetInsets(
B_USE_DEFAULT_SPACING, 0.0f,
B_USE_DEFAULT_SPACING, 0.0f)
.End()
.Add(fPagesView);
Clear();
}
PackageInfoView::~PackageInfoView()
{
}
void
PackageInfoView::AttachedToWindow()
{
}
void
PackageInfoView::SetPackage(const PackageInfoRef& packageRef)
{
if (!packageRef.IsSet()) {
Clear();
return;
}
bool switchToDefaultTab = true;
if (fPackage == packageRef) {
switchToDefaultTab = false;
} else if (fPackage.IsSet() && packageRef.IsSet() && fPackage->Name() == packageRef->Name()) {
switchToDefaultTab = false;
}
fTitleView->SetPackage(packageRef);
fPackageActionView->SetPackage(packageRef);
fPagesView->SetPackage(packageRef, switchToDefaultTab);
_SetPackageScreenshotThumb(packageRef);
fCardLayout->SetVisibleItem(1);
fPackage = packageRef;
}
void
PackageInfoView::_HandlePackageChanged(const PackageInfoChangeEvent& event)
{
uint32 changes = event.Changes();
if (0 == changes)
return;
const PackageInfoRef package = event.Package();
if (!fPackage.IsSet() || !package.IsSet())
return;
if (fPackage->Name() == package->Name()) {
if ((changes & PKG_CHANGED_LOCALIZED_TEXT) != 0 || (changes & PKG_CHANGED_SCREENSHOTS) != 0
|| (changes & PKG_CHANGED_RATINGS) != 0 || (changes & PKG_CHANGED_LOCAL_INFO) != 0) {
fPagesView->SetPackage(package, false);
}
if ((changes & PKG_CHANGED_LOCALIZED_TEXT) != 0 || (changes & PKG_CHANGED_RATINGS) != 0)
fTitleView->SetPackage(package);
if ((changes & PKG_CHANGED_LOCAL_INFO) != 0)
fPackageActionView->SetPackage(package);
fPackage = package;
}
}
void
PackageInfoView::HandlePackagesChanged(const std::vector<PackageInfoChangeEvent>& events)
{
if (!fPackage.IsSet())
return;
std::vector<PackageInfoChangeEvent>::const_iterator it;
for (it = events.begin(); it != events.end(); it++) {
const PackageInfoChangeEvent packageInfoChangeEvent = *it;
_HandlePackageChanged(packageInfoChangeEvent);
}
}
immediately. If it is not then trigger a process to start it in
the background. A message will come through later once it is
cached and ready to load.
*/
void
PackageInfoView::_SetPackageScreenshotThumb(const PackageInfoRef& package)
{
ScreenshotCoordinate desiredCoordinate = _ScreenshotThumbCoordinate(package);
const char* packageNameCStr = package->Name().String();
bool hasCachedBitmap = false;
if (desiredCoordinate.IsValid()) {
bool present = false;
if (fModel->GetPackageScreenshotRepository()->HasCachedScreenshot(desiredCoordinate,
&present)
!= B_OK) {
HDERROR("unable to ascertain if screenshot is present for pkg [%s]", packageNameCStr);
} else if (present) {
HDDEBUG("screenshot is already cached for [%s] -- will load the cached data",
package->Name().String());
_HandleScreenshotCached(package, desiredCoordinate);
hasCachedBitmap = true;
} else if (!ServerHelper::IsNetworkAvailable()) {
HDINFO("screenshot won't be cached [%s] -- network unavailable", packageNameCStr);
} else {
HDDEBUG("screenshot is not cached [%s] -- will cache it", packageNameCStr);
ProcessCoordinator* processCoordinator
= ProcessCoordinatorFactory::CacheScreenshotCoordinator(fModel, desiredCoordinate);
fProcessCoordinatorConsumer->Consume(processCoordinator);
}
} else {
HDDEBUG("no screenshot for pkg [%s]", packageNameCStr);
}
if (!hasCachedBitmap)
fPagesView->SetScreenshotThumbnail(BitmapHolderRef());
}
const ScreenshotCoordinate
PackageInfoView::_ScreenshotThumbCoordinate(const PackageInfoRef& package)
{
if (!package.IsSet())
return ScreenshotCoordinate();
PackageScreenshotInfoRef screenshotInfo = package->ScreenshotInfo();
if (!screenshotInfo.IsSet() || screenshotInfo->Count() == 0)
return ScreenshotCoordinate();
ScreenshotInfoRef screenshot = screenshotInfo->ScreenshotAtIndex(0);
if (!screenshot.IsSet())
return ScreenshotCoordinate();
uint32 screenshotSizeScaled
= MAX(static_cast<uint32>(BControlLook::ComposeIconSize(kScreenshotSize).Width()),
MAX_IMAGE_SIZE);
return ScreenshotCoordinate(screenshot->Code(), screenshotSizeScaled + 1,
screenshotSizeScaled + 1);
}
void
PackageInfoView::HandleIconsChanged()
{
fTitleView->SetIcon(fPackage);
}
contains a new screenshot. This logic can check to see if the
screenshot arriving is the one that is being waited for and
would then load the screenshot from the cache and display it.
*/
void
PackageInfoView::HandleScreenshotCached(const ScreenshotCoordinate& coordinate)
{
HDINFO("handle screenshot cached [%s] %" B_PRIu16 " x %" B_PRIu16, coordinate.Code().String(),
coordinate.Width(), coordinate.Height());
_HandleScreenshotCached(fPackage, coordinate);
}
void
PackageInfoView::_HandleScreenshotCached(const PackageInfoRef& package,
const ScreenshotCoordinate& coordinate)
{
ScreenshotCoordinate desiredCoordinate = _ScreenshotThumbCoordinate(package);
if (desiredCoordinate.IsValid() && desiredCoordinate == coordinate) {
HDDEBUG("screenshot [%s] has been cached and matched; will load",
coordinate.Code().String());
BitmapHolderRef bitmapHolderRef;
if (fModel->GetPackageScreenshotRepository()->CacheAndLoadScreenshot(coordinate,
bitmapHolderRef)
!= B_OK) {
HDERROR("unable to load the screenshot [%s]", coordinate.Code().String());
} else {
fPagesView->SetScreenshotThumbnail(bitmapHolderRef);
}
}
}
void
PackageInfoView::Clear()
{
fTitleView->Clear();
fPackageActionView->Clear();
fPagesView->Clear();
fCardLayout->SetVisibleItem((int32)0);
fPackage.Unset();
}