Skip to content

Commit

Permalink
Make Actions page editable
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-zamora committed Apr 25, 2021
1 parent 0564cae commit 9001102
Show file tree
Hide file tree
Showing 14 changed files with 597 additions and 172 deletions.
223 changes: 199 additions & 24 deletions src/cascadia/TerminalSettingsEditor/Actions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,236 @@
#include "pch.h"
#include "Actions.h"
#include "Actions.g.cpp"
#include "KeyBindingViewModel.g.cpp"
#include "ActionsPageNavigationState.g.cpp"
#include "EnumEntry.h"
#include "LibraryResources.h"

using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Data;
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Microsoft::Terminal::Settings::Model;

namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const Command& cmd) :
_Keys{ keys },
_KeyChordText{ Model::KeyChordSerialization::ToString(keys) },
_Command{ cmd }
{
// Add a property changed handler to our own property changed event.
// This propagates changes from the settings model to anybody listening to our
// unique view model members.
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"Keys")
{
_KeyChordText = Model::KeyChordSerialization::ToString(_Keys);
_NotifyChanges(L"KeyChordText");
}
});
}

void KeyBindingViewModel::ToggleEditMode()
{
// toggle edit mode
IsInEditMode(!_IsInEditMode);
if (_IsInEditMode)
{
// if we're in edit mode,
// pre-populate the text box with the current keys
_ProposedKeys = KeyChordText();
_NotifyChanges(L"ProposedKeys");
}
}

void KeyBindingViewModel::AttemptAcceptChanges()
{
auto args{ make_self<RebindKeysEventArgs>(_Keys, _Keys) };
try
{
// Attempt to convert the provided key chord text
const auto newKeyChord{ KeyChordSerialization::FromString(_ProposedKeys) };
args->NewKeys(newKeyChord);
}
catch (hresult_invalid_argument)
{
// Converting the text into a key chord failed
// TODO CARLOS:
// This is tricky. I still haven't found a way to reference the
// key chord text box. It's hidden behind the data template.
// Ideally, some kind of notification would alert the user, but
// to make it look nice, we need it to somehow target the text box.
}
_RebindKeysRequestedHandlers(*this, *args);
}

Actions::Actions()
{
InitializeComponent();

_filteredActions = winrt::single_threaded_observable_vector<Command>();
}

void Actions::OnNavigatedTo(const NavigationEventArgs& e)
{
_State = e.Parameter().as<Editor::ActionsPageNavigationState>();

std::vector<Command> keyBindingList;
for (const auto& [_, command] : _State.Settings().GlobalSettings().ActionMap().NameMap())
// Convert the key bindings from our settings into a view model representation
const auto& keyBindingMap{ _State.Settings().ActionMap().KeyBindings() };
std::vector<Editor::KeyBindingViewModel> keyBindingList;
keyBindingList.reserve(keyBindingMap.Size());
for (const auto& [keys, cmd] : keyBindingMap)
{
// Filter out nested commands, and commands that aren't bound to a
// key. This page is currently just for displaying the actions that
// _are_ bound to keys.
if (command.HasNestedCommands() || !command.Keys())
const auto& container{ make<KeyBindingViewModel>(keys, cmd) };
container.PropertyChanged({ this, &Actions::_ViewModelPropertyChangedHandler });
container.DeleteKeyBindingRequested({ this, &Actions::_ViewModelDeleteKeyBindingHandler });
container.RebindKeysRequested({ this, &Actions::_ViewModelRebindKeysHandler });
keyBindingList.push_back(container);
}

std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{});
_KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList));
}

void Actions::_ViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args)
{
const auto senderVM{ sender.as<Editor::KeyBindingViewModel>() };
const auto propertyName{ args.PropertyName() };
if (propertyName == L"IsInEditMode")
{
if (senderVM.IsInEditMode())
{
// make sure this is the only VM in edit mode
for (const auto& kbdVM : _KeyBindingList)
{
if (senderVM != kbdVM && kbdVM.IsInEditMode())
{
kbdVM.ToggleEditMode();
}
}
}
else
{
continue;
// Focus on the list view item
KeyBindingsListView().ContainerFromItem(senderVM).as<Controls::Control>().Focus(FocusState::Programmatic);
}
keyBindingList.push_back(command);
}
std::sort(begin(keyBindingList), end(keyBindingList), CommandComparator{});
_filteredActions = single_threaded_observable_vector<Command>(std::move(keyBindingList));
}

Collections::IObservableVector<Command> Actions::FilteredActions()
void Actions::_ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& /*senderVM*/, const Control::KeyChord& keys)
{
return _filteredActions;
// Update the settings model
_State.Settings().ActionMap().DeleteKeyBinding(keys);

// Find the current container in our list and remove it.
// This is much faster than rebuilding the entire ActionMap.
if (const auto index{ _GetContainerIndexByKeyChord(keys) })
{
_KeyBindingList.RemoveAt(*index);

// Focus the new item at this index
if (_KeyBindingList.Size() != 0)
{
const auto newFocusedIndex{ std::clamp(*index, 0u, _KeyBindingList.Size() - 1) };
KeyBindingsListView().ContainerFromIndex(newFocusedIndex).as<Controls::Control>().Focus(FocusState::Programmatic);
}
}
}

void Actions::_OpenSettingsClick(const IInspectable& /*sender*/,
const Windows::UI::Xaml::RoutedEventArgs& /*eventArgs*/)
void Actions::_ViewModelRebindKeysHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::RebindKeysEventArgs& args)
{
const CoreWindow window = CoreWindow::GetForCurrentThread();
const auto rAltState = window.GetKeyState(VirtualKey::RightMenu);
const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu);
const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) ||
WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down);
if (args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
// We're actually changing the key chord
const auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
const auto& conflictingCmd{ _State.Settings().ActionMap().GetActionByKeyChord(args.NewKeys()) };
if (conflictingCmd)
{
// We're about to overwrite another key chord.
// Display a confirmation dialog.
const auto& conflictingCmdName{ conflictingCmd.Name() };
if (!conflictingCmdName.empty())
{
TextBlock errorMessageTB{};
errorMessageTB.Text(RS_(L"Actions_RenameConflictConfirmationMessage"));

TextBlock conflictingCommandNameTB{};
conflictingCommandNameTB.Text(fmt::format(L"\"{}\"", conflictingCmdName));

TextBlock confirmationQuestionTB{};
confirmationQuestionTB.Text(RS_(L"Actions_RenameConflictConfirmationQuestion"));

Button acceptBtn{};
acceptBtn.Content(box_value(RS_(L"Actions_RenameConflictConfirmationAcceptButton")));
acceptBtn.Click([=](auto&, auto&) {
// remove conflicting key binding from list view
const auto containerIndex{ _GetContainerIndexByKeyChord(args.NewKeys()) };
_KeyBindingList.RemoveAt(*containerIndex);

// remove flyout
senderVM.AcceptChangesFlyout().Hide();
senderVM.AcceptChangesFlyout(nullptr);

// update settings model and view model
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
senderVMImpl->Keys(args.NewKeys());
senderVM.ToggleEditMode();
});

const auto target = altPressed ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile;
StackPanel flyoutStack{};
flyoutStack.Children().Append(errorMessageTB);
flyoutStack.Children().Append(conflictingCommandNameTB);
flyoutStack.Children().Append(confirmationQuestionTB);
flyoutStack.Children().Append(acceptBtn);

_State.RequestOpenJson(target);
Flyout acceptChangesFlyout{};
acceptChangesFlyout.Content(flyoutStack);
senderVM.AcceptChangesFlyout(acceptChangesFlyout);
return;
}
else
{
// TODO CARLOS: this command doesn't have a name. Not sure what to display here.
}
}
else
{
// update settings model
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());

// update view model (keys)
senderVMImpl->Keys(args.NewKeys());
}
}

// update view model (exit edit mode)
senderVM.ToggleEditMode();
}

// Method Desctiption:
// - performs a search on KeyBindingList by key chord.
// Arguments:
// - keys - the associated key chord of the command we're looking for
// Return Value:
// - the index of the view model referencing the command. If the command doesn't exist, nullopt
std::optional<uint32_t> Actions::_GetContainerIndexByKeyChord(const Control::KeyChord& keys)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto kbdVM{ get_self<KeyBindingViewModel>(_KeyBindingList.GetAt(i)) };
const auto& otherKeys{ kbdVM->Keys() };
if (keys.Modifiers() == otherKeys.Modifiers() && keys.Vkey() == otherKeys.Vkey())
{
return i;
}
}

// TODO CARLOS:
// an expedited search can be done if we use cmd.Name()
// to quickly search through the sorted list.
return std::nullopt;
}
}
60 changes: 47 additions & 13 deletions src/cascadia/TerminalSettingsEditor/Actions.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,65 @@
#pragma once

#include "Actions.g.h"
#include "KeyBindingViewModel.g.h"
#include "ActionsPageNavigationState.g.h"
#include "RebindKeysEventArgs.g.h"
#include "Utils.h"
#include "ViewModelHelpers.h"

namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct CommandComparator
struct KeyBindingViewModelComparator
{
bool operator()(const Model::Command& lhs, const Model::Command& rhs) const
bool operator()(const Editor::KeyBindingViewModel& lhs, const Editor::KeyBindingViewModel& rhs) const
{
return lhs.Name() < rhs.Name();
}
};

struct RebindKeysEventArgs : RebindKeysEventArgsT<RebindKeysEventArgs>
{
public:
RebindKeysEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys) :
_OldKeys{ oldKeys },
_NewKeys{ newKeys } {}

WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr);
WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr);
};

struct KeyBindingViewModel : KeyBindingViewModelT<KeyBindingViewModel>, ViewModelHelper<KeyBindingViewModel>
{
public:
KeyBindingViewModel(const Control::KeyChord& keys, const Settings::Model::Command& cmd);

hstring Name() const { return _Command.Name(); }
hstring KeyChordText() const { return _KeyChordText; }

void ToggleEditMode();
void DisableEditMode() { IsInEditMode(false); }
void AttemptAcceptChanges();
void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _Keys); }

VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, ProposedKeys);
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, Keys, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Controls::Flyout, AcceptChangesFlyout, nullptr);
TYPED_EVENT(RebindKeysRequested, Editor::KeyBindingViewModel, Editor::RebindKeysEventArgs);
TYPED_EVENT(DeleteKeyBindingRequested, Editor::KeyBindingViewModel, Terminal::Control::KeyChord);

private:
Settings::Model::Command _Command{ nullptr };
hstring _KeyChordText {};
};

struct ActionsPageNavigationState : ActionsPageNavigationStateT<ActionsPageNavigationState>
{
public:
ActionsPageNavigationState(const Model::CascadiaSettings& settings) :
_Settings{ settings } {}

void RequestOpenJson(const Model::SettingsTarget target)
{
_OpenJsonHandlers(nullptr, target);
}

WINRT_PROPERTY(Model::CascadiaSettings, Settings, nullptr)
TYPED_EVENT(OpenJson, Windows::Foundation::IInspectable, Model::SettingsTarget);
};

struct Actions : ActionsT<Actions>
Expand All @@ -39,15 +72,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation

void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e);

Windows::Foundation::Collections::IObservableVector<winrt::Microsoft::Terminal::Settings::Model::Command> FilteredActions();

WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_PROPERTY(Editor::ActionsPageNavigationState, State, nullptr);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::KeyBindingViewModel>, KeyBindingList);

private:
friend struct ActionsT<Actions>; // for Xaml to bind events
Windows::Foundation::Collections::IObservableVector<winrt::Microsoft::Terminal::Settings::Model::Command> _filteredActions{ nullptr };
void _ViewModelPropertyChangedHandler(const Windows::Foundation::IInspectable& senderVM, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
void _ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& args);
void _ViewModelRebindKeysHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::RebindKeysEventArgs& args);

void _OpenSettingsClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs);
std::optional<uint32_t> _GetContainerIndexByKeyChord(const Control::KeyChord& keys);
};
}

Expand Down
Loading

1 comment on commit 9001102

@github-actions

This comment was marked as duplicate.

Please sign in to comment.