xref: /haiku/src/apps/haikudepot/ui/RatePackageWindow.cpp (revision 02354704729d38c3b078c696adc1bbbd33cbcf72)
1 /*
2  * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2016-2020, 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 <AutoLocker.h>
15 #include <Catalog.h>
16 #include <Button.h>
17 #include <CheckBox.h>
18 #include <LayoutBuilder.h>
19 #include <MenuField.h>
20 #include <MenuItem.h>
21 #include <ScrollView.h>
22 #include <StringView.h>
23 
24 #include "AppUtils.h"
25 #include "HaikuDepotConstants.h"
26 #include "LanguageMenuUtils.h"
27 #include "Logger.h"
28 #include "MarkupParser.h"
29 #include "RatingView.h"
30 #include "ServerHelper.h"
31 #include "TextDocumentView.h"
32 #include "WebAppInterface.h"
33 
34 
35 #undef B_TRANSLATION_CONTEXT
36 #define B_TRANSLATION_CONTEXT "RatePackageWindow"
37 
38 
39 enum {
40 	MSG_SEND						= 'send',
41 	MSG_PACKAGE_RATED				= 'rpkg',
42 	MSG_STABILITY_SELECTED			= 'stbl',
43 	MSG_RATING_ACTIVE_CHANGED		= 'rtac',
44 	MSG_RATING_DETERMINATE_CHANGED	= 'rdch'
45 };
46 
47 //! Layouts the scrollbar so it looks nice with no border and the document
48 // window look.
49 class ScrollView : public BScrollView {
50 public:
51 	ScrollView(const char* name, BView* target)
52 		:
53 		BScrollView(name, target, 0, false, true, B_FANCY_BORDER)
54 	{
55 	}
56 
57 	virtual void DoLayout()
58 	{
59 		BRect innerFrame = Bounds();
60 		innerFrame.InsetBy(2, 2);
61 
62 		BScrollBar* vScrollBar = ScrollBar(B_VERTICAL);
63 		BScrollBar* hScrollBar = ScrollBar(B_HORIZONTAL);
64 
65 		if (vScrollBar != NULL)
66 			innerFrame.right -= vScrollBar->Bounds().Width() - 1;
67 		if (hScrollBar != NULL)
68 			innerFrame.bottom -= hScrollBar->Bounds().Height() - 1;
69 
70 		BView* target = Target();
71 		if (target != NULL) {
72 			Target()->MoveTo(innerFrame.left, innerFrame.top);
73 			Target()->ResizeTo(innerFrame.Width(), innerFrame.Height());
74 		}
75 
76 		if (vScrollBar != NULL) {
77 			BRect rect = innerFrame;
78 			rect.left = rect.right + 1;
79 			rect.right = rect.left + vScrollBar->Bounds().Width();
80 			rect.top -= 1;
81 			rect.bottom += 1;
82 
83 			vScrollBar->MoveTo(rect.left, rect.top);
84 			vScrollBar->ResizeTo(rect.Width(), rect.Height());
85 		}
86 
87 		if (hScrollBar != NULL) {
88 			BRect rect = innerFrame;
89 			rect.top = rect.bottom + 1;
90 			rect.bottom = rect.top + hScrollBar->Bounds().Height();
91 			rect.left -= 1;
92 			rect.right += 1;
93 
94 			hScrollBar->MoveTo(rect.left, rect.top);
95 			hScrollBar->ResizeTo(rect.Width(), rect.Height());
96 		}
97 	}
98 };
99 
100 
101 class SetRatingView : public RatingView {
102 public:
103 	SetRatingView()
104 		:
105 		RatingView("rate package view"),
106 		fPermanentRating(0.0f),
107 		fRatingDeterminate(true)
108 	{
109 		SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
110 		SetRating(fPermanentRating);
111 	}
112 
113 	virtual void MouseMoved(BPoint where, uint32 transit,
114 		const BMessage* dragMessage)
115 	{
116 		if (dragMessage != NULL)
117 			return;
118 
119 		if ((transit != B_INSIDE_VIEW && transit != B_ENTERED_VIEW)
120 			|| where.x > MinSize().width) {
121 			SetRating(fPermanentRating);
122 			return;
123 		}
124 
125 		float hoverRating = _RatingForMousePos(where);
126 		SetRating(hoverRating);
127 	}
128 
129 	virtual void MouseDown(BPoint where)
130 	{
131 		SetPermanentRating(_RatingForMousePos(where));
132 		BMessage message(MSG_PACKAGE_RATED);
133 		message.AddFloat("rating", fPermanentRating);
134 		Window()->PostMessage(&message, Window());
135 	}
136 
137 	void SetPermanentRating(float rating)
138 	{
139 		fPermanentRating = rating;
140 		SetRating(rating);
141 	}
142 
143 /*! By setting this to false, this indicates that there is no rating for the
144     set; ie NULL.  The indeterminate rating is indicated by a pale grey
145     colored star.
146 */
147 
148 	void SetRatingDeterminate(bool value) {
149 		fRatingDeterminate = value;
150 		Invalidate();
151 	}
152 
153 protected:
154 	virtual const BBitmap* StarBitmap()
155 	{
156 		if (fRatingDeterminate)
157 			return fStarBlueBitmap->Bitmap(BITMAP_SIZE_16);
158 		return fStarGrayBitmap->Bitmap(BITMAP_SIZE_16);
159 	}
160 
161 private:
162 	float _RatingForMousePos(BPoint where)
163 	{
164 		return std::min(5.0f, ceilf(5.0f * where.x / MinSize().width));
165 	}
166 
167 	float		fPermanentRating;
168 	bool		fRatingDeterminate;
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(RATING_NONE),
182 	fRatingDeterminate(false),
183 	fCommentLanguageCode(LANGUAGE_DEFAULT_CODE),
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 	fSetRatingView->SetRatingDeterminate(false);
193 	fRatingDeterminateCheckBox = new BCheckBox("has rating", NULL,
194 		new BMessage(MSG_RATING_DETERMINATE_CHANGED));
195 	fRatingDeterminateCheckBox->SetValue(B_CONTROL_OFF);
196 
197 	fTextView = new TextDocumentView();
198 	ScrollView* textScrollView = new ScrollView(
199 		"rating scroll view", fTextView);
200 
201 	// Get a TextDocument with default paragraph and character style
202 	MarkupParser parser;
203 	fRatingText = parser.CreateDocumentFromMarkup("");
204 
205 	fTextView->SetInsets(10.0f);
206 	fTextView->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR);
207 	fTextView->SetTextDocument(fRatingText);
208 	fTextView->SetTextEditor(fTextEditor);
209 
210 	// Construct stability rating popup
211 	BPopUpMenu* stabilityMenu = new BPopUpMenu(B_TRANSLATE("Stability"));
212 	fStabilityField = new BMenuField("stability",
213 		B_TRANSLATE("Stability:"), stabilityMenu);
214 	_InitStabilitiesMenu(stabilityMenu);
215 
216 	// Construct languages popup
217 	BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language"));
218 	fCommentLanguageField = new BMenuField("language",
219 		B_TRANSLATE("Comment language:"), languagesMenu);
220 	_InitLanguagesMenu(languagesMenu);
221 
222 	fRatingActiveCheckBox = new BCheckBox("rating active",
223 		B_TRANSLATE("This rating is visible to other users"),
224 		new BMessage(MSG_RATING_ACTIVE_CHANGED));
225 	// Hide the check mark by default, it will be made visible when
226 	// the user already made a rating and it is loaded
227 	fRatingActiveCheckBox->Hide();
228 
229 	// Construct buttons
230 	fCancelButton = new BButton("cancel", B_TRANSLATE("Cancel"),
231 		new BMessage(B_QUIT_REQUESTED));
232 
233 	fSendButton = new BButton("send", B_TRANSLATE("Send"),
234 		new BMessage(MSG_SEND));
235 
236 	// Build layout
237 	BLayoutBuilder::Group<>(this, B_VERTICAL)
238 		.AddGrid()
239 			.Add(ratingLabel, 0, 0)
240 			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 0)
241 				.Add(fRatingDeterminateCheckBox)
242 				.Add(fSetRatingView)
243 			.End()
244 			.AddMenuField(fStabilityField, 0, 1)
245 			.AddMenuField(fCommentLanguageField, 0, 2)
246 		.End()
247 		.Add(textScrollView)
248 		.AddGroup(B_HORIZONTAL)
249 			.Add(fRatingActiveCheckBox)
250 			.AddGlue()
251 			.Add(fCancelButton)
252 			.Add(fSendButton)
253 		.End()
254 		.SetInsets(B_USE_WINDOW_INSETS)
255 	;
256 
257 	// NOTE: Do not make Send the default button. The user might want
258 	// to type line-breaks instead of sending when hitting RETURN.
259 
260 	CenterIn(parent->Frame());
261 }
262 
263 
264 RatePackageWindow::~RatePackageWindow()
265 {
266 }
267 
268 
269 void
270 RatePackageWindow::_InitLanguagesMenu(BPopUpMenu* menu)
271 {
272 	AutoLocker<BLocker> locker(fModel.Lock());
273 	fCommentLanguageCode = fModel.Language()->PreferredLanguage()->Code();
274 
275 	LanguageMenuUtils::AddLanguagesToMenu(fModel.Language(), menu);
276 	menu->SetTargetForItems(this);
277 	LanguageMenuUtils::MarkLanguageInMenu(fCommentLanguageCode, menu);
278 }
279 
280 
281 void
282 RatePackageWindow::_InitStabilitiesMenu(BPopUpMenu* menu)
283 {
284 	AutoLocker<BLocker> locker(fModel.Lock());
285 	int32 countStabilities = fModel.CountRatingStabilities();
286 
287 	menu->SetTargetForItems(this);
288 
289 	if (0 == countStabilities) {
290 		menu->SetEnabled(false);
291 		return;
292 	}
293 
294 	for (int32 i = 0; i < countStabilities; i++) {
295 		const RatingStabilityRef stability = fModel.RatingStabilityAtIndex(i);
296 		BMessage* message = new BMessage(MSG_STABILITY_SELECTED);
297 		message->AddString("code", stability->Code());
298 		BMenuItem* item = new BMenuItem(stability->Name(), message);
299 		menu->AddItem(item);
300 
301 		if (i == 0) {
302 			fStabilityCode = stability->Code();
303 			item->SetMarked(true);
304 		}
305 	}
306 }
307 
308 
309 void
310 RatePackageWindow::DispatchMessage(BMessage* message, BHandler *handler)
311 {
312 	if (message->what == B_KEY_DOWN) {
313 		int8 key;
314 			// if the user presses escape, close the window.
315 		if ((message->FindInt8("byte", &key) == B_OK)
316 			&& key == B_ESCAPE) {
317 			Quit();
318 			return;
319 		}
320 	}
321 
322 	BWindow::DispatchMessage(message, handler);
323 }
324 
325 
326 void
327 RatePackageWindow::MessageReceived(BMessage* message)
328 {
329 	switch (message->what) {
330 		case MSG_PACKAGE_RATED:
331 			message->FindFloat("rating", &fRating);
332 			fRatingDeterminate = true;
333 			fSetRatingView->SetRatingDeterminate(true);
334 			fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON);
335 			break;
336 
337 		case MSG_STABILITY_SELECTED:
338 			message->FindString("code", &fStabilityCode);
339 			break;
340 
341 		case MSG_LANGUAGE_SELECTED:
342 			message->FindString("code", &fCommentLanguageCode);
343 			break;
344 
345 		case MSG_RATING_DETERMINATE_CHANGED:
346 			fRatingDeterminate = fRatingDeterminateCheckBox->Value()
347 				== B_CONTROL_ON;
348 			fSetRatingView->SetRatingDeterminate(fRatingDeterminate);
349 			break;
350 
351 		case MSG_RATING_ACTIVE_CHANGED:
352 		{
353 			int32 value;
354 			if (message->FindInt32("be:value", &value) == B_OK)
355 				fRatingActive = value == B_CONTROL_ON;
356 			break;
357 		}
358 
359 		case MSG_DID_ADD_USER_RATING:
360 		{
361 			BAlert* alert = new(std::nothrow) BAlert(
362 				B_TRANSLATE("User rating"),
363 				B_TRANSLATE("Your rating was uploaded successfully. "
364 					"You can update or remove it at the HaikuDepot Server "
365 					"website."),
366 				B_TRANSLATE("Close"), NULL, NULL,
367 				B_WIDTH_AS_USUAL, B_WARNING_ALERT);
368 			alert->Go();
369 			_RefreshPackageData();
370 			break;
371 		}
372 
373 		case MSG_DID_UPDATE_USER_RATING:
374 		{
375 			BAlert* alert = new(std::nothrow) BAlert(
376 				B_TRANSLATE("User rating"),
377 				B_TRANSLATE("Your rating was updated."),
378 				B_TRANSLATE("Close"), NULL, NULL,
379 				B_WIDTH_AS_USUAL, B_WARNING_ALERT);
380 			alert->Go();
381 			_RefreshPackageData();
382 			break;
383 		}
384 
385 		case MSG_SEND:
386 			_SendRating();
387 			break;
388 
389 		default:
390 			BWindow::MessageReceived(message);
391 			break;
392 	}
393 }
394 
395 /*! Refresh the data shown about the current page.  This may be useful, for
396     example when somebody adds a rating and that changes the rating of the
397     package or they add a rating and want to see that immediately.  The logic
398     should round-trip to the server so that actual data is shown.
399 */
400 
401 void
402 RatePackageWindow::_RefreshPackageData()
403 {
404 	BMessage message(MSG_SERVER_DATA_CHANGED);
405 	message.AddString("name", fPackage->Name());
406 	be_app->PostMessage(&message);
407 }
408 
409 
410 void
411 RatePackageWindow::SetPackage(const PackageInfoRef& package)
412 {
413 	BAutolock locker(this);
414 	if (!locker.IsLocked() || fWorkerThread >= 0)
415 		return;
416 
417 	fPackage = package;
418 
419 	BString windowTitle(B_TRANSLATE("Rate %Package%"));
420 	windowTitle.ReplaceAll("%Package%", package->Title());
421 	SetTitle(windowTitle);
422 
423 	// See if the user already made a rating for this package,
424 	// pre-fill the UI with that rating. (When sending the rating, the
425 	// old one will be replaced.)
426 	thread_id thread = spawn_thread(&_QueryRatingThreadEntry,
427 		"Query rating", B_NORMAL_PRIORITY, this);
428 	if (thread >= 0)
429 		_SetWorkerThread(thread);
430 }
431 
432 
433 void
434 RatePackageWindow::_SendRating()
435 {
436 	thread_id thread = spawn_thread(&_SendRatingThreadEntry,
437 		"Send rating", B_NORMAL_PRIORITY, this);
438 	if (thread >= 0)
439 		_SetWorkerThread(thread);
440 }
441 
442 
443 void
444 RatePackageWindow::_SetWorkerThread(thread_id thread)
445 {
446 	if (!Lock())
447 		return;
448 
449 	bool enabled = thread < 0;
450 
451 	fStabilityField->SetEnabled(enabled);
452 	fCommentLanguageField->SetEnabled(enabled);
453 	fSendButton->SetEnabled(enabled);
454 
455 	if (thread >= 0) {
456 		fWorkerThread = thread;
457 		resume_thread(fWorkerThread);
458 	} else {
459 		fWorkerThread = -1;
460 	}
461 
462 	Unlock();
463 }
464 
465 
466 /*static*/ int32
467 RatePackageWindow::_QueryRatingThreadEntry(void* data)
468 {
469 	RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data);
470 	window->_QueryRatingThread();
471 	return 0;
472 }
473 
474 
475 /*! A server request has been made to the server and the server has responded
476     with some data.  The data is known not to be an error and now the data can
477     be extracted into the user interface elements.
478 */
479 
480 void
481 RatePackageWindow::_RelayServerDataToUI(BMessage& response)
482 {
483 	if (Lock()) {
484 		response.FindString("code", &fRatingID);
485 		response.FindBool("active", &fRatingActive);
486 		BString comment;
487 		if (response.FindString("comment", &comment) == B_OK) {
488 			MarkupParser parser;
489 			fRatingText = parser.CreateDocumentFromMarkup(comment);
490 			fTextView->SetTextDocument(fRatingText);
491 		}
492 		if (response.FindString("userRatingStabilityCode",
493 				&fStabilityCode) == B_OK) {
494 			BMenu* menu = fStabilityField->Menu();
495 			AppUtils::MarkItemWithCodeInMenu(fStabilityCode, menu);
496 		}
497 		if (response.FindString("naturalLanguageCode",
498 			&fCommentLanguageCode) == B_OK && !comment.IsEmpty()) {
499 			LanguageMenuUtils::MarkLanguageInMenu(
500 				fCommentLanguageCode, fCommentLanguageField->Menu());
501 		}
502 		double rating;
503 		if (response.FindDouble("rating", &rating) == B_OK) {
504 			fRating = (float)rating;
505 			fRatingDeterminate = fRating >= 0.0f;
506 			fSetRatingView->SetPermanentRating(fRating);
507 		} else {
508 			fRatingDeterminate = false;
509 		}
510 
511 		fSetRatingView->SetRatingDeterminate(fRatingDeterminate);
512 		fRatingDeterminateCheckBox->SetValue(
513 			fRatingDeterminate ? B_CONTROL_ON : B_CONTROL_OFF);
514 		fRatingActiveCheckBox->SetValue(fRatingActive);
515 		fRatingActiveCheckBox->Show();
516 
517 		fSendButton->SetLabel(B_TRANSLATE("Update"));
518 
519 		Unlock();
520 	} else
521 		HDERROR("unable to acquire lock to update the ui");
522 }
523 
524 
525 void
526 RatePackageWindow::_QueryRatingThread()
527 {
528 	if (!Lock()) {
529 		HDERROR("rating query: Failed to lock window");
530 		return;
531 	}
532 
533 	PackageInfoRef package(fPackage);
534 
535 	Unlock();
536 
537 	BAutolock locker(fModel.Lock());
538 	BString nickname = fModel.Nickname();
539 	locker.Unlock();
540 
541 	if (!package.IsSet()) {
542 		HDERROR("rating query: No package");
543 		_SetWorkerThread(-1);
544 		return;
545 	}
546 
547 	WebAppInterface interface;
548 	BMessage info;
549 	const DepotInfo* depot = fModel.DepotForName(package->DepotName());
550 	BString repositoryCode;
551 
552 	if (depot != NULL)
553 		repositoryCode = depot->WebAppRepositoryCode();
554 
555 	if (repositoryCode.IsEmpty()) {
556 		HDERROR("unable to obtain the repository code for depot; %s",
557 			package->DepotName().String());
558 		BMessenger(this).SendMessage(B_QUIT_REQUESTED);
559 	} else {
560 		status_t status = interface
561 			.RetreiveUserRatingForPackageAndVersionByUser(package->Name(),
562 				package->Version(), package->Architecture(), repositoryCode,
563 				nickname, info);
564 
565 		if (status == B_OK) {
566 				// could be an error or could be a valid response envelope
567 				// containing data.
568 			switch (interface.ErrorCodeFromResponse(info)) {
569 				case ERROR_CODE_NONE:
570 				{
571 					//info.PrintToStream();
572 					BMessage result;
573 					if (info.FindMessage("result", &result) == B_OK) {
574 						_RelayServerDataToUI(result);
575 					} else {
576 						HDERROR("bad response envelope missing 'result' entry");
577 						ServerHelper::NotifyTransportError(B_BAD_VALUE);
578 						BMessenger(this).SendMessage(B_QUIT_REQUESTED);
579 					}
580 					break;
581 				}
582 				case ERROR_CODE_OBJECTNOTFOUND:
583 						// an expected response
584 					HDINFO("there was no previous rating for this"
585 						" user on this version of this package so a new rating"
586 						" will be added.");
587 					break;
588 				default:
589 					ServerHelper::NotifyServerJsonRpcError(info);
590 					BMessenger(this).SendMessage(B_QUIT_REQUESTED);
591 					break;
592 			}
593 		} else {
594 			HDERROR("an error has arisen communicating with the"
595 				" server to obtain data for an existing rating [%s]",
596 				strerror(status));
597 			ServerHelper::NotifyTransportError(status);
598 			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
599 		}
600 	}
601 
602 	_SetWorkerThread(-1);
603 }
604 
605 
606 int32
607 RatePackageWindow::_SendRatingThreadEntry(void* data)
608 {
609 	RatePackageWindow* window = reinterpret_cast<RatePackageWindow*>(data);
610 	window->_SendRatingThread();
611 	return 0;
612 }
613 
614 
615 void
616 RatePackageWindow::_SendRatingThread()
617 {
618 	if (!Lock()) {
619 		HDERROR("upload rating: Failed to lock window");
620 		return;
621 	}
622 
623 	BMessenger messenger = BMessenger(this);
624 	BString package = fPackage->Name();
625 	BString architecture = fPackage->Architecture();
626 	BString repositoryCode;
627 	int rating = (int)fRating;
628 	BString stability = fStabilityCode;
629 	BString comment = fRatingText->Text();
630 	BString languageCode = fCommentLanguageCode;
631 	BString ratingID = fRatingID;
632 	bool active = fRatingActive;
633 
634 	if (!fRatingDeterminate)
635 		rating = RATING_NONE;
636 
637 	const DepotInfo* depot = fModel.DepotForName(fPackage->DepotName());
638 
639 	if (depot != NULL)
640 		repositoryCode = depot->WebAppRepositoryCode();
641 
642 	WebAppInterface interface = fModel.GetWebAppInterface();
643 
644 	Unlock();
645 
646 	if (repositoryCode.Length() == 0) {
647 		HDERROR("unable to find the web app repository code for the local "
648 			"depot %s",
649 			fPackage->DepotName().String());
650 		return;
651 	}
652 
653 	if (stability == "unspecified")
654 		stability = "";
655 
656 	status_t status;
657 	BMessage info;
658 	if (ratingID.Length() > 0) {
659 		HDINFO("will update the existing user rating [%s]", ratingID.String());
660 		status = interface.UpdateUserRating(ratingID,
661 			languageCode, comment, stability, rating, active, info);
662 	} else {
663 		HDINFO("will create a new user rating for pkg [%s]", package.String());
664 		status = interface.CreateUserRating(package, fPackage->Version(),
665 			architecture, repositoryCode, languageCode, comment, stability,
666 			rating, info);
667 	}
668 
669 	if (status == B_OK) {
670 			// could be an error or could be a valid response envelope
671 			// containing data.
672 		switch (interface.ErrorCodeFromResponse(info)) {
673 			case ERROR_CODE_NONE:
674 			{
675 				if (ratingID.Length() > 0)
676 					messenger.SendMessage(MSG_DID_UPDATE_USER_RATING);
677 				else
678 					messenger.SendMessage(MSG_DID_ADD_USER_RATING);
679 				break;
680 			}
681 			default:
682 				ServerHelper::NotifyServerJsonRpcError(info);
683 				break;
684 		}
685 	} else {
686 		HDERROR("an error has arisen communicating with the"
687 			" server to obtain data for an existing rating [%s]",
688 			strerror(status));
689 		ServerHelper::NotifyTransportError(status);
690 	}
691 
692 	messenger.SendMessage(B_QUIT_REQUESTED);
693 	_SetWorkerThread(-1);
694 }
695