From fe8730ad3f138c7b8df8d2f47fa6f17df96852ef Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Thu, 2 Apr 2026 17:06:36 -0700 Subject: [PATCH 1/2] Replace `confirmCloseAllTabs` with `confirmOnClose` --- doc/cascadia/profiles.schema.json | 13 +- .../TerminalApp/AppActionHandlers.cpp | 4 +- .../Resources/en-US/Resources.resw | 44 +++-- src/cascadia/TerminalApp/TabManagement.cpp | 85 +++++++++- src/cascadia/TerminalApp/TerminalPage.cpp | 152 +++++++++++++++--- src/cascadia/TerminalApp/TerminalPage.h | 20 ++- src/cascadia/TerminalApp/TerminalPage.xaml | 13 +- .../TerminalSettingsEditor/Interaction.xaml | 13 +- .../InteractionViewModel.cpp | 1 + .../InteractionViewModel.h | 2 +- .../InteractionViewModel.idl | 8 +- .../Resources/en-US/Resources.resw | 20 +++ .../TerminalSettingsModel/EnumMappings.cpp | 1 + .../TerminalSettingsModel/EnumMappings.h | 1 + .../TerminalSettingsModel/EnumMappings.idl | 1 + .../GlobalAppSettings.cpp | 11 +- .../GlobalAppSettings.idl | 9 +- .../TerminalSettingsModel/MTSMSettings.h | 2 +- .../TerminalSettingsSerializationHelpers.h | 19 +++ .../TerminalSettingsModel/defaults.json | 2 +- .../SerializationTests.cpp | 2 +- 21 files changed, 352 insertions(+), 71 deletions(-) diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 1756b7cc9df..b4e4fede1b7 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -2654,10 +2654,21 @@ "type": "string" }, "warning.confirmCloseAllTabs": { + "deprecated": true, + "description": "[Deprecated] Use \"warning.confirmOnClose\" instead.", "default": true, - "description": "When set to \"true\" closing a window with multiple tabs open will require confirmation. When set to \"false\", the confirmation dialog will not appear.", "type": "boolean" }, + "warning.confirmOnClose": { + "default": "automatic", + "description": "Controls when a confirmation dialog appears before closing tabs or windows.", + "enum": [ + "never", + "automatic", + "always" + ], + "type": "string" + }, "useTabSwitcher": { "description": "[Deprecated] Replaced with the \"tabSwitcherMode\" setting.", "default": true, diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 66372a3e9e9..bb5d859c856 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -801,7 +801,7 @@ namespace winrt::TerminalApp::implementation _RemoveTabs(tabsToRemove); - actionArgs.Handled(true); + actionArgs.Handled(!tabsToRemove.empty()); } } @@ -837,7 +837,7 @@ namespace winrt::TerminalApp::implementation // tab row, until you mouse over them. Probably has something to do // with tabs not resizing down until there's a mouse exit event. - actionArgs.Handled(true); + actionArgs.Handled(!tabsToRemove.empty()); } } diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 81ca7d51f83..9b165e1c481 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -496,24 +496,48 @@ Third-Party notices A hyperlink name for the Terminal's third-party notices - + Cancel - - Close all - - + Do you want to close all windows? - - Cancel - - + Close all - + Do you want to close all tabs? + + Close all + + + Do you want to close this tab? + + + Close tab + + + Do you want to close this pane? + + + Close pane + + + Do you want to close these tabs? + + + Close tabs + + + Do you want to close these panes? + + + Close panes + + + Don't ask me again + Cancel diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 21e31fb6dd3..ef917e0ff7d 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -394,7 +394,9 @@ namespace winrt::TerminalApp::implementation // - Removes the tab (both TerminalControl and XAML) after prompting for approval // Arguments: // - tab: the tab to remove - winrt::Windows::Foundation::IAsyncAction TerminalPage::_HandleCloseTabRequested(winrt::TerminalApp::Tab tab) + // - skipConfirmClose: if true, skip the confirmOnClose check. Used when + // an aggregate confirmation has already been shown (i.e. close other tabs) + winrt::Windows::Foundation::IAsyncAction TerminalPage::_HandleCloseTabRequested(winrt::TerminalApp::Tab tab, bool skipConfirmClose) { winrt::com_ptr strong; @@ -413,6 +415,24 @@ namespace winrt::TerminalApp::implementation } } + // Skip the per-tab confirmOnClose check when the caller has already + // shown an aggregate confirmation dialog (e.g. _RemoveTabs). + if (!skipConfirmClose) + { + const auto tabImpl = _GetTabImpl(tab); + if (tabImpl && _ShouldWarnOnCloseTab(tabImpl)) + { + const auto weak = get_weak(); + + auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::Tab); + 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 +802,22 @@ 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 — Automatic doesn't warn for single pane) + const auto setting = _settings.GlobalSettings().ConfirmOnClose(); + if (setting == ConfirmOnClose::Always) + { + // If this is the last pane, closing it closes the tab, + // so use the tab dialog text instead. + const auto kind = activeTab->GetLeafPaneCount() == 1 ? ConfirmCloseDialogKind::Tab : ConfirmCloseDialogKind::Pane; + auto warningResult = co_await _ShowConfirmCloseDialog(kind); + if (!weak.get() || warningResult != ContentDialogResult::Primary) + { + co_return; + } + } + if (co_await _PaneConfirmCloseReadOnly(pane)) { if (const auto strong = weak.get()) @@ -795,10 +831,33 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Close all panes with the given IDs sequentially. + // - Shows a single aggregate confirmation dialog upfront if the confirmOnClose setting warrants it. // Arguments: - // - weakTab: weak reference to the tab that the pane belongs to. + // - weakTab: weak reference to the tab that the panes belong to. // - paneIds: collection of the IDs of the panes that are marked for removal. - void TerminalPage::_ClosePanes(weak_ref weakTab, std::vector paneIds) + safe_void_coroutine TerminalPage::_ClosePanes(weak_ref weakTab, std::vector paneIds) + { + // Show a single aggregate confirmation for closing multiple panes. + if (_settings.GlobalSettings().ConfirmOnClose() != ConfirmOnClose::Never) + { + const auto weak = get_weak(); + auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::MultiplePanes); + if (!weak.get() || warningResult != ContentDialogResult::Primary) + { + co_return; + } + } + _CloseRemainingPanes(weakTab, std::move(paneIds)); + } + + // Method Description: + // - Recursively closes panes by ID, chaining each close via the + // ClosedByParent callback. Called after confirmation has already + // been handled by _ClosePanes. + // Arguments: + // - weakTab: weak reference to the tab that the panes belong to + // - paneIds: remaining pane IDs to close + void TerminalPage::_CloseRemainingPanes(weak_ref weakTab, std::vector paneIds) { if (auto strongTab{ weakTab.get() }) { @@ -813,10 +872,9 @@ namespace winrt::TerminalApp::implementation pane->ClosedByParent([ids{ std::move(paneIds) }, weakThis{ get_weak() }, weakTab]() { if (auto strongThis{ weakThis.get() }) { - strongThis->_ClosePanes(weakTab, std::move(ids)); + strongThis->_CloseRemainingPanes(weakTab, std::move(ids)); } }); - // Close the pane which will eventually trigger the closed by parent event _HandleClosePaneRequested(pane); break; @@ -841,18 +899,33 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Closes provided tabs one by one + // - Shows a single aggregate confirmation dialog upfront if the confirmOnClose setting warrants it. // Arguments: // - tabs - tabs to remove safe_void_coroutine TerminalPage::_RemoveTabs(const std::vector tabs) { + if (tabs.empty()) + { + co_return; + } + + // Show a single aggregate confirmation instead of per-tab dialogs. const auto weak = get_weak(); + if (_settings.GlobalSettings().ConfirmOnClose() != ConfirmOnClose::Never) + { + auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::MultipleTabs); + if (!weak.get() || warningResult != ContentDialogResult::Primary) + { + co_return; + } + } for (auto& tab : tabs) { winrt::Windows::Foundation::IAsyncAction action{ nullptr }; if (const auto strong = weak.get()) { - action = _HandleCloseTabRequested(tab); + action = _HandleCloseTabRequested(tab, /*skipConfirmClose*/ true); } if (!action) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 79bbc4feb61..a3dee24eeb6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -884,26 +884,63 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - Displays a dialog to warn the user that they are about to close all open windows. - // Once the user clicks the OK button, shut down the application. - // If cancel is clicked, the dialog will close. + // - Displays the unified close confirmation dialog configured for the + // given scenario. Resets the "don't ask me again" checkbox before showing. + // If the user confirms and checked "don't ask me again", sets + // confirmOnClose to Never and writes settings to disk. // - Only one dialog can be visible at a time. If another dialog is visible // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowQuitDialog() + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowConfirmCloseDialog(ConfirmCloseDialogKind kind) { - return _ShowDialogHelper(L"QuitDialog"); - } + // Load the dialog (triggers x:Load) and configure its strings. + const auto dialog = FindName(L"ConfirmCloseDialog").as(); + switch (kind) + { + case ConfirmCloseDialogKind::CloseAll: + dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_CloseAllTitle"))); + dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_CloseAllPrimary")); + break; + case ConfirmCloseDialogKind::Window: + dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_WindowTitle"))); + dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_WindowPrimary")); + break; + case ConfirmCloseDialogKind::Tab: + dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_TabTitle"))); + dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_TabPrimary")); + break; + case ConfirmCloseDialogKind::MultiplePanes: + dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_MultiplePanesTitle"))); + dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_MultiplePanesPrimary")); + break; + case ConfirmCloseDialogKind::MultipleTabs: + dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_MultipleTabsTitle"))); + dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_MultipleTabsPrimary")); + break; + case ConfirmCloseDialogKind::Pane: + dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_PaneTitle"))); + dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_PanePrimary")); + break; + } + dialog.CloseButtonText(RS_(L"ConfirmCloseDialog_Cancel")); - // Method Description: - // - Displays a dialog for warnings found while closing the terminal app using - // key binding with multiple tabs opened. Display messages to warn user - // that more than 1 tab is opened, and once the user clicks the OK button, remove - // all the tabs and shut down and app. If cancel is clicked, the dialog will close - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseWarningDialog() - { - return _ShowDialogHelper(L"CloseAllDialog"); + // BODGY: After a ContentDialog is dismissed, FindName() can no longer + // resolve children inside it. Use Content() to get the checkbox directly. + const auto checkbox = dialog.Content().as(); + checkbox.IsChecked(false); + + auto result = ContentDialogResult::None; + if (auto presenter{ _dialogPresenter.get() }) + { + result = co_await presenter.ShowDialog(dialog); + } + + if (result == ContentDialogResult::Primary && checkbox.IsChecked().Value()) + { + _settings.GlobalSettings().ConfirmOnClose(ConfirmOnClose::Never); + _settings.WriteSettingsToDisk(); + } + + co_return result; } // Method Description: @@ -2209,12 +2246,13 @@ namespace winrt::TerminalApp::implementation // signal that we want to close everything. safe_void_coroutine TerminalPage::RequestQuit() { - if (!_displayingCloseDialog) + const auto setting = _settings.GlobalSettings().ConfirmOnClose(); + if (setting != ConfirmOnClose::Never && !_displayingCloseDialog) { _displayingCloseDialog = true; const auto weak = get_weak(); - auto warningResult = co_await _ShowQuitDialog(); + auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::CloseAll); const auto strong = weak.get(); if (!strong) { @@ -2227,9 +2265,9 @@ namespace winrt::TerminalApp::implementation { co_return; } - - QuitRequested.raise(nullptr, nullptr); } + + QuitRequested.raise(nullptr, nullptr); } void TerminalPage::PersistState() @@ -2307,12 +2345,69 @@ 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 confirmOnClose setting 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 setting = _settings.GlobalSettings().ConfirmOnClose(); + switch (setting) + { + case ConfirmOnClose::Always: + return true; + case ConfirmOnClose::Automatic: + { + // Warn if there's more than one tab. + if (_HasMultipleTabs()) + { + return true; + } + + // Warn if the one tab has more than one pane. + if (_GetTabImpl(_tabs.GetAt(0))->GetLeafPaneCount() > 1) + { + return true; + } + return false; + } + case ConfirmOnClose::Never: + default: + return false; + } + } + + // Method Description: + // - Determines whether closing a specific tab should show a confirmation + // dialog, based on the confirmOnClose setting 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 setting = _settings.GlobalSettings().ConfirmOnClose(); + switch (setting) + { + case ConfirmOnClose::Always: + return true; + case ConfirmOnClose::Automatic: + // Warn if this tab has more than one pane. + return tab->GetLeafPaneCount() > 1; + case ConfirmOnClose::Never: + default: + return false; + } + } + + // Method Description: + // - Close the terminal app. If the confirmOnClose setting indicates 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()) @@ -2321,7 +2416,14 @@ namespace winrt::TerminalApp::implementation } _DismissTabContextMenus(); _displayingCloseDialog = true; - auto warningResult = co_await _ShowCloseWarningDialog(); + + const auto weak = get_weak(); + auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::Window); + if (!weak.get()) + { + co_return; + } + _displayingCloseDialog = false; if (warningResult != ContentDialogResult::Primary) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 4b48cc0e9d9..78fc86ec4a7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -54,6 +54,16 @@ namespace winrt::TerminalApp::implementation ScrollDown = 1 }; + enum class ConfirmCloseDialogKind + { + Pane, + Tab, + MultiplePanes, + MultipleTabs, + Window, + CloseAll + }; + struct RenameWindowRequestedArgs : RenameWindowRequestedArgsT { WINRT_PROPERTY(winrt::hstring, ProposedName); @@ -301,8 +311,7 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); void _ShowAboutDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowQuitDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowCloseWarningDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowConfirmCloseDialog(ConfirmCloseDialogKind kind); winrt::Windows::Foundation::IAsyncOperation _ShowCloseReadOnlyDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowMultiLinePasteWarningDialog(); winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog(); @@ -349,7 +358,7 @@ namespace winrt::TerminalApp::implementation safe_void_coroutine _ExportTab(const Tab& tab, winrt::hstring filepath); - winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::Tab tab); + winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::Tab tab, bool skipConfirmClose = false); void _CloseTabAtIndex(uint32_t index); void _RemoveTab(const winrt::TerminalApp::Tab& tab); safe_void_coroutine _RemoveTabs(const std::vector tabs); @@ -400,9 +409,12 @@ 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); + safe_void_coroutine _ClosePanes(weak_ref weakTab, std::vector paneIds); + void _CloseRemainingPanes(weak_ref weakTab, std::vector paneIds); winrt::Windows::Foundation::IAsyncOperation _PaneConfirmCloseReadOnly(std::shared_ptr pane); void _AddPreviouslyClosedPaneOrTab(std::vector&& args); diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 40e3b838c37..1808874b938 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -86,17 +86,12 @@ Grid.Row="2" x:Load="False" /> - - - + DefaultButton="Primary"> + + - - - + + + diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp index 55a239e8e5f..f069fec12d5 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.cpp @@ -17,5 +17,6 @@ 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"); + INITIALIZE_BINDABLE_ENUM_SETTING(ConfirmOnClose, ConfirmOnClose, Model::ConfirmOnClose, L"Globals_ConfirmOnClose", L"Content"); } } diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h index c3e48715f7a..47cb3318927 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.h @@ -19,6 +19,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation GETSET_BINDABLE_ENUM_SETTING(TabSwitcherMode, Model::TabSwitcherMode, _GlobalSettings.TabSwitcherMode); GETSET_BINDABLE_ENUM_SETTING(CopyFormat, winrt::Microsoft::Terminal::Control::CopyFormat, _GlobalSettings.CopyFormatting); + GETSET_BINDABLE_ENUM_SETTING(ConfirmOnClose, Model::ConfirmOnClose, _GlobalSettings.ConfirmOnClose); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, CopyOnSelect); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, TrimBlockSelection); @@ -30,7 +31,6 @@ 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, 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..69b53b1b245 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl @@ -12,10 +12,13 @@ namespace Microsoft.Terminal.Settings.Editor InteractionViewModel(Microsoft.Terminal.Settings.Model.GlobalAppSettings globalSettings); IInspectable CurrentTabSwitcherMode; - Windows.Foundation.Collections.IObservableVector TabSwitcherModeList { get; }; + Windows.Foundation.Collections.IObservableVector TabSwitcherModeList { get; }; IInspectable CurrentCopyFormat; - Windows.Foundation.Collections.IObservableVector CopyFormatList { get; }; + Windows.Foundation.Collections.IObservableVector CopyFormatList { get; }; + + IInspectable CurrentConfirmOnClose; + Windows.Foundation.Collections.IObservableVector ConfirmOnCloseList { get; }; PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, CopyOnSelect); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, TrimBlockSelection); @@ -27,7 +30,6 @@ 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(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 e52b4c84ac6..57b4ee84b30 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. + + Warn when closing + Header for a dropdown controlling when to show a confirmation dialog before closing. + + + Controls when a confirmation dialog appears before closing tabs or windows. "Always" presents the dialog when closing any pane. + Help text associated with Globals_ConfirmOnClose. "Always" refers to Globals_ConfirmOnCloseAlways.Content. + + + Never + Option associated with Globals_ConfirmOnClose. "Never" means that the system will never display a warning when closing. + + + Always + Option associated with Globals_ConfirmOnClose. "Always" means that the system will always display a warning when closing. + + + Multiple tabs or panes + Option associated with Globals_ConfirmOnClose. The system will display a warning when multiple tabs or panes are present. + Warn when "Touch Keyboard and Handwriting Panel Service" is disabled diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp index de1bf5185da..b92994c2d66 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp @@ -43,6 +43,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation DEFINE_ENUM_MAP(Microsoft::Terminal::Control::TextMeasurement, TextMeasurement); DEFINE_ENUM_MAP(Microsoft::Terminal::Control::AmbiguousWidth, AmbiguousWidth); DEFINE_ENUM_MAP(Microsoft::Terminal::Control::WarnAboutMultiLinePaste, WarnAboutMultiLinePaste); + DEFINE_ENUM_MAP(Model::ConfirmOnClose, ConfirmOnClose); // Profile Settings DEFINE_ENUM_MAP(Model::CloseOnExitMode, CloseOnExitMode); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.h b/src/cascadia/TerminalSettingsModel/EnumMappings.h index 160c9a11b1f..e8586704506 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.h +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.h @@ -40,6 +40,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::Windows::Foundation::Collections::IMap TextMeasurement(); static winrt::Windows::Foundation::Collections::IMap AmbiguousWidth(); static winrt::Windows::Foundation::Collections::IMap WarnAboutMultiLinePaste(); + static winrt::Windows::Foundation::Collections::IMap ConfirmOnClose(); // Profile Settings static winrt::Windows::Foundation::Collections::IMap CloseOnExitMode(); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.idl b/src/cascadia/TerminalSettingsModel/EnumMappings.idl index 128260a507a..7f52aace494 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.idl +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.idl @@ -22,6 +22,7 @@ namespace Microsoft.Terminal.Settings.Model static Windows.Foundation.Collections.IMap TextMeasurement { get; }; static Windows.Foundation.Collections.IMap AmbiguousWidth { get; }; static Windows.Foundation.Collections.IMap WarnAboutMultiLinePaste { get; }; + static Windows.Foundation.Collections.IMap ConfirmOnClose { get; }; // Profile Settings static Windows.Foundation.Collections.IMap CloseOnExitMode { get; }; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 254e52bdbcb..3a5cd8cb1db 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 + // "confirmOnClose" enum. true -> Automatic, false -> Never. + { + std::optional legacyConfirmClose; + if (JsonUtils::GetValueForKey(json, LegacyConfirmCloseAllTabsKey, legacyConfirmClose)) + { + _ConfirmOnClose = legacyConfirmClose.value() ? ConfirmOnClose::Automatic : ConfirmOnClose::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..adb29b3395c 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -50,6 +50,13 @@ namespace Microsoft.Terminal.Settings.Model AfterCurrentTab, }; + enum ConfirmOnClose + { + Never = 0, + Automatic = 1, + Always = 2, + }; + [default_interface] runtimeclass GlobalAppSettings { Guid DefaultProfile; @@ -61,7 +68,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, ShowTabsFullscreen); INHERITABLE_SETTING(NewTabPosition, NewTabPosition); INHERITABLE_SETTING(Boolean, ShowTitleInTitlebar); - INHERITABLE_SETTING(Boolean, ConfirmCloseAllTabs); + INHERITABLE_SETTING(ConfirmOnClose, ConfirmOnClose); 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 b95aad938e1..ca147c707d7 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::ConfirmOnClose, ConfirmOnClose, "warning.confirmOnClose", Model::ConfirmOnClose::Automatic) \ 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..f618011b7f8 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -88,6 +88,25 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Core::MatchMode) }; }; +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::ConfirmOnClose) +{ + JSON_MAPPINGS(3) = { + pair_type{ "never", ValueType::Never }, + pair_type{ "automatic", ValueType::Automatic }, + pair_type{ "always", ValueType::Always }, + }; + + auto FromJson(const Json::Value& json) + { + return BaseEnumMapper::FromJson(json); + } + + bool CanConvert(const Json::Value& json) + { + return BaseEnumMapper::CanConvert(json); + } +}; + 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..44efab04ca0 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -24,7 +24,7 @@ "showAdminShield": true, // Miscellaneous - "confirmCloseAllTabs": true, + "warning.confirmOnClose": "automatic", "theme": "dark", "snapToGridOnResize": true, "disableAnimations": false, diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index 3112f83939e..d72a34643be 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.confirmOnClose": "automatic", "warning.inputService" : true, "warning.largePaste" : true, "warning.multiLinePaste" : "automatic", From 15f688fd074edded3dbf322a3bd8525765360688 Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 29 Apr 2026 19:57:05 -0700 Subject: [PATCH 2/2] address feedback --- src/cascadia/TerminalApp/TabManagement.cpp | 18 +++++- src/cascadia/TerminalApp/TerminalPage.cpp | 68 ++++++++++++---------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index ef917e0ff7d..0d4ba59892f 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -812,7 +812,11 @@ namespace winrt::TerminalApp::implementation // so use the tab dialog text instead. const auto kind = activeTab->GetLeafPaneCount() == 1 ? ConfirmCloseDialogKind::Tab : ConfirmCloseDialogKind::Pane; auto warningResult = co_await _ShowConfirmCloseDialog(kind); - if (!weak.get() || warningResult != ContentDialogResult::Primary) + + // Hold a strong reference to `this` for the rest of the + // method; we may be the last holder after `co_await`. + auto strong = weak.get(); + if (!strong || warningResult != ContentDialogResult::Primary) { co_return; } @@ -842,7 +846,11 @@ namespace winrt::TerminalApp::implementation { const auto weak = get_weak(); auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::MultiplePanes); - if (!weak.get() || warningResult != ContentDialogResult::Primary) + + // Hold a strong reference to `this` after the co_await; we may + // be the last holder if the page was being torn down. + auto strong = weak.get(); + if (!strong || warningResult != ContentDialogResult::Primary) { co_return; } @@ -914,7 +922,11 @@ namespace winrt::TerminalApp::implementation if (_settings.GlobalSettings().ConfirmOnClose() != ConfirmOnClose::Never) { auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::MultipleTabs); - if (!weak.get() || warningResult != ContentDialogResult::Primary) + + // Hold a strong reference to `this` after the co_await so that + // the for-loop below can safely dispatch on us. + auto strong = weak.get(); + if (!strong || warningResult != ContentDialogResult::Primary) { co_return; } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index a3dee24eeb6..562da0faff8 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -894,33 +894,38 @@ namespace winrt::TerminalApp::implementation { // Load the dialog (triggers x:Load) and configure its strings. const auto dialog = FindName(L"ConfirmCloseDialog").as(); + + winrt::hstring title; + winrt::hstring primary; switch (kind) { case ConfirmCloseDialogKind::CloseAll: - dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_CloseAllTitle"))); - dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_CloseAllPrimary")); + title = RS_(L"ConfirmCloseDialog_CloseAllTitle"); + primary = RS_(L"ConfirmCloseDialog_CloseAllPrimary"); break; case ConfirmCloseDialogKind::Window: - dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_WindowTitle"))); - dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_WindowPrimary")); + title = RS_(L"ConfirmCloseDialog_WindowTitle"); + primary = RS_(L"ConfirmCloseDialog_WindowPrimary"); break; case ConfirmCloseDialogKind::Tab: - dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_TabTitle"))); - dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_TabPrimary")); + title = RS_(L"ConfirmCloseDialog_TabTitle"); + primary = RS_(L"ConfirmCloseDialog_TabPrimary"); break; case ConfirmCloseDialogKind::MultiplePanes: - dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_MultiplePanesTitle"))); - dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_MultiplePanesPrimary")); + title = RS_(L"ConfirmCloseDialog_MultiplePanesTitle"); + primary = RS_(L"ConfirmCloseDialog_MultiplePanesPrimary"); break; case ConfirmCloseDialogKind::MultipleTabs: - dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_MultipleTabsTitle"))); - dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_MultipleTabsPrimary")); + title = RS_(L"ConfirmCloseDialog_MultipleTabsTitle"); + primary = RS_(L"ConfirmCloseDialog_MultipleTabsPrimary"); break; case ConfirmCloseDialogKind::Pane: - dialog.Title(winrt::box_value(RS_(L"ConfirmCloseDialog_PaneTitle"))); - dialog.PrimaryButtonText(RS_(L"ConfirmCloseDialog_PanePrimary")); + title = RS_(L"ConfirmCloseDialog_PaneTitle"); + primary = RS_(L"ConfirmCloseDialog_PanePrimary"); break; } + dialog.Title(winrt::box_value(title)); + dialog.PrimaryButtonText(primary); dialog.CloseButtonText(RS_(L"ConfirmCloseDialog_Cancel")); // BODGY: After a ContentDialog is dismissed, FindName() can no longer @@ -931,13 +936,23 @@ namespace winrt::TerminalApp::implementation auto result = ContentDialogResult::None; if (auto presenter{ _dialogPresenter.get() }) { + const auto weak = get_weak(); result = co_await presenter.ShowDialog(dialog); - } - if (result == ContentDialogResult::Primary && checkbox.IsChecked().Value()) - { - _settings.GlobalSettings().ConfirmOnClose(ConfirmOnClose::Never); - _settings.WriteSettingsToDisk(); + // ShowDialog blocks until the dialog is dismissed, so it is + // possible for `this` to be torn down while we wait. Re-acquire + // a strong reference before touching any of our state. + const auto strong = weak.get(); + if (!strong) + { + co_return ContentDialogResult::None; + } + + if (result == ContentDialogResult::Primary && checkbox.IsChecked().Value()) + { + _settings.GlobalSettings().ConfirmOnClose(ConfirmOnClose::Never); + _settings.WriteSettingsToDisk(); + } } co_return result; @@ -2360,18 +2375,8 @@ namespace winrt::TerminalApp::implementation return true; case ConfirmOnClose::Automatic: { - // Warn if there's more than one tab. - if (_HasMultipleTabs()) - { - return true; - } - - // Warn if the one tab has more than one pane. - if (_GetTabImpl(_tabs.GetAt(0))->GetLeafPaneCount() > 1) - { - return true; - } - return false; + // Warn if there's more than one tab, or the one tab has more than one pane. + return _HasMultipleTabs() || _GetTabImpl(_tabs.GetAt(0))->GetLeafPaneCount() > 1; } case ConfirmOnClose::Never: default: @@ -2419,7 +2424,10 @@ namespace winrt::TerminalApp::implementation const auto weak = get_weak(); auto warningResult = co_await _ShowConfirmCloseDialog(ConfirmCloseDialogKind::Window); - if (!weak.get()) + // Hold a strong reference to `this` after the co_await; we may + // be the last holder if the window was already being torn down. + auto strong = weak.get(); + if (!strong) { co_return; }