Skip to content

Commit

Permalink
Double and Triple Click Selection (microsoft#1197)
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-zamora authored Jul 11, 2019
1 parent 5b3a554 commit 6d3001f
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 10 deletions.
1 change: 1 addition & 0 deletions doc/cascadia/SettingsSchema.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Properties listed below affect the entire window, regardless of the profile sett
| `requestedTheme` | _Required_ | String | `system` | Sets the theme of the application. Possible values: `"light"`, `"dark"`, `"system"` |
| `showTerminalTitleInTitlebar` | _Required_ | Boolean | `true` | When set to `true`, titlebar displays the title of the selected tab. When set to `false`, titlebar displays "Windows Terminal". |
| `showTabsInTitlebar` | Optional | Boolean | `true` | When set to `true`, the tabs are moved into the titlebar and the titlebar disappears. When set to `false`, the titlebar sits above the tabs. |
| `wordDelimiters` | Optional | String | ` ./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}~?\u2502` | Determines the delimiters used in a double click selection. |

## Profiles
Properties listed below are specific to each unique profile.
Expand Down
21 changes: 20 additions & 1 deletion src/cascadia/TerminalApp/GlobalAppSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ static constexpr std::string_view InitialColsKey{ "initialCols" };
static constexpr std::string_view ShowTitleInTitlebarKey{ "showTerminalTitleInTitlebar" };
static constexpr std::string_view RequestedThemeKey{ "requestedTheme" };
static constexpr std::string_view ShowTabsInTitlebarKey{ "showTabsInTitlebar" };
static constexpr std::string_view WordDelimitersKey{ "wordDelimiters" };

static constexpr std::wstring_view LightThemeValue{ L"light" };
static constexpr std::wstring_view DarkThemeValue{ L"dark" };
Expand All @@ -37,7 +38,8 @@ GlobalAppSettings::GlobalAppSettings() :
_initialCols{ DEFAULT_COLS },
_showTitleInTitlebar{ true },
_showTabsInTitlebar{ true },
_requestedTheme{ ElementTheme::Default }
_requestedTheme{ ElementTheme::Default },
_wordDelimiters{ DEFAULT_WORD_DELIMITERS }
{
}

Expand Down Expand Up @@ -105,6 +107,16 @@ void GlobalAppSettings::SetRequestedTheme(const ElementTheme requestedTheme) noe
_requestedTheme = requestedTheme;
}

std::wstring GlobalAppSettings::GetWordDelimiters() const noexcept
{
return _wordDelimiters;
}

void GlobalAppSettings::SetWordDelimiters(const std::wstring wordDelimiters) noexcept
{
_wordDelimiters = wordDelimiters;
}

#pragma region ExperimentalSettings
bool GlobalAppSettings::GetShowTabsInTitlebar() const noexcept
{
Expand All @@ -128,6 +140,7 @@ void GlobalAppSettings::ApplyToSettings(TerminalSettings& settings) const noexce
settings.KeyBindings(GetKeybindings());
settings.InitialRows(_initialRows);
settings.InitialCols(_initialCols);
settings.WordDelimiters(_wordDelimiters);
}

// Method Description:
Expand All @@ -146,6 +159,7 @@ Json::Value GlobalAppSettings::ToJson() const
jsonObject[JsonKey(AlwaysShowTabsKey)] = _alwaysShowTabs;
jsonObject[JsonKey(ShowTitleInTitlebarKey)] = _showTitleInTitlebar;
jsonObject[JsonKey(ShowTabsInTitlebarKey)] = _showTabsInTitlebar;
jsonObject[JsonKey(WordDelimitersKey)] = winrt::to_string(_wordDelimiters);
jsonObject[JsonKey(RequestedThemeKey)] = winrt::to_string(_SerializeTheme(_requestedTheme));
jsonObject[JsonKey(KeybindingsKey)] = AppKeyBindingsSerialization::ToJson(_keybindings);

Expand Down Expand Up @@ -191,6 +205,11 @@ GlobalAppSettings GlobalAppSettings::FromJson(const Json::Value& json)
result._showTabsInTitlebar = showTabsInTitlebar.asBool();
}

if (auto wordDelimiters{ json[JsonKey(WordDelimitersKey)] })
{
result._wordDelimiters = GetWstringFromJson(wordDelimiters);
}

if (auto requestedTheme{ json[JsonKey(RequestedThemeKey)] })
{
result._requestedTheme = _ParseTheme(GetWstringFromJson(requestedTheme));
Expand Down
4 changes: 4 additions & 0 deletions src/cascadia/TerminalApp/GlobalAppSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class TerminalApp::GlobalAppSettings final
bool GetShowTabsInTitlebar() const noexcept;
void SetShowTabsInTitlebar(const bool showTabsInTitlebar) noexcept;

std::wstring GetWordDelimiters() const noexcept;
void SetWordDelimiters(const std::wstring wordDelimiters) noexcept;

winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme() const noexcept;

Json::Value ToJson() const;
Expand All @@ -68,6 +71,7 @@ class TerminalApp::GlobalAppSettings final
bool _showTitleInTitlebar;

bool _showTabsInTitlebar;
std::wstring _wordDelimiters;
winrt::Windows::UI::Xaml::ElementTheme _requestedTheme;

static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept;
Expand Down
70 changes: 61 additions & 9 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_actualFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false },
_touchAnchor{ std::nullopt },
_leadingSurrogate{},
_cursorTimer{}
_cursorTimer{},
_lastMouseClick{},
_lastMouseClickPos{}
{
_Create();
}
Expand Down Expand Up @@ -514,6 +516,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_cursorTimer = std::nullopt;
}

// import value from WinUser (convert from milli-seconds to micro-seconds)
_multiClickTimer = GetDoubleClickTime() * 1000;

_gotFocusRevoker = _controlRoot.GotFocus(winrt::auto_revoke, { this, &TermControl::_GotFocusHandler });
_lostFocusRevoker = _controlRoot.LostFocus(winrt::auto_revoke, { this, &TermControl::_LostFocusHandler });

Expand Down Expand Up @@ -669,13 +674,35 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
const auto cursorPosition = point.Position();
const auto terminalPosition = _GetTerminalPosition(cursorPosition);

// save location before rendering
_terminal->SetSelectionAnchor(terminalPosition);

// handle ALT key
_terminal->SetBoxSelection(altEnabled);

_renderer->TriggerSelection();
auto clickCount = _NumberOfClicks(cursorPosition, point.Timestamp());

// This formula enables the number of clicks to cycle properly between single-, double-, and triple-click.
// To increase the number of acceptable click states, simply increment MAX_CLICK_COUNT and add another if-statement
const unsigned int MAX_CLICK_COUNT = 3;
const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount;

if (multiClickMapper == 3)
{
_terminal->TripleClickSelection(terminalPosition);
_renderer->TriggerSelection();
}
else if (multiClickMapper == 2)
{
_terminal->DoubleClickSelection(terminalPosition);
_renderer->TriggerSelection();
}
else
{
// save location before rendering
_terminal->SetSelectionAnchor(terminalPosition);

_renderer->TriggerSelection();
_lastMouseClick = point.Timestamp();
_lastMouseClickPos = cursorPosition;
}
}
else if (point.Properties().IsRightButtonPressed())
{
Expand Down Expand Up @@ -1519,13 +1546,38 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
return terminalPosition;
}

// clang-format off
// Method Description:
// - Returns the number of clicks that occurred (double and triple click support)
// Arguments:
// - clickPos: the (x,y) position of a given cursor (i.e.: mouse cursor).
// NOTE: origin (0,0) is top-left.
// - clickTime: the timestamp that the click occurred
// Return Value:
// - if the click is in the same position as the last click and within the timeout, the number of clicks within that time window
// - otherwise, 1
const unsigned int TermControl::_NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime)
{
// if click occurred at a different location or past the multiClickTimer...
Timestamp delta;
THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClick, &delta));
if (clickPos != _lastMouseClickPos || delta > _multiClickTimer)
{
// exit early. This is a single click.
_multiClickCounter = 1;
}
else
{
_multiClickCounter++;
}
return _multiClickCounter;
}

// -------------------------------- WinRT Events ---------------------------------
// Winrt events need a method for adding a callback to the event and removing the callback.
// These macros will define them both for you.
DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs);
DEFINE_EVENT(TermControl, ConnectionClosed, _connectionClosedHandlers, TerminalControl::ConnectionClosedEventArgs);
DEFINE_EVENT(TermControl, CopyToClipboard, _clipboardCopyHandlers, TerminalControl::CopyToClipboardEventArgs);
DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs);
DEFINE_EVENT(TermControl, ConnectionClosed, _connectionClosedHandlers, TerminalControl::ConnectionClosedEventArgs);
DEFINE_EVENT(TermControl, CopyToClipboard, _clipboardCopyHandlers, TerminalControl::CopyToClipboardEventArgs);
DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs);
// clang-format on

Expand Down
10 changes: 10 additions & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// viewport via touch input.
std::optional<winrt::Windows::Foundation::Point> _touchAnchor;

using Timestamp = uint64_t;

// imported from WinUser
// Used for PointerPoint.Timestamp Property (https://docs.microsoft.com/en-us/uwp/api/windows.ui.input.pointerpoint.timestamp#Windows_UI_Input_PointerPoint_Timestamp)
Timestamp _multiClickTimer;
Timestamp _lastMouseClick;
unsigned int _multiClickCounter;
std::optional<winrt::Windows::Foundation::Point> _lastMouseClickPos;

// Event revokers -- we need to deregister ourselves before we die,
// lest we get callbacks afterwards.
winrt::Windows::UI::Xaml::Controls::Control::SizeChanged_revoker _sizeChangedRevoker;
Expand Down Expand Up @@ -147,6 +156,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
DWORD _GetPressedModifierKeys() const;

const COORD _GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition);
const unsigned int _NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime);
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting

_snapOnInput = settings.SnapOnInput();

_wordDelimiters = settings.WordDelimiters();

// 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
Expand Down
7 changes: 7 additions & 0 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ class Microsoft::Terminal::Core::Terminal final :
#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
const bool IsSelectionActive() const noexcept;
void DoubleClickSelection(const COORD position);
void TripleClickSelection(const COORD position);
void SetSelectionAnchor(const COORD position);
void SetEndSelectionPosition(const COORD position);
void SetBoxSelection(const bool isEnabled) noexcept;
Expand Down Expand Up @@ -149,6 +151,7 @@ class Microsoft::Terminal::Core::Terminal final :
bool _selectionActive;
SHORT _selectionAnchor_YOffset;
SHORT _endSelectionPosition_YOffset;
std::wstring _wordDelimiters;

std::shared_mutex _readWriteLock;

Expand Down Expand Up @@ -191,5 +194,9 @@ class Microsoft::Terminal::Core::Terminal final :
std::vector<SMALL_RECT> _GetSelectionRects() const;
const SHORT _ExpandWideGlyphSelectionLeft(const SHORT xPos, const SHORT yPos) const;
const SHORT _ExpandWideGlyphSelectionRight(const SHORT xPos, const SHORT yPos) const;
void _ExpandDoubleClickSelectionLeft(const COORD position);
void _ExpandDoubleClickSelectionRight(const COORD position);
const bool _isWordDelimiter(std::wstring_view cellChar) const;
const COORD _ConvertToBufferCell(const COORD viewportPos) const;
#pragma endregion
};
128 changes: 128 additions & 0 deletions src/cascadia/TerminalCore/TerminalSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,40 @@ const bool Terminal::IsSelectionActive() const noexcept
return _selectionActive;
}

// Method Description:
// - Select the sequence between delimiters defined in Settings
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
void Terminal::DoubleClickSelection(const COORD position)
{
// if you double click a delimiter, just select that one cell
COORD positionWithOffsets = _ConvertToBufferCell(position);
const auto cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
if (_isWordDelimiter(cellChar))
{
SetSelectionAnchor(position);
return;
}

// scan leftwards until delimiter is found and
// set selection anchor to one right of that spot
_ExpandDoubleClickSelectionLeft(position);

// scan rightwards until delimiter is found and
// set endSelectionPosition to one left of that spot
_ExpandDoubleClickSelectionRight(position);
}

// Method Description:
// - Select the entire row of the position clicked
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
void Terminal::TripleClickSelection(const COORD position)
{
SetSelectionAnchor({ 0, position.Y });
SetEndSelectionPosition({ _buffer->GetSize().RightInclusive(), position.Y });
}

// Method Description:
// - Record the position of the beginning of a selection
// Arguments:
Expand Down Expand Up @@ -213,3 +247,97 @@ const std::wstring Terminal::RetrieveSelectedTextFromBuffer(bool trimTrailingWhi

return result;
}

// Method Description:
// - expand the double click selection to the left (stopped by delimiter)
// Arguments:
// - position: viewport coordinate for selection
// Return Value:
// - update _selectionAnchor to new expanded location
void Terminal::_ExpandDoubleClickSelectionLeft(const COORD position)
{
// don't change the value if at/outside the boundary
if (position.X <= 0 || position.X >= _buffer->GetSize().RightInclusive())
{
return;
}

COORD positionWithOffsets = _ConvertToBufferCell(position);
const auto bufferViewport = _buffer->GetSize();
auto cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
while (positionWithOffsets.X != 0 && !_isWordDelimiter(cellChar))
{
bufferViewport.DecrementInBounds(positionWithOffsets);
cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
}

if (positionWithOffsets.X != 0 || _isWordDelimiter(cellChar))
{
// move off of delimiter to highlight properly
bufferViewport.IncrementInBounds(positionWithOffsets);
}

THROW_IF_FAILED(ShortSub(positionWithOffsets.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &positionWithOffsets.Y));
_selectionAnchor = positionWithOffsets;
_selectionAnchor_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
_selectionActive = true;
}

// Method Description:
// - expand the double click selection to the right (stopped by delimiter)
// Arguments:
// - position: viewport coordinate for selection
// Return Value:
// - update _endSelectionPosition to new expanded location
void Terminal::_ExpandDoubleClickSelectionRight(const COORD position)
{
// don't change the value if at/outside the boundary
if (position.X <= 0 || position.X >= _buffer->GetSize().RightInclusive())
{
return;
}

COORD positionWithOffsets = _ConvertToBufferCell(position);
const auto bufferViewport = _buffer->GetSize();
auto cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
while (positionWithOffsets.X != _buffer->GetSize().RightInclusive() && !_isWordDelimiter(cellChar))
{
bufferViewport.IncrementInBounds(positionWithOffsets);
cellChar = _buffer->GetCellDataAt(positionWithOffsets)->Chars();
}

if (positionWithOffsets.X != bufferViewport.RightInclusive() || _isWordDelimiter(cellChar))
{
// move off of delimiter to highlight properly
bufferViewport.DecrementInBounds(positionWithOffsets);
}

THROW_IF_FAILED(ShortSub(positionWithOffsets.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &positionWithOffsets.Y));
_endSelectionPosition = positionWithOffsets;
_endSelectionPosition_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
}

// Method Description:
// - check if buffer cell data contains delimiter for double click selection
// Arguments:
// - cellChar: the char saved to the buffer cell under observation
// Return Value:
// - true if cell data contains the delimiter.
const bool Terminal::_isWordDelimiter(std::wstring_view cellChar) const
{
return _wordDelimiters.find(cellChar) != std::wstring_view::npos;
}

// Method Description:
// - convert viewport position to the corresponding location on the buffer
// Arguments:
// - viewportPos: a coordinate on the viewport
// Return Value:
// - the corresponding location on the buffer
const COORD Terminal::_ConvertToBufferCell(const COORD viewportPos) const
{
COORD positionWithOffsets = viewportPos;
THROW_IF_FAILED(ShortSub(viewportPos.Y, gsl::narrow<SHORT>(_scrollOffset), &positionWithOffsets.Y));
THROW_IF_FAILED(ShortAdd(positionWithOffsets.Y, gsl::narrow<SHORT>(_ViewStartIndex()), &positionWithOffsets.Y));
return positionWithOffsets;
}
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettings/ICoreSettings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace Microsoft.Terminal.Settings
UInt32 CursorColor;
CursorStyle CursorShape;
UInt32 CursorHeight;
String WordDelimiters;
};

}
Loading

0 comments on commit 6d3001f

Please sign in to comment.