Add property to control dropdown speed of global summon (#9977)

## Summary of the Pull Request

Adds the `dropdownDuration` property to `globalSummon`. This controls how fast the window appears on the screen when summoned from minimized. It similarly controls the speed for sliding out of view when the window is dismissed with `"toggleVisibility": true`.

`dropdownDuration` specifies the duration in **milliseconds**. This defaults to `0` for `globalSummon`, and defaults to `200` for `quakeMode`. 200 was picked because, according to [`AnimateWindow`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-animatewindow): 

>  Typically, an animation takes 200 milliseconds to play.

Do note that you won't be able to interact with the window during the animation! Input sent during the dropdown will arrive at the end of the animation, but input sent during the slide-up _won't_. Avoid setting this to large values!

The gifs are in Teams. 

## References
* Original thread: #653
* Spec: #9274 
* megathread: #8888

## PR Checklist
* [x] Closes https://github.com/microsoft/terminal/projects/5#card-59030824
* [x] I work here
* [x] Tests added/passed
* [n/a] Requires documentation to be updated

## Detailed Description of the Pull Request / Additional comments

I had the following previously in the doc comments, but it feels better in the PR body:

- This was chosen because it was easier to implement and generally nicer than:
  * `AnimateWindow`, which would show the window borders for the duration of
    the animation, and occasionally just plain not work. Additionally, for
    `AnimateWindow` to work, the window much not be visible, so we'd need to
    first restore the window, then hide it, then animate it. That would flash
    the taskbar.
  * `SetWindowRgn` on the root HWND, which caused the xaml content to shift to
    the left, and caused a black bar to be drawn on the right of the window.
    Presumably, `SetWindowRgn` and `DwmExtendFrameIntoClientArea` did not play
    well with each other.
  * `SetWindowPos(..., SWP_NOSENDCHANGING)`, which worked the absolute best for
    longer animations, and is the closest to the actual implementation of
    `AnimateWindow`. This would resize the ROOT window, without sending resizes
    to the XAML island, allowing the content to _not_ reflow. but for a
    duration of 200ms, would only ever display ~2 frames. That's basically
    not even animation anymore, it's now just an "appear". Since that's how
    long the default animation is, if felt silly to have it basically not
    work by default.
- If a future reader would like to implement this better, **they should feel
  free to**, and not mistake my notes here as expertise. These are research
  notes into the dark and terrible land that is Win32 programming. I'm no expert. 

## Validation Steps Performed

This is the blob of json I'm testing with these days:

```jsonc
        { "keys": "ctrl+`", "command": { "action": "quakeMode" } },
        { "keys": "ctrl+1", "command": { "action": "globalSummon" } },
        // { "keys": "ctrl+2", "command": { "action": "globalSummon", "desktop": "toCurrent" } },
        // { "keys": "ctrl+2", "command": { "action": "globalSummon", "toggleVisibility": false } },
        { "keys": "ctrl+2", "command": { "action": "globalSummon", "dropdownDuration": 2000 } },
        { "keys": "ctrl+3", "command": { "action": "globalSummon", "desktop": "onCurrent" } },
        { "keys": "ctrl+4", "command": { "action": "globalSummon", "desktop": "any" } },
```

* <kbd>ctrl+\`</kbd> will summon the quake window with a _quick_ animation
* <kbd>ctrl+2</kbd> will summon the window with a  s l o w  animation
This commit is contained in:
Mike Griese 2021-05-17 07:28:46 -05:00 committed by GitHub
parent 7a41be5cd4
commit 6e11780ca6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 172 additions and 26 deletions

View file

@ -35,6 +35,7 @@ HIGHCONTRASTON
HIGHCONTRASTW
hotkeys
href
hrgn
IActivation
IApp
IAppearance

View file

@ -34,6 +34,7 @@ namespace Microsoft.Terminal.Remoting
SummonWindowBehavior();
Boolean MoveToCurrentDesktop;
Boolean ToggleVisibility;
UInt32 DropdownDuration;
// Other options:
// * CurrentMonitor
}

View file

@ -23,10 +23,12 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
SummonWindowBehavior() = default;
WINRT_PROPERTY(bool, MoveToCurrentDesktop, true);
WINRT_PROPERTY(bool, ToggleVisibility, true);
WINRT_PROPERTY(uint32_t, DropdownDuration, 0);
public:
SummonWindowBehavior(const Remoting::SummonWindowBehavior& other) :
_MoveToCurrentDesktop{ other.MoveToCurrentDesktop() },
_DropdownDuration{ other.DropdownDuration() },
_ToggleVisibility{ other.ToggleVisibility() } {};
};
}

View file

@ -1159,10 +1159,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
WINRT_PROPERTY(winrt::hstring, Name, L"");
WINRT_PROPERTY(Model::DesktopBehavior, Desktop, Model::DesktopBehavior::ToCurrent);
WINRT_PROPERTY(bool, ToggleVisibility, true);
WINRT_PROPERTY(uint32_t, DropdownDuration, 0);
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view DesktopKey{ "desktop" };
static constexpr std::string_view ToggleVisibilityKey{ "toggleVisibility" };
static constexpr std::string_view DropdownDurationKey{ "dropdownDuration" };
public:
hstring GenerateName() const;
@ -1173,6 +1175,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
return otherAsUs->_Name == _Name &&
otherAsUs->_Desktop == _Desktop &&
otherAsUs->_DropdownDuration == _DropdownDuration &&
otherAsUs->_ToggleVisibility == _ToggleVisibility;
}
return false;
@ -1183,6 +1186,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
auto args = winrt::make_self<GlobalSummonArgs>();
JsonUtils::GetValueForKey(json, NameKey, args->_Name);
JsonUtils::GetValueForKey(json, DesktopKey, args->_Desktop);
JsonUtils::GetValueForKey(json, DropdownDurationKey, args->_DropdownDuration);
JsonUtils::GetValueForKey(json, ToggleVisibilityKey, args->_ToggleVisibility);
return { *args, {} };
}
@ -1191,6 +1195,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
auto copy{ winrt::make_self<GlobalSummonArgs>() };
copy->_Name = _Name;
copy->_Desktop = _Desktop;
copy->_DropdownDuration = _DropdownDuration;
copy->_ToggleVisibility = _ToggleVisibility;
return *copy;
}
@ -1200,7 +1205,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
// LOAD BEARING: Not using make_self here _will_ break you in the future!
auto args = winrt::make_self<GlobalSummonArgs>();
// We want to summon the window with the name "_quake" specifically.
args->_Name = QuakeWindowName;
// We want the window to dropdown, with a 200ms duration.
args->_DropdownDuration = 200;
return { *args, {} };
}
size_t Hash() const

View file

@ -267,5 +267,6 @@ namespace Microsoft.Terminal.Settings.Model
String Name { get; };
DesktopBehavior Desktop { get; };
Boolean ToggleVisibility { get; };
UInt32 DropdownDuration { get; };
};
}

View file

@ -591,7 +591,7 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable /*se
{
// Summon the window whenever we dispatch a commandline to it. This will
// make it obvious when a new tab/pane is created in a window.
_window->SummonWindow(false);
_window->SummonWindow(false, 0);
_logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory());
}
@ -693,6 +693,7 @@ void AppHost::_GlobalHotkeyPressed(const long hotkeyIndex)
args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent);
args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent);
args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility());
args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration());
_windowManager.SummonWindow(args);
if (args.FoundMatch())
@ -775,7 +776,7 @@ bool AppHost::_LazyLoadDesktopManager()
void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const Remoting::SummonWindowBehavior& args)
{
_window->SummonWindow(args.ToggleVisibility());
_window->SummonWindow(args.ToggleVisibility(), args.DropdownDuration());
if (args != nullptr && args.MoveToCurrentDesktop())
{

View file

@ -1009,34 +1009,143 @@ void IslandWindow::SetGlobalHotkeys(const std::vector<winrt::Microsoft::Terminal
// - toggleVisibility: controls how we should behave when already in the foreground.
// Return Value:
// - <none>
winrt::fire_and_forget IslandWindow::SummonWindow(const bool toggleVisibility)
winrt::fire_and_forget IslandWindow::SummonWindow(const bool toggleVisibility, const uint32_t dropdownDuration)
{
// On the foreground thread:
co_await winrt::resume_foreground(_rootGrid.Dispatcher());
uint32_t actualDropdownDuration = dropdownDuration;
// If the user requested an animation, let's check if animations are enabled in the OS.
if (dropdownDuration > 0)
{
BOOL animationsEnabled = TRUE;
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0);
if (!animationsEnabled)
{
// The OS has animations disabled - we should respect that and
// disable the animation here.
//
// We're doing this here, rather than in _doSlideAnimation, to
// preempt any other specific behavior that
// _globalActivateWindow/_globalDismissWindow might do if they think
// there should be an animation (like making the window appear with
// SetWindowPlacement rather than ShowWindow)
actualDropdownDuration = 0;
}
}
// * If the user doesn't want to toggleVisibility, then just always try to
// activate.
// * If the user does want to toggleVisibility, then dismiss the window if
// we're the current foreground window.
if (toggleVisibility && GetForegroundWindow() == _window.get())
{
_globalDismissWindow();
_globalDismissWindow(actualDropdownDuration);
}
else
{
_globalActivateWindow();
_globalActivateWindow(actualDropdownDuration);
}
}
// Method Description:
// - Helper for performing a sliding animation. This will animate our _Xaml
// Island_, either growing down or shrinking up, using SetWindowRgn.
// - This function does the entire animation on the main thread (the UI thread),
// and **DOES NOT YIELD IT**. The window will be animating for the entire
// duration of dropdownDuration.
// - At the end of the animation, we'll reset the window region, so that it's as
// if nothing occurred.
// Arguments:
// - dropdownDuration: The duration to play the animation, in milliseconds. If
// 0, we won't perform a dropdown animation.
// - down: if true, increase the height from top to bottom. otherwise, decrease
// the height, from bottom to top.
// Return Value:
// - <none>
void IslandWindow::_doSlideAnimation(const uint32_t dropdownDuration, const bool down)
{
til::rectangle fullWindowSize{ GetWindowRect() };
const double fullHeight = fullWindowSize.height<double>();
const double animationDuration = dropdownDuration; // use floating-point math throughout
const auto start = std::chrono::system_clock::now();
// Do at most dropdownDuration frames. After that, just bail straight to the
// final state.
for (uint32_t i = 0; i < dropdownDuration; i++)
{
const auto end = std::chrono::system_clock::now();
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
const double dt = ::base::saturated_cast<double>(millis.count());
if (dt > animationDuration)
{
break;
}
// If going down, increase the height over time. If going up, decrease the height.
const double currentHeight = ::base::saturated_cast<double>(
down ? ((dt / animationDuration) * fullHeight) :
((1.0 - (dt / animationDuration)) * fullHeight));
wil::unique_hrgn rgn{ CreateRectRgn(0,
0,
fullWindowSize.width<int>(),
::base::saturated_cast<int>(currentHeight)) };
SetWindowRgn(_interopWindowHandle, rgn.get(), true);
// Go immediately into another frame. This prevents the window from
// doing anything else (tearing our state). A Sleep() here will cause a
// weird stutter, and causes the animation to not be as smooth.
}
// Reset the window.
SetWindowRgn(_interopWindowHandle, nullptr, true);
}
void IslandWindow::_dropdownWindow(const uint32_t dropdownDuration)
{
// First, restore the window. SetWindowPlacement has a fun undocumented
// piece of functionality where it will restore the window position
// _without_ the animation, so use that instead of ShowWindow(SW_RESTORE).
WINDOWPLACEMENT wpc{};
wpc.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(_window.get(), &wpc);
wpc.showCmd = SW_RESTORE;
SetWindowPlacement(_window.get(), &wpc);
// Now that we're visible, animate the dropdown.
_doSlideAnimation(dropdownDuration, true);
}
void IslandWindow::_slideUpWindow(const uint32_t dropdownDuration)
{
// First, animate the window sliding up.
_doSlideAnimation(dropdownDuration, false);
// Then, use SetWindowPlacement to minimize without the animation.
WINDOWPLACEMENT wpc{};
wpc.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(_window.get(), &wpc);
wpc.showCmd = SW_MINIMIZE;
SetWindowPlacement(_window.get(), &wpc);
}
// Method Description:
// - Force activate this window. This method will bring us to the foreground and
// activate us. If the window is minimized, it will restore the window. If the
// window is on another desktop, the OS will switch to that desktop.
// - If the window is minimized, and dropdownDuration is greater than 0, we'll
// perform a "slide in" animation. We won't do this if the window is already
// on the screen (since that seems silly).
// Arguments:
// - <none>
// - dropdownDuration: The duration to play the dropdown animation, in
// milliseconds. If 0, we won't perform a dropdown animation.
// Return Value:
// - <none>
void IslandWindow::_globalActivateWindow()
void IslandWindow::_globalActivateWindow(const uint32_t dropdownDuration)
{
// From: https://stackoverflow.com/a/59659421
// > The trick is to make windows think that our process and the target
@ -1047,34 +1156,54 @@ void IslandWindow::_globalActivateWindow()
// restore-down the window.
if (IsIconic(_window.get()))
{
LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_RESTORE));
if (dropdownDuration > 0)
{
_dropdownWindow(dropdownDuration);
}
else
{
LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_RESTORE));
}
}
const DWORD windowThreadProcessId = GetWindowThreadProcessId(GetForegroundWindow(), nullptr);
const DWORD currentThreadId = GetCurrentThreadId();
else
{
const DWORD windowThreadProcessId = GetWindowThreadProcessId(GetForegroundWindow(), nullptr);
const DWORD currentThreadId = GetCurrentThreadId();
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, true));
// Just in case, add the thread detach as a scope_exit, to make _sure_ we do it.
auto detachThread = wil::scope_exit([windowThreadProcessId, currentThreadId]() {
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, false));
});
LOG_IF_WIN32_BOOL_FALSE(BringWindowToTop(_window.get()));
LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_SHOW));
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, true));
// Just in case, add the thread detach as a scope_exit, to make _sure_ we do it.
auto detachThread = wil::scope_exit([windowThreadProcessId, currentThreadId]() {
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, false));
});
LOG_IF_WIN32_BOOL_FALSE(BringWindowToTop(_window.get()));
LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_SHOW));
// Activate the window too. This will force us to the virtual desktop this
// window is on, if it's on another virtual desktop.
LOG_LAST_ERROR_IF_NULL(SetActiveWindow(_window.get()));
// Activate the window too. This will force us to the virtual desktop this
// window is on, if it's on another virtual desktop.
LOG_LAST_ERROR_IF_NULL(SetActiveWindow(_window.get()));
}
}
// Method Description:
// - Minimize the window. This is called when the window is summoned, but is
// already active
// - If dropdownDuration is greater than 0, we'll perform a "slide in"
// animation, before minimizing the window.
// Arguments:
// - <none>
// - dropdownDuration: The duration to play the slide-up animation, in
// milliseconds. If 0, we won't perform a slide-up animation.
// Return Value:
// - <none>
void IslandWindow::_globalDismissWindow()
void IslandWindow::_globalDismissWindow(const uint32_t dropdownDuration)
{
LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_MINIMIZE));
if (dropdownDuration > 0)
{
_slideUpWindow(dropdownDuration);
}
else
{
LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_MINIMIZE));
}
}
bool IslandWindow::IsQuakeWindow() const noexcept

View file

@ -41,7 +41,7 @@ public:
void UnsetHotkeys(const std::vector<winrt::Microsoft::Terminal::Control::KeyChord>& hotkeyList);
void SetGlobalHotkeys(const std::vector<winrt::Microsoft::Terminal::Control::KeyChord>& hotkeyList);
winrt::fire_and_forget SummonWindow(const bool toggleVisibility);
winrt::fire_and_forget SummonWindow(const bool toggleVisibility, const uint32_t dropdownDuration);
bool IsQuakeWindow() const noexcept;
void IsQuakeWindow(bool isQuakeWindow) noexcept;
@ -93,8 +93,11 @@ protected:
void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam);
long _calculateTotalSize(const bool isWidth, const long clientSize, const long nonClientSize);
void _globalActivateWindow();
void _globalDismissWindow();
void _globalActivateWindow(const uint32_t dropdownDuration);
void _dropdownWindow(const uint32_t dropdownDuration);
void _slideUpWindow(const uint32_t dropdownDuration);
void _doSlideAnimation(const uint32_t dropdownDuration, const bool down);
void _globalDismissWindow(const uint32_t dropdownDuration);
bool _isQuakeWindow{ false };
void _enterQuakeMode();