xref: /haiku/src/bin/desklink/MediaReplicant.cpp (revision 52f7c9389475e19fc21487b38064b4390eeb6fea)
1 /*
2  * Copyright 2003-2018, Haiku. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  *		Jérôme Duval
7  *		François Revol
8  *		Marcus Overhagen
9  *		Jonas Sundström
10  *		Axel Dörfler, axeld@pinc-software.de.
11  *		Stephan Aßmus <superstippi@gmx.de>
12  *		Puck Meerburg, puck@puckipedia.nl
13  */
14 
15 
16 //! Volume control, and media shortcuts in Deskbar
17 
18 
19 #include <new>
20 #include <stdio.h>
21 
22 #include <Alert.h>
23 #include <Bitmap.h>
24 #include <Catalog.h>
25 #include <Entry.h>
26 #include <File.h>
27 #include <FindDirectory.h>
28 #include <IconUtils.h>
29 #include <MenuItem.h>
30 #include <Path.h>
31 #include <PopUpMenu.h>
32 #include <Resources.h>
33 #include <Roster.h>
34 #include <String.h>
35 #include <StringView.h>
36 #include <TextView.h>
37 #include <ToolTip.h>
38 #include <ToolTipManager.h>
39 
40 #include "desklink.h"
41 #include "MixerControl.h"
42 #include "VolumeWindow.h"
43 
44 
45 #undef B_TRANSLATION_CONTEXT
46 #define B_TRANSLATION_CONTEXT "MediaReplicant"
47 
48 
49 static const uint32 kMsgOpenMediaSettings = 'mese';
50 static const uint32 kMsgOpenSoundSettings = 'sose';
51 static const uint32 kMsgOpenMediaPlayer = 'omep';
52 static const uint32 kMsgToggleBeep = 'tdbp';
53 static const uint32 kMsgVolumeWhich = 'svwh';
54 
55 static const char* kReplicantName = "MediaReplicant";
56 	// R5 name needed, Media prefs manel removes by name
57 
58 static const char* kSettingsFile = "x-vnd.Haiku-desklink";
59 
60 
61 class VolumeToolTip : public BTextToolTip {
62 public:
63 	VolumeToolTip(int32 which = VOLUME_USE_MIXER)
64 		:
65 		BTextToolTip(""),
66 		fWhich(which)
67 	{
68 	}
69 
70 	virtual ~VolumeToolTip()
71 	{
72 	}
73 
74 	virtual void AttachedToWindow()
75 	{
76 		Update();
77 	}
78 
79 	void SetWhich(int32 which)
80 	{
81 		fWhich = which;
82 	}
83 
84 	void Update()
85 	{
86 		if (!Lock())
87 			return;
88 
89 		BTextView* view = (BTextView*)View();
90 
91 		if (fMuteMessage.Length() != 0)
92 			view->SetText(fMuteMessage.String());
93 		else {
94 			MixerControl control;
95 			control.Connect(fWhich);
96 
97 			BString text;
98 			text.SetToFormat(B_TRANSLATE("%g dB"), control.Volume());
99 			view->SetText(text.String());
100 		}
101 		Unlock();
102 	}
103 
104 	void SetMuteMessage(const char* message)
105 	{
106 		fMuteMessage = message == NULL ? "" : message;
107 	}
108 
109 private:
110 	int32			fWhich;
111 	BString			fMuteMessage;
112 };
113 
114 
115 class MediaReplicant : public BView {
116 public:
117 							MediaReplicant(BRect frame, const char* name,
118 								uint32 resizeMask = B_FOLLOW_ALL,
119 								uint32 flags = B_WILL_DRAW | B_NAVIGABLE
120 									| B_PULSE_NEEDED);
121 							MediaReplicant(BMessage* archive);
122 
123 	virtual					~MediaReplicant();
124 
125 	// archiving overrides
126 	static	MediaReplicant*	Instantiate(BMessage* data);
127 	virtual	status_t		Archive(BMessage* data, bool deep = true) const;
128 
129 	// BView overrides
130 	virtual void			AttachedToWindow();
131 	virtual void			MouseDown(BPoint point);
132 	virtual void			Draw(BRect updateRect);
133 	virtual void			MessageReceived(BMessage* message);
134 
135 private:
136 			status_t		_LaunchByPath(const char* path);
137 			status_t		_LaunchBySignature(const char* signature);
138 			void			_Launch(const char* prettyName,
139 								const char* signature, directory_which base,
140 								const char* fileName);
141 			void			_LoadSettings();
142 			void			_SaveSettings();
143 			void			_Init();
144 			BBitmap*		_LoadIcon(BResources& resources, const char* name);
145 
146 			void			_DisconnectMixer();
147 			status_t		_ConnectMixer();
148 
149 			MixerControl*	fMixerControl;
150 
151 			BBitmap*		fIcon;
152 			BBitmap*		fMutedIcon;
153 			VolumeWindow*	fVolumeSlider;
154 			bool 			fDontBeep;
155 				// don't beep on volume change
156 			int32 			fVolumeWhich;
157 				// which volume parameter to act on (Mixer/Phys.Output)
158 			bool				fMuted;
159 };
160 
161 
162 status_t
163 our_image(image_info& image)
164 {
165 	int32 cookie = 0;
166 	while (get_next_image_info(B_CURRENT_TEAM, &cookie, &image) == B_OK) {
167 		if ((char*)our_image >= (char*)image.text
168 			&& (char*)our_image <= (char*)image.text + image.text_size)
169 			return B_OK;
170 	}
171 
172 	return B_ERROR;
173 }
174 
175 
176 //	#pragma mark -
177 
178 
179 MediaReplicant::MediaReplicant(BRect frame, const char* name,
180 		uint32 resizeMask, uint32 flags)
181 	:
182 	BView(frame, name, resizeMask, flags),
183 	fMixerControl(NULL),
184 	fVolumeSlider(NULL),
185 	fMuted(false)
186 {
187 	_Init();
188 }
189 
190 
191 MediaReplicant::MediaReplicant(BMessage* message)
192 	:
193 	BView(message),
194 	fMixerControl(NULL),
195 	fVolumeSlider(NULL),
196 	fMuted(false)
197 {
198 	_Init();
199 }
200 
201 
202 MediaReplicant::~MediaReplicant()
203 {
204 	delete fIcon;
205 	_SaveSettings();
206 	_DisconnectMixer();
207 }
208 
209 
210 MediaReplicant*
211 MediaReplicant::Instantiate(BMessage* data)
212 {
213 	if (!validate_instantiation(data, kReplicantName))
214 		return NULL;
215 
216 	return new(std::nothrow) MediaReplicant(data);
217 }
218 
219 
220 status_t
221 MediaReplicant::Archive(BMessage* data, bool deep) const
222 {
223 	status_t status = BView::Archive(data, deep);
224 	if (status < B_OK)
225 		return status;
226 
227 	return data->AddString("add_on", kAppSignature);
228 }
229 
230 
231 void
232 MediaReplicant::AttachedToWindow()
233 {
234 	AdoptParentColors();
235 
236 	_ConnectMixer();
237 
238 	BView::AttachedToWindow();
239 }
240 
241 
242 void
243 MediaReplicant::Draw(BRect rect)
244 {
245 	SetDrawingMode(B_OP_OVER);
246 	DrawBitmap(fMuted ? fMutedIcon : fIcon);
247 }
248 
249 
250 void
251 MediaReplicant::MouseDown(BPoint point)
252 {
253 	int32 buttons = B_PRIMARY_MOUSE_BUTTON;
254 	if (Looper() != NULL && Looper()->CurrentMessage() != NULL)
255 		Looper()->CurrentMessage()->FindInt32("buttons", &buttons);
256 
257 	BPoint where = ConvertToScreen(point);
258 
259 	if ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0) {
260 		BPopUpMenu* menu = new BPopUpMenu("", false, false);
261 		menu->SetFont(be_plain_font);
262 
263 		menu->AddItem(new BMenuItem(
264 			B_TRANSLATE("Media preferences" B_UTF8_ELLIPSIS),
265 			new BMessage(kMsgOpenMediaSettings)));
266 		menu->AddItem(new BMenuItem(
267 			B_TRANSLATE("Sounds preferences" B_UTF8_ELLIPSIS),
268 			new BMessage(kMsgOpenSoundSettings)));
269 
270 		menu->AddSeparatorItem();
271 
272 		menu->AddItem(new BMenuItem(B_TRANSLATE("Open MediaPlayer"),
273 			new BMessage(kMsgOpenMediaPlayer)));
274 
275 		menu->AddSeparatorItem();
276 
277 		BMenu* subMenu = new BMenu(B_TRANSLATE("Options"));
278 		menu->AddItem(subMenu);
279 
280 		BMenuItem* item = new BMenuItem(B_TRANSLATE("Control physical output"),
281 			new BMessage(kMsgVolumeWhich));
282 		item->SetMarked(fVolumeWhich == VOLUME_USE_PHYS_OUTPUT);
283 		subMenu->AddItem(item);
284 
285 		item = new BMenuItem(B_TRANSLATE("Beep"),
286 			new BMessage(kMsgToggleBeep));
287 		item->SetMarked(!fDontBeep);
288 		subMenu->AddItem(item);
289 
290 		menu->SetTargetForItems(this);
291 		subMenu->SetTargetForItems(this);
292 
293 		menu->Go(where, true, true, BRect(where - BPoint(4, 4),
294 			where + BPoint(4, 4)));
295 
296 	} else if ((buttons & B_TERTIARY_MOUSE_BUTTON) != 0) {
297 		if (fMixerControl != NULL) {
298 			fMixerControl->SetMute(!fMuted);
299 			fMuted = fMixerControl->Mute();
300 			VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip());
301 			if (tip != NULL) {
302 				tip->SetMuteMessage(fMuted ? B_TRANSLATE("Muted"): NULL);
303 				tip->Update();
304 				ShowToolTip(tip);
305 			}
306 			Invalidate();
307 		}
308 
309 	} else {
310 		// Show VolumeWindow
311 		fVolumeSlider = new VolumeWindow(BRect(where.x, where.y,
312 			where.x + 207, where.y + 19), fDontBeep, fVolumeWhich);
313 		fVolumeSlider->Show();
314 	}
315 }
316 
317 
318 void
319 MediaReplicant::MessageReceived(BMessage* message)
320 {
321 	switch (message->what) {
322 		case kMsgOpenMediaPlayer:
323 			_Launch("MediaPlayer", "application/x-vnd.Haiku-MediaPlayer",
324 				B_SYSTEM_APPS_DIRECTORY, "MediaPlayer");
325 			break;
326 
327 		case kMsgOpenMediaSettings:
328 			_Launch("Media Preferences", "application/x-vnd.Haiku-Media",
329 				B_SYSTEM_PREFERENCES_DIRECTORY, "Media");
330 			break;
331 
332 		case kMsgOpenSoundSettings:
333 			_Launch("Sounds Preferences", "application/x-vnd.Haiku-Sounds",
334 				B_SYSTEM_PREFERENCES_DIRECTORY, "Sounds");
335 			break;
336 
337 		case kMsgToggleBeep:
338 		{
339 			BMenuItem* item;
340 			if (message->FindPointer("source", (void**)&item) != B_OK)
341 				return;
342 
343 			item->SetMarked(!item->IsMarked());
344 			fDontBeep = !item->IsMarked();
345 			break;
346 		}
347 
348 		case kMsgVolumeWhich:
349 		{
350 			BMenuItem* item;
351 			if (message->FindPointer("source", (void**)&item) != B_OK)
352 				return;
353 
354 			item->SetMarked(!item->IsMarked());
355 			fVolumeWhich = item->IsMarked()
356 				? VOLUME_USE_PHYS_OUTPUT : VOLUME_USE_MIXER;
357 
358 			if (_ConnectMixer() != B_OK
359 				&& fVolumeWhich == VOLUME_USE_PHYS_OUTPUT) {
360 				// unable to switch to physical output
361 				item->SetMarked(false);
362 				fVolumeWhich = VOLUME_USE_MIXER;
363 				_ConnectMixer();
364 			}
365 
366 			if (VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip())) {
367 				tip->SetWhich(fVolumeWhich);
368 				tip->Update();
369 			}
370 			break;
371 		}
372 
373 		case B_MOUSE_WHEEL_CHANGED:
374 		{
375 			float deltaY;
376 			if (message->FindFloat("be:wheel_delta_y", &deltaY) == B_OK
377 				&& deltaY != 0.0 && fMixerControl != NULL) {
378 				fMixerControl->ChangeVolumeBy(deltaY < 0 ? 6 : -6);
379 
380 				VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip());
381 				if (tip != NULL) {
382 					tip->Update();
383 					ShowToolTip(tip);
384 				}
385 			}
386 			break;
387 		}
388 
389 		case B_MEDIA_NEW_PARAMETER_VALUE:
390 		{
391 			if (fMixerControl != NULL && !fMixerControl->Connected())
392 				return;
393 
394 			bool setMuted = fMixerControl->Mute();
395 			if (setMuted != fMuted) {
396 				fMuted = setMuted;
397 				VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip());
398 				if (tip != NULL) {
399 					tip->SetMuteMessage(fMuted ? B_TRANSLATE("Muted") : NULL);
400 					tip->Update();
401 				}
402 				Invalidate();
403 			}
404 			break;
405 		}
406 
407 		case B_MEDIA_SERVER_STARTED:
408 			_ConnectMixer();
409 			break;
410 
411 		case B_MEDIA_SERVER_QUIT:
412 			_DisconnectMixer();
413 			break;
414 
415 		case B_MEDIA_NODE_CREATED:
416 		{
417 			// It's not enough to wait for B_MEDIA_SERVER_STARTED message, as
418 			// the mixer will still be getting loaded by the media server
419 			media_node mixerNode;
420 			media_node_id mixerNodeID;
421 			BMediaRoster* roster = BMediaRoster::CurrentRoster();
422 			if (roster != NULL
423 				&& message->FindInt32("media_node_id", &mixerNodeID) == B_OK
424 				&& roster->GetNodeFor(mixerNodeID, &mixerNode) == B_OK) {
425 				if (mixerNode.kind == B_SYSTEM_MIXER)
426 					_ConnectMixer();
427 				roster->ReleaseNode(mixerNode);
428 			}
429 			break;
430 		}
431 
432 		default:
433 			BView::MessageReceived(message);
434 			break;
435 	}
436 }
437 
438 
439 status_t
440 MediaReplicant::_LaunchByPath(const char* path)
441 {
442 	entry_ref ref;
443 	status_t status = get_ref_for_path(path, &ref);
444 	if (status != B_OK)
445 		return status;
446 
447 	status = be_roster->Launch(&ref);
448 	if (status != B_ALREADY_RUNNING)
449 		return status;
450 
451 	// The application runs already, bring it to front
452 
453 	app_info appInfo;
454 	status = be_roster->GetAppInfo(&ref, &appInfo);
455 	if (status != B_OK)
456 		return status;
457 
458 	return be_roster->ActivateApp(appInfo.team);
459 }
460 
461 
462 status_t
463 MediaReplicant::_LaunchBySignature(const char* signature)
464 {
465 	status_t status = be_roster->Launch(signature);
466 	if (status != B_ALREADY_RUNNING)
467 		return status;
468 
469 	// The application runs already, bring it to front
470 
471 	app_info appInfo;
472 	status = be_roster->GetAppInfo(signature, &appInfo);
473 	if (status != B_OK)
474 		return status;
475 
476 	return be_roster->ActivateApp(appInfo.team);
477 }
478 
479 
480 void
481 MediaReplicant::_Launch(const char* prettyName, const char* signature,
482 	directory_which base, const char* fileName)
483 {
484 	BPath path;
485 	status_t status = find_directory(base, &path);
486 	if (status == B_OK)
487 		path.Append(fileName);
488 
489 	// launch the application
490 	if (_LaunchBySignature(signature) != B_OK
491 		&& _LaunchByPath(path.Path()) != B_OK) {
492 		BString message = B_TRANSLATE("Couldn't launch ");
493 		message << prettyName;
494 
495 		BAlert* alert = new BAlert(
496 			B_TRANSLATE_COMMENT("desklink", "Title of an alert box"),
497 			message.String(), B_TRANSLATE("OK"));
498 		alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
499 		alert->Go();
500 	}
501 }
502 
503 
504 void
505 MediaReplicant::_LoadSettings()
506 {
507 	fDontBeep = false;
508 	fVolumeWhich = VOLUME_USE_MIXER;
509 
510 	BPath path;
511 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, false) < B_OK)
512 		return;
513 
514 	path.Append(kSettingsFile);
515 
516 	BFile settings(path.Path(), B_READ_ONLY);
517 	if (settings.InitCheck() < B_OK)
518 		return;
519 
520 	BMessage msg;
521 	if (msg.Unflatten(&settings) < B_OK)
522 		return;
523 
524 	msg.FindInt32("volwhich", &fVolumeWhich);
525 	msg.FindBool("dontbeep", &fDontBeep);
526 }
527 
528 
529 void
530 MediaReplicant::_SaveSettings()
531 {
532 	BPath path;
533 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, false) < B_OK)
534 		return;
535 
536 	path.Append(kSettingsFile);
537 
538 	BFile settings(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
539 	if (settings.InitCheck() < B_OK)
540 		return;
541 
542 	BMessage msg('CNFG');
543 	msg.AddInt32("volwhich", fVolumeWhich);
544 	msg.AddBool("dontbeep", fDontBeep);
545 
546 	ssize_t size = 0;
547 	msg.Flatten(&settings, &size);
548 }
549 
550 
551 void
552 MediaReplicant::_Init()
553 {
554 	image_info info;
555 	if (our_image(info) != B_OK)
556 		return;
557 
558 	BFile file(info.name, B_READ_ONLY);
559 	if (file.InitCheck() != B_OK)
560 		return;
561 
562 	BResources resources(&file);
563 	if (resources.InitCheck() != B_OK)
564 		return;
565 
566 	fIcon = _LoadIcon(resources, "Speaker");
567 	fMutedIcon = _LoadIcon(resources, "SpeakerMuted");
568 
569 	_LoadSettings();
570 
571 	SetToolTip(new VolumeToolTip(fVolumeWhich));
572 }
573 
574 
575 BBitmap*
576 MediaReplicant::_LoadIcon(BResources& resources, const char* name)
577 {
578 	size_t size;
579 	const void* data = resources.LoadResource(B_VECTOR_ICON_TYPE, name, &size);
580 	if (data == NULL)
581 		return NULL;
582 
583 	// Scale tray icon
584 	BBitmap* icon = new BBitmap(Bounds(), B_RGBA32);
585 	if (icon->InitCheck() != B_OK
586 		|| BIconUtils::GetVectorIcon((const uint8*)data, size, icon) != B_OK) {
587 		delete icon;
588 		return NULL;
589 	}
590 	return icon;
591 }
592 
593 
594 void
595 MediaReplicant::_DisconnectMixer()
596 {
597 	BMediaRoster* roster = BMediaRoster::CurrentRoster();
598 	if (roster == NULL)
599 		return;
600 
601 	roster->StopWatching(this, B_MEDIA_SERVER_STARTED);
602 	roster->StopWatching(this, B_MEDIA_SERVER_QUIT);
603 	roster->StopWatching(this, B_MEDIA_NODE_CREATED);
604 
605 	if (fMixerControl == NULL)
606 		return;
607 
608 	if (fMixerControl->MuteNode() != media_node::null) {
609 		roster->StopWatching(this, fMixerControl->MuteNode(),
610 			B_MEDIA_NEW_PARAMETER_VALUE);
611 	}
612 
613 	delete fMixerControl;
614 	fMixerControl = NULL;
615 }
616 
617 
618 status_t
619 MediaReplicant::_ConnectMixer()
620 {
621 	_DisconnectMixer();
622 
623 	BMediaRoster* roster = BMediaRoster::Roster();
624 	if (roster == NULL)
625 		return B_ERROR;
626 
627 	roster->StartWatching(this, B_MEDIA_SERVER_STARTED);
628 	roster->StartWatching(this, B_MEDIA_SERVER_QUIT);
629 	roster->StartWatching(this, B_MEDIA_NODE_CREATED);
630 
631 	fMixerControl = new MixerControl(fVolumeWhich);
632 
633 	const char* errorString = NULL;
634 	float volume = 0.0;
635 	fMixerControl->Connect(fVolumeWhich, &volume, &errorString);
636 
637 	if (errorString != NULL) {
638 		SetToolTip(errorString);
639 		delete fMixerControl;
640 		fMixerControl = NULL;
641 		return B_ERROR;
642 	}
643 
644 	if (fMixerControl->MuteNode() != media_node::null) {
645 		roster->StartWatching(this, fMixerControl->MuteNode(),
646 			B_MEDIA_NEW_PARAMETER_VALUE);
647 		fMuted = fMixerControl->Mute();
648 	}
649 
650 	return B_OK;
651 }
652 
653 
654 //	#pragma mark -
655 
656 
657 extern "C" BView*
658 instantiate_deskbar_item(float maxWidth, float maxHeight)
659 {
660 	return new MediaReplicant(BRect(0, 0, maxHeight - 1, maxHeight - 1),
661 		kReplicantName);
662 }
663 
664