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