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