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
49static const uint32 kMsgOpenMediaSettings = 'mese';
50static const uint32 kMsgOpenSoundSettings = 'sose';
51static const uint32 kMsgOpenMediaPlayer = 'omep';
52static const uint32 kMsgToggleBeep = 'tdbp';
53static const uint32 kMsgVolumeWhich = 'svwh';
54
55static const char* kReplicantName = "MediaReplicant";
56	// R5 name needed, Media prefs manel removes by name
57
58static const char* kSettingsFile = "x-vnd.Haiku-desklink";
59
60
61class VolumeToolTip : public BTextToolTip {
62public:
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
109private:
110	int32			fWhich;
111	BString			fMuteMessage;
112};
113
114
115class MediaReplicant : public BView {
116public:
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
135private:
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
162status_t
163our_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
179MediaReplicant::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
191MediaReplicant::MediaReplicant(BMessage* message)
192	:
193	BView(message),
194	fMixerControl(NULL),
195	fVolumeSlider(NULL),
196	fMuted(false)
197{
198	_Init();
199}
200
201
202MediaReplicant::~MediaReplicant()
203{
204	delete fIcon;
205	_SaveSettings();
206	_DisconnectMixer();
207}
208
209
210MediaReplicant*
211MediaReplicant::Instantiate(BMessage* data)
212{
213	if (!validate_instantiation(data, kReplicantName))
214		return NULL;
215
216	return new(std::nothrow) MediaReplicant(data);
217}
218
219
220status_t
221MediaReplicant::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
231void
232MediaReplicant::AttachedToWindow()
233{
234	AdoptParentColors();
235
236	_ConnectMixer();
237
238	BView::AttachedToWindow();
239}
240
241
242void
243MediaReplicant::Draw(BRect rect)
244{
245	SetDrawingMode(B_OP_OVER);
246	DrawBitmap(fMuted ? fMutedIcon : fIcon);
247}
248
249
250void
251MediaReplicant::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
318void
319MediaReplicant::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
439status_t
440MediaReplicant::_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
462status_t
463MediaReplicant::_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
480void
481MediaReplicant::_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
504void
505MediaReplicant::_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
529void
530MediaReplicant::_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
551void
552MediaReplicant::_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
575BBitmap*
576MediaReplicant::_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
594void
595MediaReplicant::_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
618status_t
619MediaReplicant::_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
657extern "C" BView*
658instantiate_deskbar_item(float maxWidth, float maxHeight)
659{
660	return new MediaReplicant(BRect(0, 0, maxHeight - 1, maxHeight - 1),
661		kReplicantName);
662}
663
664