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