* Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2016-2025, Andrew Lindesay <apl@lindesay.co.nz>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#include "RatePackageWindow.h"
#include <algorithm>
#include <stdio.h>
#include <Alert.h>
#include <AutoLocker.h>
#include <Autolock.h>
#include <Button.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <LayoutBuilder.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <ScrollView.h>
#include <StringView.h>
#include "AppUtils.h"
#include "HaikuDepotConstants.h"
#include "LanguageMenuUtils.h"
#include "Logger.h"
#include "MarkupParser.h"
#include "PackageUtils.h"
#include "RatingView.h"
#include "ServerHelper.h"
#include "SharedIcons.h"
#include "TextDocumentView.h"
#include "WebAppInterface.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "RatePackageWindow"
enum {
MSG_SEND = 'send',
MSG_PACKAGE_RATED = 'rpkg',
MSG_STABILITY_SELECTED = 'stbl',
MSG_RATING_ACTIVE_CHANGED = 'rtac',
MSG_RATING_DETERMINATE_CHANGED = 'rdch'
};
class ScrollView : public BScrollView {
public:
ScrollView(const char* name, BView* target)
:
BScrollView(name, target, 0, false, true, B_FANCY_BORDER)
{
}
virtual void DoLayout()
{
BRect innerFrame = Bounds();
innerFrame.InsetBy(2, 2);
BScrollBar* vScrollBar = ScrollBar(B_VERTICAL);
BScrollBar* hScrollBar = ScrollBar(B_HORIZONTAL);
if (vScrollBar != NULL)
innerFrame.right -= vScrollBar->Bounds().Width() - 1;
if (hScrollBar != NULL)
innerFrame.bottom -= hScrollBar->Bounds().Height() - 1;
BView* target = Target();
if (target != NULL) {
Target()->MoveTo(innerFrame.left, innerFrame.top);
Target()->ResizeTo(innerFrame.Width(), innerFrame.Height());
}
if (vScrollBar != NULL) {
BRect rect = innerFrame;
rect.left = rect.right + 1;
rect.right = rect.left + vScrollBar->Bounds().Width();
rect.top -= 1;
rect.bottom += 1;
vScrollBar->MoveTo(rect.left, rect.top);
vScrollBar->ResizeTo(rect.Width(), rect.Height());
}
if (hScrollBar != NULL) {
BRect rect = innerFrame;
rect.top = rect.bottom + 1;
rect.bottom = rect.top + hScrollBar->Bounds().Height();
rect.left -= 1;
rect.right += 1;
hScrollBar->MoveTo(rect.left, rect.top);
hScrollBar->ResizeTo(rect.Width(), rect.Height());
}
}
};
class SetRatingView : public RatingView {
public:
SetRatingView()
:
RatingView("rate package view"),
fPermanentRating(0.0f),
fRatingDeterminate(true)
{
SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
SetRating(fPermanentRating);
}
virtual void MouseMoved(BPoint where, uint32 transit, const BMessage* dragMessage)
{
if (dragMessage != NULL)
return;
if ((transit != B_INSIDE_VIEW && transit != B_ENTERED_VIEW) || where.x > MinSize().width) {
SetRating(fPermanentRating);
return;
}
float hoverRating = _RatingForMousePos(where);
SetRating(hoverRating);
}
virtual void MouseDown(BPoint where)
{
SetPermanentRating(_RatingForMousePos(where));
BMessage message(MSG_PACKAGE_RATED);
message.AddFloat("rating", fPermanentRating);
Window()->PostMessage(&message, Window());
}
void SetPermanentRating(float rating)
{
fPermanentRating = rating;
SetRating(rating);
}
set; ie NULL. The indeterminate rating is indicated by a pale grey
colored star.
*/
void SetRatingDeterminate(bool value)
{
fRatingDeterminate = value;
Invalidate();
}
protected:
virtual const BBitmap* StarBitmap()
{
if (fRatingDeterminate)
return SharedIcons::IconStarBlue16Scaled()->Bitmap();
return SharedIcons::IconStarGrey16Scaled()->Bitmap();
}
private:
float _RatingForMousePos(BPoint where)
{
return std::min(5.0f, ceilf(5.0f * where.x / MinSize().width));
}
float fPermanentRating;
bool fRatingDeterminate;
};
RatePackageWindow::RatePackageWindow(BWindow* parent, BRect frame, Model& model)
:
BWindow(frame, B_TRANSLATE("Rate package"), B_FLOATING_WINDOW_LOOK,
B_FLOATING_SUBSET_WINDOW_FEEL,
B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS | B_CLOSE_ON_ESCAPE),
fModel(model),
fRatingText(),
fTextEditor(new TextEditor(), true),
fRating(RATING_NONE),
fRatingDeterminate(false),
fCommentLanguageId(LANGUAGE_DEFAULT_ID),
fWorkerThread(-1)
{
AddToSubset(parent);
BStringView* ratingLabel = new BStringView("rating label", B_TRANSLATE("Your rating:"));
fSetRatingView = new SetRatingView();
fSetRatingView->SetRatingDeterminate(false);
fRatingDeterminateCheckBox
= new BCheckBox("has rating", NULL, new BMessage(MSG_RATING_DETERMINATE_CHANGED));
fRatingDeterminateCheckBox->SetValue(B_CONTROL_OFF);
fTextView = new TextDocumentView();
ScrollView* textScrollView = new ScrollView("rating scroll view", fTextView);
MarkupParser parser;
fRatingText = parser.CreateDocumentFromMarkup("");
fTextView->SetInsets(10.0f);
fTextView->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR);
fTextView->SetTextDocument(fRatingText);
fTextView->SetTextEditor(fTextEditor);
BPopUpMenu* stabilityMenu = new BPopUpMenu(B_TRANSLATE("Stability"));
fStabilityField = new BMenuField("stability", B_TRANSLATE("Stability:"), stabilityMenu);
_InitStabilitiesMenu(stabilityMenu);
BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language"));
fCommentLanguageField
= new BMenuField("language", B_TRANSLATE("Comment language:"), languagesMenu);
_InitLanguagesMenu(languagesMenu);
fRatingActiveCheckBox
= new BCheckBox("rating active", B_TRANSLATE("This rating is visible to other users"),
new BMessage(MSG_RATING_ACTIVE_CHANGED));
fRatingActiveCheckBox->Hide();
fCancelButton = new BButton("cancel", B_TRANSLATE("Cancel"), new BMessage(B_QUIT_REQUESTED));
fSendButton = new BButton("send", B_TRANSLATE("Send"), new BMessage(MSG_SEND));
BLayoutBuilder::Group<>(this, B_VERTICAL)
.AddGrid()
.Add(ratingLabel, 0, 0)
.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 0)
.Add(fRatingDeterminateCheckBox)
.Add(fSetRatingView)
.End()
.AddMenuField(fStabilityField, 0, 1)
.AddMenuField(fCommentLanguageField, 0, 2)
.End()
.Add(textScrollView)
.AddGroup(B_HORIZONTAL)
.Add(fRatingActiveCheckBox)
.AddGlue()
.Add(fCancelButton)
.Add(fSendButton)
.End()
.SetInsets(B_USE_WINDOW_INSETS);
CenterIn(parent->Frame());
}
RatePackageWindow::~RatePackageWindow()
{
}
void
RatePackageWindow::_InitLanguagesMenu(BPopUpMenu* menu)
{
fCommentLanguageId = fModel.PreferredLanguage()->ID();
LanguageMenuUtils::AddLanguagesToMenu(fModel.Languages(), menu);
menu->SetTargetForItems(this);
LanguageMenuUtils::MarkLanguageInMenu(fCommentLanguageId, menu);
}
void
RatePackageWindow::_InitStabilitiesMenu(BPopUpMenu* menu)
{
std::vector<RatingStabilityRef> ratingStabilities = fModel.RatingStabilities();
menu->SetTargetForItems(this);
if (ratingStabilities.empty()) {
menu->SetEnabled(false);
return;
}
std::vector<RatingStabilityRef>::const_iterator it;
for (it = ratingStabilities.begin(); it != ratingStabilities.end(); it++) {
const RatingStabilityRef ratingStability = *it;
BMessage* message = new BMessage(MSG_STABILITY_SELECTED);
message->AddString("code", ratingStability->Code());
BMenuItem* item = new BMenuItem(ratingStability->Name(), message);
menu->AddItem(item);
}
fStabilityCode = (*ratingStabilities.begin())->Code();
menu->ItemAt(0)->SetMarked(true);
}
void
RatePackageWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_PACKAGE_RATED:
message->FindFloat("rating", &fRating);
fRatingDeterminate = true;
fSetRatingView->SetRatingDeterminate(true);
fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON);
break;
case MSG_STABILITY_SELECTED:
message->FindString("code", &fStabilityCode);
break;
case MSG_LANGUAGE_SELECTED:
message->FindString("id", &fCommentLanguageId);
break;
case MSG_RATING_DETERMINATE_CHANGED:
fRatingDeterminate = fRatingDeterminateCheckBox->Value() == B_CONTROL_ON;
fSetRatingView->SetRatingDeterminate(fRatingDeterminate);
break;
case MSG_RATING_ACTIVE_CHANGED:
{
int32 value;
if (message->FindInt32("be:value", &value) == B_OK)
fRatingActive = value == B_CONTROL_ON;
break;
}
case MSG_DID_ADD_USER_RATING:
{
BAlert* alert = new(std::nothrow) BAlert(B_TRANSLATE("User rating"),
B_TRANSLATE("Your rating was uploaded successfully. You can update or remove it at "
"the HaikuDepot Server website."),
B_TRANSLATE("Close"), NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
alert->Go();
_RefreshPackageData();
break;
}
case MSG_DID_UPDATE_USER_RATING:
{
BAlert* alert = new(std::nothrow)
BAlert(B_TRANSLATE("User rating"), B_TRANSLATE("Your rating was updated."),
B_TRANSLATE("Close"), NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
alert->Go();
_RefreshPackageData();
break;
}
case MSG_SEND:
_SendRating();
break;
default:
BWindow::MessageReceived(message);
break;
}
}
example when somebody adds a rating and that changes the rating of the
package or they add a rating and want to see that immediately. The logic
should round-trip to the server so that actual data is shown.
*/
void
RatePackageWindow::_RefreshPackageData()
{
BMessage message(MSG_SERVER_DATA_CHANGED);
message.AddString("name", fPackage->Name());
be_app->PostMessage(&message);
}
void
RatePackageWindow::SetPackage(const PackageInfoRef& package)
{
if (!package.IsSet())
HDFATAL("attempt to provide an unset package");
BAutolock locker(this);
if (!locker.IsLocked() || fWorkerThread >= 0)
return;
fPackage = package;
BString packageTitle;
PackageUtils::TitleOrName(fPackage, packageTitle);
BString windowTitle(B_TRANSLATE("Rate %Package%"));
windowTitle.ReplaceAll("%Package%", packageTitle);
SetTitle(windowTitle);
thread_id thread
= spawn_thread(&_QueryRatingThreadEntry, "Query rating", B_NORMAL_PRIORITY, this);
if (thread >= 0)
_SetWorkerThread(thread);
}
void
RatePackageWindow::_SendRating()
{
thread_id thread
= spawn_thread(&_SendRatingThreadEntry, "Send rating", B_NORMAL_PRIORITY, this);
if (thread >= 0)
_SetWorkerThread(thread);
}
void
RatePackageWindow::_SetWorkerThread(thread_id thread)
{
if (!Lock())
return;
bool enabled = thread < 0;
fStabilityField->SetEnabled(enabled);
fCommentLanguageField->SetEnabled(enabled);
fSendButton->SetEnabled(enabled);
if (thread >= 0) {
fWorkerThread = thread;
resume_thread(fWorkerThread);
} else {
fWorkerThread = -1;
}
Unlock();
}
int32
RatePackageWindow::_QueryRatingThreadEntry(void* data)
{
RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data);
window->_QueryRatingThread();
return 0;
}
with some data. The data is known not to be an error and now the data can
be extracted into the user interface elements.
*/
void
RatePackageWindow::_RelayServerDataToUI(BMessage& response)
{
if (Lock()) {
response.FindString("code", &fRatingID);
response.FindBool("active", &fRatingActive);
BString comment;
if (response.FindString("comment", &comment) == B_OK) {
MarkupParser parser;
fRatingText = parser.CreateDocumentFromMarkup(comment);
fTextView->SetTextDocument(fRatingText);
}
if (response.FindString("userRatingStabilityCode", &fStabilityCode) == B_OK) {
BMenu* menu = fStabilityField->Menu();
AppUtils::MarkItemWithKeyValueInMenu(menu, "code", fStabilityCode);
}
if (response.FindString("naturalLanguageCode", &fCommentLanguageId) == B_OK
&& !comment.IsEmpty()) {
LanguageMenuUtils::MarkLanguageInMenu(fCommentLanguageId,
fCommentLanguageField->Menu());
}
double rating;
if (response.FindDouble("rating", &rating) == B_OK) {
fRating = (float)rating;
fRatingDeterminate = fRating >= 0.0f;
fSetRatingView->SetPermanentRating(fRating);
} else {
fRatingDeterminate = false;
}
fSetRatingView->SetRatingDeterminate(fRatingDeterminate);
fRatingDeterminateCheckBox->SetValue(fRatingDeterminate ? B_CONTROL_ON : B_CONTROL_OFF);
fRatingActiveCheckBox->SetValue(fRatingActive);
fRatingActiveCheckBox->Show();
fSendButton->SetLabel(B_TRANSLATE("Update"));
Unlock();
} else {
HDERROR("unable to acquire lock to update the ui");
}
}
void
RatePackageWindow::_QueryRatingThread()
{
if (!Lock()) {
HDERROR("rating query: Failed to lock window");
return;
}
PackageInfoRef package(fPackage);
Unlock();
BString nickname = fModel.Nickname();
if (!package.IsSet()) {
HDERROR("rating query: No package");
_SetWorkerThread(-1);
return;
}
PackageCoreInfoRef coreInfo = package->CoreInfo();
if (!coreInfo.IsSet()) {
HDERROR("rating query: No package core info");
_SetWorkerThread(-1);
return;
}
PackageVersionRef version = coreInfo->Version();
if (!version.IsSet()) {
HDERROR("rating query: No package version");
_SetWorkerThread(-1);
return;
}
WebAppInterfaceRef interface = fModel.WebApp();
BMessage info;
BString webAppRepositoryCode;
BString webAppRepositorySourceCode;
BString depotName = PackageUtils::DepotName(package);
DepotInfoRef depot = fModel.DepotForName(depotName);
if (depot.IsSet()) {
webAppRepositoryCode = depot->WebAppRepositoryCode();
webAppRepositorySourceCode = depot->WebAppRepositorySourceCode();
}
if (webAppRepositoryCode.IsEmpty() || webAppRepositorySourceCode.IsEmpty()) {
HDERROR("unable to obtain the repository code or repository source code for depot [%s]",
depotName.String());
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
} else {
status_t status = interface->RetrieveUserRatingForPackageAndVersionByUser(package->Name(),
*(version.Get()), coreInfo->Architecture(), webAppRepositoryCode,
webAppRepositorySourceCode, nickname, info);
if (status == B_OK) {
switch (WebAppInterface::ErrorCodeFromResponse(info)) {
case ERROR_CODE_NONE:
{
BMessage result;
if (info.FindMessage("result", &result) == B_OK) {
_RelayServerDataToUI(result);
} else {
HDERROR("bad response envelope missing 'result' entry");
ServerHelper::NotifyTransportError(B_BAD_VALUE);
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
}
break;
}
case ERROR_CODE_OBJECTNOTFOUND:
HDINFO("there was no previous rating for this"
" user on this version of this package so a new rating"
" will be added.");
break;
default:
ServerHelper::NotifyServerJsonRpcError(info);
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
break;
}
} else {
HDERROR("an error has arisen communicating with the server to obtain data for an "
"existing rating [%s]",
strerror(status));
ServerHelper::NotifyTransportError(status);
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
}
}
_SetWorkerThread(-1);
}
int32
RatePackageWindow::_SendRatingThreadEntry(void* data)
{
RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data);
window->_SendRatingThread();
return 0;
}
void
RatePackageWindow::_SendRatingThread()
{
PackageCoreInfoRef coreInfo = fPackage->CoreInfo();
if (!coreInfo.IsSet()) {
HDERROR("upload rating: package core info not set");
return;
}
PackageVersionRef version = coreInfo->Version();
if (!version.IsSet()) {
HDERROR("upload rating: package version not set");
return;
}
BString depotName = coreInfo->DepotName();
if (depotName.IsEmpty()) {
HDERROR("upload rating: depot name not set");
return;
}
BString architecture = coreInfo->Architecture();
if (architecture.IsEmpty()) {
HDERROR("upload rating: architecture not set");
return;
}
if (!Lock()) {
HDERROR("upload rating: Failed to lock window");
return;
}
BMessenger messenger = BMessenger(this);
BString package = fPackage->Name();
BString webAppRepositoryCode;
BString webAppRepositorySourceCode;
int rating = (int)fRating;
BString stability = fStabilityCode;
BString comment = fRatingText->Text();
BString languageId = fCommentLanguageId;
BString ratingID = fRatingID;
bool active = fRatingActive;
if (!fRatingDeterminate)
rating = RATING_NONE;
const DepotInfoRef depot = fModel.DepotForName(depotName);
if (depot.IsSet()) {
webAppRepositoryCode = depot->WebAppRepositoryCode();
webAppRepositorySourceCode = depot->WebAppRepositorySourceCode();
}
WebAppInterfaceRef interface = fModel.WebApp();
Unlock();
if (webAppRepositoryCode.IsEmpty()) {
HDERROR("unable to find the web app repository code for the local depot [%s]",
depotName.String());
return;
}
if (webAppRepositorySourceCode.IsEmpty()) {
HDERROR("unable to find the web app repository source code for the local depot [%s]",
depotName.String());
return;
}
if (stability == "unspecified")
stability = "";
status_t status;
BMessage info;
if (ratingID.Length() > 0) {
HDINFO("will update the existing user rating [%s]", ratingID.String());
status = interface->UpdateUserRating(ratingID, languageId, comment, stability, rating,
active, info);
} else {
HDINFO("will create a new user rating for pkg [%s]", package.String());
status = interface->CreateUserRating(package, *(version.Get()), architecture,
webAppRepositoryCode, webAppRepositorySourceCode, languageId, comment, stability,
rating, info);
}
if (status == B_OK) {
switch (WebAppInterface::ErrorCodeFromResponse(info)) {
case ERROR_CODE_NONE:
{
if (ratingID.Length() > 0)
messenger.SendMessage(MSG_DID_UPDATE_USER_RATING);
else
messenger.SendMessage(MSG_DID_ADD_USER_RATING);
break;
}
default:
ServerHelper::NotifyServerJsonRpcError(info);
break;
}
} else {
HDERROR("an error has arisen communicating with the server to obtain data for an existing "
"rating [%s]",
strerror(status));
ServerHelper::NotifyTransportError(status);
}
messenger.SendMessage(B_QUIT_REQUESTED);
_SetWorkerThread(-1);
}