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 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 username = fModel.Username(); 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.RetrieveUserRating( 574 package->Name(), package->Version(), package->Architecture(), 575 repositoryCode, username, info); 576 577 if (status == B_OK) { 578 // could be an error or could be a valid response envelope 579 // containing data. 580 switch (interface.ErrorCodeFromResponse(info)) { 581 case ERROR_CODE_NONE: 582 { 583 //info.PrintToStream(); 584 BMessage result; 585 if (info.FindMessage("result", &result) == B_OK) { 586 _RelayServerDataToUI(result); 587 } else { 588 fprintf(stderr, "bad response envelope missing 'result'" 589 "entry\n"); 590 ServerHelper::NotifyTransportError(B_BAD_VALUE); 591 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 592 } 593 break; 594 } 595 case ERROR_CODE_OBJECTNOTFOUND: 596 // an expected response 597 fprintf(stderr, "there was no previous rating for this" 598 " user on this version of this package so a new rating" 599 " will be added.\n"); 600 break; 601 default: 602 ServerHelper::NotifyServerJsonRpcError(info); 603 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 604 break; 605 } 606 } else { 607 fprintf(stderr, "an error has arisen communicating with the" 608 " server to obtain data for an existing rating [%s]\n", 609 strerror(status)); 610 ServerHelper::NotifyTransportError(status); 611 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 612 } 613 } 614 615 _SetWorkerThread(-1); 616 } 617 618 619 int32 620 RatePackageWindow::_SendRatingThreadEntry(void* data) 621 { 622 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 623 window->_SendRatingThread(); 624 return 0; 625 } 626 627 628 void 629 RatePackageWindow::_SendRatingThread() 630 { 631 if (!Lock()) { 632 fprintf(stderr, "upload rating: Failed to lock window\n"); 633 return; 634 } 635 636 BMessenger messenger = BMessenger(this); 637 BString package = fPackage->Name(); 638 BString architecture = fPackage->Architecture(); 639 BString repositoryCode; 640 int rating = (int)fRating; 641 BString stability = fStability; 642 BString comment = fRatingText->Text(); 643 BString languageCode = fCommentLanguageCode; 644 BString ratingID = fRatingID; 645 bool active = fRatingActive; 646 647 if (!fRatingDeterminate) 648 rating = RATING_NONE; 649 650 const DepotInfo* depot = fModel.DepotForName(fPackage->DepotName()); 651 652 if (depot != NULL) 653 repositoryCode = depot->WebAppRepositoryCode(); 654 655 WebAppInterface interface = fModel.GetWebAppInterface(); 656 657 Unlock(); 658 659 if (repositoryCode.Length() == 0) { 660 printf("unable to find the web app repository code for the local " 661 "depot %s\n", 662 fPackage->DepotName().String()); 663 return; 664 } 665 666 if (stability == "unspecified") 667 stability = ""; 668 669 status_t status; 670 BMessage info; 671 if (ratingID.Length() > 0) { 672 printf("will update the existing user rating [%s]\n", 673 ratingID.String()); 674 status = interface.UpdateUserRating(ratingID, 675 languageCode, comment, stability, rating, active, info); 676 } else { 677 printf("will create a new user rating for pkg [%s]\n", 678 package.String()); 679 status = interface.CreateUserRating(package, fPackage->Version(), 680 architecture, repositoryCode, languageCode, comment, stability, 681 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 fprintf(stderr, "an error has arisen communicating with the" 702 " server to obtain data for an existing rating [%s]\n", 703 strerror(status)); 704 ServerHelper::NotifyTransportError(status); 705 } 706 707 messenger.SendMessage(B_QUIT_REQUESTED); 708 _SetWorkerThread(-1); 709 } 710