Tunnel F7 keypresses directly into special handlers in TermControl (#4807)

The Xaml input stack doesn't allow an application to suppress the "caret
browsing" dialog experience triggered when you press F7.

The official recommendation from the Xaml team is to catch F7 before we
hand it off.

This commit introduces a special F7 handler and an ad-hoc implementation of event bubbling.
Runtime classes implementing a custom IF7Listener interface are
considered during a modified focus parent walk to determine who can
handle F7 specifically.

If the recipient control handles F7, we suppress the message completely.

This event bubbler has some minor issues -- the search box will not be
able to receive F7 because its parent control implements the handler.
Since search is already mostly a text box, it doesn't _need_ special
caret browsing functionality as far as I can tell.

TermControl implements its OnF7Pressed handler by synthesizing a
keybindings event and an event to feed into Terminal Core directly.

It's not possible to create a synthetic KeyPressRoutedEvent; if it were,
I would have just popped one into the traditional input queue. :)

Fixes #638.
This commit is contained in:
msftbot[bot] 2020-03-05 20:35:46 +00:00 committed by GitHub
parent a32956d620
commit a3d68d2b21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 133 additions and 2 deletions

View file

@ -704,6 +704,41 @@ namespace winrt::TerminalApp::implementation
}
}
// Method Description:
// - Implements the F7 handler (per GH#638)
// Return value:
// - whether F7 was handled
bool AppLogic::OnF7Pressed()
{
if (_root)
{
// Manually bubble the OnF7Pressed event up through the focus tree.
auto xamlRoot{ _root->XamlRoot() };
auto focusedObject{ Windows::UI::Xaml::Input::FocusManager::GetFocusedElement(xamlRoot) };
do
{
if (auto f7Listener{ focusedObject.try_as<IF7Listener>() })
{
if (f7Listener.OnF7Pressed())
{
return true;
}
// otherwise, keep walking. bubble the event manually.
}
if (auto focusedElement{ focusedObject.try_as<Windows::UI::Xaml::FrameworkElement>() })
{
focusedObject = focusedElement.Parent();
}
else
{
break; // we hit a non-FE object, stop bubbling.
}
} while (focusedObject);
}
return false;
}
// Method Description:
// - Used to tell the app that the 'X' button has been clicked and
// the user wants to close the app. We kick off the close warning

View file

@ -38,6 +38,7 @@ namespace winrt::TerminalApp::implementation
hstring Title();
void TitlebarClicked();
bool OnF7Pressed();
void WindowCloseButtonClicked();

View file

@ -3,6 +3,7 @@
import "../TerminalPage.idl";
import "../ShortcutActionDispatch.idl";
import "../IF7Listener.idl";
namespace TerminalApp
{
@ -12,7 +13,7 @@ namespace TerminalApp
MaximizedMode,
};
[default_interface] runtimeclass AppLogic {
[default_interface] runtimeclass AppLogic: IF7Listener {
AppLogic();
// For your own sanity, it's better to do setup outside the ctor.

View file

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace TerminalApp
{
// C++/winrt makes it difficult to share this idl between two projects,
// Instead, we just pin the uuid and include it in both TermControl and App
// If you update this one, please update the one in TerminalControl\TermControl.idl
// If you change this interface, please update the guid.
// If you press F7 and get a runtime error, go make sure both copies are the same.
[uuid("339e1a87-5315-4da6-96f0-565549b6472b")]
interface IF7Listener
{
Boolean OnF7Pressed();
}
}

View file

@ -186,6 +186,7 @@
<ItemGroup>
<!-- If you add idl files here, make sure to include their implementation's
header in TerminalApp.vcxproj (as well as in this file) -->
<Midl Include="../IF7Listener.idl" />
<Midl Include="../App.idl">
<DependentUpon>../App.xaml</DependentUpon>
</Midl>

View file

@ -654,6 +654,38 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
e.Handled(handled);
}
// Method Description:
// - Manually generate an F7 event into the key bindings or terminal.
// This is required as part of GH#638.
// Return value:
// - Whether F7 was handled.
bool TermControl::OnF7Pressed()
{
bool handled{ false };
auto bindings{ _settings.KeyBindings() };
const auto modifiers{ _GetPressedModifierKeys() };
if (bindings)
{
handled = bindings.TryKeyChord({
modifiers.IsCtrlPressed(),
modifiers.IsAltPressed(),
modifiers.IsShiftPressed(),
VK_F7,
});
}
if (!handled)
{
// _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason.
(void)_TrySendKeyEvent(VK_F7, 0, modifiers);
handled = true;
}
return handled;
}
void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/,
Input::KeyRoutedEventArgs const& e)
{

View file

@ -81,6 +81,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void CreateSearchBoxControl();
bool OnF7Pressed();
~TermControl();
Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer();

View file

@ -7,6 +7,17 @@ namespace Microsoft.Terminal.TerminalControl
delegate void FontSizeChangedEventArgs(Int32 width, Int32 height, Boolean isInitialChange);
delegate void ScrollPositionChangedEventArgs(Int32 viewTop, Int32 viewHeight, Int32 bufferLength);
// C++/winrt makes it difficult to share this idl between two projects,
// Instead, we just pin the uuid and include it in both TermControl and App
// If you update this one, please update TerminalApp\IF7Listener.idl.
// If you change this interface, please update the guid.
// If you press F7 and get a runtime error, go make sure both copies are the same.
[uuid("339e1a87-5315-4da6-96f0-565549b6472b")]
interface IF7Listener
{
Boolean OnF7Pressed();
}
runtimeclass CopyToClipboardEventArgs
{
String Text { get; };
@ -19,7 +30,7 @@ namespace Microsoft.Terminal.TerminalControl
void HandleClipboardData(String data);
}
[default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl
[default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, IF7Listener
{
TermControl();
TermControl(Microsoft.Terminal.Settings.IControlSettings settings, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection);

View file

@ -63,6 +63,15 @@ AppHost::~AppHost()
_app = nullptr;
}
bool AppHost::OnF7Pressed()
{
if (_logic)
{
return _logic.OnF7Pressed();
}
return false;
}
// Method Description:
// - Retrieve any commandline args passed on the commandline, and pass them to
// the app logic for processing.

View file

@ -17,6 +17,7 @@ public:
void AppTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, winrt::hstring newTitle);
void LastTabClosed(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::LastTabClosedEventArgs& args);
void Initialize();
bool OnF7Pressed();
private:
bool _useNonClientArea;

View file

@ -73,6 +73,11 @@ static void EnsureNativeArchitecture()
}
}
static bool _messageIsF7Keypress(const MSG& message)
{
return (message.message == WM_KEYDOWN || message.message == WM_SYSKEYDOWN) && message.wParam == VK_F7;
}
int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
{
TraceLoggingRegister(g_hWindowsTerminalProvider);
@ -115,6 +120,23 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
while (GetMessage(&message, nullptr, 0, 0))
{
// GH#638 (Pressing F7 brings up both the history AND a caret browsing message)
// The Xaml input stack doesn't allow an application to suppress the "caret browsing"
// dialog experience triggered when you press F7. Official recommendation from the Xaml
// team is to catch F7 before we hand it off.
// AppLogic contains an ad-hoc implementation of event bubbling for a runtime classes
// implementing a custom IF7Listener interface.
// If the recipient of IF7Listener::OnF7Pressed suggests that the F7 press has, in fact,
// been handled we can discard the message before we even translate it.
if (_messageIsF7Keypress(message))
{
if (host.OnF7Pressed())
{
// The application consumed the F7. Don't let Xaml get it.
continue;
}
}
TranslateMessage(&message);
DispatchMessage(&message);
}