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