xref: /haiku/src/apps/haikudepot/ui/RatePackageWindow.cpp (revision 6889394848e2dc9f41ff53b12141d572822ca0c6)
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