1 /* 2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2016-2020, Andrew Lindesay <apl@lindesay.co.nz>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 #include "RatePackageWindow.h" 8 9 #include <algorithm> 10 #include <stdio.h> 11 12 #include <Alert.h> 13 #include <Autolock.h> 14 #include <AutoLocker.h> 15 #include <Catalog.h> 16 #include <Button.h> 17 #include <CheckBox.h> 18 #include <LayoutBuilder.h> 19 #include <MenuField.h> 20 #include <MenuItem.h> 21 #include <ScrollView.h> 22 #include <StringView.h> 23 24 #include "AppUtils.h" 25 #include "HaikuDepotConstants.h" 26 #include "LanguageMenuUtils.h" 27 #include "Logger.h" 28 #include "MarkupParser.h" 29 #include "RatingView.h" 30 #include "ServerHelper.h" 31 #include "TextDocumentView.h" 32 #include "WebAppInterface.h" 33 34 35 #undef B_TRANSLATION_CONTEXT 36 #define B_TRANSLATION_CONTEXT "RatePackageWindow" 37 38 39 enum { 40 MSG_SEND = 'send', 41 MSG_PACKAGE_RATED = 'rpkg', 42 MSG_STABILITY_SELECTED = 'stbl', 43 MSG_RATING_ACTIVE_CHANGED = 'rtac', 44 MSG_RATING_DETERMINATE_CHANGED = 'rdch' 45 }; 46 47 //! Layouts the scrollbar so it looks nice with no border and the document 48 // window look. 49 class ScrollView : public BScrollView { 50 public: 51 ScrollView(const char* name, BView* target) 52 : 53 BScrollView(name, target, 0, false, true, B_FANCY_BORDER) 54 { 55 } 56 57 virtual void DoLayout() 58 { 59 BRect innerFrame = Bounds(); 60 innerFrame.InsetBy(2, 2); 61 62 BScrollBar* vScrollBar = ScrollBar(B_VERTICAL); 63 BScrollBar* hScrollBar = ScrollBar(B_HORIZONTAL); 64 65 if (vScrollBar != NULL) 66 innerFrame.right -= vScrollBar->Bounds().Width() - 1; 67 if (hScrollBar != NULL) 68 innerFrame.bottom -= hScrollBar->Bounds().Height() - 1; 69 70 BView* target = Target(); 71 if (target != NULL) { 72 Target()->MoveTo(innerFrame.left, innerFrame.top); 73 Target()->ResizeTo(innerFrame.Width(), innerFrame.Height()); 74 } 75 76 if (vScrollBar != NULL) { 77 BRect rect = innerFrame; 78 rect.left = rect.right + 1; 79 rect.right = rect.left + vScrollBar->Bounds().Width(); 80 rect.top -= 1; 81 rect.bottom += 1; 82 83 vScrollBar->MoveTo(rect.left, rect.top); 84 vScrollBar->ResizeTo(rect.Width(), rect.Height()); 85 } 86 87 if (hScrollBar != NULL) { 88 BRect rect = innerFrame; 89 rect.top = rect.bottom + 1; 90 rect.bottom = rect.top + hScrollBar->Bounds().Height(); 91 rect.left -= 1; 92 rect.right += 1; 93 94 hScrollBar->MoveTo(rect.left, rect.top); 95 hScrollBar->ResizeTo(rect.Width(), rect.Height()); 96 } 97 } 98 }; 99 100 101 class SetRatingView : public RatingView { 102 public: 103 SetRatingView() 104 : 105 RatingView("rate package view"), 106 fPermanentRating(0.0f), 107 fRatingDeterminate(true) 108 { 109 SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 110 SetRating(fPermanentRating); 111 } 112 113 virtual void MouseMoved(BPoint where, uint32 transit, 114 const BMessage* dragMessage) 115 { 116 if (dragMessage != NULL) 117 return; 118 119 if ((transit != B_INSIDE_VIEW && transit != B_ENTERED_VIEW) 120 || where.x > MinSize().width) { 121 SetRating(fPermanentRating); 122 return; 123 } 124 125 float hoverRating = _RatingForMousePos(where); 126 SetRating(hoverRating); 127 } 128 129 virtual void MouseDown(BPoint where) 130 { 131 SetPermanentRating(_RatingForMousePos(where)); 132 BMessage message(MSG_PACKAGE_RATED); 133 message.AddFloat("rating", fPermanentRating); 134 Window()->PostMessage(&message, Window()); 135 } 136 137 void SetPermanentRating(float rating) 138 { 139 fPermanentRating = rating; 140 SetRating(rating); 141 } 142 143 /*! By setting this to false, this indicates that there is no rating for the 144 set; ie NULL. The indeterminate rating is indicated by a pale grey 145 colored star. 146 */ 147 148 void SetRatingDeterminate(bool value) { 149 fRatingDeterminate = value; 150 Invalidate(); 151 } 152 153 protected: 154 virtual const BBitmap* StarBitmap() 155 { 156 if (fRatingDeterminate) 157 return fStarBlueBitmap->Bitmap(BITMAP_SIZE_16); 158 return fStarGrayBitmap->Bitmap(BITMAP_SIZE_16); 159 } 160 161 private: 162 float _RatingForMousePos(BPoint where) 163 { 164 return std::min(5.0f, ceilf(5.0f * where.x / MinSize().width)); 165 } 166 167 float fPermanentRating; 168 bool fRatingDeterminate; 169 }; 170 171 172 RatePackageWindow::RatePackageWindow(BWindow* parent, BRect frame, 173 Model& model) 174 : 175 BWindow(frame, B_TRANSLATE("Rate package"), 176 B_FLOATING_WINDOW_LOOK, B_FLOATING_SUBSET_WINDOW_FEEL, 177 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS), 178 fModel(model), 179 fRatingText(), 180 fTextEditor(new TextEditor(), true), 181 fRating(RATING_NONE), 182 fRatingDeterminate(false), 183 fCommentLanguageCode(LANGUAGE_DEFAULT_CODE), 184 fWorkerThread(-1) 185 { 186 AddToSubset(parent); 187 188 BStringView* ratingLabel = new BStringView("rating label", 189 B_TRANSLATE("Your rating:")); 190 191 fSetRatingView = new SetRatingView(); 192 fSetRatingView->SetRatingDeterminate(false); 193 fRatingDeterminateCheckBox = new BCheckBox("has rating", NULL, 194 new BMessage(MSG_RATING_DETERMINATE_CHANGED)); 195 fRatingDeterminateCheckBox->SetValue(B_CONTROL_OFF); 196 197 fTextView = new TextDocumentView(); 198 ScrollView* textScrollView = new ScrollView( 199 "rating scroll view", fTextView); 200 201 // Get a TextDocument with default paragraph and character style 202 MarkupParser parser; 203 fRatingText = parser.CreateDocumentFromMarkup(""); 204 205 fTextView->SetInsets(10.0f); 206 fTextView->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR); 207 fTextView->SetTextDocument(fRatingText); 208 fTextView->SetTextEditor(fTextEditor); 209 210 // Construct stability rating popup 211 BPopUpMenu* stabilityMenu = new BPopUpMenu(B_TRANSLATE("Stability")); 212 fStabilityField = new BMenuField("stability", 213 B_TRANSLATE("Stability:"), stabilityMenu); 214 _InitStabilitiesMenu(stabilityMenu); 215 216 // Construct languages popup 217 BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language")); 218 fCommentLanguageField = new BMenuField("language", 219 B_TRANSLATE("Comment language:"), languagesMenu); 220 _InitLanguagesMenu(languagesMenu); 221 222 fRatingActiveCheckBox = new BCheckBox("rating active", 223 B_TRANSLATE("This rating is visible to other users"), 224 new BMessage(MSG_RATING_ACTIVE_CHANGED)); 225 // Hide the check mark by default, it will be made visible when 226 // the user already made a rating and it is loaded 227 fRatingActiveCheckBox->Hide(); 228 229 // Construct buttons 230 fCancelButton = new BButton("cancel", B_TRANSLATE("Cancel"), 231 new BMessage(B_QUIT_REQUESTED)); 232 233 fSendButton = new BButton("send", B_TRANSLATE("Send"), 234 new BMessage(MSG_SEND)); 235 236 // Build layout 237 BLayoutBuilder::Group<>(this, B_VERTICAL) 238 .AddGrid() 239 .Add(ratingLabel, 0, 0) 240 .AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 0) 241 .Add(fRatingDeterminateCheckBox) 242 .Add(fSetRatingView) 243 .End() 244 .AddMenuField(fStabilityField, 0, 1) 245 .AddMenuField(fCommentLanguageField, 0, 2) 246 .End() 247 .Add(textScrollView) 248 .AddGroup(B_HORIZONTAL) 249 .Add(fRatingActiveCheckBox) 250 .AddGlue() 251 .Add(fCancelButton) 252 .Add(fSendButton) 253 .End() 254 .SetInsets(B_USE_WINDOW_INSETS) 255 ; 256 257 // NOTE: Do not make Send the default button. The user might want 258 // to type line-breaks instead of sending when hitting RETURN. 259 260 CenterIn(parent->Frame()); 261 } 262 263 264 RatePackageWindow::~RatePackageWindow() 265 { 266 } 267 268 269 void 270 RatePackageWindow::_InitLanguagesMenu(BPopUpMenu* menu) 271 { 272 AutoLocker<BLocker> locker(fModel.Lock()); 273 fCommentLanguageCode = fModel.Language()->PreferredLanguage()->Code(); 274 275 LanguageMenuUtils::AddLanguagesToMenu(fModel.Language(), menu); 276 menu->SetTargetForItems(this); 277 LanguageMenuUtils::MarkLanguageInMenu(fCommentLanguageCode, menu); 278 } 279 280 281 void 282 RatePackageWindow::_InitStabilitiesMenu(BPopUpMenu* menu) 283 { 284 AutoLocker<BLocker> locker(fModel.Lock()); 285 int32 countStabilities = fModel.CountRatingStabilities(); 286 287 menu->SetTargetForItems(this); 288 289 if (0 == countStabilities) { 290 menu->SetEnabled(false); 291 return; 292 } 293 294 for (int32 i = 0; i < countStabilities; i++) { 295 const RatingStabilityRef stability = fModel.RatingStabilityAtIndex(i); 296 BMessage* message = new BMessage(MSG_STABILITY_SELECTED); 297 message->AddString("code", stability->Code()); 298 BMenuItem* item = new BMenuItem(stability->Name(), message); 299 menu->AddItem(item); 300 301 if (i == 0) { 302 fStabilityCode = stability->Code(); 303 item->SetMarked(true); 304 } 305 } 306 } 307 308 309 void 310 RatePackageWindow::DispatchMessage(BMessage* message, BHandler *handler) 311 { 312 if (message->what == B_KEY_DOWN) { 313 int8 key; 314 // if the user presses escape, close the window. 315 if ((message->FindInt8("byte", &key) == B_OK) 316 && key == B_ESCAPE) { 317 Quit(); 318 return; 319 } 320 } 321 322 BWindow::DispatchMessage(message, handler); 323 } 324 325 326 void 327 RatePackageWindow::MessageReceived(BMessage* message) 328 { 329 switch (message->what) { 330 case MSG_PACKAGE_RATED: 331 message->FindFloat("rating", &fRating); 332 fRatingDeterminate = true; 333 fSetRatingView->SetRatingDeterminate(true); 334 fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON); 335 break; 336 337 case MSG_STABILITY_SELECTED: 338 message->FindString("code", &fStabilityCode); 339 break; 340 341 case MSG_LANGUAGE_SELECTED: 342 message->FindString("code", &fCommentLanguageCode); 343 break; 344 345 case MSG_RATING_DETERMINATE_CHANGED: 346 fRatingDeterminate = fRatingDeterminateCheckBox->Value() 347 == B_CONTROL_ON; 348 fSetRatingView->SetRatingDeterminate(fRatingDeterminate); 349 break; 350 351 case MSG_RATING_ACTIVE_CHANGED: 352 { 353 int32 value; 354 if (message->FindInt32("be:value", &value) == B_OK) 355 fRatingActive = value == B_CONTROL_ON; 356 break; 357 } 358 359 case MSG_DID_ADD_USER_RATING: 360 { 361 BAlert* alert = new(std::nothrow) BAlert( 362 B_TRANSLATE("User rating"), 363 B_TRANSLATE("Your rating was uploaded successfully. " 364 "You can update or remove it at the HaikuDepot Server " 365 "website."), 366 B_TRANSLATE("Close"), NULL, NULL, 367 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 368 alert->Go(); 369 _RefreshPackageData(); 370 break; 371 } 372 373 case MSG_DID_UPDATE_USER_RATING: 374 { 375 BAlert* alert = new(std::nothrow) BAlert( 376 B_TRANSLATE("User rating"), 377 B_TRANSLATE("Your rating was updated."), 378 B_TRANSLATE("Close"), NULL, NULL, 379 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 380 alert->Go(); 381 _RefreshPackageData(); 382 break; 383 } 384 385 case MSG_SEND: 386 _SendRating(); 387 break; 388 389 default: 390 BWindow::MessageReceived(message); 391 break; 392 } 393 } 394 395 /*! Refresh the data shown about the current page. This may be useful, for 396 example when somebody adds a rating and that changes the rating of the 397 package or they add a rating and want to see that immediately. The logic 398 should round-trip to the server so that actual data is shown. 399 */ 400 401 void 402 RatePackageWindow::_RefreshPackageData() 403 { 404 BMessage message(MSG_SERVER_DATA_CHANGED); 405 message.AddString("name", fPackage->Name()); 406 be_app->PostMessage(&message); 407 } 408 409 410 void 411 RatePackageWindow::SetPackage(const PackageInfoRef& package) 412 { 413 BAutolock locker(this); 414 if (!locker.IsLocked() || fWorkerThread >= 0) 415 return; 416 417 fPackage = package; 418 419 BString windowTitle(B_TRANSLATE("Rate %Package%")); 420 windowTitle.ReplaceAll("%Package%", package->Title()); 421 SetTitle(windowTitle); 422 423 // See if the user already made a rating for this package, 424 // pre-fill the UI with that rating. (When sending the rating, the 425 // old one will be replaced.) 426 thread_id thread = spawn_thread(&_QueryRatingThreadEntry, 427 "Query rating", B_NORMAL_PRIORITY, this); 428 if (thread >= 0) 429 _SetWorkerThread(thread); 430 } 431 432 433 void 434 RatePackageWindow::_SendRating() 435 { 436 thread_id thread = spawn_thread(&_SendRatingThreadEntry, 437 "Send rating", B_NORMAL_PRIORITY, this); 438 if (thread >= 0) 439 _SetWorkerThread(thread); 440 } 441 442 443 void 444 RatePackageWindow::_SetWorkerThread(thread_id thread) 445 { 446 if (!Lock()) 447 return; 448 449 bool enabled = thread < 0; 450 451 fStabilityField->SetEnabled(enabled); 452 fCommentLanguageField->SetEnabled(enabled); 453 fSendButton->SetEnabled(enabled); 454 455 if (thread >= 0) { 456 fWorkerThread = thread; 457 resume_thread(fWorkerThread); 458 } else { 459 fWorkerThread = -1; 460 } 461 462 Unlock(); 463 } 464 465 466 /*static*/ int32 467 RatePackageWindow::_QueryRatingThreadEntry(void* data) 468 { 469 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 470 window->_QueryRatingThread(); 471 return 0; 472 } 473 474 475 /*! A server request has been made to the server and the server has responded 476 with some data. The data is known not to be an error and now the data can 477 be extracted into the user interface elements. 478 */ 479 480 void 481 RatePackageWindow::_RelayServerDataToUI(BMessage& response) 482 { 483 if (Lock()) { 484 response.FindString("code", &fRatingID); 485 response.FindBool("active", &fRatingActive); 486 BString comment; 487 if (response.FindString("comment", &comment) == B_OK) { 488 MarkupParser parser; 489 fRatingText = parser.CreateDocumentFromMarkup(comment); 490 fTextView->SetTextDocument(fRatingText); 491 } 492 if (response.FindString("userRatingStabilityCode", 493 &fStabilityCode) == B_OK) { 494 BMenu* menu = fStabilityField->Menu(); 495 AppUtils::MarkItemWithCodeInMenu(fStabilityCode, menu); 496 } 497 if (response.FindString("naturalLanguageCode", 498 &fCommentLanguageCode) == B_OK && !comment.IsEmpty()) { 499 LanguageMenuUtils::MarkLanguageInMenu( 500 fCommentLanguageCode, fCommentLanguageField->Menu()); 501 } 502 double rating; 503 if (response.FindDouble("rating", &rating) == B_OK) { 504 fRating = (float)rating; 505 fRatingDeterminate = fRating >= 0.0f; 506 fSetRatingView->SetPermanentRating(fRating); 507 } else { 508 fRatingDeterminate = false; 509 } 510 511 fSetRatingView->SetRatingDeterminate(fRatingDeterminate); 512 fRatingDeterminateCheckBox->SetValue( 513 fRatingDeterminate ? B_CONTROL_ON : B_CONTROL_OFF); 514 fRatingActiveCheckBox->SetValue(fRatingActive); 515 fRatingActiveCheckBox->Show(); 516 517 fSendButton->SetLabel(B_TRANSLATE("Update")); 518 519 Unlock(); 520 } else 521 HDERROR("unable to acquire lock to update the ui"); 522 } 523 524 525 void 526 RatePackageWindow::_QueryRatingThread() 527 { 528 if (!Lock()) { 529 HDERROR("rating query: Failed to lock window"); 530 return; 531 } 532 533 PackageInfoRef package(fPackage); 534 535 Unlock(); 536 537 BAutolock locker(fModel.Lock()); 538 BString nickname = fModel.Nickname(); 539 locker.Unlock(); 540 541 if (!package.IsSet()) { 542 HDERROR("rating query: No package"); 543 _SetWorkerThread(-1); 544 return; 545 } 546 547 WebAppInterface interface; 548 BMessage info; 549 const DepotInfo* depot = fModel.DepotForName(package->DepotName()); 550 BString repositoryCode; 551 552 if (depot != NULL) 553 repositoryCode = depot->WebAppRepositoryCode(); 554 555 if (repositoryCode.IsEmpty()) { 556 HDERROR("unable to obtain the repository code for depot; %s", 557 package->DepotName().String()); 558 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 559 } else { 560 status_t status = interface 561 .RetreiveUserRatingForPackageAndVersionByUser(package->Name(), 562 package->Version(), package->Architecture(), repositoryCode, 563 nickname, info); 564 565 if (status == B_OK) { 566 // could be an error or could be a valid response envelope 567 // containing data. 568 switch (interface.ErrorCodeFromResponse(info)) { 569 case ERROR_CODE_NONE: 570 { 571 //info.PrintToStream(); 572 BMessage result; 573 if (info.FindMessage("result", &result) == B_OK) { 574 _RelayServerDataToUI(result); 575 } else { 576 HDERROR("bad response envelope missing 'result' entry"); 577 ServerHelper::NotifyTransportError(B_BAD_VALUE); 578 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 579 } 580 break; 581 } 582 case ERROR_CODE_OBJECTNOTFOUND: 583 // an expected response 584 HDINFO("there was no previous rating for this" 585 " user on this version of this package so a new rating" 586 " will be added."); 587 break; 588 default: 589 ServerHelper::NotifyServerJsonRpcError(info); 590 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 591 break; 592 } 593 } else { 594 HDERROR("an error has arisen communicating with the" 595 " server to obtain data for an existing rating [%s]", 596 strerror(status)); 597 ServerHelper::NotifyTransportError(status); 598 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 599 } 600 } 601 602 _SetWorkerThread(-1); 603 } 604 605 606 int32 607 RatePackageWindow::_SendRatingThreadEntry(void* data) 608 { 609 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 610 window->_SendRatingThread(); 611 return 0; 612 } 613 614 615 void 616 RatePackageWindow::_SendRatingThread() 617 { 618 if (!Lock()) { 619 HDERROR("upload rating: Failed to lock window"); 620 return; 621 } 622 623 BMessenger messenger = BMessenger(this); 624 BString package = fPackage->Name(); 625 BString architecture = fPackage->Architecture(); 626 BString repositoryCode; 627 int rating = (int)fRating; 628 BString stability = fStabilityCode; 629 BString comment = fRatingText->Text(); 630 BString languageCode = fCommentLanguageCode; 631 BString ratingID = fRatingID; 632 bool active = fRatingActive; 633 634 if (!fRatingDeterminate) 635 rating = RATING_NONE; 636 637 const DepotInfo* depot = fModel.DepotForName(fPackage->DepotName()); 638 639 if (depot != NULL) 640 repositoryCode = depot->WebAppRepositoryCode(); 641 642 WebAppInterface interface = fModel.GetWebAppInterface(); 643 644 Unlock(); 645 646 if (repositoryCode.Length() == 0) { 647 HDERROR("unable to find the web app repository code for the local " 648 "depot %s", 649 fPackage->DepotName().String()); 650 return; 651 } 652 653 if (stability == "unspecified") 654 stability = ""; 655 656 status_t status; 657 BMessage info; 658 if (ratingID.Length() > 0) { 659 HDINFO("will update the existing user rating [%s]", ratingID.String()); 660 status = interface.UpdateUserRating(ratingID, 661 languageCode, comment, stability, rating, active, info); 662 } else { 663 HDINFO("will create a new user rating for pkg [%s]", package.String()); 664 status = interface.CreateUserRating(package, fPackage->Version(), 665 architecture, repositoryCode, languageCode, comment, stability, 666 rating, info); 667 } 668 669 if (status == B_OK) { 670 // could be an error or could be a valid response envelope 671 // containing data. 672 switch (interface.ErrorCodeFromResponse(info)) { 673 case ERROR_CODE_NONE: 674 { 675 if (ratingID.Length() > 0) 676 messenger.SendMessage(MSG_DID_UPDATE_USER_RATING); 677 else 678 messenger.SendMessage(MSG_DID_ADD_USER_RATING); 679 break; 680 } 681 default: 682 ServerHelper::NotifyServerJsonRpcError(info); 683 break; 684 } 685 } else { 686 HDERROR("an error has arisen communicating with the" 687 " server to obtain data for an existing rating [%s]", 688 strerror(status)); 689 ServerHelper::NotifyTransportError(status); 690 } 691 692 messenger.SendMessage(B_QUIT_REQUESTED); 693 _SetWorkerThread(-1); 694 } 695