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