Add support for renaming windows (#9662)

## Summary of the Pull Request

This PR adds support for renaming windows.

![window-renaming-000](https://user-images.githubusercontent.com/18356694/113034344-9a30be00-9157-11eb-9443-975f3c294f56.gif)
![window-renaming-001](https://user-images.githubusercontent.com/18356694/113034452-b5033280-9157-11eb-9e35-e5ac80fef0bc.gif)


It does so through two new actions:
* `renameWindow` takes a `name` parameter, and attempts to set the window's name
  to the provided name. This is useful if you always want to hit <kbd>F3</kbd>
  and rename a window to "foo" (READ: probably not that useful)
* `openWindowRenamer` is more interesting: it opens a `TeachingTip` with a
  `TextBox`. When the user hits Ok, it'll request a rename for the provided
  value. This lets the user pick a new name for the window at runtime.

In both cases, if there's already a window with that name, then the monarch will
reject the rename, and pop a `Toast` in the window informing the user that the
rename failed. Nifty!

## References
* Builds on the toasts from #9523
* #5000 - process model megathread

## PR Checklist
* [x] Closes https://github.com/microsoft/terminal/projects/5#card-50771747
* [x] I work here
* [x] Tests addded (and pass with the help of #9660)
* [ ] Requires documentation to be updated

## Detailed Description of the Pull Request / Additional comments

I'm sending this PR while finishing up the tests. I figured I'll have time to sneak them in before I get the necessary reviews.

> PAIN: We can't immediately focus the textbox in the TeachingTip. It's
> not technically focusable until it is opened. However, it doesn't
> provide an even tto tell us when it is opened. That's tracked in
> microsoft/microsoft-ui-xaml#1607. So for now, the user _needs_ to
> click on the text box manually.
> We're also not using a ContentDialog for this, because in Xaml
> Islands a text box in a ContentDialog won't recieve _any_ keypresses.
> Fun!

## Validation Steps Performed

I've been playing with 

```json
        { "keys": "f1", "command": "identifyWindow" },
        { "keys": "f2", "command": "identifyWindows" },
        { "keys": "f3", "command": "openWindowRenamer" },
        { "keys": "f4", "command": { "action": "renameWindow", "name": "foo" } },
        { "keys": "f5", "command": { "action": "renameWindow", "name": "bar" } },
```

and they seem to work as expected
This commit is contained in:
Mike Griese 2021-04-02 11:00:04 -05:00 committed by GitHub
parent 4b7d955012
commit fb597ed304
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 679 additions and 13 deletions

View File

@ -85,12 +85,14 @@
"openNewTabDropdown",
"openSettings",
"openTabColorPicker",
"openWindowRenamer",
"paste",
"prevTab",
"renameTab",
"openTabRenamer",
"resetFontSize",
"resizePane",
"renameWindow",
"scrollDown",
"scrollDownPage",
"scrollUp",
@ -651,6 +653,38 @@
}
]
},
"RenameTabAction": {
"description": "Arguments corresponding to a renameTab Action",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "renameTab" },
"title": {
"type": "string",
"default": "",
"description": "A title to assign to the tab. If omitted or null, this action will restore the tab's title to the original value."
}
}
}
]
},
"RenameWindowAction": {
"description": "Arguments corresponding to a renameWindow Action",
"allOf": [
{ "$ref": "#/definitions/ShortcutAction" },
{
"properties": {
"action": { "type": "string", "pattern": "renameWindow" },
"name": {
"type": "string",
"default": "",
"description": "A name to assign to the window."
}
}
}
]
},
"Keybinding": {
"additionalProperties": false,
"properties": {
@ -679,6 +713,8 @@
{ "$ref": "#/definitions/NewWindowAction" },
{ "$ref": "#/definitions/NextTabAction" },
{ "$ref": "#/definitions/PrevTabAction" },
{ "$ref": "#/definitions/RenameTabAction" },
{ "$ref": "#/definitions/RenameWindowAction" },
{ "type": "null" }
]
},

View File

@ -85,6 +85,9 @@ namespace TerminalAppLocalTests
TEST_METHOD(NextMRUTab);
TEST_METHOD(VerifyCommandPaletteTabSwitcherOrder);
TEST_METHOD(TestWindowRenameSuccessful);
TEST_METHOD(TestWindowRenameFailure);
TEST_CLASS_SETUP(ClassSetup)
{
return true;
@ -948,4 +951,57 @@ namespace TerminalAppLocalTests
// will also dismiss itself immediately when that's called. So we can't
// really inspect the contents of the list in this test, unfortunately.
}
void TabTests::TestWindowRenameSuccessful()
{
auto page = _commonSetup();
page->RenameWindowRequested([&page](auto&&, const winrt::TerminalApp::RenameWindowRequestedArgs args) {
// In the real terminal, this would bounce up to the monarch and
// come back down. Instead, immediately call back and set the name.
page->WindowName(args.ProposedName());
});
bool windowNameChanged = false;
page->PropertyChanged([&page, &windowNameChanged](auto&&, const winrt::WUX::Data::PropertyChangedEventArgs& args) mutable {
if (args.PropertyName() == L"WindowNameForDisplay")
{
windowNameChanged = true;
}
});
TestOnUIThread([&page]() {
page->_RequestWindowRename(winrt::hstring{ L"Foo" });
});
TestOnUIThread([&]() {
VERIFY_ARE_EQUAL(L"Foo", page->_WindowName);
VERIFY_IS_TRUE(windowNameChanged,
L"The window name should have changed, and we should have raised a notification that WindowNameForDisplay changed");
});
}
void TabTests::TestWindowRenameFailure()
{
auto page = _commonSetup();
page->RenameWindowRequested([&page](auto&&, auto&&) {
// In the real terminal, this would bounce up to the monarch and
// come back down. Instead, immediately call back to tell the terminal it failed.
page->RenameFailed();
});
bool windowNameChanged = false;
page->PropertyChanged([&page, &windowNameChanged](auto&&, const winrt::WUX::Data::PropertyChangedEventArgs& args) mutable {
if (args.PropertyName() == L"WindowNameForDisplay")
{
windowNameChanged = true;
}
});
TestOnUIThread([&page]() {
page->_RequestWindowRename(winrt::hstring{ L"Foo" });
});
TestOnUIThread([&]() {
VERIFY_IS_FALSE(windowNameChanged,
L"The window name should not have changed, we should have rejected the change.");
});
}
}

View File

@ -45,6 +45,7 @@ Author(s):
#include <winrt/Windows.ui.input.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Controls.Primitives.h>
#include <winrt/Windows.UI.Xaml.Data.h>
#include <winrt/Windows.ui.xaml.media.h>
#include <winrt/Windows.ui.xaml.input.h>
#include <winrt/Windows.UI.Xaml.Markup.h>

View File

@ -25,6 +25,9 @@
<ClInclude Include="ProposeCommandlineResult.h">
<DependentUpon>Monarch.idl</DependentUpon>
</ClInclude>
<ClInclude Include="RenameRequestArgs.h">
<DependentUpon>Peasant.idl</DependentUpon>
</ClInclude>
<ClInclude Include="WindowActivatedArgs.h">
<DependentUpon>Peasant.idl</DependentUpon>
</ClInclude>
@ -51,6 +54,9 @@
<ClCompile Include="ProposeCommandlineResult.cpp">
<DependentUpon>Monarch.idl</DependentUpon>
</ClCompile>
<ClCompile Include="RenameRequestArgs.cpp">
<DependentUpon>Peasant.idl</DependentUpon>
</ClCompile>
<ClCompile Include="WindowActivatedArgs.cpp">
<DependentUpon>Peasant.idl</DependentUpon>
</ClCompile>

View File

@ -76,6 +76,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
// Add an event listener to the peasant's WindowActivated event.
peasant.WindowActivated({ this, &Monarch::_peasantWindowActivated });
peasant.IdentifyWindowsRequested({ this, &Monarch::_identifyWindows });
peasant.RenameRequested({ this, &Monarch::_renameRequested });
_peasants[newPeasantsId] = peasant;
@ -631,4 +632,52 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
};
_forAllPeasantsIgnoringTheDead(callback, onError);
}
// Method Description:
// - This is an event handler for the RenameRequested event. A
// Peasant may raise that event when they want to be renamed to something else.
// - We will check if there are any other windows with this name. If there
// are, then we'll reject the rename by setting args.Succeeded=false.
// - If there aren't any other windows with this name, then we'll set
// args.Succeeded=true, allowing the window to keep this name.
// Arguments:
// - args: Contains the requested window name and a boolean (Succeeded)
// indicating if the request was successful.
// Return Value:
// - <none>
void Monarch::_renameRequested(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args)
{
bool successfullyRenamed = false;
try
{
args.Succeeded(false);
const auto name{ args.NewName() };
// Try to find a peasant that currently has this name
const auto id = _lookupPeasantIdForName(name);
if (_getPeasant(id) == nullptr)
{
// If there is one, then oh no! The requestor is not allowed to
// be renamed.
args.Succeeded(true);
successfullyRenamed = true;
}
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_renameRequested",
TraceLoggingWideString(name.c_str(), "name", "The newly proposed name"),
TraceLoggingInt64(successfullyRenamed, "successfullyRenamed", "true if the peasant is allowed to rename themselves to that name."),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
// If this fails, we don't _really_ care. The peasant died, but
// they're the only one who cares about the result.
TraceLoggingWrite(g_hRemotingProvider,
"Monarch_renameRequested_Failed",
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
}
}
}

View File

@ -74,11 +74,15 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
void _doHandleActivatePeasant(const winrt::com_ptr<winrt::Microsoft::Terminal::Remoting::implementation::WindowActivatedArgs>& args);
void _clearOldMruEntries(const uint64_t peasantID);
void _forAllPeasantsIgnoringTheDead(std::function<void(const winrt::Microsoft::Terminal::Remoting::IPeasant&, const uint64_t)> callback,
std::function<void(const uint64_t)> errorCallback);
void _identifyWindows(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
void _forAllPeasantsIgnoringTheDead(std::function<void(const winrt::Microsoft::Terminal::Remoting::IPeasant&, const uint64_t)> callback,
std::function<void(const uint64_t)> errorCallback);
void _renameRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args);
friend class RemotingUnitTests::RemotingTests;
};
}

View File

@ -164,4 +164,34 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
}
void Peasant::RequestRename(const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args)
{
bool successfullyNotified = false;
const auto oldName{ _WindowName };
try
{
// Try/catch this, because the other side of this event is handled
// by the monarch. The monarch might have died. If they have, this
// will throw an exception. Just eat it, the election thread will
// handle hooking up the new one.
_RenameRequestedHandlers(*this, args);
if (args.Succeeded())
{
_WindowName = args.NewName();
}
successfullyNotified = true;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
TraceLoggingWrite(g_hRemotingProvider,
"Peasant_RequestRename",
TraceLoggingUInt64(GetID(), "peasantID", "Our ID"),
TraceLoggingWideString(oldName.c_str(), "oldName", "Our old name"),
TraceLoggingWideString(args.NewName().c_str(), "newName", "The proposed name"),
TraceLoggingBoolean(args.Succeeded(), "succeeded", "true if the monarch ok'd this new name for us."),
TraceLoggingBoolean(successfullyNotified, "successfullyNotified", "true if we successfully notified the monarch"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
}
}

View File

@ -5,6 +5,7 @@
#include "Peasant.g.h"
#include "../cascadia/inc/cppwinrt_utils.h"
#include "RenameRequestArgs.h"
namespace RemotingUnitTests
{
@ -24,6 +25,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
void ActivateWindow(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args);
void RequestIdentifyWindows();
void DisplayWindowId();
void RequestRename(const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args);
winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs();
@ -34,6 +36,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
TYPED_EVENT(ExecuteCommandlineRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::CommandlineArgs);
TYPED_EVENT(IdentifyWindowsRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RenameRequestArgs);
private:
Peasant(const uint64_t testPID);

View File

@ -13,6 +13,13 @@ namespace Microsoft.Terminal.Remoting
String CurrentDirectory();
};
runtimeclass RenameRequestArgs
{
RenameRequestArgs(String newName);
String NewName { get; };
Boolean Succeeded;
};
runtimeclass WindowActivatedArgs
{
WindowActivatedArgs(UInt64 peasantID, Guid desktopID, Windows.Foundation.DateTime activatedTime);
@ -37,10 +44,13 @@ namespace Microsoft.Terminal.Remoting
void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested
void DisplayWindowId(); // Tells us to display its own ID (which causes a DisplayWindowIdRequested to be raised)
void RequestRename(RenameRequestArgs args); // Tells us to raise a RenameRequested
event Windows.Foundation.TypedEventHandler<Object, WindowActivatedArgs> WindowActivated;
event Windows.Foundation.TypedEventHandler<Object, CommandlineArgs> ExecuteCommandlineRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> DisplayWindowIdRequested;
event Windows.Foundation.TypedEventHandler<Object, RenameRequestArgs> RenameRequested;
};
[default_interface] runtimeclass Peasant : IPeasant

View File

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

View File

@ -0,0 +1,30 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Class Name:
- RenameRequestArgs.h
--*/
#pragma once
#include "RenameRequestArgs.g.h"
#include "../cascadia/inc/cppwinrt_utils.h"
namespace winrt::Microsoft::Terminal::Remoting::implementation
{
struct RenameRequestArgs : public RenameRequestArgsT<RenameRequestArgs>
{
WINRT_PROPERTY(winrt::hstring, NewName);
WINRT_PROPERTY(bool, Succeeded, false);
public:
RenameRequestArgs(winrt::hstring newName) :
_NewName{ newName } {};
};
}
namespace winrt::Microsoft::Terminal::Remoting::factory_implementation
{
BASIC_FACTORY(RenameRequestArgs);
}

View File

@ -708,4 +708,47 @@ namespace winrt::TerminalApp::implementation
IdentifyWindow();
args.Handled(true);
}
void TerminalPage::_HandleRenameWindow(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (args)
{
if (const auto& realArgs = args.ActionArgs().try_as<RenameWindowArgs>())
{
const auto newName = realArgs.Name();
const auto request = winrt::make_self<implementation::RenameWindowRequestedArgs>(newName);
_RenameWindowRequestedHandlers(*this, *request);
}
}
args.Handled(false);
}
void TerminalPage::_HandleOpenWindowRenamer(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (WindowRenamer() == nullptr)
{
// We need to use FindName to lazy-load this object
if (MUX::Controls::TeachingTip tip{ FindName(L"WindowRenamer").try_as<MUX::Controls::TeachingTip>() })
{
tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl });
}
}
WindowRenamer().IsOpen(true);
// PAIN: We can't immediately focus the textbox in the TeachingTip. It's
// not technically focusable until it is opened. However, it doesn't
// provide an event to tell us when it is opened. That's tracked in
// microsoft/microsoft-ui-xaml#1607. So for now, the user _needs_ to
// click on the text box manually.
//
// We're also not using a ContentDialog for this, because in Xaml
// Islands a text box in a ContentDialog won't receive _any_ keypresses.
// Fun!
// WindowRenamerTextBox().Focus(FocusState::Programmatic);
args.Handled(true);
}
}

View File

@ -1410,4 +1410,13 @@ namespace winrt::TerminalApp::implementation
_root->WindowId(id);
}
}
void AppLogic::RenameFailed()
{
if (_root)
{
_root->RenameFailed();
}
}
}

View File

@ -61,6 +61,7 @@ namespace winrt::TerminalApp::implementation
bool AlwaysOnTop() const;
void IdentifyWindow();
void RenameFailed();
winrt::hstring WindowName();
void WindowName(const winrt::hstring& name);
uint64_t WindowId();
@ -156,6 +157,7 @@ namespace winrt::TerminalApp::implementation
FORWARDED_TYPED_EVENT(RaiseVisualBell, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, RaiseVisualBell);
FORWARDED_TYPED_EVENT(SetTaskbarProgress, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, SetTaskbarProgress);
FORWARDED_TYPED_EVENT(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested);
FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested);
#ifdef UNIT_TESTING
friend class TerminalAppLocalTests::CommandlineTest;

View File

@ -51,6 +51,7 @@ namespace TerminalApp
void IdentifyWindow();
String WindowName;
UInt64 WindowId;
void RenameFailed();
Windows.Foundation.Size GetLaunchDimensions(UInt32 dpi);
Boolean CenterOnLaunch { get; };
@ -83,5 +84,6 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> RaiseVisualBell;
event Windows.Foundation.TypedEventHandler<Object, Object> SetTaskbarProgress;
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, RenameWindowRequestedArgs> RenameWindowRequested;
}
}

View File

@ -603,10 +603,25 @@
<value>unnamed window</value>
<comment>text used to identify when a window hasn't been assigned a name by the user</comment>
</data>
<data name="WindowRenamer.Subtitle" xml:space="preserve">
<value>Enter a new name:</value>
</data>
<data name="WindowRenamer.ActionButtonContent" xml:space="preserve">
<value>OK</value>
</data>
<data name="WindowRenamer.CloseButtonContent" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="RenameFailedToast.Title" xml:space="preserve">
<value>Failed to rename window</value>
</data>
<data name="RenameFailedToast.Subtitle" xml:space="preserve">
<value>Another window with that name already exists</value>
</data>
<data name="WindowMaximizeButtonToolTip" xml:space="preserve">
<value>Maximize</value>
</data>
<data name="WindowRestoreDownButtonToolTip" xml:space="preserve">
<value>Restore Down</value>
</data>
</root>
</root>

View File

@ -15,6 +15,7 @@
#include "ColorHelper.h"
#include "DebugTapConnection.h"
#include "SettingsTab.h"
#include "RenameWindowRequestedArgs.g.cpp"
using namespace winrt;
using namespace winrt::Windows::Foundation::Collections;
@ -2568,7 +2569,7 @@ namespace winrt::TerminalApp::implementation
if (_WindowName != value)
{
_WindowName = value;
_PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowNameForDisplay" });
_PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"WindowNameForDisplay" });
}
}
@ -2584,7 +2585,7 @@ namespace winrt::TerminalApp::implementation
if (_WindowId != value)
{
_WindowId = value;
_PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowIdForDisplay" });
_PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"WindowIdForDisplay" });
}
}
@ -2613,4 +2614,72 @@ namespace winrt::TerminalApp::implementation
winrt::hstring{ fmt::format(L"<{}>", RS_(L"UnnamedWindowName")) } :
_WindowName;
}
// Method Description:
// - Called when an attempt to rename the window has failed. This will open
// the toast displaying a message to the user that the attempt to rename
// the window has failed.
// - This will load the RenameFailedToast TeachingTip the first time it's called.
// Arguments:
// - <none>
// Return Value:
// - <none>
winrt::fire_and_forget TerminalPage::RenameFailed()
{
auto weakThis{ get_weak() };
co_await winrt::resume_foreground(Dispatcher());
if (auto page{ weakThis.get() })
{
// If we haven't ever loaded the TeachingTip, then do so now and
// create the toast for it.
if (page->_windowRenameFailedToast == nullptr)
{
if (MUX::Controls::TeachingTip tip{ page->FindName(L"RenameFailedToast").try_as<MUX::Controls::TeachingTip>() })
{
page->_windowRenameFailedToast = std::make_shared<Toast>(tip);
// Make sure to use the weak ref when setting up this
// callback.
tip.Closed({ page->get_weak(), &TerminalPage::_FocusActiveControl });
}
}
if (page->_windowRenameFailedToast != nullptr)
{
page->_windowRenameFailedToast->Open();
}
}
}
// Method Description:
// - Called when the user hits the "Ok" button on the WindowRenamer TeachingTip.
// - Will raise an event that will bubble up to the monarch, asking if this
// name is acceptable.
// - If it is, we'll eventually get called back in TerminalPage::WindowName(hstring).
// - If not, then TerminalPage::RenameFailed will get called.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void TerminalPage::_WindowRenamerActionClick(const IInspectable& /*sender*/,
const IInspectable& /*eventArgs*/)
{
auto newName = WindowRenamerTextBox().Text();
_RequestWindowRename(newName);
}
void TerminalPage::_RequestWindowRename(const winrt::hstring& newName)
{
auto request = winrt::make<implementation::RenameWindowRequestedArgs>(newName);
// The WindowRenamer is _not_ a Toast - we want it to stay open until the user dismisses it.
WindowRenamer().IsOpen(false);
_RenameWindowRequestedHandlers(*this, request);
// We can't just use request.Successful here, because the handler might
// (will) be handling this asynchronously, so when control returns to
// us, this hasn't actually been handled yet. We'll get called back in
// RenameFailed if this fails.
//
// Theoretically we could do a IAsyncOperation<RenameWindowResult> kind
// of thing with co_return winrt::make<RenameWindowResult>(false).
}
}

View File

@ -7,6 +7,7 @@
#include "TerminalTab.h"
#include "AppKeyBindings.h"
#include "AppCommandlineArgs.h"
#include "RenameWindowRequestedArgs.g.h"
#include "Toast.h"
#define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
@ -35,6 +36,15 @@ namespace winrt::TerminalApp::implementation
ScrollDown = 1
};
struct RenameWindowRequestedArgs : RenameWindowRequestedArgsT<RenameWindowRequestedArgs>
{
WINRT_PROPERTY(winrt::hstring, ProposedName);
public:
RenameWindowRequestedArgs(const winrt::hstring& name) :
_ProposedName{ name } {};
};
struct TerminalPage : TerminalPageT<TerminalPage>
{
public:
@ -82,6 +92,7 @@ namespace winrt::TerminalApp::implementation
winrt::hstring KeyboardServiceDisabledText();
winrt::fire_and_forget IdentifyWindow();
winrt::fire_and_forget RenameFailed();
winrt::fire_and_forget ProcessStartupActions(Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::ActionAndArgs> actions,
const bool initial,
@ -110,6 +121,7 @@ namespace winrt::TerminalApp::implementation
TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable);
TYPED_EVENT(Initialized, IInspectable, winrt::Windows::UI::Xaml::RoutedEventArgs);
TYPED_EVENT(IdentifyWindowsRequested, IInspectable, IInspectable);
TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs);
private:
friend struct TerminalPageT<TerminalPage>; // for Xaml to bind events
@ -162,6 +174,7 @@ namespace winrt::TerminalApp::implementation
bool _shouldStartInboundListener{ false };
std::shared_ptr<Toast> _windowIdToast{ nullptr };
std::shared_ptr<Toast> _windowRenameFailedToast{ nullptr };
void _ShowAboutDialog();
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowCloseWarningDialog();
@ -311,6 +324,9 @@ namespace winrt::TerminalApp::implementation
void _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection);
void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs);
void _RequestWindowRename(const winrt::hstring& newName);
#pragma region ActionHandlers
// These are all defined in AppActionHandlers.cpp
#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action);

View File

@ -5,6 +5,11 @@ namespace TerminalApp
{
delegate void LastTabClosedEventArgs();
[default_interface] runtimeclass RenameWindowRequestedArgs
{
String ProposedName { get; };
};
interface IDialogPresenter
{
Windows.Foundation.IAsyncOperation<Windows.UI.Xaml.Controls.ContentDialogResult> ShowDialog(Windows.UI.Xaml.Controls.ContentDialog dialog);
@ -27,6 +32,7 @@ namespace TerminalApp
UInt64 WindowId;
String WindowNameForDisplay { get; };
String WindowIdForDisplay { get; };
void RenameFailed();
// We cannot use the default XAML APIs because we want to make sure
// that there's only one application-global dialog visible at a time,
@ -47,5 +53,6 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Windows.UI.Xaml.RoutedEventArgs> Initialized;
event Windows.Foundation.TypedEventHandler<Object, Object> SetTaskbarProgress;
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, RenameWindowRequestedArgs> RenameWindowRequested;
}
}

View File

@ -126,6 +126,22 @@
Title="{x:Bind WindowIdForDisplay}"
x:Load="False"
IsLightDismissEnabled="True"
Subtitle="{x:Bind WindowNameForDisplay}" />
Subtitle="{x:Bind WindowNameForDisplay, Mode=OneWay}" />
<mux:TeachingTip x:Name="RenameFailedToast"
x:Uid="RenameFailedToast"
x:Load="False"
IsLightDismissEnabled="True" />
<mux:TeachingTip x:Name="WindowRenamer"
x:Uid="WindowRenamer"
Title="{x:Bind WindowIdForDisplay}"
x:Load="False"
ActionButtonClick="_WindowRenamerActionClick">
<mux:TeachingTip.Content>
<TextBox x:Name="WindowRenamerTextBox"
Text="{x:Bind WindowName, Mode=OneWay}" />
</mux:TeachingTip.Content>
</mux:TeachingTip>
</Grid>
</Page>

View File

@ -55,6 +55,8 @@ static constexpr std::string_view TogglePaneReadOnlyKey{ "toggleReadOnlyMode" };
static constexpr std::string_view NewWindowKey{ "newWindow" };
static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" };
static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" };
static constexpr std::string_view RenameWindowKey{ "renameWindow" };
static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" };
static constexpr std::string_view ActionKey{ "action" };
@ -123,6 +125,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ NewWindowKey, ShortcutAction::NewWindow },
{ IdentifyWindowKey, ShortcutAction::IdentifyWindow },
{ IdentifyWindowsKey, ShortcutAction::IdentifyWindows },
{ RenameWindowKey, ShortcutAction::RenameWindow },
{ OpenWindowRenamerKey, ShortcutAction::OpenWindowRenamer },
};
using ParseResult = std::tuple<IActionArgs, std::vector<SettingsLoadWarnings>>;
@ -157,6 +161,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::NewWindow, NewWindowArgs::FromJson },
{ ShortcutAction::PrevTab, PrevTabArgs::FromJson },
{ ShortcutAction::NextTab, NextTabArgs::FromJson },
{ ShortcutAction::RenameWindow, RenameWindowArgs::FromJson },
{ ShortcutAction::Invalid, nullptr },
};
@ -330,6 +335,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::NewWindow, RS_(L"NewWindowCommandKey") },
{ ShortcutAction::IdentifyWindow, RS_(L"IdentifyWindowCommandKey") },
{ ShortcutAction::IdentifyWindows, RS_(L"IdentifyWindowsCommandKey") },
{ ShortcutAction::RenameWindow, RS_(L"ResetWindowNameCommandKey") },
{ ShortcutAction::OpenWindowRenamer, RS_(L"OpenWindowRenamerCommandKey") },
};
}();

View File

@ -28,6 +28,7 @@
#include "NewWindowArgs.g.cpp"
#include "PrevTabArgs.g.cpp"
#include "NextTabArgs.g.cpp"
#include "RenameWindowArgs.g.cpp"
#include <LibraryResources.h>
@ -567,4 +568,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
const auto mode = _SwitcherMode.Value() == TabSwitcherMode::MostRecentlyUsed ? L"most recently used" : L"in order";
return winrt::hstring(fmt::format(L"{}, {}", RS_(L"NextTabCommandKey"), mode));
}
winrt::hstring RenameWindowArgs::GenerateName() const
{
// "Rename window to \"{_Name}\""
// "Clear window name"
if (!_Name.empty())
{
return winrt::hstring{
fmt::format(std::wstring_view(RS_(L"RenameWindowCommandKey")),
_Name.c_str())
};
}
return RS_(L"ResetWindowNameCommandKey");
}
}

View File

@ -30,6 +30,7 @@
#include "NewWindowArgs.g.h"
#include "PrevTabArgs.g.h"
#include "NextTabArgs.g.h"
#include "RenameWindowArgs.g.h"
#include "../../cascadia/inc/cppwinrt_utils.h"
#include "JsonUtils.h"
@ -1001,6 +1002,39 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return *copy;
}
};
struct RenameWindowArgs : public RenameWindowArgsT<RenameWindowArgs>
{
RenameWindowArgs() = default;
WINRT_PROPERTY(winrt::hstring, Name);
static constexpr std::string_view NameKey{ "name" };
public:
hstring GenerateName() const;
bool Equals(const IActionArgs& other)
{
auto otherAsUs = other.try_as<RenameWindowArgs>();
if (otherAsUs)
{
return otherAsUs->_Name == _Name;
}
return false;
};
static FromJsonResult FromJson(const Json::Value& json)
{
// LOAD BEARING: Not using make_self here _will_ break you in the future!
auto args = winrt::make_self<RenameWindowArgs>();
JsonUtils::GetValueForKey(json, NameKey, args->_Name);
return { *args, {} };
}
IActionArgs Copy() const
{
auto copy{ winrt::make_self<RenameWindowArgs>() };
copy->_Name = _Name;
return *copy;
}
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation

View File

@ -246,4 +246,9 @@ namespace Microsoft.Terminal.Settings.Model
{
Windows.Foundation.IReference<TabSwitcherMode> SwitcherMode;
};
[default_interface] runtimeclass RenameWindowArgs : IActionArgs
{
String Name { get; };
};
}

View File

@ -70,4 +70,6 @@
ON_ALL_ACTIONS(FindMatch) \
ON_ALL_ACTIONS(NewWindow) \
ON_ALL_ACTIONS(IdentifyWindow) \
ON_ALL_ACTIONS(IdentifyWindows)
ON_ALL_ACTIONS(IdentifyWindows) \
ON_ALL_ACTIONS(RenameWindow) \
ON_ALL_ACTIONS(OpenWindowRenamer)

View File

@ -381,4 +381,14 @@
<data name="OpenSettingsUICommandKey" xml:space="preserve">
<value>Open Settings...</value>
</data>
<data name="RenameWindowCommandKey" xml:space="preserve">
<value>Rename window to "{0}"</value>
<comment>{0} will be replaced with a user-defined string</comment>
</data>
<data name="ResetWindowNameCommandKey" xml:space="preserve">
<value>Reset window name</value>
</data>
<data name="OpenWindowRenamerCommandKey" xml:space="preserve">
<value>Rename window...</value>
</data>
</root>

View File

@ -295,8 +295,8 @@
{ "command": "renameTab" },
{ "command": "openTabRenamer" },
{ "command": "commandPalette", "keys":"ctrl+shift+p" },
{ "command": "identifyWindow" },
{ "command": "openWindowRenamer" },
// Tab Management
// "command": "closeTab" is unbound by default.

View File

@ -59,10 +59,12 @@ namespace RemotingUnitTests
void DisplayWindowId() { throw winrt::hresult_error{}; };
Remoting::CommandlineArgs InitialArgs() { throw winrt::hresult_error{}; }
Remoting::WindowActivatedArgs GetLastActivatedArgs() { throw winrt::hresult_error{}; }
void RequestRename(const Remoting::RenameRequestArgs& /*args*/) { throw winrt::hresult_error{}; }
TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs);
TYPED_EVENT(ExecuteCommandlineRequested, winrt::Windows::Foundation::IInspectable, Remoting::CommandlineArgs);
TYPED_EVENT(IdentifyWindowsRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, Remoting::RenameRequestArgs);
};
class RemotingTests
@ -99,6 +101,10 @@ namespace RemotingUnitTests
TEST_METHOD(ProposeCommandlineForNamedDeadWindow);
TEST_METHOD(TestRenameWindowSuccessfully);
TEST_METHOD(TestRenameSameNameAsAnother);
TEST_METHOD(TestRenameSameNameAsADeadPeasant);
TEST_CLASS_SETUP(ClassSetup)
{
return true;
@ -1212,6 +1218,7 @@ namespace RemotingUnitTests
VERIFY_ARE_EQUAL(1u, m0->_peasants.size());
VERIFY_ARE_EQUAL(p2->GetID(), m0->_lookupPeasantIdForName(L"two"));
}
void RemotingTests::GetMruPeasantAfterNameLookupForDeadPeasant()
{
// This test is trying to hit the catch in Monarch::_lookupPeasantIdForName.
@ -1356,4 +1363,147 @@ namespace RemotingUnitTests
VERIFY_ARE_EQUAL(L"two", result.WindowName());
}
}
void RemotingTests::TestRenameWindowSuccessfully()
{
Log::Comment(L"Attempt to rename a window. This should succeed.");
const auto monarch0PID = 12345u;
const auto peasant1PID = 23456u;
const auto peasant2PID = 34567u;
com_ptr<Remoting::implementation::Monarch> m0;
m0.attach(new Remoting::implementation::Monarch(monarch0PID));
com_ptr<Remoting::implementation::Peasant> p1;
p1.attach(new Remoting::implementation::Peasant(peasant1PID));
com_ptr<Remoting::implementation::Peasant> p2;
p2.attach(new Remoting::implementation::Peasant(peasant2PID));
VERIFY_IS_NOT_NULL(m0);
VERIFY_IS_NOT_NULL(p1);
VERIFY_IS_NOT_NULL(p2);
p1->WindowName(L"one");
p2->WindowName(L"two");
VERIFY_ARE_EQUAL(0, p1->GetID());
VERIFY_ARE_EQUAL(0, p2->GetID());
m0->AddPeasant(*p1);
m0->AddPeasant(*p2);
VERIFY_ARE_EQUAL(1, p1->GetID());
VERIFY_ARE_EQUAL(2, p2->GetID());
VERIFY_ARE_EQUAL(2u, m0->_peasants.size());
Remoting::RenameRequestArgs eventArgs{ L"foo" };
p1->RequestRename(eventArgs);
VERIFY_IS_TRUE(eventArgs.Succeeded());
VERIFY_ARE_EQUAL(L"foo", p1->WindowName());
VERIFY_ARE_EQUAL(0, m0->_lookupPeasantIdForName(L"one"));
VERIFY_ARE_EQUAL(p2->GetID(), m0->_lookupPeasantIdForName(L"two"));
VERIFY_ARE_EQUAL(p1->GetID(), m0->_lookupPeasantIdForName(L"foo"));
}
void RemotingTests::TestRenameSameNameAsAnother()
{
Log::Comment(L"Try renaming a window to a name used by another peasant."
L" This should fail.");
const auto monarch0PID = 12345u;
const auto peasant1PID = 23456u;
const auto peasant2PID = 34567u;
com_ptr<Remoting::implementation::Monarch> m0;
m0.attach(new Remoting::implementation::Monarch(monarch0PID));
com_ptr<Remoting::implementation::Peasant> p1;
p1.attach(new Remoting::implementation::Peasant(peasant1PID));
com_ptr<Remoting::implementation::Peasant> p2;
p2.attach(new Remoting::implementation::Peasant(peasant2PID));
VERIFY_IS_NOT_NULL(m0);
VERIFY_IS_NOT_NULL(p1);
VERIFY_IS_NOT_NULL(p2);
p1->WindowName(L"one");
p2->WindowName(L"two");
VERIFY_ARE_EQUAL(0, p1->GetID());
VERIFY_ARE_EQUAL(0, p2->GetID());
m0->AddPeasant(*p1);
m0->AddPeasant(*p2);
VERIFY_ARE_EQUAL(1, p1->GetID());
VERIFY_ARE_EQUAL(2, p2->GetID());
VERIFY_ARE_EQUAL(2u, m0->_peasants.size());
Remoting::RenameRequestArgs eventArgs{ L"two" };
p1->RequestRename(eventArgs);
VERIFY_IS_FALSE(eventArgs.Succeeded());
VERIFY_ARE_EQUAL(L"one", p1->WindowName());
VERIFY_ARE_EQUAL(p1->GetID(), m0->_lookupPeasantIdForName(L"one"));
VERIFY_ARE_EQUAL(p2->GetID(), m0->_lookupPeasantIdForName(L"two"));
}
void RemotingTests::TestRenameSameNameAsADeadPeasant()
{
Log::Comment(L"We'll try renaming a window to the name of a window that"
L" has died. This should succeed, without crashing.");
const auto monarch0PID = 12345u;
const auto peasant1PID = 23456u;
const auto peasant2PID = 34567u;
com_ptr<Remoting::implementation::Monarch> m0;
m0.attach(new Remoting::implementation::Monarch(monarch0PID));
com_ptr<Remoting::implementation::Peasant> p1;
p1.attach(new Remoting::implementation::Peasant(peasant1PID));
com_ptr<Remoting::implementation::Peasant> p2;
p2.attach(new Remoting::implementation::Peasant(peasant2PID));
VERIFY_IS_NOT_NULL(m0);
VERIFY_IS_NOT_NULL(p1);
VERIFY_IS_NOT_NULL(p2);
p1->WindowName(L"one");
p2->WindowName(L"two");
VERIFY_ARE_EQUAL(0, p1->GetID());
VERIFY_ARE_EQUAL(0, p2->GetID());
m0->AddPeasant(*p1);
m0->AddPeasant(*p2);
VERIFY_ARE_EQUAL(1, p1->GetID());
VERIFY_ARE_EQUAL(2, p2->GetID());
VERIFY_ARE_EQUAL(2u, m0->_peasants.size());
Remoting::RenameRequestArgs eventArgs{ L"two" };
p1->RequestRename(eventArgs);
VERIFY_IS_FALSE(eventArgs.Succeeded());
VERIFY_ARE_EQUAL(L"one", p1->WindowName());
VERIFY_ARE_EQUAL(p1->GetID(), m0->_lookupPeasantIdForName(L"one"));
VERIFY_ARE_EQUAL(p2->GetID(), m0->_lookupPeasantIdForName(L"two"));
RemotingTests::_killPeasant(m0, p2->GetID());
p1->RequestRename(eventArgs);
VERIFY_IS_TRUE(eventArgs.Succeeded());
VERIFY_ARE_EQUAL(L"two", p1->WindowName());
VERIFY_ARE_EQUAL(p1->GetID(), m0->_lookupPeasantIdForName(L"two"));
}
}

View File

@ -250,6 +250,7 @@ void AppHost::Initialize()
_logic.LastTabClosed({ this, &AppHost::LastTabClosed });
_logic.SetTaskbarProgress({ this, &AppHost::SetTaskbarProgress });
_logic.IdentifyWindowsRequested({ this, &AppHost::_IdentifyWindowsRequested });
_logic.RenameWindowRequested({ this, &AppHost::_RenameWindowRequested });
_window->UpdateTitle(_logic.Title());
@ -618,8 +619,8 @@ GUID AppHost::_CurrentDesktopGuid()
// - <unused>
// Return Value:
// - <none>
winrt::fire_and_forget AppHost::_IdentifyWindowsRequested(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const winrt::Windows::Foundation::IInspectable& /*args*/)
winrt::fire_and_forget AppHost::_IdentifyWindowsRequested(const winrt::Windows::Foundation::IInspectable /*sender*/,
const winrt::Windows::Foundation::IInspectable /*args*/)
{
// We'll be raising an event that may result in a RPC call to the monarch -
// make sure we're on the background thread, or this will silently fail
@ -643,3 +644,33 @@ void AppHost::_DisplayWindowId(const winrt::Windows::Foundation::IInspectable& /
{
_logic.IdentifyWindow();
}
winrt::fire_and_forget AppHost::_RenameWindowRequested(const winrt::Windows::Foundation::IInspectable /*sender*/,
const winrt::TerminalApp::RenameWindowRequestedArgs args)
{
// Capture calling context.
winrt::apartment_context ui_thread;
// Switch to the BG thread - anything x-proc must happen on a BG thread
co_await winrt::resume_background();
if (auto peasant{ _windowManager.CurrentWindow() })
{
Remoting::RenameRequestArgs requestArgs{ args.ProposedName() };
peasant.RequestRename(requestArgs);
// Switch back to the UI thread. Setting the WindowName needs to happen
// on the UI thread, because it'll raise a PropertyChanged event
co_await ui_thread;
if (requestArgs.Succeeded())
{
_logic.WindowName(args.ProposedName());
}
else
{
_logic.RenameFailed();
}
}
}

View File

@ -51,9 +51,12 @@ private:
void _FindTargetWindow(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs& args);
winrt::fire_and_forget _IdentifyWindowsRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
winrt::fire_and_forget _IdentifyWindowsRequested(const winrt::Windows::Foundation::IInspectable sender,
const winrt::Windows::Foundation::IInspectable args);
void _DisplayWindowId(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
winrt::fire_and_forget _RenameWindowRequested(const winrt::Windows::Foundation::IInspectable sender,
const winrt::TerminalApp::RenameWindowRequestedArgs args);
GUID _CurrentDesktopGuid();
};

View File

@ -62,7 +62,7 @@ running this on save, but you can add a `git` hook to format before committing
XSTYLER_PATH="dotnet tool run xstyler --"
# Define path to XAML Styler configuration
XSTYLER_CONFIG="../../XamlStyler.json"
XSTYLER_CONFIG="XamlStyler.json"
echo "Running XAML Styler on committed XAML files"
git diff --cached --name-only --diff-filter=ACM | grep -e '\.xaml$' | \