Merge remote-tracking branch 'origin/dev/migrie/f/non-terminal-content-elevation-warning' into dev/migrie/f/632-on-warning-dialog

This commit is contained in:
Mike Griese 2021-09-29 08:00:44 -05:00
commit 571b8a9c79
86 changed files with 1787 additions and 327 deletions

View file

@ -1,6 +1,7 @@
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

2
.gitattributes vendored
View file

@ -3,6 +3,8 @@
###############################################################################
* -text
*.inc linguist-language=cpp
###############################################################################
# Set default behavior for command prompt diff.
#

View file

@ -2,6 +2,7 @@ admins
apc
calt
ccmp
changelog
cybersecurity
Apc
clickable
@ -42,6 +43,7 @@ Lorigin
maxed
mkmk
mru
noreply
nje
ogonek
ok'd

View file

@ -28,6 +28,7 @@ DERR
dlldata
DONTADDTORECENT
DWORDLONG
enumset
environstrings
EXPCMDFLAGS
EXPCMDSTATE

View file

@ -25,6 +25,7 @@ DWINRT
enablewttlogging
Intelli
LKG
LOCKFILE
Lxss
mfcribbon
microsoft

View file

@ -53,6 +53,7 @@ oldnewthing
opengl
osgwiki
pabhojwa
panos
paulcam
pauldotknopf
PGP

View file

@ -194,6 +194,8 @@ cascadia
cassert
castsi
catid
carlos
zamora
cazamor
CBash
cbegin
@ -2690,6 +2692,7 @@ WINDOWPOSCHANGING
windowproc
windowrect
windowsapp
windowsdeveloper
windowsinternalstring
WINDOWSIZE
windowsx

View file

@ -15,7 +15,7 @@ Import-Module .\tools\OpenConsole.psm1
Set-MsBuildDevEnvironment
Get-Format
```
After, go to Tools > Options > Text Editor > C++ > Formatting and checking "Use custom clang-format.exe file" in Visual Studio and choose the clang-format.exe in the repository at /packages/clang-format.win-x86.10.0.0/tools/clang-format.exe by clicking "browse" right under the check box.
After, go to Tools > Options > Text Editor > C++ > Formatting and check "Use custom clang-format.exe file" in Visual Studio and choose the clang-format.exe in the repository at /packages/clang-format.win-x86.10.0.0/tools/clang-format.exe by clicking "browse" right under the check box.
### Building in PowerShell

View file

@ -847,7 +847,7 @@
}
],
"required": [ "actions" ]
},
},
"CommandPaletteAction": {
"description": "Arguments for a commandPalette action",
"allOf": [
@ -1274,6 +1274,11 @@
"description": "When set to true, the Terminal's notification icon will always be shown in the notification area.",
"type": "boolean"
},
"showAdminShield": {
"default": "true",
"description": "When set to true, the Terminal's tab row will display a shield icon when the Terminal is running with administrator privileges",
"type": "boolean"
},
"useAcrylicInTabRow": {
"default": "false",
"description": "When set to true, the tab row will have an acrylic background with 50% opacity.",

View file

@ -0,0 +1,171 @@
---
author: Carlos Zamora @carlos-zamora
created on: 2019-08-30
last updated: 2021-09-17
issue id: 715
---
# Keyboard Selection
## Abstract
This spec describes a new set of non-configurable keybindings that allows the user to update a selection without the use of a mouse or stylus.
## Inspiration
ConHost allows the user to modify a selection using the keyboard. Holding `Shift` allows the user to move the second selection endpoint in accordance with the arrow keys. The selection endpoint updates by one cell per key event, allowing the user to refine the selected region.
Mark mode allows the user to create a selection using only the keyboard, then edit it as mentioned above.
## Solution Design
The fundamental solution design for keyboard selection is that the responsibilities between the Terminal Control and Terminal Core must be very distinct. The Terminal Control is responsible for handling user interaction and directing the Terminal Core to update the selection. The Terminal Core will need to update the selection according to the preferences of the Terminal Control.
Relatively recently, TerminalControl was split into `TerminalControl`, `ControlInteractivity`, and `ControlCore`. Changes made to `ControlInteractivity`, `ControlCore`, and below propagate functionality to all consumers, meaning that the WPF terminal would benefit from these changes with no additional work required.
### Fundamental Terminal Control Changes
`ControlCore::TrySendKeyEvent()` is responsible for handling the key events after key bindings are dealt with in `TermControl`. At the time of writing this spec, there are 2 cases handled in this order:
- Clear the selection (except in a few key scenarios)
- Send Key Event
The first branch will be updated to _modify_ the selection instead of usually _clearing_ it. This will happen by converting the key event into parameters to forward to `TerminalCore`, which then updates the selection appropriately.
#### Idea: Make keyboard selection a collection of standard keybindings
One idea is to introduce an `updateSelection` action that conditionally works if a selection is active (similar to the `copy` action). For these key bindings, if there is no selection, the key events are forwarded to the application.
Thanks to Keybinding Args, there would only be 1 new command:
| Action | Keybinding Args | Description |
|--|--|--|
| `updateSelection` | | If a selection exists, moves the last selection endpoint. |
| | `Enum direction { up, down, left, right }` | The direction the selection will be moved in. |
| | `Enum mode { char, word, view, buffer }` | The context for which to move the selection endpoint to. (defaults to `char`) |
By default, the following keybindings will be set:
```JS
// Character Selection
{ "command": {"action": "updateSelection", "direction": "left", "mode": "char" }, "keys": "shift+left" },
{ "command": {"action": "updateSelection", "direction": "right", "mode": "char" }, "keys": "shift+right" },
{ "command": {"action": "updateSelection", "direction": "up", "mode": "char" }, "keys": "shift+up" },
{ "command": {"action": "updateSelection", "direction": "down", "mode": "char" }, "keys": "shift+down" },
// Word Selection
{ "command": {"action": "updateSelection", "direction": "left", "mode": "word" }, "keys": "ctrl+shift+left" },
{ "command": {"action": "updateSelection", "direction": "right", "mode": "word" }, "keys": "ctrl+shift+right" },
// Viewport Selection
{ "command": {"action": "updateSelection", "direction": "left", "mode": "view" }, "keys": "shift+home" },
{ "command": {"action": "updateSelection", "direction": "right", "mode": "view" }, "keys": "shift+end" },
{ "command": {"action": "updateSelection", "direction": "up", "mode": "view" }, "keys": "shift+pgup" },
{ "command": {"action": "updateSelection", "direction": "down", "mode": "view" }, "keys": "shift+pgdn" },
// Buffer Corner Selection
{ "command": {"action": "updateSelection", "direction": "up", "mode": "buffer" }, "keys": "ctrl+shift+home" },
{ "command": {"action": "updateSelection", "direction": "down", "mode": "buffer" }, "keys": "ctrl+shift+end" },
```
These are in accordance with ConHost's keyboard selection model.
This idea was abandoned due to several reasons:
1. Keyboard selection should be a standard way to interact with a terminal across all consumers (i.e. WPF control, etc.)
2. There isn't really another set of key bindings that makes sense for this. We already hardcoded <kbd>ESC</kbd> as a way to clear the selection. This is just an extension of that.
3. Adding 12 conditionally effective key bindings takes the spot of 12 potential non-conditional key bindings. It would be nice if a different key binding could be set when the selection is not active, but that makes the settings design much more complicated.
4. 12 new items in the command palette is also pretty excessive.
5. If proven wrong when this is in WT Preview, we can revisit this and make them customizable then. It's better to add the ability to customize it later than take it away.
#### Idea: Make keyboard selection a simulation of mouse selection
It may seem that some effort can be saved by making the keyboard selection act as a simulation of mouse selection. There is a union of mouse and keyboard activity that can be represented in a single set of selection motion interfaces that are commanded by the TermControl's Mouse/Keyboard handler and adapted into appropriate motions in the Terminal Core.
However, the mouse handler operates by translating a pixel coordinate on the screen to a text buffer coordinate. This would have to be rewritten and the approach was deemed unworthy.
### Fundamental Terminal Core Changes
The Terminal Core will need to expose a `UpdateSelection()` function that is called by the keybinding handler. The following parameters will need to be passed in:
- `enum SelectionDirection`: the direction that the selection endpoint will attempt to move to. Possible values include `Up`, `Down`, `Left`, and `Right`.
- `enum SelectionExpansion`: the selection expansion mode that the selection endpoint will adhere to. Possible values include `Char`, `Word`, `View`, `Buffer`.
#### Moving by Cell
For `SelectionExpansion = Char`, the selection endpoint will be updated according to the buffer's output pattern. For **horizontal movements**, the selection endpoint will attempt to move left or right. If a viewport boundary is hit, the endpoint will wrap appropriately (i.e.: hitting the left boundary moves it to the last cell of the line above it).
For **vertical movements**, the selection endpoint will attempt to move up or down. If a **viewport boundary** is hit and there is a scroll buffer, the endpoint will move and scroll accordingly by a line.
If a **buffer boundary** is hit, the endpoint will not move. In this case, however, the event will still be considered handled.
**NOTE**: An important thing to handle properly in all cases is wide glyphs. The user should not be allowed to select a portion of a wide glyph; it should be all or none of it. When calling `_ExpandWideGlyphSelection` functions, the result must be saved to the endpoint.
#### Moving by Word
For `SelectionExpansion = Word`, the selection endpoint will also be updated according to the buffer's output pattern, as above. However, the selection will be updated in accordance with "chunk selection" (performing a double-click and dragging the mouse to expand the selection). For **horizontal movements**, the selection endpoint will be updated according to the `_ExpandDoubleClickSelection` functions. The result must be saved to the endpoint. As before, if a boundary is hit, the endpoint will wrap appropriately. See [Future Considerations](#FutureConsiderations) for how this will interact with line wrapping.
For **vertical movements**, the movement is a little more complicated than before. The selection will still respond to buffer and viewport boundaries as before. If the user is trying to move up, the selection endpoint will attempt to move up by one line, then selection will be expanded leftwards. Alternatively, if the user is trying to move down, the selection endpoint will attempt to move down by one line, then the selection will be expanded rightwards.
#### Moving by Viewport
For `SelectionExpansion = View`, the selection endpoint will be updated according to the viewport's height. Horizontal movements will be updated according to the viewport's width, thus resulting in the endpoint being moved to the left/right boundary of the viewport.
#### Moving by Buffer
For `SelectionExpansion = Buffer`, the selection endpoint will be moved to the beginning or end of all the text within the buffer. If moving up or left, set the position to 0,0 (the origin of the buffer). If moving down or right, set the position to the last character in the buffer.
**NOTE**: In all cases, horizontal movements attempting to move past the left/right viewport boundaries result in a wrap. Vertical movements attempting to move past the top/bottom viewport boundaries will scroll such that the selection is at the edge of the screen. Vertical movements attempting to move past the top/bottom buffer boundaries will be clamped to be within buffer boundaries.
Every combination of the `SelectionDirection` and `SelectionExpansion` will map to a keybinding. These pairings are shown below in the UI/UX Design --> Keybindings section.
**NOTE**: If `copyOnSelect` is enabled, we need to make sure we **DO NOT** update the clipboard on every change in selection. The user must explicitly choose to copy the selected text from the buffer.
## UI/UX Design
### Key Bindings
There will only be 1 new command that needs to be added:
| Action | Keybinding Args | Description |
|--|--|--|
| `selectAll` | | Select the entire text buffer.
By default, the following key binding will be set:
```JS
{ "command": "selectAll", "keys": "ctrl+shift+a" },
```
## Capabilities
### Accessibility
Using the keyboard is generally a more accessible experience than using the mouse. Being able to modify a selection by using the keyboard is a good first step towards making selecting text more accessible.
### Security
N/A
### Reliability
With regards to the Terminal Core, the newly introduced code should rely on already existing and tested code. Thus no crash-related bugs are expected.
With regards to Terminal Control and the settings model, crash-related bugs are not expected. However, ensuring that the selection is updated and cleared in general use-case scenarios must be ensured.
### Compatibility
N/A
### Performance, Power, and Efficiency
## Potential Issues
### Grapheme Clusters
When grapheme cluster support is inevitably added to the Text Buffer, moving by "cell" is expected to move by "character" or "cluster". This is similar to how wide glyphs are handled today. Either all of it is selected, or none of it.
## Future considerations
### Word Selection Wrap
At the time of writing this spec, expanding or moving by word is interrupted by the beginning or end of the line, regardless of the wrap flag being set. In the future, selection and the accessibility models will respect the wrap flag on the text buffer.
## Mark Mode
This functionality will be expanded to create a feature similar to Mark Mode. This will allow a user to create a selection using only the keyboard.
## Resources
- https://blogs.windows.com/windowsdeveloper/2014/10/07/console-improvements-in-the-windows-10-technical-preview/

View file

@ -12,6 +12,7 @@
<EventProvider Id="EventProvider_TerminalWin32Host" Name="56c06166-2e2e-5f4d-7ff3-74f4b78c87d6" />
<EventProvider Id="EventProvider_TerminalRemoting" Name="d6f04aad-629f-539a-77c1-73f5c3e4aa7b" />
<EventProvider Id="EventProvider_TerminalDirectX" Name="c93e739e-ae50-5a14-78e7-f171e947535d" />
<EventProvider Id="EventProvider_TerminalUIA" Name="e7ebce59-2161-572d-b263-2f16a6afb9e5"/>
<Profile Id="Terminal.Verbose.File" Name="Terminal" Description="Terminal" LoggingMode="File" DetailLevel="Verbose">
<Collectors>
<EventCollectorId Value="EventCollector_Terminal">
@ -23,6 +24,7 @@
<EventProviderId Value="EventProvider_TerminalWin32Host" />
<EventProviderId Value="EventProvider_TerminalRemoting" />
<EventProviderId Value="EventProvider_TerminalDirectX" />
<EventProviderId Value="EventProvider_TerminalUIA" />
</EventProviders>
</EventCollectorId>
</Collectors>

View file

@ -1430,12 +1430,13 @@ const til::point TextBuffer::GetGlyphStart(const til::point pos, std::optional<t
}
// Method Description:
// - Update pos to be the end of the current glyph/character. This is used for accessibility
// - Update pos to be the end of the current glyph/character.
// Arguments:
// - pos - a COORD on the word you are currently on
// - accessibilityMode - this is being used for accessibility; make the end exclusive.
// Return Value:
// - pos - The COORD for the last cell of the current glyph (exclusive)
const til::point TextBuffer::GetGlyphEnd(const til::point pos, std::optional<til::point> limitOptional) const
const til::point TextBuffer::GetGlyphEnd(const til::point pos, bool accessibilityMode, std::optional<til::point> limitOptional) const
{
COORD resultPos = pos;
const auto bufferSize = GetSize();
@ -1453,7 +1454,10 @@ const til::point TextBuffer::GetGlyphEnd(const til::point pos, std::optional<til
}
// increment one more time to become exclusive
bufferSize.IncrementInBounds(resultPos, true);
if (accessibilityMode)
{
bufferSize.IncrementInBounds(resultPos, true);
}
return resultPos;
}

View file

@ -147,7 +147,7 @@ public:
bool MoveToPreviousWord(COORD& pos, const std::wstring_view wordDelimiters) const;
const til::point GetGlyphStart(const til::point pos, std::optional<til::point> limitOptional = std::nullopt) const;
const til::point GetGlyphEnd(const til::point pos, std::optional<til::point> limitOptional = std::nullopt) const;
const til::point GetGlyphEnd(const til::point pos, bool accessibilityMode = false, std::optional<til::point> limitOptional = std::nullopt) const;
bool MoveToNextGlyph(til::point& pos, bool allowBottomExclusive = false, std::optional<til::point> limitOptional = std::nullopt) const;
bool MoveToPreviousGlyph(til::point& pos, std::optional<til::point> limitOptional = std::nullopt) const;

View file

@ -465,6 +465,10 @@ namespace SettingsModelLocalTests
"name":"action7_startingDirectoryWithTrailingSlash",
"command": { "action": "newWindow", "startingDirectory":"C:\\", "commandline": "bar.exe" }
},
{
"name":"action8_tabTitleEscaping",
"command": { "action": "newWindow", "tabTitle":"\\\";foo\\" }
}
])" };
const auto commands0Json = VerifyParseSucceeded(commands0String);
@ -473,7 +477,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(0u, commands.Size());
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
VERIFY_ARE_EQUAL(0u, warnings.size());
VERIFY_ARE_EQUAL(8u, commands.Size());
VERIFY_ARE_EQUAL(9u, commands.Size());
{
auto command = commands.Lookup(L"action0");
@ -586,5 +590,20 @@ namespace SettingsModelLocalTests
L"cmdline: \"%s\"", cmdline.c_str()));
VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\\\\" -- \"bar.exe\"", terminalArgs.ToCommandline());
}
{
auto command = commands.Lookup(L"action8_tabTitleEscaping");
VERIFY_IS_NOT_NULL(command);
VERIFY_IS_NOT_NULL(command.ActionAndArgs());
VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.ActionAndArgs().Action());
const auto& realArgs = command.ActionAndArgs().Args().try_as<NewWindowArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& terminalArgs = realArgs.TerminalArgs();
VERIFY_IS_NOT_NULL(terminalArgs);
auto cmdline = terminalArgs.ToCommandline();
Log::Comment(NoThrowString().Format(
L"cmdline: \"%s\"", cmdline.c_str()));
VERIFY_ARE_EQUAL(LR"-(--title "\\\"\;foo\\")-", terminalArgs.ToCommandline());
}
}
}

View file

@ -549,11 +549,11 @@ try
if (multiClickMapper == 3)
{
_terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansionMode::Line);
_terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Line);
}
else if (multiClickMapper == 2)
{
_terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansionMode::Word);
_terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Word);
}
else
{

View file

@ -0,0 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "GetWindowLayoutArgs.h"
#include "GetWindowLayoutArgs.g.cpp"

View file

@ -0,0 +1,32 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Class Name:
- GetWindowLayoutArgs.h
Abstract:
- This is a helper class for getting the window layout from a peasant.
Depending on if we are running on the monarch or on a peasant we might need
to switch what thread we are executing on. This gives us the option of
either returning the json result synchronously, or as a promise.
--*/
#pragma once
#include "GetWindowLayoutArgs.g.h"
#include "../cascadia/inc/cppwinrt_utils.h"
namespace winrt::Microsoft::Terminal::Remoting::implementation
{
struct GetWindowLayoutArgs : public GetWindowLayoutArgsT<GetWindowLayoutArgs>
{
WINRT_PROPERTY(winrt::hstring, WindowLayoutJson, L"");
WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncOperation<winrt::hstring>, WindowLayoutJsonAsync, nullptr)
};
}
namespace winrt::Microsoft::Terminal::Remoting::factory_implementation
{
BASIC_FACTORY(GetWindowLayoutArgs);
}

View file

@ -12,7 +12,6 @@
</PropertyGroup>
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<!-- ========================= Headers ======================== -->
<ItemGroup>
<ClInclude Include="Monarch.h">
@ -36,6 +35,12 @@
<ClInclude Include="WindowActivatedArgs.h">
<DependentUpon>Peasant.idl</DependentUpon>
</ClInclude>
<ClInclude Include="GetWindowLayoutArgs.h">
<DependentUpon>Peasant.idl</DependentUpon>
</ClInclude>
<ClInclude Include="QuitAllRequestedArgs.h">
<DependentUpon>Monarch.idl</DependentUpon>
</ClInclude>
<ClInclude Include="pch.h" />
<ClInclude Include="MonarchFactory.h" />
<ClInclude Include="Peasant.h">
@ -71,6 +76,12 @@
<ClCompile Include="WindowActivatedArgs.cpp">
<DependentUpon>Peasant.idl</DependentUpon>
</ClCompile>
<ClCompile Include="GetWindowLayoutArgs.cpp">
<DependentUpon>Peasant.idl</DependentUpon>
</ClCompile>
<ClCompile Include="QuitAllRequestedArgs.cpp">
<DependentUpon>Monarch.idl</DependentUpon>
</ClCompile>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
@ -128,6 +139,5 @@
</ItemDefinitionGroup>
<!-- ========================= Globals ======================== -->
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />
<Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
</Project>
</Project>

View file

@ -6,6 +6,7 @@
#include "Monarch.h"
#include "CommandlineArgs.h"
#include "FindTargetWindowArgs.h"
#include "QuitAllRequestedArgs.h"
#include "ProposeCommandlineResult.h"
#include "Monarch.g.cpp"
@ -135,12 +136,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
// - <none> used
// Return Value:
// - <none>
void Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const winrt::Windows::Foundation::IInspectable& /*args*/)
winrt::fire_and_forget Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const winrt::Windows::Foundation::IInspectable& /*args*/)
{
// Let the process hosting the monarch run any needed logic before
// closing all windows.
_QuitAllRequestedHandlers(*this, nullptr);
auto args = winrt::make_self<implementation::QuitAllRequestedArgs>();
_QuitAllRequestedHandlers(*this, *args);
if (const auto action = args->BeforeQuitAllAction())
{
co_await action;
}
_quitting.store(true);
// Tell all peasants to exit.
@ -994,4 +1001,28 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
_forEachPeasant(func, onError);
}
// Method Description:
// - Ask all peasants to return their window layout as json
// Arguments:
// - <none>
// Return Value:
// - The collection of window layouts from each peasant.
Windows::Foundation::Collections::IVector<winrt::hstring> Monarch::GetAllWindowLayouts()
{
std::vector<winrt::hstring> vec;
auto callback = [&](const auto& /*id*/, const auto& p) {
vec.emplace_back(p.GetWindowLayout());
};
auto onError = [](auto&& id) {
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_GetAllWindowLayouts_Failed",
TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not get a window layout from"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
};
_forEachPeasant(callback, onError);
return winrt::single_threaded_vector(std::move(vec));
}
}

View file

@ -59,13 +59,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
void SummonAllWindows();
bool DoesQuakeWindowExist();
Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Terminal::Remoting::PeasantInfo> GetPeasantInfos();
Windows::Foundation::Collections::IVector<winrt::hstring> GetAllWindowLayouts();
TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs);
private:
uint64_t _ourPID;
@ -103,8 +104,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
void _renameRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args);
void _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
winrt::fire_and_forget _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
// Method Description:
// - Helper for doing something on each and every peasant.
@ -177,6 +178,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
}
}
_clearOldMruEntries(peasantsToErase);
// A peasant died, let the app host know that the number of
// windows has changed.
_WindowClosedHandlers(nullptr, nullptr);
}
}

View file

@ -31,6 +31,12 @@ namespace Microsoft.Terminal.Remoting
Windows.Foundation.IReference<UInt64> WindowID;
}
[default_interface] runtimeclass QuitAllRequestedArgs
{
QuitAllRequestedArgs();
Windows.Foundation.IAsyncAction BeforeQuitAllAction;
}
struct PeasantInfo
{
UInt64 Id;
@ -52,12 +58,13 @@ namespace Microsoft.Terminal.Remoting
void SummonAllWindows();
Boolean DoesQuakeWindowExist();
Windows.Foundation.Collections.IVectorView<PeasantInfo> GetPeasantInfos { get; };
Windows.Foundation.Collections.IVector<String> GetAllWindowLayouts();
event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> ShowNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> HideNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowCreated;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowClosed;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitAllRequested;
event Windows.Foundation.TypedEventHandler<Object, QuitAllRequestedArgs> QuitAllRequested;
};
}

View file

@ -5,6 +5,7 @@
#include "Peasant.h"
#include "CommandlineArgs.h"
#include "SummonWindowBehavior.h"
#include "GetWindowLayoutArgs.h"
#include "Peasant.g.cpp"
#include "../../types/inc/utils.hpp"
@ -289,4 +290,24 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
// Method Description:
// - Request and return the window layout from the current TerminalPage
// Arguments:
// - <none>
// Return Value:
// - the window layout as a json string
hstring Peasant::GetWindowLayout()
{
auto args = winrt::make_self<implementation::GetWindowLayoutArgs>();
_GetWindowLayoutRequestedHandlers(nullptr, *args);
if (const auto op = args->WindowLayoutJsonAsync())
{
// This will fail if called on the UI thread, so the monarch should
// never set WindowLayoutJsonAsync.
auto str = op.get();
return str;
}
return args->WindowLayoutJson();
}
}

View file

@ -36,6 +36,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs();
winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs();
winrt::hstring GetWindowLayout();
WINRT_PROPERTY(winrt::hstring, WindowName);
WINRT_PROPERTY(winrt::hstring, ActiveTabTitle);
@ -49,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs);
private:
Peasant(const uint64_t testPID);

View file

@ -30,6 +30,11 @@ namespace Microsoft.Terminal.Remoting
Windows.Foundation.DateTime ActivatedTime { get; };
};
[default_interface] runtimeclass GetWindowLayoutArgs {
GetWindowLayoutArgs();
String WindowLayoutJson;
Windows.Foundation.IAsyncOperation<String> WindowLayoutJsonAsync;
}
enum MonitorBehavior
{
@ -69,6 +74,7 @@ namespace Microsoft.Terminal.Remoting
void RequestHideNotificationIcon();
void RequestQuitAll();
void Quit();
String GetWindowLayout();
event Windows.Foundation.TypedEventHandler<Object, WindowActivatedArgs> WindowActivated;
event Windows.Foundation.TypedEventHandler<Object, CommandlineArgs> ExecuteCommandlineRequested;
@ -78,6 +84,7 @@ namespace Microsoft.Terminal.Remoting
event Windows.Foundation.TypedEventHandler<Object, SummonWindowBehavior> SummonRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> ShowNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> HideNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, GetWindowLayoutArgs> GetWindowLayoutRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitAllRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
};

View file

@ -0,0 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "QuitAllRequestedArgs.h"
#include "QuitAllRequestedArgs.g.cpp"

View file

@ -0,0 +1,30 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Class Name:
- QuitAllRequestedArgs.h
Abstract:
- This is a helper class for allowing the monarch to run code before telling all
peasants to quit. This way the monarch can raise an event and get back a future
to wait for before continuing.
--*/
#pragma once
#include "QuitAllRequestedArgs.g.h"
#include "../cascadia/inc/cppwinrt_utils.h"
namespace winrt::Microsoft::Terminal::Remoting::implementation
{
struct QuitAllRequestedArgs : public QuitAllRequestedArgsT<QuitAllRequestedArgs>
{
WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncAction, BeforeQuitAllAction, nullptr)
};
}
namespace winrt::Microsoft::Terminal::Remoting::factory_implementation
{
BASIC_FACTORY(QuitAllRequestedArgs);
}

View file

@ -271,7 +271,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
_monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested });
_monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); });
_monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); });
_monarch.QuitAllRequested([this](auto&&, auto&&) { _QuitAllRequestedHandlers(*this, nullptr); });
_monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers });
_BecameMonarchHandlers(*this, nullptr);
}
@ -318,6 +318,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
}
}
_peasant.GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers });
TraceLoggingWrite(g_hRemotingProvider,
"WindowManager_CreateOurPeasant",
TraceLoggingUInt64(_peasant.GetID(), "peasantID", "The ID of our new peasant"),
@ -610,4 +612,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
{
winrt::get_self<implementation::Peasant>(_peasant)->ActiveTabTitle(title);
}
Windows::Foundation::Collections::IVector<winrt::hstring> WindowManager::GetAllWindowLayouts()
{
if (_monarch)
{
try
{
return _monarch.GetAllWindowLayouts();
}
CATCH_LOG()
}
return nullptr;
}
}

View file

@ -50,6 +50,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
winrt::fire_and_forget RequestQuitAll();
bool DoesQuakeWindowExist();
void UpdateActiveTabTitle(winrt::hstring title);
Windows::Foundation::Collections::IVector<winrt::hstring> GetAllWindowLayouts();
TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
@ -57,7 +58,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs);
TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs);
private:
bool _shouldCreateWindow{ false };

View file

@ -16,6 +16,8 @@ namespace Microsoft.Terminal.Remoting
void SummonAllWindows();
void RequestShowNotificationIcon();
void RequestHideNotificationIcon();
Windows.Foundation.Collections.IVector<String> GetAllWindowLayouts();
UInt64 GetNumberOfPeasants();
void RequestQuitAll();
void UpdateActiveTabTitle(String title);
@ -25,8 +27,9 @@ namespace Microsoft.Terminal.Remoting
event Windows.Foundation.TypedEventHandler<Object, Object> BecameMonarch;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowCreated;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowClosed;
event Windows.Foundation.TypedEventHandler<Object, QuitAllRequestedArgs> QuitAllRequested;
event Windows.Foundation.TypedEventHandler<Object, GetWindowLayoutArgs> GetWindowLayoutRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> ShowNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> HideNotificationIconRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitAllRequested;
};
}

View file

@ -55,9 +55,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray,
STARTUPINFOEX siEx{ 0 };
siEx.StartupInfo.cb = sizeof(STARTUPINFOEX);
// Append a "\." to the given path, so that this will work in "C:\"
auto path{ wil::str_printf<std::wstring>(LR"-(%s\.)-", pszName.get()) };
auto cmdline{ wil::str_printf<std::wstring>(LR"-("%s" -d "%s")-", GetWtExePath().c_str(), path.c_str()) };
auto cmdline{ wil::str_printf<std::wstring>(LR"-("%s" -d %s)-", GetWtExePath().c_str(), QuoteAndEscapeCommandlineArg(pszName.get()).c_str()) };
RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(
nullptr, // lpApplicationName
cmdline.data(),
@ -66,7 +64,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray,
false, // bInheritHandles
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags
nullptr, // lpEnvironment
path.data(),
pszName.get(),
&siEx.StartupInfo, // lpStartupInfo
&_piClient // lpProcessInformation
));

View file

@ -78,7 +78,7 @@ namespace winrt::TerminalApp::implementation
void TerminalPage::_HandleCloseWindow(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
CloseWindow(false);
_CloseRequestedHandlers(nullptr, nullptr);
args.Handled(true);
}

View file

@ -187,6 +187,10 @@ void AppCommandlineArgs::_buildParser()
_windowTarget,
RS_A(L"CmdWindowTargetArgDesc"));
_app.add_option("-s,--saved",
_loadPersistedLayoutIdx,
RS_A(L"CmdSavedLayoutArgDesc"));
// Subcommands
_buildNewTabParser();
_buildSplitPaneParser();
@ -700,6 +704,7 @@ void AppCommandlineArgs::_resetStateToDefault()
_swapPaneDirection = FocusDirection::None;
_focusPaneTarget = -1;
_loadPersistedLayoutIdx = -1;
// DON'T clear _launchMode here! This will get called once for every
// subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out
@ -915,6 +920,12 @@ void AppCommandlineArgs::ValidateStartupCommands()
}
}
}
std::optional<uint32_t> AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept
{
return _loadPersistedLayoutIdx >= 0 ?
std::optional{ static_cast<uint32_t>(_loadPersistedLayoutIdx) } :
std::nullopt;
}
std::optional<winrt::Microsoft::Terminal::Settings::Model::LaunchMode> AppCommandlineArgs::GetLaunchMode() const noexcept
{

View file

@ -39,6 +39,7 @@ public:
const std::string& GetExitMessage();
bool ShouldExitEarly() const noexcept;
std::optional<uint32_t> GetPersistedLayoutIdx() const noexcept;
std::optional<winrt::Microsoft::Terminal::Settings::Model::LaunchMode> GetLaunchMode() const noexcept;
int ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args);
@ -123,6 +124,7 @@ private:
std::string _exitMessage;
bool _shouldExitEarly{ false };
int _loadPersistedLayoutIdx{};
std::string _windowTarget{};
// Are you adding more args or attributes here? If they are not reset in _resetStateToDefault, make sure to reset them in FullResetState

View file

@ -582,13 +582,11 @@ namespace winrt::TerminalApp::implementation
winrt::Windows::Foundation::Size proposedSize{};
const float scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
if (_root->ShouldUsePersistedLayout(_settings))
if (const auto layout = _root->LoadPersistedLayout(_settings))
{
const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialSize())
if (layout.InitialSize())
{
proposedSize = layouts.GetAt(0).InitialSize().Value();
proposedSize = layout.InitialSize().Value();
// The size is saved as a non-scaled real pixel size,
// so we need to scale it appropriately.
proposedSize.Height = proposedSize.Height * scale;
@ -686,13 +684,11 @@ namespace winrt::TerminalApp::implementation
auto initialPosition{ _settings.GlobalSettings().InitialPosition() };
if (_root->ShouldUsePersistedLayout(_settings))
if (const auto layout = _root->LoadPersistedLayout(_settings))
{
const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialPosition())
if (layout.InitialPosition())
{
initialPosition = layouts.GetAt(0).InitialPosition().Value();
initialPosition = layout.InitialPosition().Value();
}
}
@ -1132,10 +1128,22 @@ namespace winrt::TerminalApp::implementation
// - <none>
// Return Value:
// - <none>
void AppLogic::WindowCloseButtonClicked()
void AppLogic::CloseWindow(LaunchPosition pos)
{
if (_root)
{
// If persisted layout is enabled and we are the last window closing
// we should save our state.
if (_root->ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1)
{
if (const auto layout = _root->GetWindowLayout())
{
layout.InitialPosition(pos);
const auto state = ApplicationState::SharedInstance();
state.PersistedWindowLayouts(winrt::single_threaded_vector<WindowLayout>({ layout }));
}
}
_root->CloseWindow(false);
}
}
@ -1149,6 +1157,16 @@ namespace winrt::TerminalApp::implementation
return {};
}
bool AppLogic::HasCommandlineArguments() const noexcept
{
return _hasCommandLineArguments;
}
bool AppLogic::HasSettingsStartupActions() const noexcept
{
return _hasSettingsStartupActions;
}
// Method Description:
// - Sets the initial commandline to process on startup, and attempts to
// parse it. Commands will be parsed into a list of ShortcutActions that
@ -1172,6 +1190,10 @@ namespace winrt::TerminalApp::implementation
// then it contains only the executable name and no other arguments.
_hasCommandLineArguments = args.size() > 1;
_appArgs.ValidateStartupCommands();
if (const auto idx = _appArgs.GetPersistedLayoutIdx())
{
_root->SetPersistedLayoutIdx(idx.value());
}
_root->SetStartupActions(_appArgs.GetStartupActions());
// Check if we were started as a COM server for inbound connections of console sessions
@ -1409,6 +1431,40 @@ namespace winrt::TerminalApp::implementation
return _settings.GlobalSettings().ActionMap().GlobalHotkeys();
}
bool AppLogic::ShouldUsePersistedLayout()
{
return _root != nullptr ? _root->ShouldUsePersistedLayout(_settings) : false;
}
void AppLogic::SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector<hstring>& layouts)
{
std::vector<WindowLayout> converted;
converted.reserve(layouts.Size());
for (const auto& json : layouts)
{
if (json != L"")
{
converted.emplace_back(WindowLayout::FromJson(json));
}
}
ApplicationState::SharedInstance().PersistedWindowLayouts(winrt::single_threaded_vector(std::move(converted)));
}
hstring AppLogic::GetWindowLayoutJson(LaunchPosition position)
{
if (_root != nullptr)
{
if (const auto layout = _root->GetWindowLayout())
{
layout.InitialPosition(position);
return WindowLayout::ToJson(layout);
}
}
return L"";
}
void AppLogic::IdentifyWindow()
{
if (_root)
@ -1440,8 +1496,17 @@ namespace winrt::TerminalApp::implementation
}
}
void AppLogic::SetPersistedLayoutIdx(const uint32_t idx)
{
if (_root)
{
_root->SetPersistedLayoutIdx(idx);
}
}
void AppLogic::SetNumberOfOpenWindows(const uint64_t num)
{
_numOpenWindows = num;
if (_root)
{
_root->SetNumberOfOpenWindows(num);

View file

@ -55,6 +55,8 @@ namespace winrt::TerminalApp::implementation
void Quit();
bool HasCommandlineArguments() const noexcept;
bool HasSettingsStartupActions() const noexcept;
int32_t SetStartupCommandline(array_view<const winrt::hstring> actions);
int32_t ExecuteCommandline(array_view<const winrt::hstring> actions, const winrt::hstring& cwd);
TerminalApp::FindTargetWindowResult FindTargetWindow(array_view<const winrt::hstring> actions);
@ -65,12 +67,16 @@ namespace winrt::TerminalApp::implementation
bool Fullscreen() const;
bool AlwaysOnTop() const;
bool ShouldUsePersistedLayout();
hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position);
void SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector<hstring>& layouts);
void IdentifyWindow();
void RenameFailed();
winrt::hstring WindowName();
void WindowName(const winrt::hstring& name);
uint64_t WindowId();
void WindowId(const uint64_t& id);
void SetPersistedLayoutIdx(const uint32_t idx);
void SetNumberOfOpenWindows(const uint64_t num);
bool IsQuakeWindow() const noexcept;
@ -91,7 +97,7 @@ namespace winrt::TerminalApp::implementation
void TitlebarClicked();
bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down);
void WindowCloseButtonClicked();
void CloseWindow(Microsoft::Terminal::Settings::Model::LaunchPosition position);
winrt::TerminalApp::TaskbarState TaskbarState();
@ -123,6 +129,8 @@ namespace winrt::TerminalApp::implementation
HRESULT _settingsLoadedResult = S_OK;
bool _loadedInitialSettings = false;
uint64_t _numOpenWindows{ 0 };
std::shared_mutex _dialogLock;
::TerminalApp::AppCommandlineArgs _appArgs;
@ -175,6 +183,7 @@ namespace winrt::TerminalApp::implementation
FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested);
FORWARDED_TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IsQuakeWindowChanged);
FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested);
FORWARDED_TYPED_EVENT(CloseRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, CloseRequested);
FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu);
FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested);

View file

@ -34,6 +34,8 @@ namespace TerminalApp
void RunAsUwp();
Boolean IsElevated();
Boolean HasCommandlineArguments();
Boolean HasSettingsStartupActions();
Int32 SetStartupCommandline(String[] commands);
Int32 ExecuteCommandline(String[] commands, String cwd);
String ParseCommandlineMessage { get; };
@ -55,6 +57,7 @@ namespace TerminalApp
void IdentifyWindow();
String WindowName;
UInt64 WindowId;
void SetPersistedLayoutIdx(UInt32 idx);
void SetNumberOfOpenWindows(UInt64 num);
void RenameFailed();
Boolean IsQuakeWindow();
@ -69,10 +72,14 @@ namespace TerminalApp
Boolean GetInitialAlwaysOnTop();
Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension);
void TitlebarClicked();
void WindowCloseButtonClicked();
void CloseWindow(Microsoft.Terminal.Settings.Model.LaunchPosition position);
TaskbarState TaskbarState{ get; };
Boolean ShouldUsePersistedLayout();
String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position);
void SaveWindowLayoutJsons(Windows.Foundation.Collections.IVector<String> layouts);
Boolean GetMinimizeToNotificationArea();
Boolean GetAlwaysShowNotificationIcon();
Boolean GetShowTitleInTitlebar();
@ -99,6 +106,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> SettingsChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> CloseRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
}

View file

@ -122,7 +122,12 @@ NewTerminalArgs Pane::GetTerminalArgsForPane() const
if (controlSettings.AppliedColorScheme())
{
auto name = controlSettings.AppliedColorScheme().Name();
args.ColorScheme(name);
// Only save the color scheme if it is different than the profile color
// scheme to not override any other profile appearance choices.
if (_profile.DefaultAppearance().ColorSchemeName() != name)
{
args.ColorScheme(name);
}
}
return args;
@ -182,7 +187,6 @@ Pane::BuildStartupState Pane::BuildStartupActions(uint32_t currentId, uint32_t n
if (_firstChild->_IsLeaf() && _secondChild->_IsLeaf())
{
auto actionAndArgs = buildSplitPane(_secondChild);
std::optional<uint32_t> focusedPaneId = std::nullopt;
if (_firstChild->_lastActive)
{
@ -2721,35 +2725,38 @@ Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const
void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) const
{
const auto& termControl{ _control.try_as<TermControl>() };
if (_IsLeaf() && termControl)
if (_IsLeaf())
{
// We're a leaf pane, so just add one more row or column (unless isMinimumSize
// is true, see below).
if (sizeNode.isMinimumSize)
if (termControl)
{
// If the node is of its minimum size, this size might not be snapped (it might
// be, say, half a character, or fixed 10 pixels), so snap it upward. It might
// however be already snapped, so add 1 to make sure it really increases
// (not strictly necessary but to avoid surprises).
sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher;
// We're a leaf pane, so just add one more row or column (unless isMinimumSize
// is true, see below).
if (sizeNode.isMinimumSize)
{
// If the node is of its minimum size, this size might not be snapped (it might
// be, say, half a character, or fixed 10 pixels), so snap it upward. It might
// however be already snapped, so add 1 to make sure it really increases
// (not strictly necessary but to avoid surprises).
sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher;
}
else
{
const auto cellSize = termControl.CharacterDimensions();
sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height;
}
}
else
{
const auto cellSize = termControl.CharacterDimensions();
sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height;
// If we're a leaf that didn't have a TermControl, then just increment
// by one. We have to increment by _some_ value, because this is used in
// a while() loop to find the next bigger size we can snap to. But since
// a non-terminal control doesn't really care what size it's snapped to,
// we can just say "one pixel larger is the next snap point"
sizeNode.size += 1;
}
}
else if (_IsLeaf())
{
// If we're a leaf that didn't have a TermControl, then just increment
// by one. We have to increment by _some_ value, because this is used in
// a while() loop to find the next bigger size we can snap to. But since
// a non-terminal control doesn't really care what size it's snapped to,
// we can just say "one pixel larger is the next snap point"
sizeNode.size += 1;
}
else if (!_IsLeaf())
else // !_IsLeaf()
{
// We're a parent pane, so we have to advance dimension of our children panes. In
// fact, we advance only one child (chosen later) to keep the growth fine-grained.

View file

@ -381,6 +381,9 @@
<data name="CmdFocusDesc" xml:space="preserve">
<value>Launch the window in focus mode</value>
</data>
<data name="CmdSavedLayoutArgDesc" xml:space="preserve">
<value>This parameter is an internal implementation detail and should not be used.</value>
</data>
<data name="CmdWindowTargetArgDesc" xml:space="preserve">
<value>Specify a terminal window to run the given commandline in. "0" always refers to the current window. </value>
</data>
@ -730,4 +733,7 @@
<data name="InfoBarDismissButton.Content" xml:space="preserve">
<value>Don't show again</value>
</data>
<data name="ElevationShield.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>This Terminal window is running as Admin</value>
</data>
</root>

View file

@ -522,7 +522,9 @@ namespace winrt::TerminalApp::implementation
{
// If we are supposed to save state, make sure we clear it out
// if the user manually closed all tabs.
if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings))
// Do this only if we are the last window; the monarch will notice
// we are missing and remove us that way otherwise.
if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1)
{
auto state = ApplicationState::SharedInstance();
state.PersistedWindowLayouts(nullptr);

View file

@ -4,6 +4,7 @@
#pragma once
#include "winrt/Microsoft.UI.Xaml.Controls.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
#include "TabRowControl.g.h"
@ -16,6 +17,9 @@ namespace winrt::TerminalApp::implementation
void OnNewTabButtonClick(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const& args);
void OnNewTabButtonDrop(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e);
void OnNewTabButtonDragOver(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, _PropertyChangedHandlers, false);
};
}

View file

@ -3,9 +3,11 @@
namespace TerminalApp
{
[default_interface] runtimeclass TabRowControl : Windows.UI.Xaml.Controls.ContentPresenter
[default_interface] runtimeclass TabRowControl : Windows.UI.Xaml.Controls.ContentPresenter,
Windows.UI.Xaml.Data.INotifyPropertyChanged
{
TabRowControl();
Microsoft.UI.Xaml.Controls.TabView TabView { get; };
Boolean ShowElevationShield;
}
}

View file

@ -20,6 +20,17 @@
IsAddTabButtonVisible="false"
TabWidthMode="Equal">
<mux:TabView.TabStripHeader>
<!-- EA18 is the "Shield" glyph -->
<FontIcon x:Uid="ElevationShield"
Margin="9,4,0,0"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xEA18;"
Visibility="{x:Bind ShowElevationShield, Mode=OneWay}" />
</mux:TabView.TabStripHeader>
<mux:TabView.TabStripFooter>
<mux:SplitButton x:Name="NewTabButton"
x:Uid="NewTabSplitButton"

View file

@ -121,7 +121,7 @@ namespace winrt::TerminalApp::implementation
_systemRowsToScroll = _ReadSystemRowsToScroll();
}
bool TerminalPage::_isElevated() const noexcept
bool TerminalPage::IsElevated() const noexcept
{
// use C++11 magic statics to make sure we only do this once.
// This won't change over the lifetime of the application
@ -154,7 +154,7 @@ namespace winrt::TerminalApp::implementation
_tabView = _tabRow.TabView();
_rearranging = false;
const auto isElevated = _isElevated();
const auto isElevated = IsElevated();
if (_settings.GlobalSettings().UseAcrylicInTabRow())
{
@ -281,6 +281,8 @@ namespace winrt::TerminalApp::implementation
// Setup mouse vanish attributes
SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &_shouldMouseVanish, false);
_tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield());
// Store cursor, so we can restore it, e.g., after mouse vanishing
// (we'll need to adapt this logic once we make cursor context aware)
try
@ -299,10 +301,34 @@ namespace winrt::TerminalApp::implementation
// - true if the ApplicationState should be used.
bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const
{
// If the setting is enabled, and we are the only window.
return Feature_PersistedWindowLayout::IsEnabled() &&
settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout &&
_numOpenWindows == 1;
settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout;
}
// Method Description;
// - Checks if the current window is configured to load a particular layout
// Arguments:
// - settings: The settings to use as this may be called before the page is
// fully initialized.
// Return Value:
// - non-null if there is a particular saved layout to use
std::optional<uint32_t> TerminalPage::LoadPersistedLayoutIdx(CascadiaSettings& settings) const
{
return ShouldUsePersistedLayout(settings) ? _loadFromPersistedLayoutIdx : std::nullopt;
}
WindowLayout TerminalPage::LoadPersistedLayout(CascadiaSettings& settings) const
{
if (const auto idx = LoadPersistedLayoutIdx(settings))
{
const auto i = idx.value();
const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > i)
{
return layouts.GetAt(i);
}
}
return nullptr;
}
winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e)
@ -388,30 +414,13 @@ namespace winrt::TerminalApp::implementation
{
_startupState = StartupState::InStartup;
// If the user selected to save their tab layout, we are the first
// window opened, and wt was not run with any other arguments, then
// we should use the saved settings.
auto firstActionIsDefault = [](ActionAndArgs action) {
if (action.Action() != ShortcutAction::NewTab)
{
return false;
}
// If no commands were given, we will have default args
if (const auto args = action.Args().try_as<NewTabArgs>())
{
NewTerminalArgs defaultArgs{};
return args.TerminalArgs() == nullptr || args.TerminalArgs().Equals(defaultArgs);
}
return false;
};
if (ShouldUsePersistedLayout(_settings) && _startupActions.Size() == 1 && firstActionIsDefault(_startupActions.GetAt(0)))
// If we are provided with an index, the cases where we have
// commandline args and startup actions are already handled.
if (const auto layout = LoadPersistedLayout(_settings))
{
auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > 0 && layouts.GetAt(0).TabLayout() && layouts.GetAt(0).TabLayout().Size() > 0)
if (layout.TabLayout().Size() > 0)
{
_startupActions = layouts.GetAt(0).TabLayout();
_startupActions = layout.TabLayout();
}
}
@ -1316,12 +1325,19 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - Saves the window position and tab layout to the application state
// - This does not create the InitialPosition field, that needs to be
// added externally.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalPage::PersistWindowLayout()
// - the window layout
WindowLayout TerminalPage::GetWindowLayout()
{
if (_startupState != StartupState::Initialized)
{
return nullptr;
}
std::vector<ActionAndArgs> actions;
for (auto tab : _tabs)
@ -1329,7 +1345,7 @@ namespace winrt::TerminalApp::implementation
if (auto terminalTab = _GetTerminalTabImpl(tab))
{
auto tabActions = terminalTab->BuildStartupActions();
actions.insert(actions.end(), tabActions.begin(), tabActions.end());
actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end()));
}
else if (tab.try_as<SettingsTab>())
{
@ -1338,7 +1354,7 @@ namespace winrt::TerminalApp::implementation
OpenSettingsArgs args{ SettingsTarget::SettingsUI };
action.Args(args);
actions.push_back(action);
actions.emplace_back(std::move(action));
}
}
@ -1351,7 +1367,18 @@ namespace winrt::TerminalApp::implementation
SwitchToTabArgs switchToTabArgs{ idx.value() };
action.Args(switchToTabArgs);
actions.push_back(action);
actions.emplace_back(std::move(action));
}
// If the user set a custom name, save it
if (_WindowName != L"")
{
ActionAndArgs action;
action.Action(ShortcutAction::RenameWindow);
RenameWindowArgs args{ _WindowName };
action.Args(args);
actions.emplace_back(std::move(action));
}
WindowLayout layout{};
@ -1364,33 +1391,7 @@ namespace winrt::TerminalApp::implementation
layout.InitialSize(windowSize);
if (_hostingHwnd)
{
// Get the position of the current window. This includes the
// non-client already.
RECT window{};
GetWindowRect(_hostingHwnd.value(), &window);
// We want to remove the non-client area so calculate that.
// We don't have access to the (NonClient)IslandWindow directly so
// just replicate the logic.
const auto windowStyle = static_cast<DWORD>(GetWindowLong(_hostingHwnd.value(), GWL_STYLE));
auto dpi = GetDpiForWindow(_hostingHwnd.value());
RECT nonClientArea{};
LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&nonClientArea, windowStyle, false, 0, dpi));
// The nonClientArea adjustment is negative, so subtract that out.
// This way we save the user-visible location of the terminal.
LaunchPosition pos{};
pos.X = window.left - nonClientArea.left;
pos.Y = window.top;
layout.InitialPosition(pos);
}
auto state = ApplicationState::SharedInstance();
state.PersistedWindowLayouts(winrt::single_threaded_vector<WindowLayout>({ layout }));
return layout;
}
// Method Description:
@ -1419,8 +1420,9 @@ namespace winrt::TerminalApp::implementation
if (ShouldUsePersistedLayout(_settings))
{
PersistWindowLayout();
// don't delete the ApplicationState when all of the tabs are removed.
// Don't delete the ApplicationState when all of the tabs are removed.
// If there is still a monarch living they will get the event that
// a window closed and trigger a new save without this window.
_maintainStateOnTabClose = true;
}
@ -1509,6 +1511,10 @@ namespace winrt::TerminalApp::implementation
// - Returns true if this commandline is precisely an executable in
// system32. We can use this to bypass the elevated state check, because
// we're confident that executables in that path won't have been hijacked.
// - TECHNICALLY a user can take ownership of a file in system32 and
// replace it as the system administrator. You could say it's OK though
// because you'd already have to have had admin rights to mess that
// folder up or something.
// - Will attempt to resolve environment strings.
// - Will also manually allow commandlines as generated for the default WSL
// distros.
@ -1519,7 +1525,7 @@ namespace winrt::TerminalApp::implementation
// - cmd.exe -> returns false
// - C:\windows\system32\cmd.exe /k echo sneaky sneak -> returns false
// - %SystemRoot%\System32\cmd.exe -> returns true
// - %SystemRoot%\System32\wsl.exe -d <distroname> -> returns true
// - %SystemRoot%\System32\wsl.exe -d <distro name> -> returns true
static bool _isInSystem32(std::wstring_view commandLine)
{
// use C++11 magic statics to make sure we only do this once.
@ -1548,6 +1554,9 @@ namespace winrt::TerminalApp::implementation
{
// Get the first part of the executable path
const auto start = fullCommandlinePath.wstring().substr(0, systemDirectory.size());
// Doing this as an ASCII only check might be wrong, but I'm
// guessing if system32 isn't at X:\windows\system32... this isn't
// the only thing that is going to be sad in Windows.
const auto pathEquals = til::equals_insensitive_ascii(start, systemDirectory);
if (pathEquals && std::filesystem::exists(fullCommandlinePath))
{
@ -1556,10 +1565,10 @@ namespace winrt::TerminalApp::implementation
}
// Also, if the path is literally
// %SystemRoot%\System32\wsl.exe -d <distroname>
// %SystemRoot%\System32\wsl.exe -d <distro name>
// then allow it.
// Stolen from _tryMangleStartingDirectoryForWSL
// Largely stolen from _tryMangleStartingDirectoryForWSL in ConptyConnection.
// Find the first space, quote or the end of the string -- we'll look
// for wsl before that.
const auto terminator{ commandLine.find_first_of(LR"(" )", 1) }; // look past the first character in case it starts with "
@ -1584,7 +1593,9 @@ namespace winrt::TerminalApp::implementation
}
// Get everything after the wsl.exe
const auto arguments{ terminator == std::wstring_view::npos ? std::wstring_view{} : commandLine.substr(terminator + 1) };
const auto arguments{ terminator == std::wstring_view::npos ?
std::wstring_view{} :
commandLine.substr(terminator + 1) };
const auto dashD{ arguments.find(L"-d ") };
// If we found a "-d " IMMEDIATELY AFTER wsl.exe. If it wasn't
@ -1622,9 +1633,9 @@ namespace winrt::TerminalApp::implementation
// - true if we should prompt the user for approval.
bool TerminalPage::_shouldPromptForCommandline(const winrt::hstring& cmdline) const
{
// NOTE: For debugging purposes, changing this to `true || _isElevated()`
// NOTE: For debugging purposes, changing this to `true || IsElevated()`
// is a handy way of forcing the elevation logic, even when unelevated.
if (_isElevated())
if (IsElevated())
{
// If the cmdline is EXACTLY an executable in
// `C:\WINDOWS\System32`, then ignore this check.
@ -1686,6 +1697,7 @@ namespace winrt::TerminalApp::implementation
tabImpl->UpdateTitle();
}
}
// return false so we make sure to iterate on every leaf.
return false;
});
}
@ -2592,6 +2604,8 @@ namespace winrt::TerminalApp::implementation
// enabled application-wide, so we don't need to check it each time we
// want to create an animation.
WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations());
_tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield());
}
// This is a helper to aid in sorting commands by their `Name`s, alphabetically.
@ -3441,6 +3455,11 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::SetPersistedLayoutIdx(const uint32_t idx)
{
_loadFromPersistedLayoutIdx = idx;
}
void TerminalPage::SetNumberOfOpenWindows(const uint64_t num)
{
_numOpenWindows = num;

View file

@ -59,6 +59,9 @@ namespace winrt::TerminalApp::implementation
void Create();
bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const;
std::optional<uint32_t> LoadPersistedLayoutIdx(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const;
winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const;
Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout();
winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e);
@ -82,7 +85,6 @@ namespace winrt::TerminalApp::implementation
bool AlwaysOnTop() const;
void SetStartupActions(std::vector<Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions);
void PersistWindowLayout();
void SetInboundListener(bool isEmbedding);
static std::vector<Microsoft::Terminal::Settings::Model::ActionAndArgs> ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args);
@ -111,10 +113,12 @@ namespace winrt::TerminalApp::implementation
void WindowId(const uint64_t& value);
void SetNumberOfOpenWindows(const uint64_t value);
void SetPersistedLayoutIdx(const uint32_t value);
winrt::hstring WindowIdForDisplay() const noexcept;
winrt::hstring WindowNameForDisplay() const noexcept;
bool IsQuakeWindow() const noexcept;
bool IsElevated() const noexcept;
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
@ -132,6 +136,7 @@ namespace winrt::TerminalApp::implementation
TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs);
TYPED_EVENT(IsQuakeWindowChanged, IInspectable, IInspectable);
TYPED_EVENT(SummonWindowRequested, IInspectable, IInspectable);
TYPED_EVENT(CloseRequested, IInspectable, IInspectable);
TYPED_EVENT(OpenSystemMenu, IInspectable, IInspectable);
TYPED_EVENT(QuitRequested, IInspectable, IInspectable);
@ -165,6 +170,7 @@ namespace winrt::TerminalApp::implementation
bool _isAlwaysOnTop{ false };
winrt::hstring _WindowName{};
uint64_t _WindowId{ 0 };
std::optional<uint32_t> _loadFromPersistedLayoutIdx{};
uint64_t _numOpenWindows{ 0 };
bool _maintainStateOnTabClose{ false };

View file

@ -33,7 +33,6 @@ namespace TerminalApp
UInt64 WindowId;
String WindowNameForDisplay { get; };
String WindowIdForDisplay { get; };
void SetNumberOfOpenWindows(UInt64 num);
void RenameFailed();
Boolean IsQuakeWindow();
@ -58,6 +57,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, RenameWindowRequestedArgs> RenameWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> CloseRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
}
}

View file

@ -109,11 +109,6 @@
</TextBlock>
</ContentDialog>
<!-- <ContentDialog x:Name="ApproveCommandlineWarning"
x:Uid="ApproveCommandlineWarning"
x:Load="False"
DefaultButton="Primary" />-->
<local:CommandPalette x:Name="CommandPalette"
Grid.Row="1"
VerticalAlignment="Stretch"

View file

@ -453,12 +453,25 @@ namespace winrt::TerminalApp::implementation
// 1 for the child after the first split.
auto state = _rootPane->BuildStartupActions(0, 1);
ActionAndArgs newTabAction{};
newTabAction.Action(ShortcutAction::NewTab);
NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() };
newTabAction.Args(newTabArgs);
{
ActionAndArgs newTabAction{};
newTabAction.Action(ShortcutAction::NewTab);
NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() };
newTabAction.Args(newTabArgs);
state.args.emplace(state.args.begin(), std::move(newTabAction));
state.args.emplace(state.args.begin(), std::move(newTabAction));
}
if (_runtimeTabColor)
{
ActionAndArgs setColorAction{};
setColorAction.Action(ShortcutAction::SetTabColor);
SetTabColorArgs setColorArgs{ _runtimeTabColor.value() };
setColorAction.Args(setColorArgs);
state.args.emplace_back(std::move(setColorAction));
}
// If we only have one arg, we only have 1 pane so we don't need any
// special focus logic
@ -522,6 +535,8 @@ namespace winrt::TerminalApp::implementation
// Add a event handlers to the new panes' GotFocus event. When the pane
// gains focus, we'll mark it as the new active pane.
const auto& termControl{ control.try_as<TermControl>() };
// _AttachEventHandlersToControl will immediately bail if the provided
// control is null (READ: if the pane didn't have a TermControl)
_AttachEventHandlersToControl(newPane->Id().value(), termControl);
_AttachEventHandlersToPane(original);
_AttachEventHandlersToPane(newPane);

View file

@ -359,21 +359,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const ControlKeyStates modifiers,
const bool keyDown)
{
// When there is a selection active, escape should clear it and NOT flow through
// to the terminal. With any other keypress, it should clear the selection AND
// flow through to the terminal.
// Update the selection, if it's present
// GH#6423 - don't dismiss selection if the key that was pressed was a
// modifier key. We'll wait for a real keystroke to dismiss the
// GH #7395 - don't dismiss selection when taking PrintScreen
// selection.
// GH#8522, GH#3758 - Only dismiss the selection on key _down_. If we
// dismiss on key up, then there's chance that we'll immediately dismiss
// GH#8522, GH#3758 - Only modify the selection on key _down_. If we
// modify on key up, then there's chance that we'll immediately dismiss
// a selection created by an action bound to a keydown.
if (HasSelection() &&
!KeyEvent::IsModifierKey(vkey) &&
vkey != VK_SNAPSHOT &&
keyDown)
{
// try to update the selection
if (const auto updateSlnParams{ ::Terminal::ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) })
{
auto lock = _terminal->LockForWriting();
_terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second);
_renderer->TriggerSelection();
return true;
}
// GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination.
if (!modifiers.IsWinPressed())
{
@ -381,6 +388,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_renderer->TriggerSelection();
}
// When there is a selection active, escape should clear it and NOT flow through
// to the terminal. With any other keypress, it should clear the selection AND
// flow through to the terminal.
if (vkey == VK_ESCAPE)
{
return true;
@ -1399,18 +1409,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// handle ALT key
_terminal->SetBlockSelection(altEnabled);
::Terminal::SelectionExpansionMode mode = ::Terminal::SelectionExpansionMode::Cell;
::Terminal::SelectionExpansion mode = ::Terminal::SelectionExpansion::Char;
if (numberOfClicks == 1)
{
mode = ::Terminal::SelectionExpansionMode::Cell;
mode = ::Terminal::SelectionExpansion::Char;
}
else if (numberOfClicks == 2)
{
mode = ::Terminal::SelectionExpansionMode::Word;
mode = ::Terminal::SelectionExpansion::Word;
}
else if (numberOfClicks == 3)
{
mode = ::Terminal::SelectionExpansionMode::Line;
mode = ::Terminal::SelectionExpansion::Line;
}
// Update the selection appropriately
@ -1435,7 +1445,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_terminal->SetSelectionEnd(terminalPosition, mode);
selectionNeedsToBeCopied = true;
}
else if (mode != ::Terminal::SelectionExpansionMode::Cell || shiftEnabled)
else if (mode != ::Terminal::SelectionExpansion::Char || shiftEnabled)
{
// If we are handling a double / triple-click or shift+single click
// we establish selection using the selected mode
@ -1534,5 +1544,4 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return hstring(ss.str());
}
}

View file

@ -199,6 +199,7 @@ public:
const COORD GetSelectionEnd() const noexcept override;
const std::wstring_view GetConsoleTitle() const noexcept override;
void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute) override;
const bool IsUiaDataInitialized() const noexcept override;
#pragma endregion
void SetWriteInputCallback(std::function<void(std::wstring&)> pfn) noexcept;
@ -227,16 +228,30 @@ public:
#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
enum class SelectionExpansionMode
enum class SelectionDirection
{
Cell,
Word,
Line
Left,
Right,
Up,
Down
};
void MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode);
enum class SelectionExpansion
{
Char,
Word,
Line, // Mouse selection only!
Viewport,
Buffer
};
void MultiClickSelection(const COORD viewportPos, SelectionExpansion expansionMode);
void SetSelectionAnchor(const COORD position);
void SetSelectionEnd(const COORD position, std::optional<SelectionExpansionMode> newExpansionMode = std::nullopt);
void SetSelectionEnd(const COORD position, std::optional<SelectionExpansion> newExpansionMode = std::nullopt);
void SetBlockSelection(const bool isEnabled) noexcept;
void UpdateSelection(SelectionDirection direction, SelectionExpansion mode);
using UpdateSelectionParams = std::optional<std::pair<SelectionDirection, SelectionExpansion>>;
static UpdateSelectionParams ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey);
const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace);
#pragma endregion
@ -308,7 +323,7 @@ private:
std::optional<SelectionAnchors> _selection;
bool _blockSelection;
std::wstring _wordDelimiters;
SelectionExpansionMode _multiClickSelectionMode;
SelectionExpansion _multiClickSelectionMode;
#pragma endregion
// TODO: These members are not shared by an alt-buffer. They should be
@ -375,6 +390,10 @@ private:
std::pair<COORD, COORD> _PivotSelection(const COORD targetPos, bool& targetStart) const;
std::pair<COORD, COORD> _ExpandSelectionAnchors(std::pair<COORD, COORD> anchors) const;
COORD _ConvertToBufferCell(const COORD viewportPos) const;
void _MoveByChar(SelectionDirection direction, COORD& pos);
void _MoveByWord(SelectionDirection direction, COORD& pos);
void _MoveByViewport(SelectionDirection direction, COORD& pos);
void _MoveByBuffer(SelectionDirection direction, COORD& pos);
#pragma endregion
Microsoft::Console::VirtualTerminal::SgrStack _sgrStack;

View file

@ -100,8 +100,8 @@ const bool Terminal::IsBlockSelection() const noexcept
// - Perform a multi-click selection at viewportPos expanding according to the expansionMode
// Arguments:
// - viewportPos: the (x,y) coordinate on the visible viewport
// - expansionMode: the SelectionExpansionMode to dictate the boundaries of the selection anchors
void Terminal::MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode)
// - expansionMode: the SelectionExpansion to dictate the boundaries of the selection anchors
void Terminal::MultiClickSelection(const COORD viewportPos, SelectionExpansion expansionMode)
{
// set the selection pivot to expand the selection using SetSelectionEnd()
_selection = SelectionAnchors{};
@ -124,7 +124,7 @@ void Terminal::SetSelectionAnchor(const COORD viewportPos)
_selection = SelectionAnchors{};
_selection->pivot = _ConvertToBufferCell(viewportPos);
_multiClickSelectionMode = SelectionExpansionMode::Cell;
_multiClickSelectionMode = SelectionExpansion::Char;
SetSelectionEnd(viewportPos);
_selection->start = _selection->pivot;
@ -136,7 +136,7 @@ void Terminal::SetSelectionAnchor(const COORD viewportPos)
// Arguments:
// - viewportPos: the (x,y) coordinate on the visible viewport
// - newExpansionMode: overwrites the _multiClickSelectionMode for this function call. Used for ShiftClick
void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional<SelectionExpansionMode> newExpansionMode)
void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional<SelectionExpansion> newExpansionMode)
{
if (!_selection.has_value())
{
@ -210,15 +210,15 @@ std::pair<COORD, COORD> Terminal::_ExpandSelectionAnchors(std::pair<COORD, COORD
const auto bufferSize = _buffer->GetSize();
switch (_multiClickSelectionMode)
{
case SelectionExpansionMode::Line:
case SelectionExpansion::Line:
start = { bufferSize.Left(), start.Y };
end = { bufferSize.RightInclusive(), end.Y };
break;
case SelectionExpansionMode::Word:
case SelectionExpansion::Word:
start = _buffer->GetWordStart(start, _wordDelimiters);
end = _buffer->GetWordEnd(end, _wordDelimiters);
break;
case SelectionExpansionMode::Cell:
case SelectionExpansion::Char:
default:
// no expansion is necessary
break;
@ -235,6 +235,229 @@ void Terminal::SetBlockSelection(const bool isEnabled) noexcept
_blockSelection = isEnabled;
}
Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey)
{
if (mods.IsShiftPressed() && !mods.IsAltPressed())
{
if (mods.IsCtrlPressed())
{
// Ctrl + Shift + _
switch (vkey)
{
case VK_LEFT:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Word };
case VK_RIGHT:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Word };
case VK_HOME:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Buffer };
case VK_END:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Buffer };
}
}
else
{
// Shift + _
switch (vkey)
{
case VK_HOME:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Viewport };
case VK_END:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Viewport };
case VK_PRIOR:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Up, SelectionExpansion::Viewport };
case VK_NEXT:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Down, SelectionExpansion::Viewport };
case VK_LEFT:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Char };
case VK_RIGHT:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Char };
case VK_UP:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Up, SelectionExpansion::Char };
case VK_DOWN:
return UpdateSelectionParams{ std::in_place, SelectionDirection::Down, SelectionExpansion::Char };
}
}
}
return std::nullopt;
}
void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion mode)
{
// 1. Figure out which endpoint to update
// One of the endpoints is the pivot, signifying that the other endpoint is the one we want to move.
const bool movingEnd{ _selection->start == _selection->pivot };
auto targetPos{ movingEnd ? _selection->end : _selection->start };
// 2. Perform the movement
switch (mode)
{
case SelectionExpansion::Char:
_MoveByChar(direction, targetPos);
break;
case SelectionExpansion::Word:
_MoveByWord(direction, targetPos);
break;
case SelectionExpansion::Viewport:
_MoveByViewport(direction, targetPos);
break;
case SelectionExpansion::Buffer:
_MoveByBuffer(direction, targetPos);
break;
}
// 3. Actually modify the selection
// NOTE: targetStart doesn't matter here
bool targetStart = false;
std::tie(_selection->start, _selection->end) = _PivotSelection(targetPos, targetStart);
// 4. Scroll (if necessary)
if (const auto viewport = _GetVisibleViewport(); !viewport.IsInBounds(targetPos))
{
if (const auto amtAboveView = viewport.Top() - targetPos.Y; amtAboveView > 0)
{
// anchor is above visible viewport, scroll by that amount
_scrollOffset += amtAboveView;
}
else
{
// anchor is below visible viewport, scroll by that amount
const auto amtBelowView = targetPos.Y - viewport.BottomInclusive();
_scrollOffset -= amtBelowView;
}
_NotifyScrollEvent();
_buffer->GetRenderTarget().TriggerScroll();
}
}
void Terminal::_MoveByChar(SelectionDirection direction, COORD& pos)
{
switch (direction)
{
case SelectionDirection::Left:
_buffer->GetSize().DecrementInBounds(pos);
pos = _buffer->GetGlyphStart(pos);
break;
case SelectionDirection::Right:
_buffer->GetSize().IncrementInBounds(pos);
pos = _buffer->GetGlyphEnd(pos);
break;
case SelectionDirection::Up:
{
const auto bufferSize{ _buffer->GetSize() };
pos = { pos.X, std::clamp(base::ClampSub<short, short>(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) };
break;
}
case SelectionDirection::Down:
{
const auto bufferSize{ _buffer->GetSize() };
pos = { pos.X, std::clamp(base::ClampAdd<short, short>(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) };
break;
}
}
}
void Terminal::_MoveByWord(SelectionDirection direction, COORD& pos)
{
switch (direction)
{
case SelectionDirection::Left:
const auto wordStartPos{ _buffer->GetWordStart(pos, _wordDelimiters) };
if (_buffer->GetSize().CompareInBounds(_selection->pivot, pos) < 0)
{
// If we're moving towards the pivot, move one more cell
pos = wordStartPos;
_buffer->GetSize().DecrementInBounds(pos);
}
else if (wordStartPos == pos)
{
// already at the beginning of the current word,
// move to the beginning of the previous word
_buffer->GetSize().DecrementInBounds(pos);
pos = _buffer->GetWordStart(pos, _wordDelimiters);
}
else
{
// move to the beginning of the current word
pos = wordStartPos;
}
break;
case SelectionDirection::Right:
const auto wordEndPos{ _buffer->GetWordEnd(pos, _wordDelimiters) };
if (_buffer->GetSize().CompareInBounds(pos, _selection->pivot) < 0)
{
// If we're moving towards the pivot, move one more cell
pos = _buffer->GetWordEnd(pos, _wordDelimiters);
_buffer->GetSize().IncrementInBounds(pos);
}
else if (wordEndPos == pos)
{
// already at the end of the current word,
// move to the end of the next word
_buffer->GetSize().IncrementInBounds(pos);
pos = _buffer->GetWordEnd(pos, _wordDelimiters);
}
else
{
// move to the end of the current word
pos = wordEndPos;
}
break;
case SelectionDirection::Up:
_MoveByChar(direction, pos);
pos = _buffer->GetWordStart(pos, _wordDelimiters);
break;
case SelectionDirection::Down:
_MoveByChar(direction, pos);
pos = _buffer->GetWordEnd(pos, _wordDelimiters);
break;
}
}
void Terminal::_MoveByViewport(SelectionDirection direction, COORD& pos)
{
const auto bufferSize{ _buffer->GetSize() };
switch (direction)
{
case SelectionDirection::Left:
pos = { bufferSize.Left(), pos.Y };
break;
case SelectionDirection::Right:
pos = { bufferSize.RightInclusive(), pos.Y };
break;
case SelectionDirection::Up:
{
const auto viewportHeight{ _mutableViewport.Height() };
const auto newY{ base::ClampSub<short, short>(pos.Y, viewportHeight) };
pos = newY < bufferSize.Top() ? bufferSize.Origin() : COORD{ pos.X, newY };
break;
}
case SelectionDirection::Down:
{
const auto viewportHeight{ _mutableViewport.Height() };
const auto mutableBottom{ _mutableViewport.BottomInclusive() };
const auto newY{ base::ClampAdd<short, short>(pos.Y, viewportHeight) };
pos = newY > mutableBottom ? COORD{ bufferSize.RightInclusive(), mutableBottom } : COORD{ pos.X, newY };
break;
}
}
}
void Terminal::_MoveByBuffer(SelectionDirection direction, COORD& pos)
{
const auto bufferSize{ _buffer->GetSize() };
switch (direction)
{
case SelectionDirection::Left:
case SelectionDirection::Up:
pos = bufferSize.Origin();
break;
case SelectionDirection::Right:
case SelectionDirection::Down:
pos = { bufferSize.RightInclusive(), _mutableViewport.BottomInclusive() };
break;
}
}
// Method Description:
// - clear selection data and disable rendering it
#pragma warning(disable : 26440) // changing this to noexcept would require a change to ConHost's selection model

View file

@ -206,7 +206,7 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd)
realCoordEnd.Y -= gsl::narrow<short>(_VisibleStartIndex());
SetSelectionAnchor(realCoordStart);
SetSelectionEnd(realCoordEnd, SelectionExpansionMode::Cell);
SetSelectionEnd(realCoordEnd, SelectionExpansion::Char);
}
const std::wstring_view Terminal::GetConsoleTitle() const noexcept
@ -250,3 +250,12 @@ bool Terminal::IsScreenReversed() const noexcept
{
return _screenReversed;
}
const bool Terminal::IsUiaDataInitialized() const noexcept
{
// GH#11135: Windows Terminal needs to create and return an automation peer
// when a screen reader requests it. However, the terminal might not be fully
// initialized yet. So we use this to check if any crucial components of
// UiaData are not yet initialized.
return !!_buffer;
}

View file

@ -304,8 +304,8 @@
<comment>An option to choose from for the "First window preference" setting. Open the default profile.</comment>
</data>
<data name="Globals_FirstWindowPreferencePersistedWindowLayout.Content" xml:space="preserve">
<value>Open tabs from a previous session</value>
<comment>An option to choose from for the "First window preference" setting. Reopen the layout from the last session.</comment>
<value>Open windows from a previous session</value>
<comment>An option to choose from for the "First window preference" setting. Reopen the layouts from the last session.</comment>
</data>
<data name="Globals_LaunchMode.Header" xml:space="preserve">
<value>Launch mode</value>

View file

@ -38,6 +38,7 @@
#include "MultipleActionsArgs.g.cpp"
#include <LibraryResources.h>
#include <WtExeUtils.h>
using namespace winrt::Microsoft::Terminal::Control;
@ -121,15 +122,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
if (!StartingDirectory().empty())
{
// If the directory ends in a '\', we need to add another one on so that the enclosing quote added
// afterwards isn't escaped
const auto trailingBackslashEscape = StartingDirectory().back() == L'\\' ? L"\\" : L"";
ss << fmt::format(L"--startingDirectory \"{}{}\" ", StartingDirectory(), trailingBackslashEscape);
ss << fmt::format(L"--startingDirectory {} ", QuoteAndEscapeCommandlineArg(StartingDirectory()));
}
if (!TabTitle().empty())
{
ss << fmt::format(L"--title \"{}\" ", TabTitle());
ss << fmt::format(L"--title {} ", QuoteAndEscapeCommandlineArg(TabTitle()));
}
if (TabColor())
@ -152,7 +150,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
if (!ColorScheme().empty())
{
ss << fmt::format(L"--colorScheme \"{}\" ", ColorScheme());
ss << fmt::format(L"--colorScheme {} ", QuoteAndEscapeCommandlineArg(ColorScheme()));
}
if (!Commandline().empty())

View file

@ -870,6 +870,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
struct SetTabColorArgs : public SetTabColorArgsT<SetTabColorArgs>
{
SetTabColorArgs() = default;
SetTabColorArgs(Windows::UI::Color tabColor) :
_TabColor{ tabColor } {}
ACTION_ARG(Windows::Foundation::IReference<Windows::UI::Color>, TabColor, nullptr);
static constexpr std::string_view ColorKey{ "color" };
@ -1588,6 +1590,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
struct RenameWindowArgs : public RenameWindowArgsT<RenameWindowArgs>
{
RenameWindowArgs() = default;
RenameWindowArgs(winrt::hstring name) :
_Name{ name } {};
ACTION_ARG(winrt::hstring, Name);
static constexpr std::string_view NameKey{ "name" };
@ -1875,9 +1879,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
BASIC_FACTORY(NewTabArgs);
BASIC_FACTORY(MoveFocusArgs);
BASIC_FACTORY(MovePaneArgs);
BASIC_FACTORY(SetTabColorArgs);
BASIC_FACTORY(SwapPaneArgs);
BASIC_FACTORY(SplitPaneArgs);
BASIC_FACTORY(SetColorSchemeArgs);
BASIC_FACTORY(RenameWindowArgs);
BASIC_FACTORY(ExecuteCommandlineArgs);
BASIC_FACTORY(CloseOtherTabsArgs);
BASIC_FACTORY(CloseTabsAfterArgs);

View file

@ -215,6 +215,7 @@ namespace Microsoft.Terminal.Settings.Model
[default_interface] runtimeclass SetTabColorArgs : IActionArgs
{
SetTabColorArgs(Windows.UI.Color tabColor);
Windows.Foundation.IReference<Windows.UI.Color> TabColor { get; };
};
@ -296,6 +297,7 @@ namespace Microsoft.Terminal.Settings.Model
[default_interface] runtimeclass RenameWindowArgs : IActionArgs
{
RenameWindowArgs(String name);
String Name { get; };
};

View file

@ -64,6 +64,31 @@ using namespace ::Microsoft::Terminal::Settings::Model;
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
winrt::hstring WindowLayout::ToJson(const Model::WindowLayout& layout)
{
JsonUtils::ConversionTrait<Model::WindowLayout> trait;
auto json = trait.ToJson(layout);
Json::StreamWriterBuilder wbuilder;
const auto content = Json::writeString(wbuilder, json);
return hstring{ til::u8u16(content) };
}
Model::WindowLayout WindowLayout::FromJson(const hstring& str)
{
auto data = til::u16u8(str);
std::string errs;
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
Json::Value root;
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
JsonUtils::ConversionTrait<Model::WindowLayout> trait;
return trait.FromJson(root);
}
ApplicationState::ApplicationState(const std::filesystem::path& stateRoot) noexcept :
_sharedPath{ stateRoot / stateFileName },
_userPath{ stateRoot / unelevatedStateFileName },

View file

@ -43,6 +43,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
struct WindowLayout : WindowLayoutT<WindowLayout>
{
static winrt::hstring ToJson(const Model::WindowLayout& layout);
static Model::WindowLayout FromJson(const winrt::hstring& json);
WINRT_PROPERTY(Windows::Foundation::Collections::IVector<Model::ActionAndArgs>, TabLayout, nullptr);
WINRT_PROPERTY(winrt::Windows::Foundation::IReference<Model::LaunchPosition>, InitialPosition, nullptr);
WINRT_PROPERTY(winrt::Windows::Foundation::IReference<winrt::Windows::Foundation::Size>, InitialSize, nullptr);

View file

@ -15,6 +15,9 @@ namespace Microsoft.Terminal.Settings.Model
{
WindowLayout();
static String ToJson(WindowLayout layout);
static WindowLayout FromJson(String json);
Windows.Foundation.Collections.IVector<ActionAndArgs> TabLayout;
Windows.Foundation.IReference<LaunchPosition> InitialPosition;
Windows.Foundation.IReference<Windows.Foundation.Size> InitialSize;

View file

@ -553,9 +553,11 @@ Model::Profile CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs&
FindProfile(GlobalSettings().DefaultProfile()) :
ProfileDefaults();
}
// For compatibility with the stable version's behavior, return the default by GUID in all other cases.
return FindProfile(GlobalSettings().DefaultProfile());
else
{
// For compatibility with the stable version's behavior, return the default by GUID in all other cases.
return FindProfile(GlobalSettings().DefaultProfile());
}
}
// Method Description:

View file

@ -67,10 +67,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static void _rethrowSerializationExceptionWithLocationInfo(const JsonUtils::DeserializationError& e, const std::string_view& settingsString);
static Json::Value _parseJSON(const std::string_view& content);
static const Json::Value& _getJSONValue(const Json::Value& json, const std::string_view& key) noexcept;
static bool _isValidProfileObject(const Json::Value& profileJson);
gsl::span<const winrt::com_ptr<implementation::Profile>> _getNonUserOriginProfiles() const;
void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings);
void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, bool updatesKeyAllowed = false);
void _appendProfile(winrt::com_ptr<implementation::Profile>&& profile, ParsedSettings& settings);
static void _addParentProfile(const winrt::com_ptr<implementation::Profile>& profile, ParsedSettings& settings);
void _executeGenerator(const IDynamicProfileGenerator& generator);
std::unordered_set<std::wstring_view> _ignoredNamespaces;

View file

@ -178,22 +178,7 @@ void SettingsLoader::MergeInboxIntoUserSettings()
{
for (const auto& profile : inboxSettings.profiles)
{
if (const auto [it, inserted] = userSettings.profilesByGuid.emplace(profile->Guid(), profile); !inserted)
{
// If inserted is false, we got a matching user profile with identical GUID.
// --> The generated profile is a parent of the existing user profile.
it->second->InsertParent(profile);
}
else
{
// If inserted is true, then this is a generated profile that doesn't exist in the user's settings.
// While emplace() has already created an appropriate entry in .profilesByGuid, we still need to
// add it to .profiles (which is basically a sorted list of .profilesByGuid's values).
//
// When a user modifies a profile they shouldn't modify the (static/constant)
// inbox profile of course. That's why we need to call CreateChild here.
userSettings.profiles.emplace_back(CreateChild(profile));
}
_addParentProfile(profile, userSettings);
}
}
@ -216,7 +201,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
try
{
const auto content = ReadUTF8File(fragmentExt.path());
_parse(OriginTag::Fragment, source, content, fragmentSettings);
_parse(OriginTag::Fragment, source, content, fragmentSettings, true);
for (const auto& fragmentProfile : fragmentSettings.profiles)
{
@ -229,7 +214,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
}
else
{
_appendProfile(CreateChild(fragmentProfile), userSettings);
_addParentProfile(fragmentProfile, userSettings);
}
}
@ -433,17 +418,6 @@ const Json::Value& SettingsLoader::_getJSONValue(const Json::Value& json, const
return Json::Value::nullSingleton();
}
// Returns true if the given Json::Value looks like a profile.
// We introduced a bug (GH#9962, fixed in GH#9964) that would result in one or
// more nameless, guid-less profiles being emitted into the user's settings file.
// Those profiles would show up in the list as "Default" later.
bool SettingsLoader::_isValidProfileObject(const Json::Value& profileJson)
{
return profileJson.isObject() &&
(profileJson.isMember(NameKey.data(), NameKey.data() + NameKey.size()) || // has a name (can generate a guid)
profileJson.isMember(GuidKey.data(), GuidKey.data() + GuidKey.size())); // or has a guid
}
// We treat userSettings.profiles as an append-only array and will
// append profiles into the userSettings as necessary in this function.
// _userProfileCount stores the number of profiles that were in userJSON during construction.
@ -458,7 +432,7 @@ gsl::span<const winrt::com_ptr<Profile>> SettingsLoader::_getNonUserOriginProfil
}
// Parses the given JSON string ("content") and fills a ParsedSettings instance with it.
void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, bool updatesKeyAllowed)
{
const auto json = content.empty() ? Json::Value{ Json::ValueType::objectValue } : _parseJSON(content);
const auto& profilesObject = _getJSONValue(json, ProfilesKey);
@ -507,28 +481,39 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source
for (const auto& profileJson : profilesArray)
{
if (_isValidProfileObject(profileJson))
auto profile = Profile::FromJson(profileJson);
profile->Origin(origin);
// The Guid() generation below depends on the value of Source().
// --> Provide one if we got one.
if (!source.empty())
{
auto profile = Profile::FromJson(profileJson);
profile->Origin(origin);
profile->Source(source);
}
// The Guid() generation below depends on the value of Source().
// --> Provide one if we got one.
if (!source.empty())
{
profile->Source(source);
}
// The Guid() getter generates one from Name() and Source() if none exists otherwise.
// We want to ensure that every profile has a GUID no matter what, not just to
// cache the value, but also to make them consistently identifiable later on.
if (!profile->HasGuid())
// The Guid() getter generates one from Name() and Source() if none exists otherwise.
// We want to ensure that every profile has a GUID no matter what, not just to
// cache the value, but also to make them consistently identifiable later on.
if (!profile->HasGuid())
{
if (profile->HasName())
{
profile->Guid(profile->Guid());
}
_appendProfile(std::move(profile), settings);
else if (!updatesKeyAllowed || profile->Updates() == winrt::guid{})
{
// We introduced a bug (GH#9962, fixed in GH#9964) that would result in one or
// more nameless, guid-less profiles being emitted into the user's settings file.
// Those profiles would show up in the list as "Default" later.
//
// Fragments however can contain an alternative "updates" key, which works similar to the "guid".
// If updatesKeyAllowed is true (see FindFragmentsAndMergeIntoUserSettings) we permit
// such Guid-less, Name-less profiles as long as they have a valid Updates field.
continue;
}
}
_appendProfile(std::move(profile), settings);
}
}
}
@ -549,6 +534,28 @@ void SettingsLoader::_appendProfile(winrt::com_ptr<Profile>&& profile, ParsedSet
}
}
// If the given ParsedSettings instance contains a profile with the given profile's GUID,
// the profile is added as a parent. Otherwise a new child profile is created.
void SettingsLoader::_addParentProfile(const winrt::com_ptr<implementation::Profile>& profile, ParsedSettings& settings)
{
if (const auto [it, inserted] = settings.profilesByGuid.emplace(profile->Guid(), profile); !inserted)
{
// If inserted is false, we got a matching user profile with identical GUID.
// --> The generated profile is a parent of the existing user profile.
it->second->InsertParent(profile);
}
else
{
// If inserted is true, then this is a generated profile that doesn't exist in the user's settings.
// While emplace() has already created an appropriate entry in .profilesByGuid, we still need to
// add it to .profiles (which is basically a sorted list of .profilesByGuid's values).
//
// When a user modifies a profile they shouldn't modify the (static/constant)
// inbox profile of course. That's why we need to call CreateChild here.
settings.profiles.emplace_back(CreateChild(profile));
}
}
// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator)

View file

@ -51,6 +51,7 @@ static constexpr std::string_view TrimBlockSelectionKey{ "trimBlockSelection" };
static constexpr std::string_view AlwaysShowNotificationIconKey{ "alwaysShowNotificationIcon" };
static constexpr std::string_view MinimizeToNotificationAreaKey{ "minimizeToNotificationArea" };
static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" };
static constexpr std::string_view ShowAdminShieldKey{ "showAdminShield" };
static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" };
@ -121,6 +122,8 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
globals->_MinimizeToNotificationArea = _MinimizeToNotificationArea;
globals->_AlwaysShowNotificationIcon = _AlwaysShowNotificationIcon;
globals->_DisabledProfileSources = _DisabledProfileSources;
globals->_ShowAdminShield = _ShowAdminShield;
globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile;
globals->_defaultProfile = _defaultProfile;
@ -227,6 +230,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json)
JsonUtils::GetValueForKey(json, AlwaysShowNotificationIconKey, _AlwaysShowNotificationIcon);
JsonUtils::GetValueForKey(json, DisabledProfileSourcesKey, _DisabledProfileSources);
JsonUtils::GetValueForKey(json, ShowAdminShieldKey, _ShowAdminShield);
static constexpr std::array bindingsKeys{ LegacyKeybindingsKey, ActionsKey };
for (const auto& jsonKey : bindingsKeys)
{
@ -324,6 +329,7 @@ Json::Value GlobalAppSettings::ToJson() const
JsonUtils::SetValueForKey(json, MinimizeToNotificationAreaKey, _MinimizeToNotificationArea);
JsonUtils::SetValueForKey(json, AlwaysShowNotificationIconKey, _AlwaysShowNotificationIcon);
JsonUtils::SetValueForKey(json, DisabledProfileSourcesKey, _DisabledProfileSources);
JsonUtils::SetValueForKey(json, ShowAdminShieldKey, _ShowAdminShield);
// clang-format on
json[JsonKey(ActionsKey)] = _actionMap->ToJson();

View file

@ -99,6 +99,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowNotificationIcon, false);
INHERITABLE_SETTING(Model::GlobalAppSettings, winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, nullptr);
INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L"");
INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ShowAdminShield, true);
private:
#ifdef NDEBUG

View file

@ -84,6 +84,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_SETTING(Boolean, MinimizeToNotificationArea);
INHERITABLE_SETTING(Boolean, AlwaysShowNotificationIcon);
INHERITABLE_SETTING(IVector<String>, DisabledProfileSources);
INHERITABLE_SETTING(Boolean, ShowAdminShield);
Windows.Foundation.Collections.IMapView<String, ColorScheme> ColorSchemes();
void AddColorScheme(ColorScheme scheme);

View file

@ -20,6 +20,7 @@
"showTerminalTitleInTitlebar": true,
"tabWidthMode": "equal",
"tabSwitcherMode": "inOrder",
"showAdminShield": true,
// Miscellaneous
"confirmCloseAllTabs": true,

View file

@ -76,6 +76,7 @@ namespace RemotingUnitTests
void Summon(const Remoting::SummonWindowBehavior& /*args*/) { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); };
void RequestShowNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); };
void RequestHideNotificationIcon() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); };
winrt::hstring GetWindowLayout() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); };
void RequestQuitAll() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); };
void Quit() { throw winrt::hresult_error(winrt::hresult{ (int32_t)0x800706ba }); };
TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs);
@ -88,6 +89,7 @@ namespace RemotingUnitTests
TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, Remoting::GetWindowLayoutArgs);
};
class RemotingTests

View file

@ -130,7 +130,7 @@ namespace TerminalCoreUnitTests
DummyRenderTarget emptyRT;
term.Create({ 10, 10 }, scrollback, emptyRT);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansionMode::Word);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansion::Word);
ValidateSingleRowSelection(term, expected);
};
@ -142,7 +142,7 @@ namespace TerminalCoreUnitTests
DummyRenderTarget emptyRT;
term.Create({ 10, 10 }, scrollback, emptyRT);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansionMode::Line);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansion::Line);
ValidateSingleRowSelection(term, expected);
};
@ -501,7 +501,7 @@ namespace TerminalCoreUnitTests
// Simulate double click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word);
// Validate selection area
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, (4 + gsl::narrow<SHORT>(text.size()) - 1), 10 }));
@ -519,7 +519,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word);
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
@ -546,7 +546,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (15,10)
// this is over the '>' char
auto clickPos = COORD{ 15, 10 };
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word);
// ---Validate selection area---
// "Terminal" is in class 2
@ -572,7 +572,7 @@ namespace TerminalCoreUnitTests
term.Write(text);
// Simulate double click at (x,y) = (5,10)
term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansionMode::Word);
term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansion::Word);
// Simulate move to (x,y) = (21,10)
//
@ -601,7 +601,7 @@ namespace TerminalCoreUnitTests
term.Write(text);
// Simulate double click at (x,y) = (21,10)
term.MultiClickSelection({ 21, 10 }, Terminal::SelectionExpansionMode::Word);
term.MultiClickSelection({ 21, 10 }, Terminal::SelectionExpansion::Word);
// Simulate move to (x,y) = (5,10)
//
@ -622,7 +622,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Line);
// Validate selection area
ValidateSingleRowSelection(term, SMALL_RECT({ 0, 10, 99, 10 }));
@ -636,7 +636,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Line);
// Simulate move to (x,y) = (7,10)
term.SetSelectionEnd({ 7, 10 });
@ -653,7 +653,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Line);
// Simulate move to (x,y) = (5,11)
term.SetSelectionEnd({ 5, 11 });
@ -691,7 +691,7 @@ namespace TerminalCoreUnitTests
// Step 1: Create a selection on "doubleClickMe"
{
// Simulate double click at (x,y) = (5,10)
term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansionMode::Word);
term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansion::Word);
// Validate selection area: "doubleClickMe" selected
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 16, 10 }));
@ -704,7 +704,7 @@ namespace TerminalCoreUnitTests
// buffer: doubleClickMe dragThroughHere
// ^ ^
// start finish
term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Cell);
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Char);
// Validate selection area: "doubleClickMe drag" selected
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 21, 10 }));
@ -717,7 +717,7 @@ namespace TerminalCoreUnitTests
// buffer: doubleClickMe dragThroughHere
// ^ ^ ^
// start click finish
term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Word);
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Word);
// Validate selection area: "doubleClickMe dragThroughHere" selected
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 32, 10 }));
@ -730,7 +730,7 @@ namespace TerminalCoreUnitTests
// buffer: doubleClickMe dragThroughHere |
// ^ ^ ^
// start click finish (boundary)
term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Line);
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Line);
// Validate selection area: "doubleClickMe dragThroughHere..." selected
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 99, 10 }));
@ -743,7 +743,7 @@ namespace TerminalCoreUnitTests
// buffer: doubleClickMe dragThroughHere
// ^ ^ ^
// start click finish
term.SetSelectionEnd({ 21, 10 }, ::Terminal::SelectionExpansionMode::Word);
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Word);
// Validate selection area: "doubleClickMe dragThroughHere" selected
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 32, 10 }));
@ -825,7 +825,7 @@ namespace TerminalCoreUnitTests
// Step 4: Shift+Click at (5,10)
{
term.SetSelectionEnd({ 5, 10 }, ::Terminal::SelectionExpansionMode::Cell);
term.SetSelectionEnd({ 5, 10 }, Terminal::SelectionExpansion::Char);
// Validate selection area
// NOTE: Pivot should still be (10, 10)
@ -834,7 +834,7 @@ namespace TerminalCoreUnitTests
// Step 5: Shift+Click back at (20,10)
{
term.SetSelectionEnd({ 20, 10 }, ::Terminal::SelectionExpansionMode::Cell);
term.SetSelectionEnd({ 20, 10 }, Terminal::SelectionExpansion::Char);
// Validate selection area
// NOTE: Pivot should still be (10, 10)

View file

@ -104,3 +104,40 @@ _TIL_INLINEPREFIX const std::wstring& GetWtExePath()
}();
return exePath;
}
// Method Description:
// - Quotes and escapes the given string so that it can be used as a command-line arg.
// - e.g. given `\";foo\` will return `"\\\"\;foo\\"` so that the caller can construct a command-line
// using something such as `fmt::format(L"wt --title {}", QuoteAndQuoteAndEscapeCommandlineArg(TabTitle()))`.
// Arguments:
// - arg - the command-line argument to quote and escape.
// Return Value:
// - the quoted and escaped command-line argument.
_TIL_INLINEPREFIX std::wstring QuoteAndEscapeCommandlineArg(const std::wstring_view& arg)
{
std::wstring out;
out.reserve(arg.size() + 2);
out.push_back(L'"');
size_t backslashes = 0;
for (const auto ch : arg)
{
if (ch == L'\\')
{
backslashes++;
}
else
{
if (ch == L';' || ch == L'"')
{
out.append(backslashes + 1, L'\\');
}
backslashes = 0;
}
out.push_back(ch);
}
out.append(backslashes, L'\\');
out.push_back(L'"');
return out;
}

View file

@ -20,6 +20,7 @@ using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace ::Microsoft::Console;
using namespace ::Microsoft::Console::Types;
using namespace std::chrono_literals;
// This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx
// "If the high-order bit is 1, the key is down; otherwise, it is up."
@ -29,7 +30,8 @@ AppHost::AppHost() noexcept :
_app{},
_windowManager{},
_logic{ nullptr }, // don't make one, we're going to take a ref on app's
_window{ nullptr }
_window{ nullptr },
_getWindowLayoutThrottler{} // this will get set if we become the monarch
{
_logic = _app.Logic(); // get a ref to app's logic
@ -84,6 +86,12 @@ AppHost::AppHost() noexcept :
_window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop());
_window->MakeWindow();
_windowManager.GetWindowLayoutRequested([this](auto&&, const winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs& args) {
// The peasants are running on separate threads, so they'll need to
// swap what context they are in to the ui thread to get the actual layout.
args.WindowLayoutJsonAsync(_GetWindowLayoutAsync());
});
_windowManager.BecameMonarch({ this, &AppHost::_BecomeMonarch });
if (_windowManager.IsMonarch())
{
@ -227,7 +235,47 @@ void AppHost::_HandleCommandlineArgs()
// is created.
if (_windowManager.IsMonarch())
{
_logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants());
const auto numPeasants = _windowManager.GetNumberOfPeasants();
const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (_logic.ShouldUsePersistedLayout() && layouts && layouts.Size() > 0)
{
uint32_t startIdx = 0;
// We want to create a window for every saved layout.
// If we are the only window, and no commandline arguments were provided
// then we should just use the current window to load the first layout.
// Otherwise create this window normally with its commandline, and create
// a new window using the first saved layout information.
// The 2nd+ layout will always get a new window.
if (numPeasants == 1 && !_logic.HasCommandlineArguments() && !_logic.HasSettingsStartupActions())
{
_logic.SetPersistedLayoutIdx(startIdx);
startIdx += 1;
}
// Create new windows for each of the other saved layouts.
for (const auto size = layouts.Size(); startIdx < size; startIdx += 1)
{
auto newWindowArgs = fmt::format(L"{0} -w new -s {1}", args[0], startIdx);
STARTUPINFO si;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
wil::unique_process_information pi;
LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr,
newWindowArgs.data(),
nullptr, // lpProcessAttributes
nullptr, // lpThreadAttributes
false, // bInheritHandles
DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, // doCreationFlags
nullptr, // lpEnvironment
nullptr, // lpStartingDirectory
&si, // lpStartupInfo
&pi // lpProcessInformation
));
}
}
_logic.SetNumberOfOpenWindows(numPeasants);
}
_logic.WindowName(peasant.WindowName());
_logic.WindowId(peasant.GetID());
@ -264,7 +312,16 @@ void AppHost::Initialize()
// Register the 'X' button of the window for a warning experience of multiple
// tabs opened, this is consistent with Alt+F4 closing
_window->WindowCloseButtonClicked([this]() { _logic.WindowCloseButtonClicked(); });
_window->WindowCloseButtonClicked([this]() {
const auto pos = _GetWindowLaunchPosition();
_logic.CloseWindow(pos);
});
// If the user requests a close in another way handle the same as if the 'X'
// was clicked.
_logic.CloseRequested([this](auto&&, auto&&) {
const auto pos = _GetWindowLaunchPosition();
_logic.CloseWindow(pos);
});
// Add an event handler to plumb clicks in the titlebar area down to the
// application layer.
@ -354,6 +411,24 @@ void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*se
_window->Close();
}
LaunchPosition AppHost::_GetWindowLaunchPosition()
{
// Get the position of the current window. This includes the
// non-client already.
const auto window = _window->GetWindowRect();
const auto dpi = _window->GetCurrentDpi();
const auto nonClientArea = _window->GetNonClientFrame(dpi);
// The nonClientArea adjustment is negative, so subtract that out.
// This way we save the user-visible location of the terminal.
LaunchPosition pos{};
pos.X = window.left - nonClientArea.left;
pos.Y = window.top;
return pos;
}
// Method Description:
// - Resize the window we're about to create to the appropriate dimensions, as
// specified in the settings. This will be called during the handling of
@ -641,6 +716,31 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable send
_logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory());
}
// Method Description:
// - Asynchronously get the window layout from the current page. This is
// done async because we need to switch between the ui thread and the calling
// thread.
// - NB: The peasant calling this must not be running on the UI thread, otherwise
// they will crash since they just call .get on the async operation.
// Arguments:
// - <none>
// Return Value:
// - The window layout as a json string.
winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> AppHost::_GetWindowLayoutAsync()
{
winrt::apartment_context peasant_thread;
// Use the main thread since we are accessing controls.
co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher());
const auto pos = _GetWindowLaunchPosition();
const auto layoutJson = _logic.GetWindowLayoutJson(pos);
// go back to give the result to the peasant.
co_await peasant_thread;
co_return layoutJson;
}
// Method Description:
// - Event handler for the WindowManager::FindTargetWindowRequested event. The
// manager will ask us how to figure out what the target window is for a set
@ -694,8 +794,13 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s
// and subscribe for updates if there are any changes to that number.
_logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants());
_windowManager.WindowCreated([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); });
_windowManager.WindowClosed([this](auto&&, auto&&) { _logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); });
_windowManager.WindowCreated([this](auto&&, auto&&) {
_getWindowLayoutThrottler.value()();
_logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants()); });
_windowManager.WindowClosed([this](auto&&, auto&&) {
_getWindowLayoutThrottler.value()();
_logic.SetNumberOfOpenWindows(_windowManager.GetNumberOfPeasants());
});
// These events are coming from peasants that become or un-become quake windows.
_windowManager.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequested(); });
@ -703,6 +808,48 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s
// If the monarch receives a QuitAll event it will signal this event to be
// ran before each peasant is closed.
_windowManager.QuitAllRequested({ this, &AppHost::_QuitAllRequested });
// The monarch should be monitoring if it should save the window layout.
if (!_getWindowLayoutThrottler.has_value())
{
// We want at least some delay to prevent the first save from overwriting
// the data as we try load windows initially.
_getWindowLayoutThrottler.emplace(std::move(std::chrono::seconds(10)), std::move([this]() { _SaveWindowLayoutsRepeat(); }));
_getWindowLayoutThrottler.value()();
}
}
winrt::Windows::Foundation::IAsyncAction AppHost::_SaveWindowLayouts()
{
// Make sure we run on a background thread to not block anything.
co_await winrt::resume_background();
if (_logic.ShouldUsePersistedLayout())
{
const auto layoutJsons = _windowManager.GetAllWindowLayouts();
_logic.SaveWindowLayoutJsons(layoutJsons);
}
co_return;
}
winrt::fire_and_forget AppHost::_SaveWindowLayoutsRepeat()
{
// Make sure we run on a background thread to not block anything.
co_await winrt::resume_background();
co_await _SaveWindowLayouts();
// Don't need to save too frequently.
co_await 30s;
// As long as we are supposed to keep saving, request another save.
// This will be delayed by the throttler so that at most one save happens
// per 10 seconds, if a save is requested by another source simultaneously.
if (_getWindowLayoutThrottler.has_value())
{
_getWindowLayoutThrottler.value()();
}
}
void AppHost::_listenForInboundConnections()
@ -1053,10 +1200,18 @@ void AppHost::_RequestQuitAll(const winrt::Windows::Foundation::IInspectable&,
}
void AppHost::_QuitAllRequested(const winrt::Windows::Foundation::IInspectable&,
const winrt::Windows::Foundation::IInspectable&)
const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args)
{
// TODO: GH#9800: For now, nothing needs to be done before the monarch closes all windows.
// Later when we have state saving that should go here.
// Make sure that the current timer is destroyed so that it doesn't attempt
// to run while we are in the middle of quitting.
if (_getWindowLayoutThrottler.has_value())
{
_getWindowLayoutThrottler.reset();
}
// Tell the monarch to wait for the window layouts to save before
// everyone quits.
args.BeforeQuitAllAction(_SaveWindowLayouts());
}
void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender,

View file

@ -4,6 +4,7 @@
#include "pch.h"
#include "NonClientIslandWindow.h"
#include "NotificationIcon.h"
#include <til/throttled_func.h>
class AppHost
{
@ -31,7 +32,12 @@ private:
bool _shouldCreateWindow{ false };
bool _useNonClientArea{ false };
std::optional<til::throttled_func_trailing<>> _getWindowLayoutThrottler;
winrt::Windows::Foundation::IAsyncAction _SaveWindowLayouts();
winrt::fire_and_forget _SaveWindowLayoutsRepeat();
void _HandleCommandlineArgs();
winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition();
void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode);
void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender,
@ -53,6 +59,8 @@ private:
void _DispatchCommandline(winrt::Windows::Foundation::IInspectable sender,
winrt::Microsoft::Terminal::Remoting::CommandlineArgs args);
winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> _GetWindowLayoutAsync();
void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args);
@ -95,7 +103,7 @@ private:
const winrt::Windows::Foundation::IInspectable& args);
void _QuitAllRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
const winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs& args);
void _CreateNotificationIcon();
void _DestroyNotificationIcon();

View file

@ -133,6 +133,11 @@ public:
return _window.get();
}
UINT GetCurrentDpi() const noexcept
{
return ::GetDpiForWindow(_window.get());
}
float GetCurrentDpiScale() const noexcept
{
const auto dpi = ::GetDpiForWindow(_window.get());

View file

@ -69,5 +69,6 @@ public:
const COORD GetSelectionAnchor() const noexcept;
const COORD GetSelectionEnd() const noexcept;
void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr);
const bool IsUiaDataInitialized() const noexcept override { return true; }
#pragma endregion
};

View file

@ -2253,7 +2253,7 @@ void TextBufferTests::GetGlyphBoundaries()
_buffer->Write(iter, target);
auto start = _buffer->GetGlyphStart(target);
auto end = _buffer->GetGlyphEnd(target);
auto end = _buffer->GetGlyphEnd(target, true);
VERIFY_ARE_EQUAL(test.start, start);
VERIFY_ARE_EQUAL(wideGlyph ? test.wideGlyphEnd : test.normalEnd, end);

View file

@ -392,6 +392,11 @@ public:
{
}
const bool IsUiaDataInitialized() const noexcept
{
return true;
}
const std::wstring GetHyperlinkUri(uint16_t /*id*/) const noexcept
{
return {};

View file

@ -21,6 +21,7 @@
#include "til/replace.h"
#include "til/string.h"
#include "til/pmr.h"
#include "til/enumset.h"
// Use keywords on TraceLogging providers to specify the category
// of event that we are emitting for filtering purposes.

132
src/inc/til/enumset.h Normal file
View file

@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <bitset>
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
// By design, this class hides several methods in the std::bitset class
// so they can be called with an enum parameter instead of a size_t, so
// we need to disable the "hides a non-virtual function" warning.
#pragma warning(push)
#pragma warning(disable : 26434)
// til::enumset is a subclass of std::bitset, storing a fixed size array of
// boolean elements, the positions in the array being identified by values
// from a given enumerated type. By default it holds the same number of
// bits as a size_t value.
template<typename Type, size_t Bits = std::numeric_limits<size_t>::digits>
class enumset : public std::bitset<Bits>
{
using _base = std::bitset<Bits>;
public:
using reference = typename _base::reference;
enumset() = default;
// Method Description:
// - Constructs a new bitset with the given list of positions set to true.
template<typename... Args, typename = std::enable_if_t<std::conjunction_v<std::is_same<Type, Args>...>>>
constexpr enumset(const Args... positions) noexcept :
_base((... | (1ULL << static_cast<size_t>(positions))))
{
}
// Method Description:
// - Returns the value of the bit at the given position.
constexpr bool operator[](const Type pos) const
{
return _base::operator[](static_cast<size_t>(pos));
}
// Method Description:
// - Returns a reference to the bit at the given position.
reference operator[](const Type pos)
{
return _base::operator[](static_cast<size_t>(pos));
}
// Method Description:
// - Returns the value of the bit at the given position.
// Throws std::out_of_range if it is not a valid position
// in the bitset.
bool test(const Type pos) const
{
return _base::test(static_cast<size_t>(pos));
}
// Method Description:
// - Returns true if any of the bits are set to true.
bool any() const noexcept
{
return _base::any();
}
// Method Description:
// - Returns true if any of the bits in the given positions are true.
template<typename... Args, typename = std::enable_if_t<std::conjunction_v<std::is_same<Type, Args>...>>>
bool any(const Args... positions) const noexcept
{
return (enumset{ positions... } & *this) != 0;
}
// Method Description:
// - Returns true if all of the bits are set to true.
bool all() const noexcept
{
return _base::all();
}
// Method Description:
// - Returns true if all of the bits in the given positions are true.
template<typename... Args, typename = std::enable_if_t<std::conjunction_v<std::is_same<Type, Args>...>>>
bool all(const Args... positions) const noexcept
{
return (enumset{ positions... } & *this) == enumset{ positions... };
}
// Method Description:
// - Sets the bit in the given position to the specified value.
enumset& set(const Type pos, const bool val = true)
{
_base::set(static_cast<size_t>(pos), val);
return *this;
}
// Method Description:
// - Resets the bit in the given position to false.
enumset& reset(const Type pos)
{
_base::reset(static_cast<size_t>(pos));
return *this;
}
// Method Description:
// - Flips the bit at the given position.
enumset& flip(const Type pos)
{
_base::flip(static_cast<size_t>(pos));
return *this;
}
// Method Description:
// - Sets all of the bits in the given positions to true.
template<typename... Args>
enumset& set_all(const Args... positions)
{
return (..., set(positions));
}
// Method Description:
// - Resets all of the bits in the given positions to false.
template<typename... Args>
enumset& reset_all(const Args... positions)
{
return (..., reset(positions));
}
};
#pragma warning(pop)
}

View file

@ -493,6 +493,7 @@ void DxEngine::_ComputePixelShaderSettings() noexcept
// actual failure from the API itself.
[[nodiscard]] HRESULT DxEngine::_CreateSurfaceHandle() noexcept
{
#pragma warning(suppress : 26447)
wil::unique_hmodule hDComp{ LoadLibraryEx(L"Dcomp.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) };
RETURN_LAST_ERROR_IF(hDComp.get() == nullptr);

View file

@ -0,0 +1,169 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "WexTestClass.h"
using namespace WEX::Logging;
class EnumSetTests
{
TEST_CLASS(EnumSetTests);
TEST_METHOD(Constructors)
{
enum class Flags
{
Zero,
One,
Two,
Three,
Four
};
{
Log::Comment(L"Default constructor with no bits set");
til::enumset<Flags> flags;
VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong());
}
{
Log::Comment(L"Constructor with bit 3 set");
til::enumset<Flags> flags{ Flags::Three };
VERIFY_ARE_EQUAL(0b01000u, flags.to_ulong());
}
{
Log::Comment(L"Constructor with bits 0, 2, and 4 set");
til::enumset<Flags> flags{ Flags::Zero, Flags::Two, Flags::Four };
VERIFY_ARE_EQUAL(0b10101u, flags.to_ulong());
}
}
TEST_METHOD(SetResetFlipMethods)
{
enum class Flags
{
Zero,
One,
Two,
Three,
Four
};
Log::Comment(L"Start with no bits set");
til::enumset<Flags> flags;
VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong());
Log::Comment(L"Set bit 2 to true");
flags.set(Flags::Two);
VERIFY_ARE_EQUAL(0b00100u, flags.to_ulong());
Log::Comment(L"Flip bit 4 to true");
flags.flip(Flags::Four);
VERIFY_ARE_EQUAL(0b10100u, flags.to_ulong());
Log::Comment(L"Set bit 0 to true");
flags.set(Flags::Zero);
VERIFY_ARE_EQUAL(0b10101u, flags.to_ulong());
Log::Comment(L"Reset bit 2 to false, leaving 0 and 4 true");
flags.reset(Flags::Two);
VERIFY_ARE_EQUAL(0b10001u, flags.to_ulong());
Log::Comment(L"Set bit 0 to false, leaving 4 true");
flags.set(Flags::Zero, false);
VERIFY_ARE_EQUAL(0b10000u, flags.to_ulong());
Log::Comment(L"Flip bit 4, leaving all bits false ");
flags.flip(Flags::Four);
VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong());
Log::Comment(L"Set bits 0, 3, and 2");
flags.set_all(Flags::Zero, Flags::Three, Flags::Two);
VERIFY_ARE_EQUAL(0b01101u, flags.to_ulong());
Log::Comment(L"Reset bits 3, 4 (already reset), and 0, leaving 2 true");
flags.reset_all(Flags::Three, Flags::Four, Flags::Zero);
VERIFY_ARE_EQUAL(0b00100u, flags.to_ulong());
}
TEST_METHOD(TestMethods)
{
enum class Flags
{
Zero,
One,
Two,
Three,
Four
};
Log::Comment(L"Start with bits 0, 2, and 4 set");
til::enumset<Flags> flags{ Flags::Zero, Flags::Two, Flags::Four };
VERIFY_ARE_EQUAL(0b10101u, flags.to_ulong());
Log::Comment(L"Test bits 1 and 2 with the test method");
VERIFY_IS_FALSE(flags.test(Flags::One));
VERIFY_IS_TRUE(flags.test(Flags::Two));
Log::Comment(L"Test bit 3 and 4 with the array operator");
VERIFY_IS_FALSE(flags[Flags::Three]);
VERIFY_IS_TRUE(flags[Flags::Four]);
Log::Comment(L"Test if any bits are set");
VERIFY_IS_TRUE(flags.any());
Log::Comment(L"Test if either bit 1 or 3 are set");
VERIFY_IS_FALSE(flags.any(Flags::One, Flags::Three));
Log::Comment(L"Test if either bit 1 or 4 are set");
VERIFY_IS_TRUE(flags.any(Flags::One, Flags::Four));
Log::Comment(L"Test if all bits are set");
VERIFY_IS_FALSE(flags.all());
Log::Comment(L"Test if both bits 0 and 4 are set");
VERIFY_IS_TRUE(flags.all(Flags::Zero, Flags::Four));
Log::Comment(L"Test if both bits 0 and 3 are set");
VERIFY_IS_FALSE(flags.all(Flags::Zero, Flags::Three));
}
TEST_METHOD(ArrayReferenceOperator)
{
enum class Flags
{
Zero,
One,
Two,
Three,
Four
};
Log::Comment(L"Start with no bits set");
til::enumset<Flags> flags;
VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong());
Log::Comment(L"Test bit 3 reference is false");
auto reference = flags[Flags::Three];
VERIFY_IS_FALSE(reference);
VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong());
Log::Comment(L"Set bit 3 reference to true");
flags.set(Flags::Three);
VERIFY_IS_TRUE(reference);
VERIFY_ARE_EQUAL(0b01000u, flags.to_ulong());
Log::Comment(L"Reset bit 3 reference to false");
flags.reset(Flags::Three);
VERIFY_IS_FALSE(reference);
VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong());
Log::Comment(L"Flip bit 3 reference to true");
reference.flip();
VERIFY_IS_TRUE(reference);
VERIFY_ARE_EQUAL(0b01000u, flags.to_ulong());
Log::Comment(L"Flip bit 3 reference back to false");
reference.flip();
VERIFY_IS_FALSE(reference);
VERIFY_ARE_EQUAL(0b00000u, flags.to_ulong());
}
};

View file

@ -17,6 +17,7 @@
<ClCompile Include="BitmapTests.cpp" />
<ClCompile Include="CoalesceTests.cpp" />
<ClCompile Include="ColorTests.cpp" />
<ClCompile Include="EnumSetTests.cpp" />
<ClCompile Include="MathTests.cpp" />
<ClCompile Include="mutex.cpp" />
<ClCompile Include="OperatorTests.cpp" />

View file

@ -9,6 +9,7 @@
<ClCompile Include="BitmapTests.cpp" />
<ClCompile Include="CoalesceTests.cpp" />
<ClCompile Include="ColorTests.cpp" />
<ClCompile Include="EnumSetTests.cpp" />
<ClCompile Include="MathTests.cpp" />
<ClCompile Include="mutex.cpp" />
<ClCompile Include="OperatorTests.cpp" />

View file

@ -40,6 +40,7 @@ namespace Microsoft::Console::Types
virtual const COORD GetSelectionAnchor() const noexcept = 0;
virtual const COORD GetSelectionEnd() const noexcept = 0;
virtual void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr) = 0;
virtual const bool IsUiaDataInitialized() const noexcept = 0;
};
// See docs/virtual-dtors.md for an explanation of why this is weird.

View file

@ -220,21 +220,19 @@ IFACEMETHODIMP ScreenInfoUiaProviderBase::SetFocus()
IFACEMETHODIMP ScreenInfoUiaProviderBase::GetSelection(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal)
{
RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal);
*ppRetVal = nullptr;
_LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_UnlockConsole();
});
RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal);
*ppRetVal = nullptr;
HRESULT hr = S_OK;
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
// make a safe array
HRESULT hr = S_OK;
*ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 1);
if (*ppRetVal == nullptr)
{
return E_OUTOFMEMORY;
}
RETURN_HR_IF_NULL(E_OUTOFMEMORY, *ppRetVal);
WRL::ComPtr<UiaTextRangeBase> range;
if (!_pData->IsSelectionActive())
@ -272,19 +270,18 @@ IFACEMETHODIMP ScreenInfoUiaProviderBase::GetSelection(_Outptr_result_maybenull_
IFACEMETHODIMP ScreenInfoUiaProviderBase::GetVisibleRanges(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal)
{
RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal);
*ppRetVal = nullptr;
_LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_UnlockConsole();
});
RETURN_HR_IF_NULL(E_INVALIDARG, ppRetVal);
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
// make a safe array
*ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 1);
if (*ppRetVal == nullptr)
{
return E_OUTOFMEMORY;
}
RETURN_HR_IF_NULL(E_OUTOFMEMORY, *ppRetVal);
WRL::ComPtr<UiaTextRangeBase> range;
const auto bufferSize = _pData->GetTextBuffer().GetSize();

View file

@ -16,9 +16,6 @@ HRESULT TermControlUiaProvider::RuntimeClassInitialize(_In_ ::Microsoft::Console
RETURN_IF_FAILED(ScreenInfoUiaProviderBase::RuntimeClassInitialize(uiaData));
_controlInfo = controlInfo;
// TODO GitHub #1914: Re-attach Tracing to UIA Tree
//Tracing::s_TraceUia(nullptr, ApiCall::Constructor, nullptr);
return S_OK;
}
@ -26,11 +23,6 @@ IFACEMETHODIMP TermControlUiaProvider::Navigate(_In_ NavigateDirection direction
_COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, ppProvider);
// TODO GitHub #1914: Re-attach Tracing to UIA Tree
/*ApiMsgNavigate apiMsg;
apiMsg.Direction = direction;
Tracing::s_TraceUia(this, ApiCall::Navigate, &apiMsg);*/
*ppProvider = nullptr;
if (direction == NavigateDirection_Parent)
@ -122,6 +114,12 @@ HRESULT TermControlUiaProvider::GetSelectionRange(_In_ IRawElementProviderSimple
RETURN_HR_IF_NULL(E_INVALIDARG, ppUtr);
*ppUtr = nullptr;
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized() || !_pData->IsSelectionActive());
const auto start = _pData->GetSelectionAnchor();
// we need to make end exclusive

View file

@ -63,20 +63,6 @@ IFACEMETHODIMP TermControlUiaTextRange::Clone(_Outptr_result_maybenull_ ITextRan
return hr;
}
#if defined(_DEBUG) && defined(UiaTextRangeBase_DEBUG_MSGS)
OutputDebugString(L"Clone\n");
std::wstringstream ss;
ss << _id << L" cloned to " << (static_cast<UiaTextRangeBase*>(*ppRetVal))->_id;
std::wstring str = ss.str();
OutputDebugString(str.c_str());
OutputDebugString(L"\n");
#endif
// TODO GitHub #1914: Re-attach Tracing to UIA Tree
// tracing
/*ApiMsgClone apiMsg;
apiMsg.CloneId = static_cast<UiaTextRangeBase*>(*ppRetVal)->GetId();
Tracing::s_TraceUia(this, ApiCall::Clone, &apiMsg);*/
return S_OK;
}

View file

@ -220,15 +220,18 @@ IFACEMETHODIMP UiaTextRangeBase::CompareEndpoints(_In_ TextPatternRangeEndpoint
_Out_ int* pRetVal) noexcept
try
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
RETURN_HR_IF_NULL(E_INVALIDARG, pRetVal);
*pRetVal = 0;
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
// get the text range that we're comparing to
const UiaTextRangeBase* range = static_cast<UiaTextRangeBase*>(pTargetRange);
if (range == nullptr)
{
return E_INVALIDARG;
}
RETURN_HR_IF_NULL(E_INVALIDARG, range);
// get endpoint value that we're comparing to
const auto other = range->GetEndpoint(targetEndpoint);
@ -240,10 +243,7 @@ try
// This is a temporary solution to comparing two UTRs from different TextBuffers
// Ensure both endpoints fit in the current buffer.
const auto bufferSize = _pData->GetTextBuffer().GetSize();
if (!bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true))
{
return E_FAIL;
}
RETURN_HR_IF(E_FAIL, !bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true));
// compare them
*pRetVal = bufferSize.CompareInBounds(mine, other, true);
@ -259,6 +259,7 @@ IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexc
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
try
{
@ -295,7 +296,7 @@ void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit)
if (unit == TextUnit_Character)
{
_start = buffer.GetGlyphStart(_start, documentEnd);
_end = buffer.GetGlyphEnd(_start, documentEnd);
_end = buffer.GetGlyphEnd(_start, true, documentEnd);
}
else if (unit <= TextUnit_Word)
{
@ -444,6 +445,12 @@ try
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
*ppRetVal = nullptr;
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
// AttributeIDs that require special handling
switch (attributeId)
{
@ -605,6 +612,12 @@ try
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
*ppRetVal = nullptr;
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
const std::wstring queryText{ text, SysStringLen(text) };
const auto bufferSize = _getOptimizedBufferSize();
const auto sensitivity = ignoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive;
@ -730,6 +743,12 @@ try
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
VariantInit(pRetVal);
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
// AttributeIDs that require special handling
switch (attributeId)
{
@ -817,13 +836,14 @@ CATCH_RETURN();
IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept
{
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
*ppRetVal = nullptr;
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr);
*ppRetVal = nullptr;
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
try
{
@ -925,21 +945,19 @@ CATCH_RETURN();
IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal) noexcept
try
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
RETURN_HR_IF_NULL(E_INVALIDARG, pRetVal);
RETURN_HR_IF(E_INVALIDARG, maxLength < -1);
*pRetVal = nullptr;
if (maxLength < -1)
{
return E_INVALIDARG;
}
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
const auto maxLengthOpt = (maxLength == -1) ?
std::nullopt :
std::optional<unsigned int>{ maxLength };
_pData->LockConsole();
auto Unlock = wil::scope_exit([this]() noexcept {
_pData->UnlockConsole();
});
const auto text = _getTextValue(maxLengthOpt);
Unlock.reset();
@ -1013,6 +1031,7 @@ try
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
// We can abstract this movement by moving _start
// GH#7342: check if we're past the documentEnd
@ -1075,15 +1094,13 @@ IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoin
{
RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr);
*pRetVal = 0;
if (count == 0)
{
return S_OK;
}
_pData->LockConsole();
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
RETURN_HR_IF(S_OK, count == 0);
// GH#7342: check if we're past the documentEnd
// If so, clamp each endpoint to the end of the document.
@ -1141,10 +1158,8 @@ try
});
const UiaTextRangeBase* range = static_cast<UiaTextRangeBase*>(pTargetRange);
if (range == nullptr)
{
return E_INVALIDARG;
}
RETURN_HR_IF_NULL(E_INVALIDARG, range);
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
// TODO GH#5406: create a different UIA parent object for each TextBuffer
// This is a temporary solution to comparing two UTRs from different TextBuffers
@ -1152,10 +1167,7 @@ try
const auto bufferSize = _pData->GetTextBuffer().GetSize();
const auto mine = GetEndpoint(endpoint);
const auto other = range->GetEndpoint(targetEndpoint);
if (!bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true))
{
return E_FAIL;
}
RETURN_HR_IF(E_FAIL, !bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true));
SetEndpoint(endpoint, range->GetEndpoint(targetEndpoint));
@ -1171,6 +1183,7 @@ try
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
if (IsDegenerate())
{
@ -1215,6 +1228,7 @@ try
auto Unlock = wil::scope_exit([&]() noexcept {
_pData->UnlockConsole();
});
RETURN_HR_IF(E_FAIL, !_pData->IsUiaDataInitialized());
const auto oldViewport = _pData->GetViewport().ToInclusive();
const auto viewportHeight = _getViewportHeight(oldViewport);

View file

@ -0,0 +1,82 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
#################################
# New-TerminalStackedChangelog generates a markdown file with attribution
# over a set of revision ranges.
# Dustin uses it when he's writing the changelog.
#
## For example, generating the changelog for both 1.9 and 1.10 might look like this:
# $ New-TerminalStackedChangelog 1.9..release-1.9, 1.10..release-1.10
## The output will be a markdown-like document. Each commit will be converted into a
## single-line entry with attribution and an optional count:
#
# * Foo the bar (thanks @PanosP!)
# * [2] Fix the bug
#
# Entries with a count were present in both changelists.
#
# If you don't have the release tags/branches checked out locally, you might
# need to do something like:
#
# $ New-TerminalStackedChangelog origin/release-1.8..origin/release-1.9
[CmdletBinding()]
Param(
[string[]]$RevisionRanges
)
Function Test-MicrosoftPerson($email) {
Return $email -like "*@microsoft.com" -Or $email -like "pankaj.d*"
}
Function Generate-Thanks($Entry) {
# We don't need to thank ourselves for doing our jobs
If ($_.Microsoft) {
""
} ElseIf (-Not [string]::IsNullOrEmpty($_.PossibleUsername)) {
" (thanks @{0}!)" -f $_.PossibleUsername
} Else {
" (thanks @<{0}>!)" -f $_.Email
}
}
$usernameRegex = [regex]::new("(?:\d+\+)?(?<name>[^@]+)@users.noreply.github.com")
Function Get-PossibleUserName($email) {
$match = $usernameRegex.Match($email)
if ($null -Ne $match) {
return $match.Groups["name"].Value
}
return $null
}
$Entries = @()
ForEach ($RevisionRange in $RevisionRanges) {
# --pretty=format notes:
# - %an: author name
# - %x1C: print a literal FS (0x1C), which will be used as a delimiter
# - %ae: author email
# - %x1C: another FS
# - %s: subject, the title of the commit
$NewEntries = & git log $RevisionRange "--pretty=format:%an%x1C%ae%x1C%s" |
ConvertFrom-CSV -Delimiter "`u{001C}" -Header Author,Email,Subject
$Entries += $NewEntries | % { [PSCustomObject]@{
Author = $_.Author;
Email = $_.Email;
Subject = $_.Subject;
Microsoft = (Test-MicrosoftPerson $_.Email);
PossibleUsername = (Get-PossibleUserName $_.Email);
} }
}
$Unique = $Entries | Group-Object Subject | %{ $_.Group[0] | Add-Member Count $_.Count -Force -PassThru }
$Unique | % {
$c = ""
If ($_.Count -Gt 1) {
$c = "[{0}] " -f $_.Count
}
"* {0}{1}{2}" -f ($c, $_.Subject, (Generate-Thanks $_))
}