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->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR); 203 fTextView->SetTextDocument(fRatingText); 204 fTextView->SetTextEditor(fTextEditor); 205 206 // Construct stability rating popup 207 BPopUpMenu* stabilityMenu = new BPopUpMenu(B_TRANSLATE("Stability")); 208 fStabilityField = new BMenuField("stability", 209 B_TRANSLATE("Stability:"), stabilityMenu); 210 211 fStabilityCodes.Add(StabilityRating( 212 B_TRANSLATE("Not specified"), "unspecified")); 213 fStabilityCodes.Add(StabilityRating( 214 B_TRANSLATE("Stable"), "stable")); 215 fStabilityCodes.Add(StabilityRating( 216 B_TRANSLATE("Mostly stable"), "mostlystable")); 217 fStabilityCodes.Add(StabilityRating( 218 B_TRANSLATE("Unstable but usable"), "unstablebutusable")); 219 fStabilityCodes.Add(StabilityRating( 220 B_TRANSLATE("Very unstable"), "veryunstable")); 221 fStabilityCodes.Add(StabilityRating( 222 B_TRANSLATE("Does not start"), "nostart")); 223 224 add_stabilities_to_menu(fStabilityCodes, stabilityMenu); 225 stabilityMenu->SetTargetForItems(this); 226 227 fStability = fStabilityCodes.ItemAt(0).Name(); 228 stabilityMenu->ItemAt(0)->SetMarked(true); 229 230 // Construct languages popup 231 BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language")); 232 fCommentLanguageField = new BMenuField("language", 233 B_TRANSLATE("Comment language:"), languagesMenu); 234 235 add_languages_to_menu(fModel.SupportedLanguages(), languagesMenu); 236 languagesMenu->SetTargetForItems(this); 237 238 BMenuItem* defaultItem = languagesMenu->ItemAt( 239 fModel.SupportedLanguages().IndexOf(fCommentLanguage)); 240 if (defaultItem != NULL) 241 defaultItem->SetMarked(true); 242 243 fRatingActiveCheckBox = new BCheckBox("rating active", 244 B_TRANSLATE("Other users can see this rating"), 245 new BMessage(MSG_RATING_ACTIVE_CHANGED)); 246 // Hide the check mark by default, it will be made visible when 247 // the user already made a rating and it is loaded 248 fRatingActiveCheckBox->Hide(); 249 250 // Construct buttons 251 fCancelButton = new BButton("cancel", B_TRANSLATE("Cancel"), 252 new BMessage(B_QUIT_REQUESTED)); 253 254 fSendButton = new BButton("send", B_TRANSLATE("Send"), 255 new BMessage(MSG_SEND)); 256 257 // Build layout 258 BLayoutBuilder::Group<>(this, B_VERTICAL) 259 .AddGrid() 260 .Add(ratingLabel, 0, 0) 261 .Add(fSetRatingView, 1, 0) 262 .AddMenuField(fStabilityField, 0, 1) 263 .AddMenuField(fCommentLanguageField, 0, 2) 264 .End() 265 .Add(textScrollView) 266 .AddGroup(B_HORIZONTAL) 267 .Add(fRatingActiveCheckBox) 268 .AddGlue() 269 .Add(fCancelButton) 270 .Add(fSendButton) 271 .End() 272 .SetInsets(B_USE_WINDOW_INSETS) 273 ; 274 275 // NOTE: Do not make Send the default button. The user might want 276 // to type line-breaks instead of sending when hitting RETURN. 277 278 CenterIn(parent->Frame()); 279 } 280 281 282 RatePackageWindow::~RatePackageWindow() 283 { 284 } 285 286 287 void 288 RatePackageWindow::MessageReceived(BMessage* message) 289 { 290 switch (message->what) { 291 case MSG_PACKAGE_RATED: 292 message->FindFloat("rating", &fRating); 293 break; 294 295 case MSG_STABILITY_SELECTED: 296 message->FindString("name", &fStability); 297 break; 298 299 case MSG_LANGUAGE_SELECTED: 300 message->FindString("code", &fCommentLanguage); 301 break; 302 303 case MSG_RATING_ACTIVE_CHANGED: 304 { 305 int32 value; 306 if (message->FindInt32("be:value", &value) == B_OK) 307 fRatingActive = value == B_CONTROL_ON; 308 break; 309 } 310 311 case MSG_SEND: 312 _SendRating(); 313 break; 314 315 default: 316 BWindow::MessageReceived(message); 317 break; 318 } 319 } 320 321 322 void 323 RatePackageWindow::SetPackage(const PackageInfoRef& package) 324 { 325 BAutolock locker(this); 326 if (!locker.IsLocked() || fWorkerThread >= 0) 327 return; 328 329 fPackage = package; 330 331 BString windowTitle(B_TRANSLATE("Rate %Package%")); 332 windowTitle.ReplaceAll("%Package%", package->Title()); 333 SetTitle(windowTitle); 334 335 // See if the user already made a rating for this package, 336 // pre-fill the UI with that rating. (When sending the rating, the 337 // old one will be replaced.) 338 thread_id thread = spawn_thread(&_QueryRatingThreadEntry, 339 "Query rating", B_NORMAL_PRIORITY, this); 340 if (thread >= 0) 341 _SetWorkerThread(thread); 342 } 343 344 345 void 346 RatePackageWindow::_SendRating() 347 { 348 thread_id thread = spawn_thread(&_SendRatingThreadEntry, 349 "Send rating", B_NORMAL_PRIORITY, this); 350 if (thread >= 0) 351 _SetWorkerThread(thread); 352 } 353 354 355 void 356 RatePackageWindow::_SetWorkerThread(thread_id thread) 357 { 358 if (!Lock()) 359 return; 360 361 bool enabled = thread < 0; 362 363 // fTextEditor->SetEnabled(enabled); 364 // fSetRatingView->SetEnabled(enabled); 365 fStabilityField->SetEnabled(enabled); 366 fCommentLanguageField->SetEnabled(enabled); 367 fSendButton->SetEnabled(enabled); 368 369 if (thread >= 0) { 370 fWorkerThread = thread; 371 resume_thread(fWorkerThread); 372 } else { 373 fWorkerThread = -1; 374 } 375 376 Unlock(); 377 } 378 379 380 int32 381 RatePackageWindow::_QueryRatingThreadEntry(void* data) 382 { 383 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 384 window->_QueryRatingThread(); 385 return 0; 386 } 387 388 389 void 390 RatePackageWindow::_QueryRatingThread() 391 { 392 if (!Lock()) { 393 fprintf(stderr, "rating query: Failed to lock window\n"); 394 return; 395 } 396 397 PackageInfoRef package(fPackage); 398 399 Unlock(); 400 401 BAutolock locker(fModel.Lock()); 402 BString username = fModel.Username(); 403 locker.Unlock(); 404 405 if (package.Get() == NULL) { 406 fprintf(stderr, "rating query: No package\n"); 407 _SetWorkerThread(-1); 408 return; 409 } 410 411 WebAppInterface interface; 412 BMessage info; 413 const DepotInfo* depot = fModel.DepotForName(package->DepotName()); 414 BString repositoryCode; 415 416 if (depot != NULL) 417 repositoryCode = depot->WebAppRepositoryCode(); 418 419 if (repositoryCode.Length() == 0) { 420 printf("unable to obtain the repository code for depot; %s\n", 421 package->DepotName().String()); 422 } else { 423 status_t status = interface.RetrieveUserRating( 424 package->Name(), package->Version(), package->Architecture(), 425 repositoryCode, username, info); 426 427 // info.PrintToStream(); 428 429 BMessage result; 430 if (status == B_OK && info.FindMessage("result", &result) == B_OK 431 && Lock()) { 432 433 result.FindString("code", &fRatingID); 434 result.FindBool("active", &fRatingActive); 435 BString comment; 436 if (result.FindString("comment", &comment) == B_OK) { 437 MarkupParser parser; 438 fRatingText = parser.CreateDocumentFromMarkup(comment); 439 fTextView->SetTextDocument(fRatingText); 440 } 441 if (result.FindString("userRatingStabilityCode", 442 &fStability) == B_OK) { 443 int32 index = 0; 444 for (int32 i = fStabilityCodes.CountItems() - 1; i >= 0; i--) { 445 const StabilityRating& stability 446 = fStabilityCodes.ItemAtFast(i); 447 if (stability.Name() == fStability) { 448 index = i; 449 break; 450 } 451 } 452 BMenuItem* item = fStabilityField->Menu()->ItemAt(index); 453 if (item != NULL) 454 item->SetMarked(true); 455 } 456 if (result.FindString("naturalLanguageCode", 457 &fCommentLanguage) == B_OK) { 458 BMenuItem* item = fCommentLanguageField->Menu()->ItemAt( 459 fModel.SupportedLanguages().IndexOf(fCommentLanguage)); 460 if (item != NULL) 461 item->SetMarked(true); 462 } 463 double rating; 464 if (result.FindDouble("rating", &rating) == B_OK) { 465 fRating = (float)rating; 466 fSetRatingView->SetPermanentRating(fRating); 467 } 468 469 fRatingActiveCheckBox->SetValue(fRatingActive); 470 fRatingActiveCheckBox->Show(); 471 472 fSendButton->SetLabel(B_TRANSLATE("Update")); 473 474 Unlock(); 475 } else { 476 fprintf(stderr, "rating query: Failed response: %s\n", 477 strerror(status)); 478 if (!info.IsEmpty()) 479 info.PrintToStream(); 480 } 481 } 482 483 _SetWorkerThread(-1); 484 } 485 486 487 int32 488 RatePackageWindow::_SendRatingThreadEntry(void* data) 489 { 490 RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data); 491 window->_SendRatingThread(); 492 return 0; 493 } 494 495 496 void 497 RatePackageWindow::_SendRatingThread() 498 { 499 if (!Lock()) { 500 fprintf(stderr, "upload rating: Failed to lock window\n"); 501 return; 502 } 503 504 BString package = fPackage->Name(); 505 BString architecture = fPackage->Architecture(); 506 BString repositoryCode; 507 int rating = (int)fRating; 508 BString stability = fStability; 509 BString comment = fRatingText->Text(); 510 BString languageCode = fCommentLanguage; 511 BString ratingID = fRatingID; 512 bool active = fRatingActive; 513 514 const DepotInfo* depot = fModel.DepotForName(fPackage->DepotName()); 515 516 if (depot != NULL) 517 repositoryCode = depot->WebAppRepositoryCode(); 518 519 WebAppInterface interface = fModel.GetWebAppInterface(); 520 521 Unlock(); 522 523 if (repositoryCode.Length() == 0) { 524 printf("unable to find the web app repository code for the local " 525 "depot %s\n", 526 fPackage->DepotName().String()); 527 return; 528 } 529 530 if (stability == "unspecified") 531 stability = ""; 532 533 status_t status; 534 BMessage info; 535 if (ratingID.Length() > 0) { 536 status = interface.UpdateUserRating(ratingID, 537 languageCode, comment, stability, rating, active, info); 538 } else { 539 status = interface.CreateUserRating(package, architecture, 540 repositoryCode, languageCode, comment, stability, rating, info); 541 } 542 543 BString error = B_TRANSLATE( 544 "There was a puzzling response from the web service."); 545 546 BMessage result; 547 if (status == B_OK) { 548 if (info.FindMessage("result", &result) == B_OK) { 549 error = ""; 550 } else if (info.FindMessage("error", &result) == B_OK) { 551 result.PrintToStream(); 552 BString message; 553 if (result.FindString("message", &message) == B_OK) { 554 if (message == "objectnotfound") { 555 error = B_TRANSLATE("The package was not found by the " 556 "web service. This probably means that it comes " 557 "from a depot which is not tracked there. Rating " 558 "such packages is unfortunately not supported."); 559 } else { 560 error << B_TRANSLATE(" It responded with: "); 561 error << message; 562 } 563 } 564 } 565 } else { 566 error = B_TRANSLATE( 567 "It was not possible to contact the web service."); 568 } 569 570 if (!error.IsEmpty()) { 571 BString failedTitle; 572 if (ratingID.Length() > 0) 573 failedTitle = B_TRANSLATE("Failed to update rating"); 574 else 575 failedTitle = B_TRANSLATE("Failed to rate package"); 576 577 BAlert* alert = new(std::nothrow) BAlert( 578 failedTitle, 579 error, 580 B_TRANSLATE("Close"), NULL, NULL, 581 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 582 583 if (alert != NULL) 584 alert->Go(); 585 586 fprintf(stderr, 587 B_TRANSLATE("Failed to create or update rating: %s\n"), 588 error.String()); 589 if (!info.IsEmpty()) 590 info.PrintToStream(); 591 592 _SetWorkerThread(-1); 593 } else { 594 _SetWorkerThread(-1); 595 596 fModel.PopulatePackage(fPackage, 597 Model::POPULATE_FORCE | Model::POPULATE_USER_RATINGS); 598 599 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 600 601 BString message; 602 if (ratingID.Length() > 0) { 603 message = B_TRANSLATE("Your rating was updated successfully."); 604 } else { 605 message = B_TRANSLATE("Your rating was uploaded successfully. " 606 "You can update or remove it at any time by rating the " 607 "package again."); 608 } 609 610 BAlert* alert = new(std::nothrow) BAlert( 611 B_TRANSLATE("Success"), 612 message, 613 B_TRANSLATE("Close")); 614 615 if (alert != NULL) 616 alert->Go(); 617 } 618 } 619