* Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2016-2018, 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 <Autolock.h>
#include <Catalog.h>
#include <Button.h>
#include <CheckBox.h>
#include <LayoutBuilder.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <PopUpMenu.h>
#include <ScrollView.h>
#include <StringView.h>
#include "HaikuDepotConstants.h"
#include "MarkupParser.h"
#include "RatingView.h"
#include "ServerHelper.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_LANGUAGE_SELECTED = 'lngs',
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 fStarBlueBitmap.Bitmap(SharedBitmap::SIZE_16);
return fStarGrayBitmap.Bitmap(SharedBitmap::SIZE_16);
}
private:
float _RatingForMousePos(BPoint where)
{
return std::min(5.0f, ceilf(5.0f * where.x / MinSize().width));
}
float fPermanentRating;
bool fRatingDeterminate;
};
static void
add_stabilities_to_menu(const StabilityRatingList& stabilities, BMenu* menu)
{
for (int i = 0; i < stabilities.CountItems(); i++) {
const StabilityRating& stability = stabilities.ItemAtFast(i);
BMessage* message = new BMessage(MSG_STABILITY_SELECTED);
message->AddString("name", stability.Name());
BMenuItem* item = new BMenuItem(stability.Label(), message);
menu->AddItem(item);
}
}
static void
add_languages_to_menu(const StringList& languages, BMenu* menu)
{
for (int i = 0; i < languages.CountItems(); i++) {
const BString& language = languages.ItemAtFast(i);
BMessage* message = new BMessage(MSG_LANGUAGE_SELECTED);
message->AddString("code", language);
BMenuItem* item = new BMenuItem(language, message);
menu->AddItem(item);
}
}
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),
fModel(model),
fRatingText(),
fTextEditor(new TextEditor(), true),
fRating(-1.0f),
fCommentLanguage(fModel.PreferredLanguage()),
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);
fStabilityCodes.Add(StabilityRating(
B_TRANSLATE("Not specified"), "unspecified"));
fStabilityCodes.Add(StabilityRating(
B_TRANSLATE("Stable"), "stable"));
fStabilityCodes.Add(StabilityRating(
B_TRANSLATE("Mostly stable"), "mostlystable"));
fStabilityCodes.Add(StabilityRating(
B_TRANSLATE("Unstable but usable"), "unstablebutusable"));
fStabilityCodes.Add(StabilityRating(
B_TRANSLATE("Very unstable"), "veryunstable"));
fStabilityCodes.Add(StabilityRating(
B_TRANSLATE("Does not start"), "nostart"));
add_stabilities_to_menu(fStabilityCodes, stabilityMenu);
stabilityMenu->SetTargetForItems(this);
fStability = fStabilityCodes.ItemAt(0).Name();
stabilityMenu->ItemAt(0)->SetMarked(true);
BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language"));
fCommentLanguageField = new BMenuField("language",
B_TRANSLATE("Comment language:"), languagesMenu);
add_languages_to_menu(fModel.SupportedLanguages(), languagesMenu);
languagesMenu->SetTargetForItems(this);
BMenuItem* defaultItem = languagesMenu->ItemAt(
fModel.SupportedLanguages().IndexOf(fCommentLanguage));
if (defaultItem != NULL)
defaultItem->SetMarked(true);
fRatingActiveCheckBox = new BCheckBox("rating active",
B_TRANSLATE("Other users can see this rating"),
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::DispatchMessage(BMessage* message, BHandler *handler)
{
if (message->what == B_KEY_DOWN) {
int8 key;
if ((message->FindInt8("byte", &key) == B_OK)
&& key == B_ESCAPE) {
Quit();
return;
}
}
BWindow::DispatchMessage(message, handler);
}
void
RatePackageWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_PACKAGE_RATED:
message->FindFloat("rating", &fRating);
fSetRatingView->SetRatingDeterminate(true);
fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON);
break;
case MSG_STABILITY_SELECTED:
message->FindString("name", &fStability);
break;
case MSG_LANGUAGE_SELECTED:
message->FindString("code", &fCommentLanguage);
break;
case MSG_RATING_DETERMINATE_CHANGED:
fSetRatingView->SetRatingDeterminate(
fRatingDeterminateCheckBox->Value() == B_CONTROL_ON);
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)
{
BAutolock locker(this);
if (!locker.IsLocked() || fWorkerThread >= 0)
return;
fPackage = package;
BString windowTitle(B_TRANSLATE("Rate %Package%"));
windowTitle.ReplaceAll("%Package%", package->Title());
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",
&fStability) == B_OK) {
int32 index = 0;
for (int32 i = fStabilityCodes.CountItems() - 1; i >= 0; i--) {
const StabilityRating& stability
= fStabilityCodes.ItemAtFast(i);
if (stability.Name() == fStability) {
index = i;
break;
}
}
BMenuItem* item = fStabilityField->Menu()->ItemAt(index);
if (item != NULL)
item->SetMarked(true);
}
if (response.FindString("naturalLanguageCode",
&fCommentLanguage) == B_OK) {
BMenuItem* item = fCommentLanguageField->Menu()->ItemAt(
fModel.SupportedLanguages().IndexOf(fCommentLanguage));
if (item != NULL)
item->SetMarked(true);
}
double rating;
if (response.FindDouble("rating", &rating) == B_OK) {
fRating = (float)rating;
fSetRatingView->SetPermanentRating(fRating);
fSetRatingView->SetRatingDeterminate(true);
fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON);
} else {
fSetRatingView->SetRatingDeterminate(false);
fRatingDeterminateCheckBox->SetValue(B_CONTROL_OFF);
}
fRatingActiveCheckBox->SetValue(fRatingActive);
fRatingActiveCheckBox->Show();
fSendButton->SetLabel(B_TRANSLATE("Update"));
Unlock();
} else {
fprintf(stderr, "unable to acquire lock to update the ui\n");
}
}
void
RatePackageWindow::_QueryRatingThread()
{
if (!Lock()) {
fprintf(stderr, "rating query: Failed to lock window\n");
return;
}
PackageInfoRef package(fPackage);
Unlock();
BAutolock locker(fModel.Lock());
BString username = fModel.Username();
locker.Unlock();
if (package.Get() == NULL) {
fprintf(stderr, "rating query: No package\n");
_SetWorkerThread(-1);
return;
}
WebAppInterface interface;
BMessage info;
const DepotInfo* depot = fModel.DepotForName(package->DepotName());
BString repositoryCode;
if (depot != NULL)
repositoryCode = depot->WebAppRepositoryCode();
if (repositoryCode.IsEmpty()) {
printf("unable to obtain the repository code for depot; %s\n",
package->DepotName().String());
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
} else {
status_t status = interface.RetrieveUserRating(
package->Name(), package->Version(), package->Architecture(),
repositoryCode, username, info);
if (status == B_OK) {
switch (interface.ErrorCodeFromResponse(info)) {
case ERROR_CODE_NONE:
{
BMessage result;
if (info.FindMessage("result", &result) == B_OK) {
_RelayServerDataToUI(result);
} else {
fprintf(stderr, "bad response envelope missing 'result'"
"entry\n");
ServerHelper::NotifyTransportError(B_BAD_VALUE);
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
}
break;
}
case ERROR_CODE_OBJECTNOTFOUND:
fprintf(stderr, "there was no previous rating for this"
" user on this version of this package so a new rating"
" will be added.\n");
break;
default:
ServerHelper::NotifyServerJsonRpcError(info);
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
break;
}
} else {
fprintf(stderr, "an error has arisen communicating with the"
" server to obtain data for an existing rating [%s]\n",
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()
{
if (!Lock()) {
fprintf(stderr, "upload rating: Failed to lock window\n");
return;
}
BMessenger messenger = BMessenger(this);
BString package = fPackage->Name();
BString architecture = fPackage->Architecture();
BString repositoryCode;
int rating = (int)fRating;
BString stability = fStability;
BString comment = fRatingText->Text();
BString languageCode = fCommentLanguage;
BString ratingID = fRatingID;
bool active = fRatingActive;
const DepotInfo* depot = fModel.DepotForName(fPackage->DepotName());
if (depot != NULL)
repositoryCode = depot->WebAppRepositoryCode();
WebAppInterface interface = fModel.GetWebAppInterface();
Unlock();
if (repositoryCode.Length() == 0) {
printf("unable to find the web app repository code for the local "
"depot %s\n",
fPackage->DepotName().String());
return;
}
if (stability == "unspecified")
stability = "";
status_t status;
BMessage info;
if (ratingID.Length() > 0) {
printf("will update the existing user rating [%s]\n",
ratingID.String());
status = interface.UpdateUserRating(ratingID,
languageCode, comment, stability, rating, active, info);
} else {
printf("will create a new user rating for pkg [%s]\n",
package.String());
status = interface.CreateUserRating(package, fPackage->Version(),
architecture, repositoryCode, languageCode, comment, stability,
rating, info);
}
if (status == B_OK) {
switch (interface.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 {
fprintf(stderr, "an error has arisen communicating with the"
" server to obtain data for an existing rating [%s]\n",
strerror(status));
ServerHelper::NotifyTransportError(status);
}
messenger.SendMessage(B_QUIT_REQUESTED);
_SetWorkerThread(-1);
}