1 /* 2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2016-2022, 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 webAppRepositoryCode; 551 BString webAppRepositorySourceCode; 552 553 if (depot != NULL) { 554 webAppRepositoryCode = depot->WebAppRepositoryCode(); 555 webAppRepositorySourceCode = depot->WebAppRepositorySourceCode(); 556 } 557 558 if (webAppRepositoryCode.IsEmpty() 559 || webAppRepositorySourceCode.IsEmpty()) { 560 HDERROR("unable to obtain the repository code or repository source " 561 "code for depot; %s", package->DepotName().String()); 562 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 563 } else { 564 status_t status = interface 565 .RetreiveUserRatingForPackageAndVersionByUser(package->Name(), 566 package->Version(), package->Architecture(), 567 webAppRepositoryCode, webAppRepositorySourceCode, 568 nickname, info); 569 570 if (status == B_OK) { 571 // could be an error or could be a valid response envelope 572 // containing data. 573 switch (interface.ErrorCodeFromResponse(info)) { 574 case ERROR_CODE_NONE: 575 { 576 //info.PrintToStream(); 577 BMessage result; 578 if (info.FindMessage("result", &result) == B_OK) { 579 _RelayServerDataToUI(result); 580 } else { 581 HDERROR("bad response envelope missing 'result' entry"); 582 ServerHelper::NotifyTransportError(B_BAD_VALUE); 583 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 584 } 585 break; 586 } 587 case ERROR_CODE_OBJECTNOTFOUND: 588 // an expected response 589 HDINFO("there was no previous rating for this" 590 " user on this version of this package so a new rating" 591 " will be added."); 592 break; 593 default: 594 ServerHelper::NotifyServerJsonRpcError(info); 595 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 596 break; 597 } 598 } else { 599 HDERROR("an error has arisen communicating with the" 600 " server to obtain data for an existing rating [%s]", 601 strerror(status)); 602 ServerHelper::NotifyTransportError(status); 603 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 604 } 605 } 606 607 _SetWorkerThread(-1); 608 } 609 610 611 int32 612 RatePackageWindow::_SendRatingThreadEntry(void* data) 613 { 614 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 615 window->_SendRatingThread(); 616 return 0; 617 } 618 619 620 void 621 RatePackageWindow::_SendRatingThread() 622 { 623 if (!Lock()) { 624 HDERROR("upload rating: Failed to lock window"); 625 return; 626 } 627 628 BMessenger messenger = BMessenger(this); 629 BString package = fPackage->Name(); 630 BString architecture = fPackage->Architecture(); 631 BString webAppRepositoryCode; 632 BString webAppRepositorySourceCode; 633 int rating = (int)fRating; 634 BString stability = fStabilityCode; 635 BString comment = fRatingText->Text(); 636 BString languageCode = fCommentLanguageCode; 637 BString ratingID = fRatingID; 638 bool active = fRatingActive; 639 640 if (!fRatingDeterminate) 641 rating = RATING_NONE; 642 643 const DepotInfo* depot = fModel.DepotForName(fPackage->DepotName()); 644 645 if (depot != NULL) { 646 webAppRepositoryCode = depot->WebAppRepositoryCode(); 647 webAppRepositorySourceCode = depot->WebAppRepositorySourceCode(); 648 } 649 650 WebAppInterface interface = fModel.GetWebAppInterface(); 651 652 Unlock(); 653 654 if (webAppRepositoryCode.IsEmpty()) { 655 HDERROR("unable to find the web app repository code for the " 656 "local depot %s", 657 fPackage->DepotName().String()); 658 return; 659 } 660 661 if (webAppRepositorySourceCode.IsEmpty()) { 662 HDERROR("unable to find the web app repository source code for the " 663 "local depot %s", 664 fPackage->DepotName().String()); 665 return; 666 } 667 668 if (stability == "unspecified") 669 stability = ""; 670 671 status_t status; 672 BMessage info; 673 if (ratingID.Length() > 0) { 674 HDINFO("will update the existing user rating [%s]", ratingID.String()); 675 status = interface.UpdateUserRating(ratingID, 676 languageCode, comment, stability, rating, active, info); 677 } else { 678 HDINFO("will create a new user rating for pkg [%s]", package.String()); 679 status = interface.CreateUserRating(package, fPackage->Version(), 680 architecture, webAppRepositoryCode, webAppRepositorySourceCode, 681 languageCode, comment, stability, rating, info); 682 } 683 684 if (status == B_OK) { 685 // could be an error or could be a valid response envelope 686 // containing data. 687 switch (interface.ErrorCodeFromResponse(info)) { 688 case ERROR_CODE_NONE: 689 { 690 if (ratingID.Length() > 0) 691 messenger.SendMessage(MSG_DID_UPDATE_USER_RATING); 692 else 693 messenger.SendMessage(MSG_DID_ADD_USER_RATING); 694 break; 695 } 696 default: 697 ServerHelper::NotifyServerJsonRpcError(info); 698 break; 699 } 700 } else { 701 HDERROR("an error has arisen communicating with the" 702 " server to obtain data for an existing rating [%s]", 703 strerror(status)); 704 ServerHelper::NotifyTransportError(status); 705 } 706 707 messenger.SendMessage(B_QUIT_REQUESTED); 708 _SetWorkerThread(-1); 709 } 710