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 <PopUpMenu.h> 22 #include <ScrollView.h> 23 #include <StringView.h> 24 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(SharedBitmap::SIZE_16); 158 return fStarGrayBitmap->Bitmap(SharedBitmap::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 static void 173 add_stabilities_to_menu(const StabilityRatingList& stabilities, BMenu* menu) 174 { 175 for (int i = 0; i < stabilities.CountItems(); i++) { 176 const StabilityRating& stability = stabilities.ItemAtFast(i); 177 BMessage* message = new BMessage(MSG_STABILITY_SELECTED); 178 message->AddString("name", stability.Name()); 179 BMenuItem* item = new BMenuItem(stability.Label(), message); 180 menu->AddItem(item); 181 } 182 } 183 184 185 RatePackageWindow::RatePackageWindow(BWindow* parent, BRect frame, 186 Model& model) 187 : 188 BWindow(frame, B_TRANSLATE("Rate package"), 189 B_FLOATING_WINDOW_LOOK, B_FLOATING_SUBSET_WINDOW_FEEL, 190 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS), 191 fModel(model), 192 fRatingText(), 193 fTextEditor(new TextEditor(), true), 194 fRating(RATING_NONE), 195 fRatingDeterminate(false), 196 fCommentLanguageCode(LANGUAGE_DEFAULT_CODE), 197 fWorkerThread(-1) 198 { 199 AddToSubset(parent); 200 201 BStringView* ratingLabel = new BStringView("rating label", 202 B_TRANSLATE("Your rating:")); 203 204 fSetRatingView = new SetRatingView(); 205 fSetRatingView->SetRatingDeterminate(false); 206 fRatingDeterminateCheckBox = new BCheckBox("has rating", NULL, 207 new BMessage(MSG_RATING_DETERMINATE_CHANGED)); 208 fRatingDeterminateCheckBox->SetValue(B_CONTROL_OFF); 209 210 fTextView = new TextDocumentView(); 211 ScrollView* textScrollView = new ScrollView( 212 "rating scroll view", fTextView); 213 214 // Get a TextDocument with default paragraph and character style 215 MarkupParser parser; 216 fRatingText = parser.CreateDocumentFromMarkup(""); 217 218 fTextView->SetInsets(10.0f); 219 fTextView->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR); 220 fTextView->SetTextDocument(fRatingText); 221 fTextView->SetTextEditor(fTextEditor); 222 223 // Construct stability rating popup 224 BPopUpMenu* stabilityMenu = new BPopUpMenu(B_TRANSLATE("Stability")); 225 fStabilityField = new BMenuField("stability", 226 B_TRANSLATE("Stability:"), stabilityMenu); 227 228 fStabilityCodes.Add(StabilityRating( 229 B_TRANSLATE("Not specified"), "unspecified")); 230 fStabilityCodes.Add(StabilityRating( 231 B_TRANSLATE("Stable"), "stable")); 232 fStabilityCodes.Add(StabilityRating( 233 B_TRANSLATE("Mostly stable"), "mostlystable")); 234 fStabilityCodes.Add(StabilityRating( 235 B_TRANSLATE("Unstable but usable"), "unstablebutusable")); 236 fStabilityCodes.Add(StabilityRating( 237 B_TRANSLATE("Very unstable"), "veryunstable")); 238 fStabilityCodes.Add(StabilityRating( 239 B_TRANSLATE("Does not start"), "nostart")); 240 241 add_stabilities_to_menu(fStabilityCodes, stabilityMenu); 242 stabilityMenu->SetTargetForItems(this); 243 244 fStability = fStabilityCodes.ItemAt(0).Name(); 245 stabilityMenu->ItemAt(0)->SetMarked(true); 246 247 248 { 249 AutoLocker<BLocker> locker(fModel.Lock()); 250 fCommentLanguageCode = fModel.Language()->PreferredLanguage()->Code(); 251 252 // Construct languages popup 253 BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language")); 254 fCommentLanguageField = new BMenuField("language", 255 B_TRANSLATE("Comment language:"), languagesMenu); 256 257 LanguageMenuUtils::AddLanguagesToMenu(fModel.Language(), languagesMenu); 258 languagesMenu->SetTargetForItems(this); 259 LanguageMenuUtils::MarkLanguageInMenu(fCommentLanguageCode, 260 languagesMenu); 261 } 262 263 fRatingActiveCheckBox = new BCheckBox("rating active", 264 B_TRANSLATE("This rating is visible to other users"), 265 new BMessage(MSG_RATING_ACTIVE_CHANGED)); 266 // Hide the check mark by default, it will be made visible when 267 // the user already made a rating and it is loaded 268 fRatingActiveCheckBox->Hide(); 269 270 // Construct buttons 271 fCancelButton = new BButton("cancel", B_TRANSLATE("Cancel"), 272 new BMessage(B_QUIT_REQUESTED)); 273 274 fSendButton = new BButton("send", B_TRANSLATE("Send"), 275 new BMessage(MSG_SEND)); 276 277 // Build layout 278 BLayoutBuilder::Group<>(this, B_VERTICAL) 279 .AddGrid() 280 .Add(ratingLabel, 0, 0) 281 .AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 0) 282 .Add(fRatingDeterminateCheckBox) 283 .Add(fSetRatingView) 284 .End() 285 .AddMenuField(fStabilityField, 0, 1) 286 .AddMenuField(fCommentLanguageField, 0, 2) 287 .End() 288 .Add(textScrollView) 289 .AddGroup(B_HORIZONTAL) 290 .Add(fRatingActiveCheckBox) 291 .AddGlue() 292 .Add(fCancelButton) 293 .Add(fSendButton) 294 .End() 295 .SetInsets(B_USE_WINDOW_INSETS) 296 ; 297 298 // NOTE: Do not make Send the default button. The user might want 299 // to type line-breaks instead of sending when hitting RETURN. 300 301 CenterIn(parent->Frame()); 302 } 303 304 305 RatePackageWindow::~RatePackageWindow() 306 { 307 } 308 309 310 void 311 RatePackageWindow::DispatchMessage(BMessage* message, BHandler *handler) 312 { 313 if (message->what == B_KEY_DOWN) { 314 int8 key; 315 // if the user presses escape, close the window. 316 if ((message->FindInt8("byte", &key) == B_OK) 317 && key == B_ESCAPE) { 318 Quit(); 319 return; 320 } 321 } 322 323 BWindow::DispatchMessage(message, handler); 324 } 325 326 327 void 328 RatePackageWindow::MessageReceived(BMessage* message) 329 { 330 switch (message->what) { 331 case MSG_PACKAGE_RATED: 332 message->FindFloat("rating", &fRating); 333 fRatingDeterminate = true; 334 fSetRatingView->SetRatingDeterminate(true); 335 fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON); 336 break; 337 338 case MSG_STABILITY_SELECTED: 339 message->FindString("name", &fStability); 340 break; 341 342 case MSG_LANGUAGE_SELECTED: 343 message->FindString("code", &fCommentLanguageCode); 344 break; 345 346 case MSG_RATING_DETERMINATE_CHANGED: 347 fRatingDeterminate = fRatingDeterminateCheckBox->Value() 348 == B_CONTROL_ON; 349 fSetRatingView->SetRatingDeterminate(fRatingDeterminate); 350 break; 351 352 case MSG_RATING_ACTIVE_CHANGED: 353 { 354 int32 value; 355 if (message->FindInt32("be:value", &value) == B_OK) 356 fRatingActive = value == B_CONTROL_ON; 357 break; 358 } 359 360 case MSG_DID_ADD_USER_RATING: 361 { 362 BAlert* alert = new(std::nothrow) BAlert( 363 B_TRANSLATE("User rating"), 364 B_TRANSLATE("Your rating was uploaded successfully. " 365 "You can update or remove it at the HaikuDepot Server " 366 "website."), 367 B_TRANSLATE("Close"), NULL, NULL, 368 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 369 alert->Go(); 370 _RefreshPackageData(); 371 break; 372 } 373 374 case MSG_DID_UPDATE_USER_RATING: 375 { 376 BAlert* alert = new(std::nothrow) BAlert( 377 B_TRANSLATE("User rating"), 378 B_TRANSLATE("Your rating was updated."), 379 B_TRANSLATE("Close"), NULL, NULL, 380 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 381 alert->Go(); 382 _RefreshPackageData(); 383 break; 384 } 385 386 case MSG_SEND: 387 _SendRating(); 388 break; 389 390 default: 391 BWindow::MessageReceived(message); 392 break; 393 } 394 } 395 396 /*! Refresh the data shown about the current page. This may be useful, for 397 example when somebody adds a rating and that changes the rating of the 398 package or they add a rating and want to see that immediately. The logic 399 should round-trip to the server so that actual data is shown. 400 */ 401 402 void 403 RatePackageWindow::_RefreshPackageData() 404 { 405 BMessage message(MSG_SERVER_DATA_CHANGED); 406 message.AddString("name", fPackage->Name()); 407 be_app->PostMessage(&message); 408 } 409 410 411 void 412 RatePackageWindow::SetPackage(const PackageInfoRef& package) 413 { 414 BAutolock locker(this); 415 if (!locker.IsLocked() || fWorkerThread >= 0) 416 return; 417 418 fPackage = package; 419 420 BString windowTitle(B_TRANSLATE("Rate %Package%")); 421 windowTitle.ReplaceAll("%Package%", package->Title()); 422 SetTitle(windowTitle); 423 424 // See if the user already made a rating for this package, 425 // pre-fill the UI with that rating. (When sending the rating, the 426 // old one will be replaced.) 427 thread_id thread = spawn_thread(&_QueryRatingThreadEntry, 428 "Query rating", B_NORMAL_PRIORITY, this); 429 if (thread >= 0) 430 _SetWorkerThread(thread); 431 } 432 433 434 void 435 RatePackageWindow::_SendRating() 436 { 437 thread_id thread = spawn_thread(&_SendRatingThreadEntry, 438 "Send rating", B_NORMAL_PRIORITY, this); 439 if (thread >= 0) 440 _SetWorkerThread(thread); 441 } 442 443 444 void 445 RatePackageWindow::_SetWorkerThread(thread_id thread) 446 { 447 if (!Lock()) 448 return; 449 450 bool enabled = thread < 0; 451 452 fStabilityField->SetEnabled(enabled); 453 fCommentLanguageField->SetEnabled(enabled); 454 fSendButton->SetEnabled(enabled); 455 456 if (thread >= 0) { 457 fWorkerThread = thread; 458 resume_thread(fWorkerThread); 459 } else { 460 fWorkerThread = -1; 461 } 462 463 Unlock(); 464 } 465 466 467 /*static*/ int32 468 RatePackageWindow::_QueryRatingThreadEntry(void* data) 469 { 470 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 471 window->_QueryRatingThread(); 472 return 0; 473 } 474 475 476 /*! A server request has been made to the server and the server has responded 477 with some data. The data is known not to be an error and now the data can 478 be extracted into the user interface elements. 479 */ 480 481 void 482 RatePackageWindow::_RelayServerDataToUI(BMessage& response) 483 { 484 if (Lock()) { 485 response.FindString("code", &fRatingID); 486 response.FindBool("active", &fRatingActive); 487 BString comment; 488 if (response.FindString("comment", &comment) == B_OK) { 489 MarkupParser parser; 490 fRatingText = parser.CreateDocumentFromMarkup(comment); 491 fTextView->SetTextDocument(fRatingText); 492 } 493 if (response.FindString("userRatingStabilityCode", 494 &fStability) == B_OK) { 495 int32 index = 0; 496 for (int32 i = fStabilityCodes.CountItems() - 1; i >= 0; i--) { 497 const StabilityRating& stability 498 = fStabilityCodes.ItemAtFast(i); 499 if (stability.Name() == fStability) { 500 index = i; 501 break; 502 } 503 } 504 BMenuItem* item = fStabilityField->Menu()->ItemAt(index); 505 if (item != NULL) 506 item->SetMarked(true); 507 } 508 if (response.FindString("naturalLanguageCode", 509 &fCommentLanguageCode) == B_OK) { 510 LanguageMenuUtils::MarkLanguageInMenu( 511 fCommentLanguageCode, fCommentLanguageField->Menu()); 512 } 513 double rating; 514 if (response.FindDouble("rating", &rating) == B_OK) { 515 fRating = (float)rating; 516 fRatingDeterminate = fRating >= 0.0f; 517 fSetRatingView->SetPermanentRating(fRating); 518 } else { 519 fRatingDeterminate = false; 520 } 521 522 fSetRatingView->SetRatingDeterminate(fRatingDeterminate); 523 fRatingDeterminateCheckBox->SetValue( 524 fRatingDeterminate ? B_CONTROL_ON : B_CONTROL_OFF); 525 fRatingActiveCheckBox->SetValue(fRatingActive); 526 fRatingActiveCheckBox->Show(); 527 528 fSendButton->SetLabel(B_TRANSLATE("Update")); 529 530 Unlock(); 531 } else 532 HDERROR("unable to acquire lock to update the ui"); 533 } 534 535 536 void 537 RatePackageWindow::_QueryRatingThread() 538 { 539 if (!Lock()) { 540 HDERROR("rating query: Failed to lock window"); 541 return; 542 } 543 544 PackageInfoRef package(fPackage); 545 546 Unlock(); 547 548 BAutolock locker(fModel.Lock()); 549 BString nickname = fModel.Nickname(); 550 locker.Unlock(); 551 552 if (package.Get() == NULL) { 553 HDERROR("rating query: No package"); 554 _SetWorkerThread(-1); 555 return; 556 } 557 558 WebAppInterface interface; 559 BMessage info; 560 const DepotInfo* depot = fModel.DepotForName(package->DepotName()); 561 BString repositoryCode; 562 563 if (depot != NULL) 564 repositoryCode = depot->WebAppRepositoryCode(); 565 566 if (repositoryCode.IsEmpty()) { 567 HDERROR("unable to obtain the repository code for depot; %s", 568 package->DepotName().String()); 569 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 570 } else { 571 status_t status = interface 572 .RetreiveUserRatingForPackageAndVersionByUser(package->Name(), 573 package->Version(), package->Architecture(), repositoryCode, 574 nickname, info); 575 576 if (status == B_OK) { 577 // could be an error or could be a valid response envelope 578 // containing data. 579 switch (interface.ErrorCodeFromResponse(info)) { 580 case ERROR_CODE_NONE: 581 { 582 //info.PrintToStream(); 583 BMessage result; 584 if (info.FindMessage("result", &result) == B_OK) { 585 _RelayServerDataToUI(result); 586 } else { 587 HDERROR("bad response envelope missing 'result' entry"); 588 ServerHelper::NotifyTransportError(B_BAD_VALUE); 589 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 590 } 591 break; 592 } 593 case ERROR_CODE_OBJECTNOTFOUND: 594 // an expected response 595 HDINFO("there was no previous rating for this" 596 " user on this version of this package so a new rating" 597 " will be added."); 598 break; 599 default: 600 ServerHelper::NotifyServerJsonRpcError(info); 601 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 602 break; 603 } 604 } else { 605 HDERROR("an error has arisen communicating with the" 606 " server to obtain data for an existing rating [%s]", 607 strerror(status)); 608 ServerHelper::NotifyTransportError(status); 609 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 610 } 611 } 612 613 _SetWorkerThread(-1); 614 } 615 616 617 int32 618 RatePackageWindow::_SendRatingThreadEntry(void* data) 619 { 620 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 621 window->_SendRatingThread(); 622 return 0; 623 } 624 625 626 void 627 RatePackageWindow::_SendRatingThread() 628 { 629 if (!Lock()) { 630 HDERROR("upload rating: Failed to lock window"); 631 return; 632 } 633 634 BMessenger messenger = BMessenger(this); 635 BString package = fPackage->Name(); 636 BString architecture = fPackage->Architecture(); 637 BString repositoryCode; 638 int rating = (int)fRating; 639 BString stability = fStability; 640 BString comment = fRatingText->Text(); 641 BString languageCode = fCommentLanguageCode; 642 BString ratingID = fRatingID; 643 bool active = fRatingActive; 644 645 if (!fRatingDeterminate) 646 rating = RATING_NONE; 647 648 const DepotInfo* depot = fModel.DepotForName(fPackage->DepotName()); 649 650 if (depot != NULL) 651 repositoryCode = depot->WebAppRepositoryCode(); 652 653 WebAppInterface interface = fModel.GetWebAppInterface(); 654 655 Unlock(); 656 657 if (repositoryCode.Length() == 0) { 658 HDERROR("unable to find the web app repository code for the local " 659 "depot %s", 660 fPackage->DepotName().String()); 661 return; 662 } 663 664 if (stability == "unspecified") 665 stability = ""; 666 667 status_t status; 668 BMessage info; 669 if (ratingID.Length() > 0) { 670 HDINFO("will update the existing user rating [%s]", ratingID.String()); 671 status = interface.UpdateUserRating(ratingID, 672 languageCode, comment, stability, rating, active, info); 673 } else { 674 HDINFO("will create a new user rating for pkg [%s]", package.String()); 675 status = interface.CreateUserRating(package, fPackage->Version(), 676 architecture, repositoryCode, languageCode, comment, stability, 677 rating, info); 678 } 679 680 if (status == B_OK) { 681 // could be an error or could be a valid response envelope 682 // containing data. 683 switch (interface.ErrorCodeFromResponse(info)) { 684 case ERROR_CODE_NONE: 685 { 686 if (ratingID.Length() > 0) 687 messenger.SendMessage(MSG_DID_UPDATE_USER_RATING); 688 else 689 messenger.SendMessage(MSG_DID_ADD_USER_RATING); 690 break; 691 } 692 default: 693 ServerHelper::NotifyServerJsonRpcError(info); 694 break; 695 } 696 } else { 697 HDERROR("an error has arisen communicating with the" 698 " server to obtain data for an existing rating [%s]", 699 strerror(status)); 700 ServerHelper::NotifyTransportError(status); 701 } 702 703 messenger.SendMessage(B_QUIT_REQUESTED); 704 _SetWorkerThread(-1); 705 } 706