xref: /haiku/src/apps/haikudepot/ui/RatePackageWindow.cpp (revision 97dfeb96704e5dbc5bec32ad7b21379d0125e031)
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->Name(), 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->Name();
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 
571 		fModel.PopulatePackage(fPackage,
572 			Model::POPULATE_FORCE | Model::POPULATE_USER_RATINGS);
573 
574 		BMessenger(this).SendMessage(B_QUIT_REQUESTED);
575 
576 		BString message;
577 		if (ratingID.Length() > 0) {
578 			message = B_TRANSLATE("Your rating was updated successfully.");
579 		} else {
580 			message = B_TRANSLATE("Your rating was uploaded successfully. "
581 				"You can update or remove it at any time by rating the "
582 				"package again.");
583 		}
584 
585 		BAlert* alert = new(std::nothrow) BAlert(
586 			B_TRANSLATE("Success"),
587 			message,
588 			B_TRANSLATE("Close"));
589 
590 		if (alert != NULL)
591 			alert->Go();
592 	}
593 }
594