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