diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 81ca7d51f83..3ba168f32f6 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -514,6 +514,24 @@ Do you want to close all tabs? + + Cancel + + + Close tab + + + Do you want to close this tab? + + + Cancel + + + Close pane + + + Do you want to close this pane? + Cancel diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 21e31fb6dd3..6becb409997 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -413,6 +413,25 @@ namespace winrt::TerminalApp::implementation } } + // Check if we should warn before closing this tab based on the + // confirmCloseOn flags (e.g. multiple panes, always, etc.) + { + const auto tabImpl = _GetTabImpl(tab); + if (tabImpl && _ShouldWarnOnCloseTab(tabImpl)) + { + const auto weak = get_weak(); + + auto warningResult = co_await _ShowCloseTabWarningDialog(); + + strong = weak.get(); + + if (!strong || warningResult != ContentDialogResult::Primary) + { + co_return; + } + } + } + auto t = winrt::get_self(tab); auto actions = t->BuildStartupActions(BuildStartupKind::None); _AddPreviouslyClosedPaneOrTab(std::move(actions)); @@ -782,6 +801,28 @@ namespace winrt::TerminalApp::implementation if (const auto pane{ activeTab->GetActivePane() }) { const auto weak = get_weak(); + + // Check if we should warn before closing a single pane + // (only triggers on Always — or MultipleProcesses in the future) + const auto flags = _settings.GlobalSettings().ConfirmCloseOn(); + if (WI_IsFlagSet(flags, ConfirmCloseOn::Always)) + { + auto warningResult = co_await _ShowClosePaneWarningDialog(); + + if (const auto strong = weak.get()) + { + if (warningResult != ContentDialogResult::Primary) + { + co_return; + } + } + else + { + co_return; + } + } + // TODO GH#6549: ConfirmCloseOn::MultipleProcesses check here + if (co_await _PaneConfirmCloseReadOnly(pane)) { if (const auto strong = weak.get()) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index e98de6b9957..4aedabc020f 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -906,6 +906,22 @@ namespace winrt::TerminalApp::implementation return _ShowDialogHelper(L"CloseAllDialog"); } + // Method Description: + // - Displays a dialog for warnings found while closing a tab that has + // multiple panes or other conditions that warrant a warning. + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseTabWarningDialog() + { + return _ShowDialogHelper(L"CloseTabDialog"); + } + + // Method Description: + // - Displays a dialog for warnings found while closing a single pane + // (e.g. when confirmCloseOn includes "always"). + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowClosePaneWarningDialog() + { + return _ShowDialogHelper(L"ClosePaneDialog"); + } + // Method Description: // - Displays a dialog for warnings found while closing the terminal tab marked as read-only winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseReadOnlyDialog() @@ -2308,12 +2324,84 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - Close the terminal app. If there is more - // than one tab opened, show a warning dialog. + // - Determines whether a close-window action should show a confirmation + // dialog, based on the confirmCloseOn flags and the current window state. + // Arguments: + // - + // Return Value: + // - true if a warning dialog should be shown before closing the window. + bool TerminalPage::_ShouldWarnOnClose() const + { + const auto flags = _settings.GlobalSettings().ConfirmCloseOn(); + + // Always flag means warn unconditionally. + if (WI_IsFlagSet(flags, ConfirmCloseOn::Always)) + { + return true; + } + + // MultipleTabs: warn if there's more than one tab. + if (WI_IsFlagSet(flags, ConfirmCloseOn::MultipleTabs) && _HasMultipleTabs()) + { + return true; + } + + // MultiplePanes: warn if any tab has more than one pane. + if (WI_IsFlagSet(flags, ConfirmCloseOn::MultiplePanes)) + { + for (const auto tab : _tabs) + { + if (const auto impl = _GetTabImpl(tab)) + { + if (impl->GetLeafPaneCount() > 1) + { + return true; + } + } + } + } + + // TODO GH#6549: ConfirmCloseOn::MultipleProcesses + // Needs TermControl to expose whether >1 client process is connected. + + return false; + } + + // Method Description: + // - Determines whether closing a specific tab should show a confirmation + // dialog, based on the confirmCloseOn flags and the tab's state. + // Arguments: + // - tab: The tab being closed. + // Return Value: + // - true if a warning dialog should be shown before closing the tab. + bool TerminalPage::_ShouldWarnOnCloseTab(const winrt::com_ptr& tab) const + { + const auto flags = _settings.GlobalSettings().ConfirmCloseOn(); + + // Always flag means warn unconditionally. + if (WI_IsFlagSet(flags, ConfirmCloseOn::Always)) + { + return true; + } + + // MultiplePanes: warn if this tab has more than one pane. + if (WI_IsFlagSet(flags, ConfirmCloseOn::MultiplePanes) && tab->GetLeafPaneCount() > 1) + { + return true; + } + + // TODO GH#6549: ConfirmCloseOn::MultipleProcesses + // Needs TermControl to expose whether >1 client process is connected. + + return false; + } + + // Method Description: + // - Close the terminal app. If the confirmCloseOn flags indicate we should + // warn for the current window state, show a warning dialog. safe_void_coroutine TerminalPage::CloseWindow() { - if (_HasMultipleTabs() && - _settings.GlobalSettings().ConfirmCloseAllTabs() && + if (_ShouldWarnOnClose() && !_displayingCloseDialog) { if (_newTabButton && _newTabButton.Flyout()) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 4b48cc0e9d9..f83419f3a15 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -303,6 +303,8 @@ namespace winrt::TerminalApp::implementation void _ShowAboutDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowQuitDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowCloseWarningDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowCloseTabWarningDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowClosePaneWarningDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowCloseReadOnlyDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowMultiLinePasteWarningDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog(); @@ -400,6 +402,8 @@ namespace winrt::TerminalApp::implementation TerminalApp::Tab _GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept; void _HandleClosePaneRequested(std::shared_ptr pane); + bool _ShouldWarnOnClose() const; + bool _ShouldWarnOnCloseTab(const winrt::com_ptr& tab) const; safe_void_coroutine _SetFocusedTab(const winrt::TerminalApp::Tab tab); safe_void_coroutine _CloseFocusedPane(); void _ClosePanes(weak_ref weakTab, std::vector paneIds); diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 40e3b838c37..5262a0d66ec 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -98,6 +98,18 @@ x:Load="False" DefaultButton="Primary" /> + + + + - - - + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp index 55a239e8e5f..123e7f96ee0 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp @@ -18,4 +18,68 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation INITIALIZE_BINDABLE_ENUM_SETTING(TabSwitcherMode, TabSwitcherMode, TabSwitcherMode, L"Globals_TabSwitcherMode", L"Content"); INITIALIZE_BINDABLE_ENUM_SETTING(CopyFormat, CopyFormat, winrt::Microsoft::Terminal::Control::CopyFormat, L"Globals_CopyFormat", L"Content"); } + + hstring InteractionViewModel::ConfirmCloseOnPreview() const + { + const auto flags = ConfirmCloseOn(); + if (WI_IsFlagSet(flags, Model::ConfirmCloseOn::Always)) + { + return RS_(L"Globals_ConfirmCloseOnAlways/Content"); + } + else if (flags == Model::ConfirmCloseOn::Never) + { + return RS_(L"Globals_ConfirmCloseOnNever"); + } + + std::vector resultList; + resultList.reserve(2); + if (WI_IsFlagSet(flags, Model::ConfirmCloseOn::MultipleTabs)) + { + resultList.emplace_back(RS_(L"Globals_ConfirmCloseOnMultipleTabs/Content")); + } + if (WI_IsFlagSet(flags, Model::ConfirmCloseOn::MultiplePanes)) + { + resultList.emplace_back(RS_(L"Globals_ConfirmCloseOnMultiplePanes/Content")); + } + + hstring result{}; + for (auto&& entry : resultList) + { + if (result.empty()) + { + result = entry; + } + else + { + result = result + L", " + entry; + } + } + return result; + } + + bool InteractionViewModel::IsConfirmCloseOnFlagSet(const uint32_t flag) + { + return (WI_EnumValue(ConfirmCloseOn()) & flag) == flag; + } + + void InteractionViewModel::SetConfirmCloseOnAlways(winrt::Windows::Foundation::IReference on) + { + auto current = ConfirmCloseOn(); + WI_UpdateFlag(current, Model::ConfirmCloseOn::Always, winrt::unbox_value(on)); + ConfirmCloseOn(current); + } + + void InteractionViewModel::SetConfirmCloseOnMultipleTabs(winrt::Windows::Foundation::IReference on) + { + auto current = ConfirmCloseOn(); + WI_UpdateFlag(current, Model::ConfirmCloseOn::MultipleTabs, winrt::unbox_value(on)); + ConfirmCloseOn(current); + } + + void InteractionViewModel::SetConfirmCloseOnMultiplePanes(winrt::Windows::Foundation::IReference on) + { + auto current = ConfirmCloseOn(); + WI_UpdateFlag(current, Model::ConfirmCloseOn::MultiplePanes, winrt::unbox_value(on)); + ConfirmCloseOn(current); + } } diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h index c3e48715f7a..bf2e6314c7a 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h @@ -30,7 +30,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, DetectURLs); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, SearchWebDefaultQueryUrl); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, WordDelimiters); - PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, ConfirmCloseAllTabs); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, ConfirmCloseOn); + hstring ConfirmCloseOnPreview() const; + bool IsConfirmCloseOnFlagSet(const uint32_t flag); + void SetConfirmCloseOnAlways(winrt::Windows::Foundation::IReference on); + void SetConfirmCloseOnMultipleTabs(winrt::Windows::Foundation::IReference on); + void SetConfirmCloseOnMultiplePanes(winrt::Windows::Foundation::IReference on); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, InputServiceWarning); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, WarnAboutLargePaste); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, WarnAboutMultiLinePaste); diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl index ba77aec55f5..b0e46d29c0e 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl @@ -27,7 +27,12 @@ namespace Microsoft.Terminal.Settings.Editor PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, DetectURLs); PERMANENT_OBSERVABLE_PROJECTED_SETTING(String, SearchWebDefaultQueryUrl); PERMANENT_OBSERVABLE_PROJECTED_SETTING(String, WordDelimiters); - PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ConfirmCloseAllTabs); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(Microsoft.Terminal.Settings.Model.ConfirmCloseOn, ConfirmCloseOn); + String ConfirmCloseOnPreview { get; }; + Boolean IsConfirmCloseOnFlagSet(UInt32 flag); + void SetConfirmCloseOnAlways(Windows.Foundation.IReference on); + void SetConfirmCloseOnMultipleTabs(Windows.Foundation.IReference on); + void SetConfirmCloseOnMultiplePanes(Windows.Foundation.IReference on); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, InputServiceWarning); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, WarnAboutLargePaste); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Microsoft.Terminal.Control.WarnAboutMultiLinePaste, WarnAboutMultiLinePaste); diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index dc20fbecbf7..a1af610dc73 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -2197,6 +2197,26 @@ Warn when closing more than one tab Header for a control to toggle whether to show a confirm dialog box when closing the application with multiple tabs open. + + Confirm before closing + Header for a group of checkboxes controlling when to show a confirmation dialog before closing. + + + Always + Checkbox label: always confirm before closing a window, tab, or pane. + + + Multiple tabs + Checkbox label: confirm before closing a window with more than one tab. + + + Multiple panes + Checkbox label: confirm before closing a tab with more than one pane. + + + Never + Preview text shown when no confirm-close-on flags are set. + Warn when "Touch Keyboard and Handwriting Panel Service" is disabled diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 254e52bdbcb..e64dd2296b5 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -160,7 +160,16 @@ void GlobalAppSettings::LayerJson(const Json::Value& json, const OriginTag origi _fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyInputServiceWarningKey, _InputServiceWarning) || _fixupsAppliedDuringLoad; _fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyWarnAboutLargePasteKey, _WarnAboutLargePaste) || _fixupsAppliedDuringLoad; _fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyWarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste) || _fixupsAppliedDuringLoad; - _fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyConfirmCloseAllTabsKey, _ConfirmCloseAllTabs) || _fixupsAppliedDuringLoad; + // GH#6549 - Migrate legacy "confirmCloseAllTabs" boolean to the new + // "confirmCloseOn" flags enum. true -> MultipleTabs|MultiplePanes, false -> Never. + { + std::optional legacyConfirmClose; + if (JsonUtils::GetValueForKey(json, LegacyConfirmCloseAllTabsKey, legacyConfirmClose)) + { + _ConfirmCloseOn = legacyConfirmClose.value() ? winrt::Microsoft::Terminal::Settings::Model::ConfirmCloseOn::MultipleTabs | winrt::Microsoft::Terminal::Settings::Model::ConfirmCloseOn::MultiplePanes : winrt::Microsoft::Terminal::Settings::Model::ConfirmCloseOn::Never; + _fixupsAppliedDuringLoad = true; + } + } #define GLOBAL_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \ JsonUtils::GetValueForKey(json, jsonKey, _##name); \ diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 77bcfc494da..2fba21e1c30 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -50,6 +50,17 @@ namespace Microsoft.Terminal.Settings.Model AfterCurrentTab, }; + [flags] + enum ConfirmCloseOn + { + Never = 0x0, + Always = 0x1, + MultipleTabs = 0x2, + MultiplePanes = 0x4, + // TODO GH#6549: Future - requires TermControl to expose client count + // MultipleProcesses = 0x8, + }; + [default_interface] runtimeclass GlobalAppSettings { Guid DefaultProfile; @@ -61,7 +72,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, ShowTabsFullscreen); INHERITABLE_SETTING(NewTabPosition, NewTabPosition); INHERITABLE_SETTING(Boolean, ShowTitleInTitlebar); - INHERITABLE_SETTING(Boolean, ConfirmCloseAllTabs); + INHERITABLE_SETTING(ConfirmCloseOn, ConfirmCloseOn); INHERITABLE_SETTING(String, Language); INHERITABLE_SETTING(Microsoft.UI.Xaml.Controls.TabViewWidthMode, TabWidthMode); INHERITABLE_SETTING(Boolean, UseAcrylicInTabRow); diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 4f99bda187a..1a694b59345 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -38,7 +38,7 @@ Author(s): X(bool, AlwaysShowTabs, "alwaysShowTabs", true) \ X(Model::NewTabPosition, NewTabPosition, "newTabPosition", Model::NewTabPosition::AfterLastTab) \ X(bool, ShowTitleInTitlebar, "showTerminalTitleInTitlebar", true) \ - X(bool, ConfirmCloseAllTabs, "warning.confirmCloseAllTabs", true) \ + X(Model::ConfirmCloseOn, ConfirmCloseOn, "warning.confirmCloseOn", Model::ConfirmCloseOn::MultipleTabs | Model::ConfirmCloseOn::MultiplePanes) \ X(Model::ThemePair, Theme, "theme") \ X(hstring, Language, "language") \ X(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, "tabWidthMode", winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal) \ diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 9c071945a29..994f040869a 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -88,6 +88,38 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Core::MatchMode) }; }; +JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::ConfirmCloseOn) +{ + static constexpr std::array mappings = { + pair_type{ "never", AllClear }, + pair_type{ "always", ValueType::Always }, + pair_type{ "multipleTabs", ValueType::MultipleTabs }, + pair_type{ "multiplePanes", ValueType::MultiplePanes }, + // TODO GH#6549: Future - requires TermControl to expose client count + // pair_type{ "multipleProcesses", ValueType::MultipleProcesses }, + }; + + auto FromJson(const Json::Value& json) + { + // Support legacy boolean: true -> MultipleTabs|MultiplePanes, false -> Never + if (json.isBool()) + { + return json.asBool() ? ValueType::MultipleTabs | ValueType::MultiplePanes : AllClear; + } + return BaseFlagMapper::FromJson(json); + } + + bool CanConvert(const Json::Value& json) + { + return BaseFlagMapper::CanConvert(json) || json.isBool(); + } + + Json::Value ToJson(const ::winrt::Microsoft::Terminal::Settings::Model::ConfirmCloseOn& val) + { + return BaseFlagMapper::ToJson(val); + } +}; + JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::BellStyle) { static constexpr std::array mappings = { diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 9d9474cd9c5..7eb3321e401 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -24,7 +24,7 @@ "showAdminShield": true, // Miscellaneous - "confirmCloseAllTabs": true, + "warning.confirmCloseOn": ["multipleTabs", "multiplePanes"], "theme": "dark", "snapToGridOnResize": true, "disableAnimations": false, diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index 3112f83939e..40f041907bb 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -125,7 +125,7 @@ namespace SettingsModelUnitTests "trimPaste": true, - "warning.confirmCloseAllTabs" : true, + "warning.confirmCloseOn": ["multipleTabs", "multiplePanes"], "warning.inputService" : true, "warning.largePaste" : true, "warning.multiLinePaste" : "automatic",