diff --git a/doc/cascadia/SettingsSchema.md b/doc/cascadia/SettingsSchema.md
index 4ef60e9dc05..ecbaae39293 100644
--- a/doc/cascadia/SettingsSchema.md
+++ b/doc/cascadia/SettingsSchema.md
@@ -6,6 +6,7 @@ Properties listed below affect the entire window, regardless of the profile sett
| Property | Necessity | Type | Default | Description |
| -------- | --------- | ---- | ------- | ----------- |
| `alwaysShowTabs` | _Required_ | Boolean | `true` | When set to `true`, tabs are always displayed. When set to `false` and `showTabsInTitlebar` is set to `false`, tabs only appear after typing Ctrl + T. |
+| `copyOnSelect` | Optional | Boolean | `false` | When set to `true`, a selection is immediately copied to your clipboard upon creation. When set to `false`, the selection persists and awaits further action. |
| `defaultProfile` | _Required_ | String | PowerShell guid | Sets the default profile. Opens by typing Ctrl + T or by clicking the '+' icon. The guid of the desired default profile is used as the value. |
| `initialCols` | _Required_ | Integer | `120` | The number of columns displayed in the window upon first load. |
| `initialRows` | _Required_ | Integer | `30` | The number of rows displayed in the window upon first load. |
diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest
index ef9516047af..a447bc8fdc2 100644
--- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest
+++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.manifest
@@ -9,6 +9,7 @@
+
diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp
index accaf8a7f4f..6c88340b844 100644
--- a/src/cascadia/TerminalApp/App.cpp
+++ b/src/cascadia/TerminalApp/App.cpp
@@ -680,19 +680,15 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - Attempt to load the settings. If we fail for any reason, returns an error.
- // Arguments:
- // - saveOnLoad: If true, after loading the settings, we should re-write
- // them to the file, to make sure the schema is updated. See
- // `CascadiaSettings::LoadAll` for details.
// Return Value:
// - S_OK if we successfully parsed the settings, otherwise an appropriate HRESULT.
- [[nodiscard]] HRESULT App::_TryLoadSettings(const bool saveOnLoad) noexcept
+ [[nodiscard]] HRESULT App::_TryLoadSettings() noexcept
{
HRESULT hr = E_FAIL;
try
{
- auto newSettings = CascadiaSettings::LoadAll(saveOnLoad);
+ auto newSettings = CascadiaSettings::LoadAll();
_settings = std::move(newSettings);
const auto& warnings = _settings->GetWarnings();
hr = warnings.size() == 0 ? S_OK : S_FALSE;
@@ -733,7 +729,7 @@ namespace winrt::TerminalApp::implementation
// we should display the loading error.
// * We can't display the error now, because we might not have a
// UI yet. We'll display the error in _OnLoaded.
- _settingsLoadedResult = _TryLoadSettings(true);
+ _settingsLoadedResult = _TryLoadSettings();
if (FAILED(_settingsLoadedResult))
{
@@ -813,7 +809,7 @@ namespace winrt::TerminalApp::implementation
// - don't change the settings (and don't actually apply the new settings)
// - don't persist them.
// - display a loading error
- _settingsLoadedResult = _TryLoadSettings(false);
+ _settingsLoadedResult = _TryLoadSettings();
if (FAILED(_settingsLoadedResult))
{
@@ -1494,11 +1490,19 @@ namespace winrt::TerminalApp::implementation
const auto controlConnection = _CreateConnectionFromSettings(realGuid, controlSettings);
- TermControl newControl{ controlSettings, controlConnection };
-
const int focusedTabIndex = _GetFocusedTabIndex();
auto focusedTab = _tabs[focusedTabIndex];
+ const auto canSplit = splitType == Pane::SplitState::Horizontal ? focusedTab->CanAddHorizontalSplit() :
+ focusedTab->CanAddVerticalSplit();
+
+ if (!canSplit)
+ {
+ return;
+ }
+
+ TermControl newControl{ controlSettings, controlConnection };
+
// Hookup our event handlers to the new terminal
_RegisterTerminalEvents(newControl, focusedTab);
@@ -1546,6 +1550,7 @@ namespace winrt::TerminalApp::implementation
}
Clipboard::SetContent(dataPack);
+ Clipboard::Flush();
});
}
diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h
index 82ad67954e3..47350c76056 100644
--- a/src/cascadia/TerminalApp/App.h
+++ b/src/cascadia/TerminalApp/App.h
@@ -85,7 +85,7 @@ namespace winrt::TerminalApp::implementation
void _ShowLoadWarningsDialog();
void _ShowLoadErrorsDialog(const winrt::hstring& titleKey, const winrt::hstring& contentKey);
- [[nodiscard]] HRESULT _TryLoadSettings(const bool saveOnLoad) noexcept;
+ [[nodiscard]] HRESULT _TryLoadSettings() noexcept;
void _LoadSettings();
void _OpenSettings();
diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h
index f5d9155b023..ea72bc9474d 100644
--- a/src/cascadia/TerminalApp/CascadiaSettings.h
+++ b/src/cascadia/TerminalApp/CascadiaSettings.h
@@ -42,7 +42,7 @@ class TerminalApp::CascadiaSettings final
CascadiaSettings();
~CascadiaSettings();
- static std::unique_ptr LoadAll(const bool saveOnLoad = true);
+ static std::unique_ptr LoadAll();
void SaveAll() const;
winrt::Microsoft::Terminal::Settings::TerminalSettings MakeSettings(std::optional profileGuid) const;
diff --git a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp
index 4975c624a88..50753b8dabd 100644
--- a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp
+++ b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp
@@ -30,12 +30,9 @@ static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
// it will load the settings from our packaged localappdata. If we're
// running as an unpackaged application, it will read it from the path
// we've set under localappdata.
-// Arguments:
-// - saveOnLoad: If true, we'll write the settings back out after we load them,
-// to make sure the schema is updated.
// Return Value:
// - a unique_ptr containing a new CascadiaSettings object.
-std::unique_ptr CascadiaSettings::LoadAll(const bool saveOnLoad)
+std::unique_ptr CascadiaSettings::LoadAll()
{
std::unique_ptr resultPtr;
std::optional fileData = _ReadSettings();
@@ -70,22 +67,6 @@ std::unique_ptr CascadiaSettings::LoadAll(const bool saveOnLoa
// If this throws, the app will catch it and use the default settings (temporarily)
resultPtr->_ValidateSettings();
-
- const bool foundWarnings = resultPtr->_warnings.size() > 0;
-
- // Don't save on load if there were warnings - we tried to gracefully
- // handle them.
- if (saveOnLoad && !foundWarnings)
- {
- // Logically compare the json we've parsed from the file to what
- // we'd serialize at runtime. If the values are different, then
- // write the updated schema back out.
- const Json::Value reserialized = resultPtr->ToJson();
- if (reserialized != root)
- {
- resultPtr->SaveAll();
- }
- }
}
else
{
diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp
index 4f6a9d8d396..eba5beedca7 100644
--- a/src/cascadia/TerminalApp/GlobalAppSettings.cpp
+++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp
@@ -24,6 +24,7 @@ static constexpr std::string_view ShowTitleInTitlebarKey{ "showTerminalTitleInTi
static constexpr std::string_view RequestedThemeKey{ "requestedTheme" };
static constexpr std::string_view ShowTabsInTitlebarKey{ "showTabsInTitlebar" };
static constexpr std::string_view WordDelimitersKey{ "wordDelimiters" };
+static constexpr std::string_view CopyOnSelectKey{ "copyOnSelect" };
static constexpr std::wstring_view LightThemeValue{ L"light" };
static constexpr std::wstring_view DarkThemeValue{ L"dark" };
@@ -39,7 +40,8 @@ GlobalAppSettings::GlobalAppSettings() :
_showTitleInTitlebar{ true },
_showTabsInTitlebar{ true },
_requestedTheme{ ElementTheme::Default },
- _wordDelimiters{ DEFAULT_WORD_DELIMITERS }
+ _wordDelimiters{ DEFAULT_WORD_DELIMITERS },
+ _copyOnSelect{ false }
{
}
@@ -117,6 +119,16 @@ void GlobalAppSettings::SetWordDelimiters(const std::wstring wordDelimiters) noe
_wordDelimiters = wordDelimiters;
}
+bool GlobalAppSettings::GetCopyOnSelect() const noexcept
+{
+ return _copyOnSelect;
+}
+
+void GlobalAppSettings::SetCopyOnSelect(const bool copyOnSelect) noexcept
+{
+ _copyOnSelect = copyOnSelect;
+}
+
#pragma region ExperimentalSettings
bool GlobalAppSettings::GetShowTabsInTitlebar() const noexcept
{
@@ -141,6 +153,7 @@ void GlobalAppSettings::ApplyToSettings(TerminalSettings& settings) const noexce
settings.InitialRows(_initialRows);
settings.InitialCols(_initialCols);
settings.WordDelimiters(_wordDelimiters);
+ settings.CopyOnSelect(_copyOnSelect);
}
// Method Description:
@@ -160,6 +173,7 @@ Json::Value GlobalAppSettings::ToJson() const
jsonObject[JsonKey(ShowTitleInTitlebarKey)] = _showTitleInTitlebar;
jsonObject[JsonKey(ShowTabsInTitlebarKey)] = _showTabsInTitlebar;
jsonObject[JsonKey(WordDelimitersKey)] = winrt::to_string(_wordDelimiters);
+ jsonObject[JsonKey(CopyOnSelectKey)] = _copyOnSelect;
jsonObject[JsonKey(RequestedThemeKey)] = winrt::to_string(_SerializeTheme(_requestedTheme));
jsonObject[JsonKey(KeybindingsKey)] = AppKeyBindingsSerialization::ToJson(_keybindings);
@@ -210,6 +224,11 @@ GlobalAppSettings GlobalAppSettings::FromJson(const Json::Value& json)
result._wordDelimiters = GetWstringFromJson(wordDelimiters);
}
+ if (auto copyOnSelect{ json[JsonKey(CopyOnSelectKey)] })
+ {
+ result._copyOnSelect = copyOnSelect.asBool();
+ }
+
if (auto requestedTheme{ json[JsonKey(RequestedThemeKey)] })
{
result._requestedTheme = _ParseTheme(GetWstringFromJson(requestedTheme));
diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.h b/src/cascadia/TerminalApp/GlobalAppSettings.h
index 293ba743233..11dab24ccaa 100644
--- a/src/cascadia/TerminalApp/GlobalAppSettings.h
+++ b/src/cascadia/TerminalApp/GlobalAppSettings.h
@@ -50,6 +50,9 @@ class TerminalApp::GlobalAppSettings final
std::wstring GetWordDelimiters() const noexcept;
void SetWordDelimiters(const std::wstring wordDelimiters) noexcept;
+ bool GetCopyOnSelect() const noexcept;
+ void SetCopyOnSelect(const bool copyOnSelect) noexcept;
+
winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme() const noexcept;
Json::Value ToJson() const;
@@ -72,6 +75,7 @@ class TerminalApp::GlobalAppSettings final
bool _showTabsInTitlebar;
std::wstring _wordDelimiters;
+ bool _copyOnSelect;
winrt::Windows::UI::Xaml::ElementTheme _requestedTheme;
static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept;
diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp
index 5262bf45148..10e3bd09d6a 100644
--- a/src/cascadia/TerminalApp/Pane.cpp
+++ b/src/cascadia/TerminalApp/Pane.cpp
@@ -777,6 +777,31 @@ void Pane::_ApplySplitDefinitions()
}
}
+// Method Description:
+// - Determines whether the pane can be split vertically
+// Arguments:
+// - splitType: what type of split we want to create.
+// Return Value:
+// - True if the pane can be split vertically. False otherwise.
+bool Pane::CanSplitVertical()
+{
+ if (!_IsLeaf())
+ {
+ if (_firstChild->_HasFocusedChild())
+ {
+ return _firstChild->CanSplitVertical();
+ }
+ else if (_secondChild->_HasFocusedChild())
+ {
+ return _secondChild->CanSplitVertical();
+ }
+
+ return false;
+ }
+
+ return _CanSplit(SplitState::Vertical);
+}
+
// Method Description:
// - Vertically split the focused pane in our tree of panes, and place the given
// TermControl into the newly created pane. If we're the focused pane, then
@@ -806,6 +831,31 @@ void Pane::SplitVertical(const GUID& profile, const TermControl& control)
_Split(SplitState::Vertical, profile, control);
}
+// Method Description:
+// - Determines whether the pane can be split horizontally
+// Arguments:
+// - splitType: what type of split we want to create.
+// Return Value:
+// - True if the pane can be split horizontally. False otherwise.
+bool Pane::CanSplitHorizontal()
+{
+ if (!_IsLeaf())
+ {
+ if (_firstChild->_HasFocusedChild())
+ {
+ return _firstChild->CanSplitHorizontal();
+ }
+ else if (_secondChild->_HasFocusedChild())
+ {
+ return _secondChild->CanSplitHorizontal();
+ }
+
+ return false;
+ }
+
+ return _CanSplit(SplitState::Horizontal);
+}
+
// Method Description:
// - Horizontally split the focused pane in our tree of panes, and place the given
// TermControl into the newly created pane. If we're the focused pane, then
@@ -834,6 +884,40 @@ void Pane::SplitHorizontal(const GUID& profile, const TermControl& control)
_Split(SplitState::Horizontal, profile, control);
}
+// Method Description:
+// - Determines whether the pane can be split.
+// Arguments:
+// - splitType: what type of split we want to create.
+// Return Value:
+// - True if the pane can be split. False otherwise.
+bool Pane::_CanSplit(SplitState splitType)
+{
+ const bool changeWidth = _splitState == SplitState::Vertical;
+
+ const Size actualSize{ gsl::narrow_cast(_root.ActualWidth()),
+ gsl::narrow_cast(_root.ActualHeight()) };
+
+ const Size minSize = _GetMinSize();
+
+ if (splitType == SplitState::Vertical)
+ {
+ const auto widthMinusSeparator = actualSize.Width - PaneSeparatorSize;
+ const auto newWidth = widthMinusSeparator * Half;
+
+ return newWidth > minSize.Width;
+ }
+
+ if (splitType == SplitState::Horizontal)
+ {
+ const auto heightMinusSeparator = actualSize.Height - PaneSeparatorSize;
+ const auto newHeight = heightMinusSeparator * Half;
+
+ return newHeight > minSize.Height;
+ }
+
+ return false;
+}
+
// Method Description:
// - Does the bulk of the work of creating a new split. Initializes our UI,
// creates a new Pane to host the control, registers event handlers.
diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h
index 2fb4a2e499f..31979b6d090 100644
--- a/src/cascadia/TerminalApp/Pane.h
+++ b/src/cascadia/TerminalApp/Pane.h
@@ -49,7 +49,10 @@ class Pane : public std::enable_shared_from_this
bool ResizePane(const winrt::TerminalApp::Direction& direction);
bool NavigateFocus(const winrt::TerminalApp::Direction& direction);
+ bool CanSplitHorizontal();
void SplitHorizontal(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
+
+ bool CanSplitVertical();
void SplitVertical(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void Close();
@@ -79,6 +82,7 @@ class Pane : public std::enable_shared_from_this
bool _HasFocusedChild() const noexcept;
void _SetupChildCloseHandlers();
+ bool _CanSplit(SplitState splitType);
void _Split(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void _CreateRowColDefinitions(const winrt::Windows::Foundation::Size& rootSize);
void _CreateSplitContent();
diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp
index f8ac1199c36..4870d227f4b 100644
--- a/src/cascadia/TerminalApp/Tab.cpp
+++ b/src/cascadia/TerminalApp/Tab.cpp
@@ -203,6 +203,15 @@ void Tab::Scroll(const int delta)
});
}
+// Method Description:
+// - Determines whether the focused pane has sufficient space to be split vertically.
+// Return Value:
+// - True if the focused pane can be split horizontally. False otherwise.
+bool Tab::CanAddVerticalSplit()
+{
+ return _rootPane->CanSplitVertical();
+}
+
// Method Description:
// - Vertically split the focused pane in our tree of panes, and place the
// given TermControl into the newly created pane.
@@ -216,6 +225,15 @@ void Tab::AddVerticalSplit(const GUID& profile, TermControl& control)
_rootPane->SplitVertical(profile, control);
}
+// Method Description:
+// - Determines whether the focused pane has sufficient space to be split horizontally.
+// Return Value:
+// - True if the focused pane can be split horizontally. False otherwise.
+bool Tab::CanAddHorizontalSplit()
+{
+ return _rootPane->CanSplitHorizontal();
+}
+
// Method Description:
// - Horizontally split the focused pane in our tree of panes, and place the
// given TermControl into the newly created pane.
diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h
index 4de1ca87a26..99c782201e3 100644
--- a/src/cascadia/TerminalApp/Tab.h
+++ b/src/cascadia/TerminalApp/Tab.h
@@ -19,7 +19,9 @@ class Tab
void SetFocused(const bool focused);
void Scroll(const int delta);
+ bool CanAddVerticalSplit();
void AddVerticalSplit(const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
+ bool CanAddHorizontalSplit();
void AddHorizontalSplit(const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void UpdateFocus();
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index cc9ac5c5f25..757f03b93ea 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -773,15 +773,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}
else if (point.Properties().IsRightButtonPressed())
{
- // copy selection, if one exists
- if (_terminal->IsSelectionActive())
+ // copyOnSelect causes right-click to always paste
+ if (_terminal->IsCopyOnSelectActive() || !_terminal->IsSelectionActive())
{
- CopySelectionToClipboard(!shiftEnabled);
+ PasteTextFromClipboard();
}
- // paste selection, otherwise
else
{
- PasteTextFromClipboard();
+ CopySelectionToClipboard(!shiftEnabled);
}
}
}
@@ -808,7 +807,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse)
{
- if (_terminal->IsSelectionActive() && point.Properties().IsLeftButtonPressed())
+ if (point.Properties().IsLeftButtonPressed())
{
const auto cursorPosition = point.Position();
_SetEndSelectionPointAtCursor(cursorPosition);
@@ -885,7 +884,19 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
const auto ptr = args.Pointer();
- if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch)
+ if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse)
+ {
+ const auto modifiers = static_cast(args.KeyModifiers());
+ // static_cast to a uint32_t because we can't use the WI_IsFlagSet
+ // macro directly with a VirtualKeyModifiers
+ const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift));
+
+ if (_terminal->IsCopyOnSelectActive())
+ {
+ CopySelectionToClipboard(!shiftEnabled);
+ }
+ }
+ else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch)
{
_touchAnchor = std::nullopt;
}
@@ -1387,35 +1398,41 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}
// Method Description:
- // - get text from buffer and send it to the Windows Clipboard (CascadiaWin32:main.cpp). Also removes rendering of selection.
+ // - Given a copy-able selection, get the selected text from the buffer and send it to the
+ // Windows Clipboard (CascadiaWin32:main.cpp).
+ // - CopyOnSelect does NOT clear the selection
// Arguments:
// - trimTrailingWhitespace: enable removing any whitespace from copied selection
// and get text to appear on separate lines.
bool TermControl::CopySelectionToClipboard(bool trimTrailingWhitespace)
{
- if (_terminal != nullptr && _terminal->IsSelectionActive())
+ // no selection --> nothing to copy
+ if (_terminal == nullptr || !_terminal->IsSelectionActive())
{
- // extract text from buffer
- const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace);
+ return false;
+ }
+ // extract text from buffer
+ const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace);
- // convert text: vector --> string
- std::wstring textData;
- for (const auto& text : bufferData.text)
- {
- textData += text;
- }
+ // convert text: vector --> string
+ std::wstring textData;
+ for (const auto& text : bufferData.text)
+ {
+ textData += text;
+ }
- // convert text to HTML format
- const auto htmlData = TextBuffer::GenHTML(bufferData, _actualFont.GetUnscaledSize().Y, _actualFont.GetFaceName(), "Windows Terminal");
+ // convert text to HTML format
+ const auto htmlData = TextBuffer::GenHTML(bufferData, _actualFont.GetUnscaledSize().Y, _actualFont.GetFaceName(), "Windows Terminal");
+ if (!_terminal->IsCopyOnSelectActive())
+ {
_terminal->ClearSelection();
-
- // send data up for clipboard
- auto copyArgs = winrt::make_self(winrt::hstring(textData.data(), textData.size()), winrt::to_hstring(htmlData));
- _clipboardCopyHandlers(*this, *copyArgs);
- return true;
}
- return false;
+
+ // send data up for clipboard
+ auto copyArgs = winrt::make_self(winrt::hstring(textData.data(), textData.size()), winrt::to_hstring(htmlData));
+ _clipboardCopyHandlers(*this, *copyArgs);
+ return true;
}
// Method Description:
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 1ff68683325..4e01f033af1 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -45,6 +45,8 @@ Terminal::Terminal() :
_snapOnInput{ true },
_boxSelection{ false },
_selectionActive{ false },
+ _allowSingleCharSelection{ false },
+ _copyOnSelect{ false },
_selectionAnchor{ 0, 0 },
_endSelectionPosition{ 0, 0 }
{
@@ -135,6 +137,8 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting
_wordDelimiters = settings.WordDelimiters();
+ _copyOnSelect = settings.CopyOnSelect();
+
// TODO:MSFT:21327402 - if HistorySize has changed, resize the buffer so we
// have a smaller scrollback. We should do this carefully - if the new buffer
// size is smaller than where the mutable viewport currently is, we'll want
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 7dbe42363ef..876d21dacfc 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -146,6 +146,7 @@ class Microsoft::Terminal::Core::Terminal final :
#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
+ const bool IsCopyOnSelectActive() const noexcept;
void DoubleClickSelection(const COORD position);
void TripleClickSelection(const COORD position);
void SetSelectionAnchor(const COORD position);
@@ -183,6 +184,8 @@ class Microsoft::Terminal::Core::Terminal final :
COORD _endSelectionPosition;
bool _boxSelection;
bool _selectionActive;
+ bool _allowSingleCharSelection;
+ bool _copyOnSelect;
SHORT _selectionAnchor_YOffset;
SHORT _endSelectionPosition_YOffset;
std::wstring _wordDelimiters;
@@ -234,5 +237,6 @@ class Microsoft::Terminal::Core::Terminal final :
COORD _ExpandDoubleClickSelectionRight(const COORD position) const;
const bool _isWordDelimiter(std::wstring_view cellChar) const;
const COORD _ConvertToBufferCell(const COORD viewportPos) const;
+ const bool _isSingleCellSelection() const noexcept;
#pragma endregion
};
diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp
index 866f3fb13a7..2131a762d24 100644
--- a/src/cascadia/TerminalCore/TerminalSelection.cpp
+++ b/src/cascadia/TerminalCore/TerminalSelection.cpp
@@ -14,7 +14,7 @@ std::vector Terminal::_GetSelectionRects() const
{
std::vector selectionArea;
- if (!_selectionActive)
+ if (!IsSelectionActive())
{
return selectionArea;
}
@@ -67,7 +67,7 @@ std::vector Terminal::_GetSelectionRects() const
if (_multiClickSelectionMode == SelectionExpansionMode::Word)
{
const auto cellChar = _buffer->GetCellDataAt(selectionAnchorWithOffset)->Chars();
- if (_selectionAnchor == _endSelectionPosition && _isWordDelimiter(cellChar))
+ if (_isSingleCellSelection() && _isWordDelimiter(cellChar))
{
// only highlight the cell if you double click a delimiter
}
@@ -142,15 +142,39 @@ const SHORT Terminal::_ExpandWideGlyphSelectionRight(const SHORT xPos, const SHO
return position.X;
}
+// Method Description:
+// - Checks if selection is on a single cell
+// Return Value:
+// - bool representing if selection is only a single cell. Used for copyOnSelect
+const bool Terminal::_isSingleCellSelection() const noexcept
+{
+ return (_selectionAnchor == _endSelectionPosition);
+}
+
// Method Description:
// - Checks if selection is active
// Return Value:
// - bool representing if selection is active. Used to decide copy/paste on right click
const bool Terminal::IsSelectionActive() const noexcept
{
+ // A single cell selection is not considered an active selection,
+ // if it's not allowed
+ if (!_allowSingleCharSelection && _isSingleCellSelection())
+ {
+ return false;
+ }
return _selectionActive;
}
+// Method Description:
+// - Checks if the CopyOnSelect setting is active
+// Return Value:
+// - true if feature is active, false otherwise.
+const bool Terminal::IsCopyOnSelectActive() const noexcept
+{
+ return _copyOnSelect;
+}
+
// Method Description:
// - Select the sequence between delimiters defined in Settings
// Arguments:
@@ -211,6 +235,8 @@ void Terminal::SetSelectionAnchor(const COORD position)
_selectionAnchor_YOffset = gsl::narrow(_ViewStartIndex());
_selectionActive = true;
+ _allowSingleCharSelection = (_copyOnSelect) ? false : true;
+
SetEndSelectionPosition(position);
_multiClickSelectionMode = SelectionExpansionMode::Cell;
@@ -230,6 +256,11 @@ void Terminal::SetEndSelectionPosition(const COORD position)
// copy value of ViewStartIndex to support scrolling
// and update on new buffer output (used in _GetSelectionRects())
_endSelectionPosition_YOffset = gsl::narrow(_ViewStartIndex());
+
+ if (_copyOnSelect && !_isSingleCellSelection())
+ {
+ _allowSingleCharSelection = true;
+ }
}
// Method Description:
@@ -246,6 +277,7 @@ void Terminal::SetBoxSelection(const bool isEnabled) noexcept
void Terminal::ClearSelection()
{
_selectionActive = false;
+ _allowSingleCharSelection = false;
_selectionAnchor = { 0, 0 };
_endSelectionPosition = { 0, 0 };
_selectionAnchor_YOffset = 0;
diff --git a/src/cascadia/TerminalSettings/ICoreSettings.idl b/src/cascadia/TerminalSettings/ICoreSettings.idl
index 99e08d9c0aa..b715bd70868 100644
--- a/src/cascadia/TerminalSettings/ICoreSettings.idl
+++ b/src/cascadia/TerminalSettings/ICoreSettings.idl
@@ -28,6 +28,7 @@ namespace Microsoft.Terminal.Settings
CursorStyle CursorShape;
UInt32 CursorHeight;
String WordDelimiters;
+ Boolean CopyOnSelect;
};
}
diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalSettings/TerminalSettings.cpp
index f36ff913b97..70c2d4f7a5f 100644
--- a/src/cascadia/TerminalSettings/TerminalSettings.cpp
+++ b/src/cascadia/TerminalSettings/TerminalSettings.cpp
@@ -21,6 +21,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation
_cursorShape{ CursorStyle::Vintage },
_cursorHeight{ DEFAULT_CURSOR_HEIGHT },
_wordDelimiters{ DEFAULT_WORD_DELIMITERS },
+ _copyOnSelect{ false },
_useAcrylic{ false },
_closeOnExit{ true },
_tintOpacity{ 0.5 },
@@ -149,6 +150,16 @@ namespace winrt::Microsoft::Terminal::Settings::implementation
_wordDelimiters = value;
}
+ bool TerminalSettings::CopyOnSelect()
+ {
+ return _copyOnSelect;
+ }
+
+ void TerminalSettings::CopyOnSelect(bool value)
+ {
+ _copyOnSelect = value;
+ }
+
bool TerminalSettings::UseAcrylic()
{
return _useAcrylic;
diff --git a/src/cascadia/TerminalSettings/terminalsettings.h b/src/cascadia/TerminalSettings/terminalsettings.h
index 8979e546c0a..c701631da8a 100644
--- a/src/cascadia/TerminalSettings/terminalsettings.h
+++ b/src/cascadia/TerminalSettings/terminalsettings.h
@@ -47,6 +47,8 @@ namespace winrt::Microsoft::Terminal::Settings::implementation
void CursorHeight(uint32_t value);
hstring WordDelimiters();
void WordDelimiters(hstring const& value);
+ bool CopyOnSelect();
+ void CopyOnSelect(bool value);
// ------------------------ End of Core Settings -----------------------
bool UseAcrylic();
@@ -116,6 +118,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation
winrt::Windows::UI::Xaml::Media::Stretch _backgroundImageStretchMode;
winrt::Windows::UI::Xaml::HorizontalAlignment _backgroundImageHorizontalAlignment;
winrt::Windows::UI::Xaml::VerticalAlignment _backgroundImageVerticalAlignment;
+ bool _copyOnSelect;
hstring _commandline;
hstring _startingDir;
hstring _startingTitle;
diff --git a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h
index 3024f4a34d7..eb5e4011a7c 100644
--- a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h
+++ b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h
@@ -32,6 +32,7 @@ namespace TerminalCoreUnitTests
CursorStyle CursorShape() const noexcept { return CursorStyle::Vintage; }
uint32_t CursorHeight() { return 42UL; }
winrt::hstring WordDelimiters() { return winrt::to_hstring(DEFAULT_WORD_DELIMITERS.c_str()); }
+ bool CopyOnSelect() { return _copyOnSelect; }
// other implemented methods
uint32_t GetColorTableEntry(int32_t) const { return 123; }
@@ -47,6 +48,7 @@ namespace TerminalCoreUnitTests
void CursorShape(CursorStyle const&) noexcept {}
void CursorHeight(uint32_t) {}
void WordDelimiters(winrt::hstring) {}
+ void CopyOnSelect(bool copyOnSelect) { _copyOnSelect = copyOnSelect; }
// other unimplemented methods
void SetColorTableEntry(int32_t /* index */, uint32_t /* value */) {}
@@ -55,5 +57,6 @@ namespace TerminalCoreUnitTests
int32_t _historySize;
int32_t _initialRows;
int32_t _initialCols;
+ bool _copyOnSelect{ false };
};
}
diff --git a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp
index 01465aa3d3a..530f7b29d04 100644
--- a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp
@@ -504,5 +504,63 @@ namespace TerminalCoreUnitTests
selection = term.GetViewport().ConvertToOrigin(selectionRects.at(1)).ToInclusive();
VERIFY_ARE_EQUAL(selection, SMALL_RECT({ 0, 11, 99, 11 }));
}
+
+ TEST_METHOD(CopyOnSelect)
+ {
+ Terminal term;
+ DummyRenderTarget emptyRT;
+ term.Create({ 100, 100 }, 0, emptyRT);
+
+ // set copyOnSelect for terminal
+ auto settings = winrt::make(0, 100, 100);
+ settings.CopyOnSelect(true);
+ term.UpdateSettings(settings);
+
+ // Simulate click at (x,y) = (5,10)
+ term.SetSelectionAnchor({ 5, 10 });
+
+ // Simulate move to (x,y) = (5,10)
+ // (So, no movement)
+ term.SetEndSelectionPosition({ 5, 10 });
+
+ // Case 1: single cell selection not allowed
+ {
+ // Simulate renderer calling TriggerSelection and acquiring selection area
+ auto selectionRects = term.GetSelectionRects();
+
+ // Validate selection area
+ VERIFY_ARE_EQUAL(selectionRects.size(), static_cast(0));
+
+ // single cell selection should not be allowed
+ // thus, selection is NOT active
+ VERIFY_IS_FALSE(term.IsSelectionActive());
+ }
+
+ // Case 2: move off of single cell
+ term.SetEndSelectionPosition({ 6, 10 });
+ { // Simulate renderer calling TriggerSelection and acquiring selection area
+ auto selectionRects = term.GetSelectionRects();
+
+ // Validate selection area
+ VERIFY_ARE_EQUAL(selectionRects.size(), static_cast(1));
+ auto selection = term.GetViewport().ConvertToOrigin(selectionRects.at(0)).ToInclusive();
+ VERIFY_ARE_EQUAL(selection, SMALL_RECT({ 5, 10, 6, 10 }));
+ VERIFY_IS_TRUE(term.IsSelectionActive());
+ }
+
+ // Case 3: move back onto single cell (now allowed)
+ term.SetEndSelectionPosition({ 5, 10 });
+ { // Simulate renderer calling TriggerSelection and acquiring selection area
+ auto selectionRects = term.GetSelectionRects();
+
+ // Validate selection area
+ VERIFY_ARE_EQUAL(selectionRects.size(), static_cast(1));
+ auto selection = term.GetViewport().ConvertToOrigin(selectionRects.at(0)).ToInclusive();
+ VERIFY_ARE_EQUAL(selection, SMALL_RECT({ 5, 10, 5, 10 }));
+
+ // single cell selection should now be allowed
+ VERIFY_IS_TRUE(term.IsSelectionActive());
+ }
+ }
};
}
diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.manifest b/src/cascadia/WindowsTerminal/WindowsTerminal.manifest
index 674a264e7c2..a447bc8fdc2 100644
--- a/src/cascadia/WindowsTerminal/WindowsTerminal.manifest
+++ b/src/cascadia/WindowsTerminal/WindowsTerminal.manifest
@@ -9,7 +9,8 @@
-
+
+