Compare commits

...

19 Commits

Author SHA1 Message Date
Dustin L. Howett
671552671d Use the actual process image instead of module filename to dedup session (#19415)
Apparently, `GetModuleFileNameW` returns exactly the path (or prefix, in
case of a DLL) passed to `CreateProcess` casing and all. Since we were
using it to generate the uniquing hash for Portable and Unpackaged
instances, this meant that `C:\Terminal\wt` and `C:\TeRmInAl\wt` were
considered different instances. Whoops.

Using `QueryFullProcessImageNameW` instead results in canonicalization.
Maybe the kernel does it. I don't know. What I do know is that it works
more correctly.

(`Query...` goes through the kernel, while `GetModule...` goes through
the loader. Interesting!)

Closes #19253

(cherry picked from commit 9d7ea77cc8)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgfkTuo
Service-Version: 1.24
2025-10-08 17:55:14 -05:00
PankajBhojwani
58a5088735 Only do "keys" fixups for non-nested, non-iterable commands (#19408)
## Summary of the Pull Request
When we introduced action IDs, we separated "commands" from
"keybindings", and introduced fixup logic to rewrite the legacy-style
command blocks into the new version. However we don't do any ID logic
for nested and iterable commands, so make sure we don't inform the
loader for fixups in those cases.

## Validation Steps Performed
We no longer repeatedly attempt to fixup the settings file when we see a
`"keys"` entry in a nested/iterable command block

## PR Checklist
- [x] Closes #18736

(cherry picked from commit 04676bd31a)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgfjzjQ
Service-Version: 1.24
2025-10-08 17:55:13 -05:00
Dustin L. Howett
c5f99b69d5 Remove the leading fire from the taskbar progress handler (#19403)
If the progress state hasn't been set for more than 200ms, we shouldn't
even bother flickering the old state.

This prevents applications from making the tab (and the taskbar icon)
flicker.

We were reviewing #19394 and decided that the _original_ behavior before
Leonard's throttling fix was somewhat unfortunate as well. An
application that sets an indeterminate state for 10ms and then clears it
shouldn't be able to make any part of the application flicker, fast _or_
slow.

Removing the leading fire time from the throttled function ensures that
it will only fire once every 200ms, and only with the state most
recently set. It will not debounce (so setting the progress every 150ms
will not prevent it from updating.)

Closes #19394

(cherry picked from commit 998ab586e1)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgfZOsU PVTI_lADOAF3p4s4BBcTlzgfb1NE
Service-Version: 1.24
2025-10-08 17:55:11 -05:00
Dustin L. Howett
6f5b293a56 Avoid reentrancy issues when dropping AppHost, even harder (#19395)
The previous fix in #19296 moved the _destruction_ of AppHost into the
tail end after we manipulate the `_windows` vector; however, it kept the
part which calls into XAML (`Close`) before the `erase`. I suspect that
we still had some reentrancy issues, where we cached an iterator before
the list was modified by another window close event.

That is:

```mermaid
sequenceDiagram
		Emperor->>Emperor: Close Window
		Emperor->>+AppHost: Close (a)
		AppHost->>XAML: Close
		XAML-->>Emperor: pump loop
		Emperor->>Emperor: Close Window
		Emperor->>+AppHost: Close (b)
		AppHost->>XAML: Close
		XAML-->>Emperor: pump loop
		AppHost->>-Emperor: Closed
		Emperor->>Emperor: erase(b)
		AppHost->>-Emperor: Closed
		Emperor->>Emperor: erase(a)
```

Moving the `Close()` to after the `erase` ensures that there are no
cached iterators that survive beyond XAML pumping the message loop.

Fixes 8d41ace3

(cherry picked from commit 5976de1600)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgfScpM
Service-Version: 1.24
2025-10-08 17:55:10 -05:00
Dustin L. Howett
e33bc3d137 build: separate vpack creation from vpack publication (#19380)
This will allow us to publish vpacks without making the build fail
waiting for us to *merge* those vpacks into Windows. It also gives us
better control over when and where the vpack update gets merged.

(cherry picked from commit 6b428577b9)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgfLAks
Service-Version: 1.24
2025-09-25 13:37:20 -05:00
Dustin L. Howett
6aae2d0b72 Move newTabMenu creation to Settings fixups (#19353)
Some of the other settings fixups require there to be a valid
NewTabMenu, rather than just a temporary object. Since the resolving all
the menu entries after loading already forces the user to have a
`newTabMenu`, let's just codify it as a real fixup.

I've moved the SSH folder fixup after the settings fixup because it
relies on there being a NTM.

I decided not to make this fixup write back to the user's settings.
There are a couple reasons for this, all of which are flimsy.

- There are a number of tests that test fixup behavior, especially those
around actions, which would need to be updated for this new mandatory
key. I did not think it proper to add `newTabMenu` to ten unrelated
tests that only contain actions (for example.)
- We actually don't currently have mandatory keys. But this one was
always being added anyway, in a later phase...
- It's consistent with the existing behavior.

Closes #19356

(cherry picked from commit e80aadd98b)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzge0HjU
Service-Version: 1.24
2025-09-16 18:31:42 -05:00
Dustin L. Howett
148a86c81c VsDev: reject VS instances which do not actually contain devshell/devcmd (#19352)
Closes #19169

(cherry picked from commit 1926c4601c)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzge0Hto
Service-Version: 1.24
2025-09-16 18:31:40 -05:00
Dustin L. Howett
94e96ca048 Avoid generating SSH profiles using stale memory (#19354)
You can't return a `string_view` to a temporary. It's a miracle this
ever worked.

Broken since inception in a5f9c85c39

Closes #19355

(cherry picked from commit 46b9572e60)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzge0HmE
Service-Version: 1.24
2025-09-16 18:31:39 -05:00
Ayman Bagabas
b53d7df066 Add support for VT horizontal mouse wheel events (#19248)
This adds support for horizontal mouse wheel events (`WM_MOUSEHWHEEL`).
With this change, applications running in the terminal can now receive
and respond to horizontal scroll inputs from the mouse/trackpad.

Closes #19245
Closes #10329

(cherry picked from commit 814f78ed2c)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgep1sM
Service-Version: 1.24
2025-09-16 18:31:37 -05:00
Leonard Hecker
1194ad0343 Fix behavior of split-pane for existing windows (#19347)
Closes #18815

## Validation Steps Performed
* `wt -w 0 sp` splits the current tab 

(cherry picked from commit 0aee174e68)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeV1i4
Service-Version: 1.24
2025-09-16 14:38:44 -05:00
John Cavanaugh
04e1290102 Fix terminal profile schema to allow null in keybinding id (#19332)
Fixes the terminal profile jsonschema to allow for null in the id. This
is to match the current implementation when disabling a built in default
keybind.

(cherry picked from commit eb16eb26ab)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgemwL0
Service-Version: 1.24
2025-09-16 14:38:43 -05:00
Leonard Hecker
f5083714c1 Fix a crash in _makeCursorVisible (#19329)
Fixes the crash and also makes `SnapOnOutput` a bit nicer.

Closes #19325

## Validation Steps Performed
* Launch vim in WSL
* Exit
* No crash 

(cherry picked from commit 384932183f)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgegVZ8
Service-Version: 1.24
2025-09-16 14:38:41 -05:00
Leonard Hecker
7642eec206 Fix right click on tabs closing them (#19273)
I do not like this.

## Validation Steps Performed
* Enable close buttons on tabs
* Open a tab
* Close the tab with middle click
* Open a tab
* Right click the tab
* Tab doesn't close, Menu opens 

(cherry picked from commit 4a34a76504)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeAlgM
Service-Version: 1.24
2025-09-16 14:38:40 -05:00
Myungchul Keum
37c674dd6e Adjust "Dimidium" color scheme (#19303)
- Add Selection BG color
- Make Bright white brighter

## Summary of the Pull Request
Final tune for Dimidium color scheme before its release.

## References and Relevant Issues
#18563

## Detailed Description of the Pull Request / Additional comments
I made little change to Dimidium color scheme.

<img width="640" height="174" alt="cmp-lightness1c"
src="https://github.com/user-attachments/assets/2e4aa6ca-5864-4901-b323-2e2bb2bf00e8"
/>

![preview-terminal](https://github.com/user-attachments/assets/8a53c54d-942a-44a2-9ee7-9ff8a6d2dfab)

<img width="584" height="207" alt="image"
src="https://github.com/user-attachments/assets/b70b0759-7961-4f8f-aaa7-762fc48e425b"
/>

- Adjusted "Bright white" slightly brighter, hoping it can be
distinguished better from "White".
- Defined "Selection Background" color.

This will be the final tune for Dimidum color scheme.

(cherry picked from commit 8011f3e28c)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeMuoQ
Service-Version: 1.24
2025-09-05 14:42:10 -05:00
Leonard Hecker
3a57ba54a0 Avoid reentrancy issues when dropping AppHost (#19296)
tl;dr: ~Apphost() may pump the message loop.
That's no bueno. See comments in the diff.

Additionally, this PR enables `_assertIsMainThread` in
release to trace down mysterious crashes in those builds.

(cherry picked from commit 8d41ace320)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeJeYA
Service-Version: 1.24
2025-09-05 14:42:09 -05:00
Leonard Hecker
fc465bdae6 Fix CoreWindow being destroyed after handoff (#19298)
As per: https://github.com/microsoft/terminal/discussions/19280#discussioncomment-14237148

## Validation Steps Performed
* Launch wtd via handoff (spawn cmd, etc.)
* Shift+Click the tab bar + button to create a new window
* Close the initial window
* UI doesn't lock up 

(cherry picked from commit 7849b00cbd)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeLN9s
Service-Version: 1.24
2025-09-05 14:42:08 -05:00
Leonard Hecker
04a5dffdcd Fix a race condition around Open/CloseClipboard (#19297)
tl;dr: Open/CloseClipboard are surprisingly not thread-safe.

## Validation Steps Performed
* Copy a large amount of text (>1MB)
* Run `edit.exe`
* Press and hold Ctrl+Shift+V
* Doesn't crash 

(cherry picked from commit 5899343237)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeLIjw
Service-Version: 1.24
2025-09-05 14:42:07 -05:00
Dustin L. Howett
894e57769b Include Profile.BellSound as a media resource (#19289)
I legitimately cannot figure out how I forgot this. Bell should support
all the same validation as other media resources! Technically this means
you can set `bellSound` to `desktopWallpaper`, but... we'll pretend that
makes sense.

I reworked the viewmodel to be a little more sensible. It no longer
requires somebody else to check that its files exist. The settings UI
now also displays `File not found` in the _preview_ for the bell if it
is a single file which failed validation!

(cherry picked from commit 4272151adc)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeF9qU
Service-Version: 1.24
2025-09-05 14:42:06 -05:00
Dustin L. Howett
eb206177a4 sb: add appId to the StoreBroker blobs (new AERO requirement) (#19290)
> _I am altering the deal. Pray I do not alter it further._
> -the AERO team, maybe

(cherry picked from commit bd14f69080)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgeF9fQ
Service-Version: 1.24
2025-09-05 14:42:05 -05:00
49 changed files with 466 additions and 308 deletions

View File

@@ -20,6 +20,7 @@
"DisableAutoPackageNameFormatting": false
},
"appSubmission": {
"appId": "9N8G5RFZ9XK3",
"productId": "00014050269303149694",
"targetPublishMode": "NotSet",
"targetPublishDate": null,

View File

@@ -20,6 +20,7 @@
"DisableAutoPackageNameFormatting": false
},
"appSubmission": {
"appId": "9N0DX20HK701",
"productId": "00013926773940052066",
"targetPublishMode": "NotSet",
"targetPublishDate": null,

View File

@@ -4,7 +4,7 @@
"collection": "microsoft",
"project": "OS",
"repo": "os.2020",
"name": "official/rs_we_adept_e4d2",
"name": "official/ge_current_directwinpd_deep",
"workitem": "38106206",
"CheckinFiles": [
{

View File

@@ -53,8 +53,12 @@ parameters:
displayName: "Publish Symbols to MSDL"
type: boolean
default: true
- name: createVpack
displayName: "Create a VPack for Windows"
type: boolean
default: false
- name: publishVpackToWindows
displayName: "Publish VPack to Windows"
displayName: "Publish above VPack to Windows"
type: boolean
default: false
@@ -89,6 +93,7 @@ extends:
clientId: $(SigningOriginalClientId)
terminalInternalPackageVersion: ${{ parameters.terminalInternalPackageVersion }}
publishSymbolsToPublic: ${{ parameters.publishSymbolsToPublic }}
createVpack: ${{ parameters.createVpack }}
publishVpackToWindows: ${{ parameters.publishVpackToWindows }}
symbolPublishingSubscription: $(SymbolPublishingServiceConnection)
symbolPublishingProject: $(SymbolPublishingProject)

View File

@@ -49,6 +49,9 @@ parameters:
- name: symbolExpiryTime
type: string
default: 36530 # This is the default from PublishSymbols@2
- name: createVpack
type: boolean
default: false
- name: publishVpackToWindows
type: boolean
default: false
@@ -192,8 +195,8 @@ extends:
ob_outputDirectory: $(JobOutputDirectory)
ob_artifactBaseName: $(JobOutputArtifactName)
### This job is also in charge of submitting the vpack to Windows if it's enabled
ob_createvpack_enabled: ${{ and(parameters.buildTerminal, parameters.publishVpackToWindows) }}
ob_updateOSManifest_enabled: ${{ and(parameters.buildTerminal, parameters.publishVpackToWindows) }}
ob_createvpack_enabled: ${{ and(parameters.buildTerminal, parameters.createVpack) }}
ob_updateOSManifest_enabled: ${{ and(parameters.buildTerminal, parameters.createVpack, parameters.publishVpackToWindows) }}
### If enabled above, these options are in play.
ob_createvpack_packagename: 'WindowsTerminal.app'
ob_createvpack_owneralias: 'conhost@microsoft.com'
@@ -229,7 +232,7 @@ extends:
New-Item "$(JobOutputDirectory)/vpack" -Type Directory
displayName: Make sure the vpack directory exists
- ${{ if parameters.publishVpackToWindows }}:
- ${{ if parameters.createVpack }}:
- pwsh: |-
Copy-Item -Verbose -Path "$(MsixBundlePath)" -Destination (Join-Path "$(JobOutputDirectory)/vpack" 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle')
displayName: Stage msixbundle for vpack

View File

@@ -2302,8 +2302,15 @@
"additionalProperties": false,
"properties": {
"id": {
"description": "The ID of the command this keybinding should execute.",
"type": "string"
"description": "The ID of the command this keybinding should execute (or null to disable a default).",
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"keys": {
"description": "Defines the key combinations used to call the command. It must be composed of...\n -any number of modifiers (ctrl/alt/shift)\n -a non-modifier key",

View File

@@ -44,10 +44,6 @@ namespace winrt::TerminalApp::implementation
{
_args = { value.begin(), value.end() };
_parseResult = _parsed.ParseArgs(_args);
if (_parseResult == 0)
{
_parsed.ValidateStartupCommands();
}
}
winrt::com_array<winrt::hstring> CommandlineArgs::Commandline()

View File

@@ -1062,7 +1062,6 @@ namespace winrt::TerminalApp::implementation
dispatcher,
til::throttled_func_options{
.delay = std::chrono::milliseconds{ 200 },
.leading = true,
.trailing = true,
},
[weakThis]() {

View File

@@ -157,12 +157,7 @@ namespace winrt::TerminalApp::implementation
// Set this tab's icon to the icon from the content
_UpdateTabIcon(*newTabImpl);
// This is necessary, because WinUI does not have support for middle clicks.
// Its Tapped event doesn't provide the information what button was used either.
tabViewItem.PointerPressed({ this, &TerminalPage::_OnTabPointerPressed });
tabViewItem.PointerReleased({ this, &TerminalPage::_OnTabPointerReleased });
tabViewItem.PointerExited({ this, &TerminalPage::_OnTabPointerExited });
tabViewItem.PointerEntered({ this, &TerminalPage::_OnTabPointerEntered });
// When the tab requests close, try to close it (prompt for approval, if required)
newTabImpl->CloseRequested([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) {
@@ -664,7 +659,7 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - returns a tab corresponding to a view item. This might return null,
// so make sure to check the result!
winrt::TerminalApp::Tab TerminalPage::_GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept
winrt::TerminalApp::Tab TerminalPage::_GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept
{
uint32_t tabIndexFromControl{};
const auto items{ _tabView.TabItems() };
@@ -876,60 +871,64 @@ namespace winrt::TerminalApp::implementation
_UpdateTabView();
}
// Method Description:
// - Additional responses to clicking on a TabView's item. Currently, just remove tab with middle click
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
void TerminalPage::_OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e)
{
if (eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
if (!_tabItemMiddleClickHookEnabled || !e.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
if (const auto tabViewItem{ sender.try_as<MUX::Controls::TabViewItem>() })
{
_tabPointerMiddleButtonPressed = tabViewItem.CapturePointer(eventArgs.Pointer());
_tabPointerMiddleButtonExited = false;
}
eventArgs.Handled(true);
return;
}
const auto tabViewItem = sender.try_as<MUX::Controls::TabViewItem>();
if (!tabViewItem || !tabViewItem.CapturePointer(e.Pointer()))
{
return;
}
_tabItemMiddleClickExited = false;
_tabItemMiddleClickPointerEntered = tabViewItem.PointerEntered(winrt::auto_revoke, [this](auto&&, auto&& e) {
_tabItemMiddleClickExited = false;
e.Handled(true);
});
_tabItemMiddleClickPointerExited = tabViewItem.PointerExited(winrt::auto_revoke, [this](auto&&, auto&& e) {
_tabItemMiddleClickExited = true;
e.Handled(true);
});
_tabItemMiddleClickPointerCaptureLost = tabViewItem.PointerCaptureLost(winrt::auto_revoke, [this](auto&& sender, auto&& e) {
// The WinUI TabView calls CapturePointer() internally and it's not reference counted,
// so when it calls ReleasePointerCapture() in its PointerReleased handler,
// we get a PointerCaptureLost before we receive the PointerReleased event.
// This makes typical handling of PointerReleased events on our side difficult.
// Well, whatever, now we just hook PointerCaptureLost because we know WinUI will trigger it.
_tabItemMiddleClickPointerEntered.revoke();
_tabItemMiddleClickPointerExited.revoke();
_tabItemMiddleClickPointerCaptureLost.revoke();
if (!_tabItemMiddleClickExited && !e.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_OnTabPointerReleasedCloseTab(std::move(sender));
}
e.Handled(true);
});
e.Handled(true);
}
// Method Description:
// - Tracking pointer state for tab remove
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerReleased(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
safe_void_coroutine TerminalPage::_OnTabPointerReleasedCloseTab(IInspectable sender)
{
if (_tabPointerMiddleButtonPressed && !eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_tabPointerMiddleButtonPressed = false;
if (auto tabViewItem{ sender.try_as<MUX::Controls::TabViewItem>() })
{
tabViewItem.ReleasePointerCapture(eventArgs.Pointer());
if (!_tabPointerMiddleButtonExited)
{
_OnTabPointerReleasedCloseTab(std::move(tabViewItem));
}
}
eventArgs.Handled(true);
}
}
safe_void_coroutine TerminalPage::_OnTabPointerReleasedCloseTab(winrt::Microsoft::UI::Xaml::Controls::TabViewItem sender)
{
const auto tab = _GetTabByTabViewItem(sender);
if (!tab)
{
co_return;
}
// WinUI asynchronously updates its tab view items, so it may happen that we're given a
// `TabViewItem` that still contains a `Tab` which has actually already been removed.
// First we must yield once, to flush out whatever TabView is currently doing.
const auto strong = get_strong();
co_await wil::resume_foreground(Dispatcher());
const auto tab = _GetTabByTabViewItem(sender);
if (!tab)
{
co_return;
}
// `tab.Shutdown()` in `_RemoveTab()` sets the content to null = This checks if the tab is closed.
if (tab.Content())
{
@@ -937,34 +936,6 @@ namespace winrt::TerminalApp::implementation
}
}
// Method Description:
// - Tracking pointer state for tab remove
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerEntered(const IInspectable& /*sender*/, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
{
if (eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_tabPointerMiddleButtonExited = false;
eventArgs.Handled(true);
}
}
// Method Description:
// - Tracking pointer state for tab remove
// Arguments:
// - sender: the control that originated this event (TabViewItem)
// - eventArgs: the event's constituent arguments
void TerminalPage::_OnTabPointerExited(const IInspectable& /*sender*/, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
{
if (eventArgs.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed())
{
_tabPointerMiddleButtonExited = true;
eventArgs.Handled(true);
}
}
void TerminalPage::_UpdatedSelectedTab(const winrt::TerminalApp::Tab& tab)
{
// Unfocus all the tabs.

View File

@@ -60,8 +60,42 @@ namespace winrt
namespace clipboard
{
wil::unique_close_clipboard_call open(HWND hwnd)
static SRWLOCK lock = SRWLOCK_INIT;
struct ClipboardHandle
{
explicit ClipboardHandle(bool open) :
_open{ open }
{
}
~ClipboardHandle()
{
if (_open)
{
ReleaseSRWLockExclusive(&lock);
CloseClipboard();
}
}
explicit operator bool() const noexcept
{
return _open;
}
private:
bool _open = false;
};
ClipboardHandle open(HWND hwnd)
{
// Turns out, OpenClipboard/CloseClipboard are not thread-safe whatsoever,
// and on CloseClipboard, the GetClipboardData handle may get freed.
// The problem is that WinUI also uses OpenClipboard (through WinRT which uses OLE),
// and so even with this mutex we can still crash randomly if you copy something via WinUI.
// Makes you wonder how many Windows apps are subtly broken, huh.
AcquireSRWLockExclusive(&lock);
bool success = false;
// OpenClipboard may fail to acquire the internal lock --> retry.
@@ -80,7 +114,12 @@ namespace clipboard
Sleep(sleep);
}
return wil::unique_close_clipboard_call{ success };
if (!success)
{
ReleaseSRWLockExclusive(&lock);
}
return ClipboardHandle{ success };
}
void write(wil::zwstring_view text, std::string_view html, std::string_view rtf)
@@ -344,6 +383,8 @@ namespace winrt::TerminalApp::implementation
{
const auto visibility = theme.Tab() ? theme.Tab().ShowCloseButton() : Settings::Model::TabCloseButtonVisibility::Always;
_tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never;
switch (visibility)
{
case Settings::Model::TabCloseButtonVisibility::Never:
@@ -3808,6 +3849,8 @@ namespace winrt::TerminalApp::implementation
theme.Tab().ShowCloseButton() :
Settings::Model::TabCloseButtonVisibility::Always;
_tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never;
for (const auto& tab : _tabs)
{
tab.CloseButtonVisibility(visibility);

View File

@@ -391,7 +391,7 @@ namespace winrt::TerminalApp::implementation
std::optional<uint32_t> _GetTabIndex(const TerminalApp::Tab& tab) const noexcept;
TerminalApp::Tab _GetFocusedTab() const noexcept;
winrt::com_ptr<Tab> _GetFocusedTabImpl() const noexcept;
TerminalApp::Tab _GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept;
TerminalApp::Tab _GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept;
void _HandleClosePaneRequested(std::shared_ptr<Pane> pane);
safe_void_coroutine _SetFocusedTab(const winrt::TerminalApp::Tab tab);
@@ -435,13 +435,18 @@ namespace winrt::TerminalApp::implementation
void _TabDragStarted(const IInspectable& sender, const IInspectable& eventArgs);
void _TabDragCompleted(const IInspectable& sender, const IInspectable& eventArgs);
bool _tabPointerMiddleButtonPressed{ false };
bool _tabPointerMiddleButtonExited{ false };
// BODGY: WinUI's TabView has a broken close event handler:
// If the close button is disabled, middle-clicking the tab raises no close
// event. Because that's dumb, we implement our own middle-click handling.
// `_tabItemMiddleClickHookEnabled` is true whenever the close button is hidden,
// and that enables all of the rest of this machinery (and this workaround).
bool _tabItemMiddleClickHookEnabled = false;
bool _tabItemMiddleClickExited = false;
PointerEntered_revoker _tabItemMiddleClickPointerEntered;
PointerExited_revoker _tabItemMiddleClickPointerExited;
PointerCaptureLost_revoker _tabItemMiddleClickPointerCaptureLost;
void _OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
void _OnTabPointerReleased(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
safe_void_coroutine _OnTabPointerReleasedCloseTab(winrt::Microsoft::UI::Xaml::Controls::TabViewItem sender);
void _OnTabPointerEntered(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
void _OnTabPointerExited(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
safe_void_coroutine _OnTabPointerReleasedCloseTab(IInspectable sender);
void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs);
void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs);

View File

@@ -279,7 +279,7 @@ namespace winrt::TerminalApp::implementation
auto sounds{ _profile.BellSound() };
if (sounds && sounds.Size() > 0)
{
winrt::hstring soundPath{ wil::ExpandEnvironmentStringsW<std::wstring>(sounds.GetAt(rand() % sounds.Size()).c_str()) };
winrt::hstring soundPath{ sounds.GetAt(rand() % sounds.Size()).Resolved() };
winrt::Windows::Foundation::Uri uri{ soundPath };
_playBellSound(uri);
}

View File

@@ -1048,6 +1048,11 @@ namespace winrt::TerminalApp::implementation
// (or called TerminalWindow::Initialize)
if (_appArgs->ExitCode() == 0)
{
// The existing logic (before this commit) strictly relied on
// ValidateStartupCommands() only to be called for new windows.
// It modifies the actions it stores.
parsedArgs.ValidateStartupCommands();
// If the size of the arguments list is 1,
// then it contains only the executable name and no other arguments.
_hasCommandLineArguments = _appArgs->CommandlineRef().size() > 1;

View File

@@ -485,7 +485,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - modifiers: The modifiers pressed during this event, in the form of a VirtualKeyModifiers
// - delta: the mouse wheel delta that triggered this event.
bool ControlInteractivity::MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
const int32_t delta,
const Core::Point delta,
const Core::Point pixelPosition,
const Control::MouseButtonState buttonState)
{
@@ -506,9 +506,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// PointerPoint to work with. So, we're just going to do a
// mousewheel event manually
return _sendMouseEventHelper(terminalPosition,
WM_MOUSEWHEEL,
delta.Y != 0 ? WM_MOUSEWHEEL : WM_MOUSEHWHEEL,
modifiers,
::base::saturated_cast<short>(delta),
::base::saturated_cast<short>(delta.Y != 0 ? delta.Y : delta.X),
buttonState);
}
@@ -517,15 +517,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (ctrlPressed && shiftPressed && _core->Settings().ScrollToChangeOpacity())
{
_mouseTransparencyHandler(delta);
_mouseTransparencyHandler(delta.Y);
}
else if (ctrlPressed && !shiftPressed && _core->Settings().ScrollToZoom())
{
_mouseZoomHandler(delta);
_mouseZoomHandler(delta.Y);
}
else
{
_mouseScrollHandler(delta, pixelPosition, WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown));
_mouseScrollHandler(delta.Y, pixelPosition, WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown));
}
return false;
}
@@ -659,7 +659,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return _core->IsVtMouseModeEnabled();
}
bool ControlInteractivity::_shouldSendAlternateScroll(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const int32_t delta)
bool ControlInteractivity::_shouldSendAlternateScroll(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const Core::Point delta)
{
// If the user is holding down Shift, suppress mouse events
// TODO GH#4875: disable/customize this functionality
@@ -667,7 +667,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return false;
}
return _core->ShouldSendAlternateScroll(WM_MOUSEWHEEL, delta);
if (delta.Y != 0)
{
return _core->ShouldSendAlternateScroll(WM_MOUSEWHEEL, delta.Y);
}
else
{
return _core->ShouldSendAlternateScroll(WM_MOUSEHWHEEL, delta.X);
}
}
// Method Description:

View File

@@ -74,7 +74,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void TouchReleased();
bool MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
const int32_t delta,
const Core::Point delta,
const Core::Point pixelPosition,
const Control::MouseButtonState state);
@@ -153,7 +153,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _hyperlinkHandler(const std::wstring_view uri);
bool _canSendVTMouseInput(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers);
bool _shouldSendAlternateScroll(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const int32_t delta);
bool _shouldSendAlternateScroll(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const Core::Point delta);
til::point _getTerminalPosition(const til::point pixelPosition, bool roundToNearestCell);

View File

@@ -60,7 +60,7 @@ namespace Microsoft.Terminal.Control
void TouchReleased();
Boolean MouseWheel(Microsoft.Terminal.Core.ControlKeyStates modifiers,
Int32 delta,
Microsoft.Terminal.Core.Point delta,
Microsoft.Terminal.Core.Point pixelPosition,
MouseButtonState state);

View File

@@ -11,6 +11,6 @@ namespace Microsoft.Terminal.Control
[uuid("65b8b8c5-988f-43ff-aba9-e89368da1598")]
interface IMouseWheelListener
{
Boolean OnMouseWheel(Windows.Foundation.Point coord, Int32 delta, Boolean leftButtonDown, Boolean midButtonDown, Boolean rightButtonDown);
Boolean OnMouseWheel(Windows.Foundation.Point coord, Microsoft.Terminal.Core.Point delta, Boolean leftButtonDown, Boolean midButtonDown, Boolean rightButtonDown);
}
}

View File

@@ -2164,15 +2164,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
RestorePointerCursor.raise(*this, nullptr);
const auto point = args.GetCurrentPoint(*this);
// GH#10329 - we don't need to handle horizontal scrolls. Only vertical ones.
// So filter out the horizontal ones.
if (point.Properties().IsHorizontalMouseWheel())
{
return;
}
auto delta = point.Properties().MouseWheelDelta();
auto result = _interactivity.MouseWheel(ControlKeyStates{ args.KeyModifiers() },
point.Properties().MouseWheelDelta(),
point.Properties().IsHorizontalMouseWheel() ?
Core::Point{ delta, 0 } :
Core::Point{ 0, delta },
_toTerminalOrigin(point.Position()),
TermControl::GetPressedMouseButtons(point));
if (result)
@@ -2192,7 +2188,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - delta: the mouse wheel delta that triggered this event.
// - state: the state for each of the mouse buttons individually (pressed/unpressed)
bool TermControl::OnMouseWheel(const Windows::Foundation::Point location,
const int32_t delta,
const Core::Point delta,
const bool leftButtonDown,
const bool midButtonDown,
const bool rightButtonDown)

View File

@@ -147,7 +147,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down);
bool OnMouseWheel(const Windows::Foundation::Point location, const int32_t delta, const bool leftButtonDown, const bool midButtonDown, const bool rightButtonDown);
bool OnMouseWheel(const Windows::Foundation::Point location, const winrt::Microsoft::Terminal::Core::Point delta, const bool leftButtonDown, const bool midButtonDown, const bool rightButtonDown);
~TermControl();

View File

@@ -720,7 +720,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
}
_MarkDuplicateBellSoundDirectories();
_CheckBellSoundsExistence();
_NotifyChanges(L"CurrentBellSounds");
}
@@ -732,19 +731,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
if (!_profile.HasBellSound())
{
std::vector<hstring> newSounds;
std::vector<IMediaResource> newSounds;
if (const auto inheritedSounds = _profile.BellSound())
{
// copy inherited bell sounds to the current layer
newSounds.reserve(inheritedSounds.Size());
for (const auto sound : inheritedSounds)
{
newSounds.push_back(sound);
}
newSounds = wil::to_vector(inheritedSounds);
}
// if we didn't inherit any bell sounds,
// we should still set the bell sound to an empty list (instead of null)
_profile.BellSound(winrt::single_threaded_vector<hstring>(std::move(newSounds)));
_profile.BellSound(winrt::single_threaded_vector(std::move(newSounds)));
}
}
@@ -768,56 +762,35 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
}
// Method Description:
// - Check if the bell sounds exist on disk. Mark any that don't exist
// so that they show the appropriate UI
safe_void_coroutine ProfileViewModel::_CheckBellSoundsExistence()
BellSoundViewModel::BellSoundViewModel(const Model::IMediaResource& resource) :
_resource{ resource }
{
co_await winrt::resume_background();
std::vector<Editor::BellSoundViewModel> markedSounds;
for (auto&& sound : _CurrentBellSounds)
if (_resource.Ok() && _resource.Path() != _resource.Resolved())
{
if (!std::filesystem::exists(std::wstring_view{ sound.Path() }))
{
markedSounds.push_back(sound);
}
// If the resource was resolved to something other than its path, show the path!
_ShowDirectory = true;
}
co_await winrt::resume_foreground(_dispatcher);
for (auto&& sound : markedSounds)
{
get_self<BellSoundViewModel>(sound)->FileExists(false);
}
}
BellSoundViewModel::BellSoundViewModel(hstring path) :
_Path{ path }
{
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
if (args.PropertyName() == L"FileExists")
{
_NotifyChanges(L"DisplayPath", L"SubText");
}
});
}
hstring BellSoundViewModel::DisplayPath() const
{
if (_FileExists)
if (_resource.Ok())
{
// filename
const std::filesystem::path filePath{ std::wstring_view{ _Path } };
// filename; start from the resolved path to show where it actually landed
auto resolvedPath{ _resource.Resolved() };
const std::filesystem::path filePath{ std::wstring_view{ resolvedPath } };
return hstring{ filePath.filename().wstring() };
}
return _Path;
return _resource.Path();
}
hstring BellSoundViewModel::SubText() const
{
if (_FileExists)
if (_resource.Ok())
{
// Directory
const std::filesystem::path filePath{ std::wstring_view{ _Path } };
auto resolvedPath{ _resource.Resolved() };
const std::filesystem::path filePath{ std::wstring_view{ resolvedPath } };
return hstring{ filePath.parent_path().wstring() };
}
return RS_(L"Profile_BellSoundNotFound");
@@ -825,17 +798,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
hstring ProfileViewModel::BellSoundPreview()
{
const auto& currentSound = BellSound();
if (!currentSound || currentSound.Size() == 0)
if (!_CurrentBellSounds || _CurrentBellSounds.Size() == 0)
{
return RS_(L"Profile_BellSoundPreviewDefault");
}
else if (currentSound.Size() == 1)
if (_CurrentBellSounds.Size() > 1)
{
std::filesystem::path filePath{ std::wstring_view{ currentSound.GetAt(0) } };
return hstring{ filePath.filename().wstring() };
return RS_(L"Profile_BellSoundPreviewMultiple");
}
return RS_(L"Profile_BellSoundPreviewMultiple");
const auto currentBellSound = _CurrentBellSounds.GetAt(0);
if (currentBellSound.FileExists())
{
return currentBellSound.DisplayPath();
}
return RS_(L"Profile_BellSoundNotFound");
}
void ProfileViewModel::RequestAddBellSound(hstring path)
@@ -844,9 +822,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
// copy it over to the current layer and apply modifications
_PrepareModelForBellSoundModification();
// No need to check if the file exists. We came from the FilePicker. That's good enough.
_CurrentBellSounds.Append(winrt::make<BellSoundViewModel>(path));
_profile.BellSound().Append(path);
auto bellResource{ MediaResourceHelper::FromString(path) };
bellResource.Resolve(path); // No need to check if the file exists. We came from the FilePicker. That's good enough.
_CurrentBellSounds.Append(winrt::make<BellSoundViewModel>(bellResource));
_profile.BellSound().Append(bellResource);
_NotifyChanges(L"CurrentBellSounds");
}

View File

@@ -30,13 +30,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
struct BellSoundViewModel : BellSoundViewModelT<BellSoundViewModel>, ViewModelHelper<BellSoundViewModel>
{
public:
BellSoundViewModel(hstring path);
BellSoundViewModel(const Model::IMediaResource& resource);
hstring Path() const { return _resource.Path(); }
bool FileExists() const { return _resource.Ok(); }
hstring DisplayPath() const;
hstring SubText() const;
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, FileExists, true);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, Path);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, ShowDirectory);
private:
Model::IMediaResource _resource;
};
struct ProfileViewModel : ProfileViewModelT<ProfileViewModel>, ViewModelHelper<ProfileViewModel>
@@ -190,7 +193,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void _InitializeCurrentBellSounds();
void _PrepareModelForBellSoundModification();
void _MarkDuplicateBellSoundDirectories();
safe_void_coroutine _CheckBellSoundsExistence();
static Windows::Foundation::Collections::IObservableVector<Editor::Font> _MonospaceFontList;
static Windows::Foundation::Collections::IObservableVector<Editor::Font> _FontList;
static Windows::Foundation::Collections::IVector<Windows::Foundation::IInspectable> _BuiltInIcons;

View File

@@ -27,7 +27,7 @@ namespace Microsoft.Terminal.Settings.Editor
runtimeclass BellSoundViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
String Path;
String Path { get; };
String DisplayPath { get; };
String SubText { get; };
Boolean ShowDirectory { get; };
@@ -149,7 +149,7 @@ namespace Microsoft.Terminal.Settings.Editor
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, SnapOnInput);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AltGrAliasing);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Settings.Model.BellStyle, BellStyle);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Windows.Foundation.Collections.IVector<String>, BellSound);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Windows.Foundation.Collections.IVector<Microsoft.Terminal.Settings.Model.IMediaResource>, BellSound);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, Elevate);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, RightClickContextMenu);

View File

@@ -82,18 +82,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
command->LogSettingChanges(_changeLog);
AddAction(*command, keys);
if (jsonBlock.isMember(JsonKey(KeysKey)))
{
// there are keys in this command block meaning this is the legacy style -
// inform the loader that fixups are needed
_fixupsAppliedDuringLoad = true;
}
if (jsonBlock.isMember(JsonKey(ActionKey)) && !jsonBlock.isMember(JsonKey(IterateOnKey)) && origin == OriginTag::User && !jsonBlock.isMember(JsonKey(IDKey)))
if (jsonBlock.isMember(JsonKey(ActionKey)) && !jsonBlock.isMember(JsonKey(IterateOnKey)) && origin == OriginTag::User &&
(!jsonBlock.isMember(JsonKey(IDKey)) || jsonBlock.isMember(JsonKey(KeysKey))))
{
// for non-nested non-iterable commands,
// if there's no ID in the command block we will generate one for the user -
// inform the loader that the ID needs to be written into the json
// if there's no ID in the command block we will generate one for the user,
// or if there are keys in this command block that means this is the legacy style -
// in either case, inform the loader that fixups are needed
_fixupsAppliedDuringLoad = true;
}
}

View File

@@ -559,6 +559,7 @@ bool SettingsLoader::AddDynamicProfileFolders()
folderEntry->Inlining(FolderEntryInlining::Auto);
folderEntry->RawEntries(winrt::single_threaded_vector<Model::NewTabMenuEntry>({ *matchProfilesEntry }));
// NewTabMenu is guaranteed to exist by FixupUserSettings, which runs before this fixup.
userSettings.globals->NewTabMenu().Append(folderEntry.as<Model::NewTabMenuEntry>());
state->SSHFolderGenerated(true);
return true;
@@ -689,6 +690,16 @@ bool SettingsLoader::FixupUserSettings()
fixedUp = true;
}
// Terminal 1.24
// Ensure that the user always has a newTabMenu. We used to do this last, after
// resolving all of the new tab menu entries, but there was no conceivable reason
// that it should happen so late.
if (!userSettings.globals->HasNewTabMenu())
{
userSettings.globals->NewTabMenu(winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} }));
// This one does not need to be written back to the settings file immediately, it can wait until we write one for another reason.
}
return fixedUp;
}
@@ -1263,8 +1274,8 @@ try
// DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles.
// Similarly FixupUserSettings returns true, when it encountered settings that were patched up.
mustWriteToDisk |= loader.DisableDeletedProfiles();
mustWriteToDisk |= loader.AddDynamicProfileFolders();
mustWriteToDisk |= loader.FixupUserSettings();
mustWriteToDisk |= loader.AddDynamicProfileFolders();
// If this throws, the app will catch it and use the default settings.
const auto settings = winrt::make_self<CascadiaSettings>(std::move(loader));
@@ -1745,16 +1756,6 @@ void CascadiaSettings::_resolveNewTabMenuProfiles() const
{
remainingProfilesEntry.Profiles(remainingProfiles);
}
// If the configuration does not have a "newTabMenu" field, GlobalAppSettings
// will return a default value containing just a "remainingProfiles" entry. However,
// this value is regenerated on every "get" operation, so the effect of setting
// the remaining profiles above will be undone. So only in the case that no custom
// value is present in GlobalAppSettings, we will store the modified default value.
if (!_globals->HasNewTabMenu())
{
_globals->NewTabMenu(entries);
}
}
// Method Description:

View File

@@ -95,7 +95,7 @@ Author(s):
X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \
X(bool, RightClickContextMenu, "rightClickContextMenu", false) \
X(Windows::Foundation::Collections::IVector<winrt::hstring>, BellSound, "bellSound", nullptr) \
X(Windows::Foundation::Collections::IVector<IMediaResource>, BellSound, "bellSound", nullptr) \
X(bool, Elevate, "elevate", false) \
X(bool, AutoMarkPrompts, "autoMarkPrompts", true) \
X(bool, ShowMarks, "showMarksOnScrollbar", false) \

View File

@@ -121,16 +121,10 @@ winrt::com_ptr<Profile> Profile::CopySettings() const
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_COPY)
#undef PROFILE_SETTINGS_COPY
// BellSound is an IVector<hstring>, so we need to manually copy it over
if (_BellSound)
{
std::vector<hstring> sounds;
sounds.reserve(_BellSound->Size());
for (const auto& sound : *_BellSound)
{
sounds.emplace_back(sound);
}
profile->_BellSound = single_threaded_vector(std::move(sounds));
// BellSound is an IVector<>, so we need to make a new vector pointing at the same objects
profile->_BellSound = winrt::single_threaded_vector(wil::to_vector(*_BellSound));
}
if (_UnfocusedAppearance)
@@ -560,6 +554,16 @@ void Profile::ResolveMediaResources(const Model::MediaResourceResolver& resolver
{
container->ResolveMediaResources(resolver);
}
if (const auto [bellSoundSource, bellSounds]{ _getBellSoundOverrideSourceAndValueImpl() };
bellSoundSource && bellSounds && *bellSounds && bellSounds->Size() > 0)
{
for (const auto& bellSound : *bellSounds)
{
ResolveMediaResource(bellSoundSource->_Origin, bellSoundSource->SourceBasePath, bellSound, resolver);
}
// It's important that we keep the invalid bell sounds in the list, because we may want to write it back out to disk
}
}
void Profile::LogSettingChanges(std::set<std::string>& changes, const std::string_view& context) const

View File

@@ -76,7 +76,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IMap<String COMMA String>, EnvironmentVariables);
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector<String>, BellSound);
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector<IMediaResource>, BellSound);
INHERITABLE_PROFILE_SETTING(Boolean, Elevate);
INHERITABLE_PROFILE_SETTING(Boolean, AutoMarkPrompts);

View File

@@ -35,19 +35,14 @@ using namespace winrt::Microsoft::Terminal::Settings::Model;
/*static*/ const std::wregex SshHostGenerator::_configKeyValueRegex{ LR"(^\s*(\w+)\s+([^\s]+.*[^\s])\s*$)" };
/*static*/ std::wstring_view SshHostGenerator::_getProfileName(const std::wstring_view& hostName) noexcept
winrt::hstring _getProfileName(const std::wstring_view& hostName) noexcept
{
return std::wstring_view{ L"" + PROFILE_TITLE_PREFIX + hostName };
return winrt::hstring{ fmt::format(FMT_COMPILE(L"{0}{1}"), PROFILE_TITLE_PREFIX, hostName) };
}
/*static*/ std::wstring_view SshHostGenerator::_getProfileIconPath() noexcept
winrt::hstring _getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept
{
return PROFILE_ICON_PATH;
}
/*static*/ std::wstring_view SshHostGenerator::_getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept
{
return std::wstring_view{ L"\"" + sshExePath + L"\" " + hostName };
return winrt::hstring{ fmt::format(FMT_COMPILE(LR"("{0}" {1})"), sshExePath, hostName) };
}
/*static*/ bool SshHostGenerator::_tryFindSshExePath(std::wstring& sshExePath) noexcept
@@ -164,8 +159,8 @@ void SshHostGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementatio
{
const auto profile{ CreateDynamicProfile(_getProfileName(hostName)) };
profile->Commandline(winrt::hstring{ _getProfileCommandLine(sshExePath, hostName) });
profile->Icon(winrt::hstring{ _getProfileIconPath() });
profile->Commandline(_getProfileCommandLine(sshExePath, hostName));
profile->Icon(winrt::hstring{ PROFILE_ICON_PATH });
profiles.emplace_back(profile);
}

View File

@@ -31,10 +31,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model
private:
static const std::wregex _configKeyValueRegex;
static std::wstring_view _getProfileName(const std::wstring_view& hostName) noexcept;
static std::wstring_view _getProfileIconPath() noexcept;
static std::wstring_view _getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept;
static bool _tryFindSshExePath(std::wstring& sshExePath) noexcept;
static bool _tryParseConfigKeyValue(const std::wstring_view& line, std::wstring& key, std::wstring& value) noexcept;
static void _getHostNamesFromConfigFile(const std::wstring_view& configPath, std::vector<std::wstring>& hostNames) noexcept;

View File

@@ -26,12 +26,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model
void GenerateProfiles(const VsSetupConfiguration::VsSetupInstance& instance, bool hidden, std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
private:
bool IsInstanceValid(const VsSetupConfiguration::VsSetupInstance&) const
bool IsInstanceValid(const VsSetupConfiguration::VsSetupInstance& instance) const
{
// We only support version of VS from 15.0.
// Per heaths: The [ISetupConfiguration] COM server only supports Visual Studio 15.0 and newer anyway.
// Eliding the version range will improve the discovery performance by not having to parse or compare the versions.
return true;
std::error_code ec;
return std::filesystem::exists(GetDevCmdScriptPath(instance), ec) && !ec;
}
std::wstring GetProfileGuidSeed(const VsSetupConfiguration::VsSetupInstance& instance) const

View File

@@ -28,7 +28,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
private:
bool IsInstanceValid(const VsSetupConfiguration::VsSetupInstance& instance) const
{
return instance.VersionInRange(L"[16.2,)");
std::error_code ec;
return instance.VersionInRange(L"[16.2,)") && std::filesystem::exists(GetDevShellModulePath(instance), ec) && !ec;
}
std::wstring GetProfileGuidSeed(const VsSetupConfiguration::VsSetupInstance& instance) const

View File

@@ -85,7 +85,7 @@
"background": "#141414",
"foreground": "#BAB7B6",
"cursorColor": "#37E57B",
"selectionBackground": "#FFFFFF",
"selectionBackground": "#8DB8E5",
"black": "#000000",
"red": "#CF494C",
"green": "#60B442",
@@ -101,7 +101,7 @@
"brightBlue": "#688DFD",
"brightPurple": "#ED6FE9",
"brightCyan": "#32E0FB",
"brightWhite": "#D3D8D9"
"brightWhite": "#DEE3E4"
},
{
"name": "Ottosson",

View File

@@ -170,7 +170,7 @@ namespace ControlUnitTests
// The mouse location and buttons don't matter here.
interactivity->MouseWheel(modifiers,
30,
Core::Point{ 0, 30 },
Core::Point{ 0, 0 },
buttonState);
}
@@ -188,7 +188,7 @@ namespace ControlUnitTests
// The mouse location and buttons don't matter here.
interactivity->MouseWheel(modifiers,
-30,
Core::Point{ 0, -30 },
Core::Point{ 0, 0 },
buttonState);
}
@@ -245,7 +245,7 @@ namespace ControlUnitTests
expectedTop = 20;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
Core::Point{ 0, WHEEL_DELTA },
Core::Point{ 0, 0 },
buttonState);
@@ -254,18 +254,18 @@ namespace ControlUnitTests
{
expectedTop--;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
Core::Point{ 0, WHEEL_DELTA },
Core::Point{ 0, 0 },
buttonState);
}
Log::Comment(L"Scrolling up more should do nothing");
expectedTop = 0;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
Core::Point{ 0, WHEEL_DELTA },
Core::Point{ 0, 0 },
buttonState);
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
Core::Point{ 0, WHEEL_DELTA },
Core::Point{ 0, 0 },
buttonState);
@@ -275,7 +275,7 @@ namespace ControlUnitTests
Log::Comment(NoThrowString().Format(L"---scroll down #%d---", i));
expectedTop++;
interactivity->MouseWheel(modifiers,
-WHEEL_DELTA,
Core::Point{ 0, -WHEEL_DELTA },
Core::Point{ 0, 0 },
buttonState);
Log::Comment(NoThrowString().Format(L"internal scrollbar pos:%f", interactivity->_internalScrollbarPosition));
@@ -283,11 +283,11 @@ namespace ControlUnitTests
Log::Comment(L"Scrolling down more should do nothing");
expectedTop = 21;
interactivity->MouseWheel(modifiers,
-WHEEL_DELTA,
Core::Point{ 0, -WHEEL_DELTA },
Core::Point{ 0, 0 },
buttonState);
interactivity->MouseWheel(modifiers,
-WHEEL_DELTA,
Core::Point{ 0, -WHEEL_DELTA },
Core::Point{ 0, 0 },
buttonState);
}
@@ -444,7 +444,7 @@ namespace ControlUnitTests
Log::Comment(L"Scroll up a line, with the left mouse button selected");
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
Core::Point{ 0, WHEEL_DELTA },
cursorPosition1.to_core_point(),
leftMouseDown);
@@ -492,55 +492,55 @@ namespace ControlUnitTests
const Core::Point mousePos{ 0, 0 };
Control::MouseButtonState state{};
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 1/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
Log::Comment(L"Scroll up 4 more times. Once we're at 3/5 scrolls, "
L"we'll round the internal scrollbar position to scrolling to the next row.");
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 2/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 3/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 3/5
VERIFY_ARE_EQUAL(20, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 4/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 4/5
VERIFY_ARE_EQUAL(20, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 5/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 5/5
VERIFY_ARE_EQUAL(20, core->ScrollOffset());
Log::Comment(L"Jump to line 5, so we can scroll down from there.");
interactivity->UpdateScrollbar(5);
VERIFY_ARE_EQUAL(5, core->ScrollOffset());
Log::Comment(L"Scroll down 5 times, at which point we should accumulate a whole row of delta.");
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 1/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, -delta }, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(5, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 2/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, -delta }, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(5, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 3/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, -delta }, mousePos, state); // 3/5
VERIFY_ARE_EQUAL(6, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 4/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, -delta }, mousePos, state); // 4/5
VERIFY_ARE_EQUAL(6, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 5/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, -delta }, mousePos, state); // 5/5
VERIFY_ARE_EQUAL(6, core->ScrollOffset());
Log::Comment(L"Jump to the bottom.");
interactivity->UpdateScrollbar(21);
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
Log::Comment(L"Scroll a bit, then emit a line of text. We should reset our internal scroll position.");
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 1/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 2/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n"));
VERIFY_ARE_EQUAL(22, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 1/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(22, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 2/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(22, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 3/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 3/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 4/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 4/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 5/5
interactivity->MouseWheel(modifiers, Core::Point{ 0, delta }, mousePos, state); // 5/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
}
@@ -709,7 +709,7 @@ namespace ControlUnitTests
{
expectedTop--;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
Core::Point{ 0, WHEEL_DELTA },
Core::Point{ 0, 0 },
noMouseDown);
}

View File

@@ -91,6 +91,7 @@ namespace SettingsModelUnitTests
TEST_METHOD(ProfileInheritsInvalidIconAndHasNoCommandline);
TEST_METHOD(ProfileSpecifiesNullIcon);
TEST_METHOD(ProfileSpecifiesNullIconAndHasNoCommandline);
TEST_METHOD(ProfileOverwritesBellSound);
// FRAGMENT BEHAVIORS
TEST_METHOD(FragmentUpdatesBaseProfile);
@@ -125,7 +126,11 @@ namespace SettingsModelUnitTests
"backgroundImage": "imagePathFromBase",
"guid": "{862d46aa-cc9c-4e6c-b872-9cadaafcdbbe}",
"icon": "iconFromBase",
"name": "Base"
"name": "Base",
"bellSound": [
"C:\\Windows\\Media\\Alarm01.wav",
"C:\\Windows\\Media\\Alarm02.wav"
]
},
{
"backgroundImage": "focusedImagePathFromBase",
@@ -167,7 +172,7 @@ namespace SettingsModelUnitTests
]
})" };
static constexpr int numberOfMediaResourcesInDefaultSettings{ 9 };
static constexpr int numberOfMediaResourcesInDefaultSettings{ 11 };
struct Fragment
{
@@ -1021,6 +1026,37 @@ namespace SettingsModelUnitTests
VERIFY_IS_TRUE(icon.Ok()); // Profile with commandline always has an icon
VERIFY_ARE_EQUAL(cmdCommandline, icon.Resolved());
}
// A profile replaces the bell sounds (2) in the base settings; all bell sounds retained
void MediaResourceTests::ProfileOverwritesBellSound()
{
WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{};
winrt::com_ptr<implementation::CascadiaSettings> settings;
{
auto [t, e] = requireCalled([&](auto&&, auto&&, auto&& resource) {
// All resources are invalid.
resource.Reject();
});
g_mediaResolverHook = t;
settings = createSettings(R"({
"profiles": {
"list": [
{
"guid": "{862d46aa-cc9c-4e6c-b872-9cadaafcdbbe}",
"bellSound": [
"does not matter; resolved rejected"
],
},
]
}
})");
}
auto profile{ settings->GetProfileByName(L"Base") };
auto bellSounds{ profile.BellSound() };
VERIFY_ARE_EQUAL(1u, bellSounds.Size());
VERIFY_IS_FALSE(bellSounds.GetAt(0).Ok());
}
#pragma endregion
#pragma region Real Resolver Tests

View File

@@ -745,7 +745,7 @@ void AppHost::_RaiseVisualBell(const winrt::Windows::Foundation::IInspectable&,
// - delta: the wheel delta that triggered this event.
// Return Value:
// - <none>
void AppHost::_WindowMouseWheeled(const winrt::Windows::Foundation::Point coord, const int32_t delta)
void AppHost::_WindowMouseWheeled(const winrt::Windows::Foundation::Point coord, const winrt::Microsoft::Terminal::Core::Point delta)
{
if (_windowLogic)
{

View File

@@ -73,7 +73,7 @@ private:
void _RaiseVisualBell(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& arg);
void _WindowMouseWheeled(const winrt::Windows::Foundation::Point coord, const int32_t delta);
void _WindowMouseWheeled(const winrt::Windows::Foundation::Point coord, const winrt::Microsoft::Terminal::Core::Point delta);
void _WindowActivated(bool activated);
void _WindowMoved();

View File

@@ -595,6 +595,7 @@ void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam
WindowCloseButtonClicked.raise();
return 0;
}
case WM_MOUSEHWHEEL:
case WM_MOUSEWHEEL:
try
{
@@ -623,7 +624,11 @@ void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam
const auto scale = GetCurrentDpiScale();
const winrt::Windows::Foundation::Point real{ relative.x / scale, relative.y / scale };
const auto wheelDelta = static_cast<short>(HIWORD(wparam));
winrt::Microsoft::Terminal::Core::Point wheelDelta{ 0, static_cast<int32_t>(HIWORD(wparam)) };
if (message == WM_MOUSEHWHEEL)
{
std::swap(wheelDelta.X, wheelDelta.Y);
}
// Raise an event, so any listeners can handle the mouse wheel event manually.
MouseScrolled.raise(real, wheelDelta);

View File

@@ -74,7 +74,7 @@ public:
til::event<winrt::delegate<>> DragRegionClicked;
til::event<winrt::delegate<>> WindowCloseButtonClicked;
til::event<winrt::delegate<void(winrt::Windows::Foundation::Point, int32_t)>> MouseScrolled;
til::event<winrt::delegate<void(winrt::Windows::Foundation::Point, winrt::Microsoft::Terminal::Core::Point)>> MouseScrolled;
til::event<winrt::delegate<void(bool)>> WindowActivated;
til::event<winrt::delegate<void()>> NotifyNotificationIconPressed;
til::event<winrt::delegate<void()>> NotifyWindowHidden;

View File

@@ -253,6 +253,29 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args
_windowCount += 1;
_windows.emplace_back(std::move(host));
if (_windowCount == 1)
{
// The first CoreWindow is created implicitly by XAML and parented to the
// first XAML island. We parent it to our initial window for 2 reasons:
// * On Windows 10 the CoreWindow will show up as a visible window on the taskbar
// due to a WinUI bug, and this will hide it, because our initial window is hidden.
// * When we DestroyWindow() the island it will destroy the CoreWindow,
// and it's not possible to recreate it. That's also a WinUI bug.
//
// Note that this must be done after the first window (= first island) is created.
if (const auto coreWindow = winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread())
{
if (const auto interop = coreWindow.try_as<ICoreWindowInterop>())
{
HWND coreHandle = nullptr;
if (SUCCEEDED(interop->get_WindowHandle(&coreHandle)) && coreHandle)
{
SetParent(coreHandle, _window.get());
}
}
}
}
}
AppHost* WindowEmperor::_mostRecentWindow() const noexcept
@@ -292,7 +315,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
}
if (!IsPackaged())
{
const auto path = wil::GetModuleFileNameW<std::wstring>(nullptr);
const auto path = wil::QueryFullProcessImageNameW<std::wstring>();
const auto hash = til::hash(path);
#ifdef _WIN64
fmt::format_to(std::back_inserter(windowClassName), FMT_COMPILE(L" {:016x}"), hash);
@@ -395,24 +418,6 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
LOG_IF_WIN32_BOOL_FALSE(SetCurrentDirectoryW(system32.c_str()));
}
// The first CoreWindow is created implicitly by XAML and parented to the
// first XAML island. We parent it to our initial window for 2 reasons:
// * On Windows 10 the CoreWindow will show up as a visible window on the taskbar
// due to a WinUI bug, and this will hide it, because our initial window is hidden.
// * When we DestroyWindow() the island it will destroy the CoreWindow,
// and it's not possible to recreate it. That's also a WinUI bug.
if (const auto coreWindow = winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread())
{
if (const auto interop = coreWindow.try_as<ICoreWindowInterop>())
{
HWND coreHandle = nullptr;
if (SUCCEEDED(interop->get_WindowHandle(&coreHandle)) && coreHandle)
{
SetParent(coreHandle, _window.get());
}
}
}
{
TerminalConnection::ConptyConnection::NewConnection([this](TerminalConnection::ConptyConnection conn) {
TerminalApp::CommandlineArgs args;
@@ -544,6 +549,8 @@ void WindowEmperor::_dispatchSpecialKey(const MSG& msg) const
void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs args)
{
_assertIsMainThread();
const auto exitCode = args.ExitCode();
if (const auto msg = args.ExitMessage(); !msg.empty())
@@ -681,6 +688,8 @@ safe_void_coroutine WindowEmperor::_dispatchCommandlineCurrentDesktop(winrt::Ter
bool WindowEmperor::_summonWindow(const SummonWindowSelectionArgs& args) const
{
_assertIsMainThread();
AppHost* window = nullptr;
if (args.WindowID)
@@ -721,6 +730,8 @@ bool WindowEmperor::_summonWindow(const SummonWindowSelectionArgs& args) const
void WindowEmperor::_summonAllWindows() const
{
_assertIsMainThread();
TerminalApp::SummonWindowBehavior args;
args.ToggleVisibility(false);
@@ -858,6 +869,9 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
// Did the window counter get out of sync? It shouldn't.
assert(_windowCount == gsl::narrow_cast<int32_t>(_windows.size()));
// !!! NOTE !!!
// At least theoretically the lParam pointer may be invalid.
// We should only access it if we find it in our _windows array.
const auto host = reinterpret_cast<AppHost*>(lParam);
auto it = _windows.begin();
const auto end = _windows.end();
@@ -866,8 +880,16 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
{
if (host == it->get())
{
host->Close();
// NOTE: AppHost::Close is highly non-trivial.
//
// It _may_ call into XAML, which _may_ pump the message loop, which would then recursively
// re-enter this function, which _may_ then handle another WM_CLOSE_TERMINAL_WINDOW,
// which would change the _windows array, and invalidate our iterator and crash.
//
// We can prevent this by deferring Close() until after the erase() call.
const auto strong = *it;
_windows.erase(it);
strong->Close();
break;
}
}

View File

@@ -84,14 +84,14 @@ private:
int32_t _windowCount = 0;
int32_t _messageBoxCount = 0;
#ifdef NDEBUG
#if 0 // #ifdef NDEBUG
static constexpr void _assertIsMainThread() noexcept
{
}
#else
void _assertIsMainThread() const noexcept
{
assert(_mainThreadId == GetCurrentThreadId());
WI_ASSERT_MSG(_mainThreadId == GetCurrentThreadId(), "This part of WindowEmperor must be accessed from the UI thread");
}
DWORD _mainThreadId = GetCurrentThreadId();
#endif

View File

@@ -1718,6 +1718,19 @@ void SCREEN_INFORMATION::SnapOnInput(const WORD vkey)
}
}
SCREEN_INFORMATION::SnapOnScopeExit SCREEN_INFORMATION::SnapOnOutput() noexcept
{
const auto call =
// We don't need to snap-on-output the alt buffer since it doesn't scroll anyway.
// More importantly though, in the current architecture the alt buffer gets deallocated
// the second we receive a \033[?1049l, which makes our this pointer hazardous to use.
_psiMainBuffer == nullptr &&
// We only want to snap if the user didn't intentionally scroll away.
// The snapping logic could be improved, but it's alright for now.
_viewport.IsInBounds(_textBuffer->GetCursor().GetPosition());
return SnapOnScopeExit{ call ? this : nullptr };
}
void SCREEN_INFORMATION::_makeCursorVisible()
{
if (_textBuffer->GetCursor().IsVisible())

View File

@@ -50,6 +50,25 @@ class ConversionAreaInfo; // forward decl window. circular reference
class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console::IIoProvider
{
public:
// This little helper works like wil::scope_exit but is slimmer
// (= easier to optimize) and has a concrete type (= can declare).
struct SnapOnScopeExit
{
~SnapOnScopeExit()
{
if (self)
{
try
{
self->_makeCursorVisible();
}
CATCH_LOG();
}
}
SCREEN_INFORMATION* self;
};
[[nodiscard]] static NTSTATUS CreateInstance(_In_ til::size coordWindowSize,
const FontInfo fontInfo,
_In_ til::size coordScreenBufferSize,
@@ -73,16 +92,7 @@ public:
void MakeCurrentCursorVisible();
void MakeCursorVisible(til::point position);
void SnapOnInput(WORD vkey);
auto SnapOnOutput() noexcept
{
const auto inBounds = _viewport.IsInBounds(_textBuffer->GetCursor().GetPosition());
return wil::scope_exit([this, inBounds]() {
if (inBounds)
{
_makeCursorVisible();
}
});
}
SnapOnScopeExit SnapOnOutput() noexcept;
void ClipToScreenBuffer(_Inout_ til::inclusive_rect* const psrClip) const;

View File

@@ -567,7 +567,7 @@ BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo,
if (!fShiftPressed && !pSelection->IsInSelectingState())
{
short sDelta = 0;
if (Message == WM_MOUSEWHEEL)
if (Message == WM_MOUSEWHEEL || Message == WM_MOUSEHWHEEL)
{
sDelta = GET_WHEEL_DELTA_WPARAM(wParam);
}

View File

@@ -127,10 +127,13 @@ public:
wch = L'!';
break;
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
Log::Comment(NoThrowString().Format(L"MOUSEWHEEL"));
wch = L'`' + (sScrollDelta > 0 ? 0 : 1);
break;
case WM_MOUSEHWHEEL:
Log::Comment(NoThrowString().Format(L"MOUSEHWHEEL"));
wch = L'b' + (sScrollDelta > 0 ? 1 : 0);
break;
case WM_MOUSEMOVE:
default:
Log::Comment(NoThrowString().Format(L"DEFAULT"));
@@ -168,9 +171,11 @@ public:
result = 3 + 0x20; // we add 0x20 to hover events, which are all encoded as WM_MOUSEMOVE events
break;
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
result = (sScrollDelta > 0 ? 64 : 65);
break;
case WM_MOUSEHWHEEL:
result = (sScrollDelta > 0 ? 67 : 66);
break;
default:
result = 0;
break;
@@ -582,6 +587,12 @@ public:
Log::Comment(L"Test mouse wheel scrolling down");
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1B[B"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {}));
Log::Comment(L"Test mouse wheel scrolling right");
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1B[C"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEHWHEEL, noModifierKeys, WHEEL_DELTA, {}));
Log::Comment(L"Test mouse wheel scrolling left");
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1B[D"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEHWHEEL, noModifierKeys, -WHEEL_DELTA, {}));
Log::Comment(L"Enable cursor keys mode");
mouseInput.SetInputMode(TerminalInput::Mode::CursorKey, true);
@@ -591,6 +602,12 @@ public:
Log::Comment(L"Test mouse wheel scrolling down");
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1BOB"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {}));
Log::Comment(L"Test mouse wheel scrolling right");
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1BOC"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEHWHEEL, noModifierKeys, WHEEL_DELTA, {}));
Log::Comment(L"Test mouse wheel scrolling left");
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1BOD"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEHWHEEL, noModifierKeys, -WHEEL_DELTA, {}));
Log::Comment(L"Confirm no effect when scroll mode is disabled");
mouseInput.UseAlternateScreenBuffer();
mouseInput.SetInputMode(TerminalInput::Mode::AlternateScroll, false);

View File

@@ -163,9 +163,11 @@ static constexpr wchar_t _windowsButtonToXEncoding(const unsigned int button,
xvalue = 1;
break;
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
xvalue = delta > 0 ? 0x40 : 0x41;
break;
case WM_MOUSEHWHEEL:
xvalue = delta > 0 ? 0x43 : 0x42;
break;
default:
xvalue = 0;
break;
@@ -221,9 +223,11 @@ static constexpr int _windowsButtonToSGREncoding(const unsigned int button,
xvalue = 3;
break;
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
xvalue = delta > 0 ? 0x40 : 0x41;
break;
case WM_MOUSEHWHEEL:
xvalue = delta > 0 ? 0x43 : 0x42;
break;
default:
xvalue = 0;
break;
@@ -378,7 +382,7 @@ TerminalInput::OutputType TerminalInput::HandleMouse(const til::point position,
if (ShouldSendAlternateScroll(button, delta))
{
return _makeAlternateScrollOutput(delta);
return _makeAlternateScrollOutput(button, delta);
}
return {};
@@ -493,14 +497,32 @@ bool TerminalInput::ShouldSendAlternateScroll(const unsigned int button, const s
// - Sends a sequence to the input corresponding to cursor up / down depending on the sScrollDelta.
// Parameters:
// - delta: The scroll wheel delta of the input event
TerminalInput::OutputType TerminalInput::_makeAlternateScrollOutput(const short delta) const
TerminalInput::OutputType TerminalInput::_makeAlternateScrollOutput(const unsigned int button, const short delta) const
{
if (delta > 0)
if (button == WM_MOUSEWHEEL)
{
return MakeOutput(_keyMap.at(VK_UP));
if (delta > 0)
{
return MakeOutput(_keyMap.at(VK_UP));
}
else
{
return MakeOutput(_keyMap.at(VK_DOWN));
}
}
else if (button == WM_MOUSEHWHEEL)
{
if (delta > 0)
{
return MakeOutput(_keyMap.at(VK_RIGHT));
}
else
{
return MakeOutput(_keyMap.at(VK_LEFT));
}
}
else
{
return MakeOutput(_keyMap.at(VK_DOWN));
return {};
}
}

View File

@@ -111,7 +111,7 @@ namespace Microsoft::Console::VirtualTerminal
[[nodiscard]] OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isRelease, bool isHover, short modifierKeyState, short delta);
[[nodiscard]] OutputType _makeAlternateScrollOutput(short delta) const;
[[nodiscard]] OutputType _makeAlternateScrollOutput(unsigned int button, short delta) const;
static constexpr unsigned int s_GetPressedButton(const MouseButtonState state) noexcept;
#pragma endregion

View File

@@ -897,6 +897,22 @@ bool InputStateMachineEngine::_UpdateSGRMouseButtonState(const VTID id,
eventFlags |= MOUSE_WHEELED;
break;
}
case CsiMouseButtonCodes::ScrollLeft:
{
// set high word to proper scroll direction
// scroll intensity is assumed to be constant value
buttonState |= SCROLL_DELTA_BACKWARD;
eventFlags |= MOUSE_HWHEELED;
break;
}
case CsiMouseButtonCodes::ScrollRight:
{
// set high word to proper scroll direction
// scroll intensity is assumed to be constant value
buttonState |= SCROLL_DELTA_FORWARD;
eventFlags |= MOUSE_HWHEELED;
break;
}
case CsiMouseButtonCodes::Released:
// hover event, we still want to send these but we don't
// need to do anything special here, so just break

View File

@@ -111,6 +111,8 @@ namespace Microsoft::Console::VirtualTerminal
Released = 3,
ScrollForward = 4,
ScrollBack = 5,
ScrollLeft = 6,
ScrollRight = 7,
};
constexpr unsigned short CsiMouseModifierCode_Drag = 32;

View File

@@ -1194,8 +1194,10 @@ void InputEngineTest::SGRMouseTest_Scroll()
// NOTE: scrolling events do NOT send a mouse up event
const std::vector<std::tuple<SGR_PARAMS, MOUSE_EVENT_PARAMS>> testData = {
// TEST INPUT EXPECTED OUTPUT
{ { CsiMouseButtonCodes::ScrollForward, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { SCROLL_DELTA_FORWARD, 0, { 0, 0 }, MOUSE_WHEELED } },
{ { CsiMouseButtonCodes::ScrollBack, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { SCROLL_DELTA_BACKWARD, 0, { 0, 0 }, MOUSE_WHEELED } },
{ { CsiMouseButtonCodes::ScrollForward, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { SCROLL_DELTA_FORWARD, 0, { 0, 0 }, MOUSE_WHEELED } },
{ { CsiMouseButtonCodes::ScrollBack, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { SCROLL_DELTA_BACKWARD, 0, { 0, 0 }, MOUSE_WHEELED } },
{ { CsiMouseButtonCodes::ScrollLeft, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { SCROLL_DELTA_BACKWARD, 0, { 0, 0 }, MOUSE_HWHEELED } },
{ { CsiMouseButtonCodes::ScrollRight, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { SCROLL_DELTA_FORWARD, 0, { 0, 0 }, MOUSE_HWHEELED } },
};
// clang-format on
VerifySGRMouseData(testData);