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