diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 0459c36bcfa..2fcdab15b9b 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -33,6 +33,7 @@ static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Wi winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_focusedBorderBrush = { nullptr }; winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_unfocusedBorderBrush = { nullptr }; +winrt::Windows::Media::Playback::MediaPlayer Pane::s_bellPlayer = { nullptr }; Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFocused) : _control{ control }, @@ -69,6 +70,36 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo _FocusFirstChild(); e.Handled(true); }); + + if (!s_bellPlayer) + { + try + { + s_bellPlayer = winrt::Windows::Media::Playback::MediaPlayer(); + if (s_bellPlayer) + { + // BODGY + // + // Manually leak a ref to the MediaPlayer we just instantiated. + // We're doing this just like the way we do in AppHost with the + // App itself. + // + // We have to do this because there's some bug in the OS with + // the way a MediaPlayer gets torn down. At time of writing (Nov + // 2021), if you search for `remove_SoundLevelChanged` in the OS + // repo, you'll find a pile of bugs. + // + // We tried moving the MediaPlayer singleton up to the + // TerminalPage, but alas, that teardown had the same problem. + // So _whatever_. We'll leak it here. It needs to last the + // lifetime of the app anyways, and it'll get cleaned up when the + // Terminal is closed, so whatever. + winrt::Windows::Media::Playback::MediaPlayer p{ s_bellPlayer }; + ::winrt::detach_abi(p); + } + } + CATCH_LOG(); + } } Pane::Pane(std::shared_ptr first, @@ -1028,6 +1059,23 @@ void Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundatio } } +winrt::fire_and_forget Pane::_playBellSound(winrt::Windows::Foundation::Uri uri) +{ + auto weakThis{ shared_from_this() }; + + co_await winrt::resume_foreground(_root.Dispatcher()); + if (auto pane{ weakThis.get() }) + { + if (s_bellPlayer) + { + const auto source{ winrt::Windows::Media::Core::MediaSource::CreateFromUri(uri) }; + const auto item{ winrt::Windows::Media::Playback::MediaPlaybackItem(source) }; + s_bellPlayer.Source(item); + s_bellPlayer.Play(); + } + } +} + // Method Description: // - Plays a warning note when triggered by the BEL control character, // using the sound configured for the "Critical Stop" system event.` @@ -1051,8 +1099,18 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible)) { // Audible is set, play the sound - const auto soundAlias = reinterpret_cast(SND_ALIAS_SYSTEMHAND); - PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); + auto sounds{ _profile.BellSound() }; + if (sounds && sounds.Size() > 0) + { + winrt::hstring soundPath{ wil::ExpandEnvironmentStringsW(sounds.GetAt(rand() % sounds.Size()).c_str()) }; + winrt::Windows::Foundation::Uri uri{ soundPath }; + _playBellSound(uri); + } + else + { + const auto soundAlias = reinterpret_cast(SND_ALIAS_SYSTEMHAND); + PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); + } } if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window)) diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index b9918e8271c..acef331a379 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -241,6 +241,8 @@ class Pane : public std::enable_shared_from_this bool _zoomed{ false }; + static winrt::Windows::Media::Playback::MediaPlayer s_bellPlayer; + bool _IsLeaf() const noexcept; bool _HasFocusedChild() const noexcept; void _SetupChildCloseHandlers(); @@ -292,6 +294,8 @@ class Pane : public std::enable_shared_from_this SplitState _convertAutomaticOrDirectionalSplitState(const winrt::Microsoft::Terminal::Settings::Model::SplitDirection& splitType) const; + winrt::fire_and_forget _playBellSound(winrt::Windows::Foundation::Uri uri); + // Function Description: // - Returns true if the given direction can be used with the given split // type. diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index b41d25d3523..92e8003e4ef 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -45,6 +45,9 @@ #include #include #include +#include +#include +#include #include #include diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index be29f15decd..ceb47069311 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -307,9 +307,16 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils val.reserve(json.size()); ConversionTrait trait; - for (const auto& element : json) + if (json.isArray()) + { + for (const auto& element : json) + { + val.push_back(trait.FromJson(element)); + } + } + else { - val.push_back(trait.FromJson(element)); + val.push_back(trait.FromJson(json)); } return val; @@ -318,7 +325,9 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils bool CanConvert(const Json::Value& json) const { ConversionTrait trait; - return json.isArray() && std::all_of(json.begin(), json.end(), [trait](const auto& json) mutable -> bool { return trait.CanConvert(json); }); + // If there's only one element provided, then see if we can convert + // that single element into a length-1 array + return (json.isArray() && std::all_of(json.begin(), json.end(), [trait](const auto& json) mutable -> bool { return trait.CanConvert(json); })) || trait.CanConvert(json); } Json::Value ToJson(const std::vector& val) diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index a190e01669c..0d5faab1769 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -74,7 +74,8 @@ Author(s): X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Graceful) \ X(hstring, TabTitle, "tabTitle") \ X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \ - X(bool, UseAtlasEngine, "experimental.useAtlasEngine", false) + X(bool, UseAtlasEngine, "experimental.useAtlasEngine", false) \ + X(Windows::Foundation::Collections::IVector, BellSound, "bellSound", nullptr) #define MTSM_FONT_SETTINGS(X) \ X(hstring, FontFace, "face", DEFAULT_FONT_FACE) \ diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp index 163731fab37..aae5e76b45c 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.cpp +++ b/src/cascadia/TerminalSettingsModel/Profile.cpp @@ -30,6 +30,7 @@ static constexpr std::string_view FontInfoKey{ "font" }; static constexpr std::string_view PaddingKey{ "padding" }; static constexpr std::string_view TabColorKey{ "tabColor" }; static constexpr std::string_view UnfocusedAppearanceKey{ "unfocusedAppearance" }; +static constexpr std::string_view BellSoundKey{ "bellSound" }; Profile::Profile(guid guid) noexcept : _Guid(guid) @@ -104,6 +105,7 @@ winrt::com_ptr Profile::CopySettings() const profile->_Hidden = _Hidden; profile->_TabColor = _TabColor; profile->_Padding = _Padding; + profile->_Origin = _Origin; profile->_FontInfo = *fontInfo; profile->_DefaultAppearance = *defaultAppearance; @@ -178,6 +180,7 @@ void Profile::LayerJson(const Json::Value& json) #define PROFILE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \ JsonUtils::GetValueForKey(json, jsonKey, _##name); + MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_LAYER_JSON) #undef PROFILE_SETTINGS_LAYER_JSON @@ -318,6 +321,7 @@ Json::Value Profile::ToJson() const #define PROFILE_SETTINGS_TO_JSON(type, name, jsonKey, ...) \ JsonUtils::SetValueForKey(json, jsonKey, _##name); + MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_TO_JSON) #undef PROFILE_SETTINGS_TO_JSON diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 35df4468350..41b9c9e018f 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -80,5 +80,6 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, AltGrAliasing); INHERITABLE_PROFILE_SETTING(BellStyle, BellStyle); INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine); + INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector, BellSound); } } diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index cc4654cb060..3a77f9f940a 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -348,7 +348,7 @@ void AppHost::Initialize() _window->SetContent(_logic.GetRoot()); _window->OnAppInitialized(); - // THIS IS A HACK + // BODGY // // We've got a weird crash that happens terribly inconsistently, but pretty // readily on migrie's laptop, only in Debug mode. Apparently, there's some