Files
terminal-microsoft/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
Carlos Zamora 0d846aeb4d Add New Tab Menu Customization to Settings UI (#18015)
## Summary of the Pull Request
Adds customization for the New Tab Menu to the settings UI.

- Settings Model changes:
- The Settings UI generally works by creating a copy of the entire
settings model objects on which we apply the changes to. Turns out, we
completely left the NewTabMenu out of that process. So I went ahead and
implemented it.
   -  `FolderEntry`
- `FolderEntry` exposes `Entries()` (used by the new tab menu to figure
out what to actually render) and `RawEntries()` (the actual JSON data
deserialized into settings model objects). I went ahead and exposed
`RawEntries()` since we'll need to apply changes to it to then
serialize.
- View Model:
- `NewTabMenuViewModel` is the main view model that interacts with the
page. It maintains the current view of items and applies changes to the
settings model.
- `NewTabMenuEntryViewModel` and all of the other `_EntryViewModel`
classes are wrappers for the settings model NTM entries.
- `FolderTreeViewEntry` encapsulates `FolderEntryViewModel`. It allows
us to construct a `TreeView` of just folders.
- View changes and additions:
   - Added FontIconGlyph to the SettingContainer
   - Added a New Tab Menu item to the navigation view
- Adding entries: a stack of SettingContainers is used here. We use the
new `FontIconGlyph` to make this look nice!
- Reordering entries: drag and drop is supported! This might not work in
admin mode though, and we can't drag and drop into folders. Buttons were
added to make this keyboard accessible.
- To move entries into a folder, a button was added which then displays
a TreeView of all folders.
   - Multiple entries can be moved to a folder or deleted at once!
   - Breadcrumbs are used for folders
- When a folder is entered, additional controls are displayed to
customize that folder.
 
## Verification
-  a11y pass
-  keyboard accessible
- scenarios:
   -  add entries (except actions)
   -  changes propagated to settings model (aka "saving works")
   -  reorder entries
   -  move entries to an existing folder
   -  delete multiple entries
   -  delete individual entries
   -  display entries (including actions)

## Follow-ups
- [ ] add support for adding and editing action entries
- [ ] when we discard changes or save, it would be cool if we could stay
on the same page
- [ ] allow customizing the folder entry _before_ adding it (current
workaround is to add it, then edit it)
- [ ] improve UI for setting icon (reuse UI from #17965)
2024-12-03 15:07:13 -08:00

145 lines
4.9 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "FolderEntry.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "FolderEntry.g.cpp"
using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Windows::Foundation::Collections;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view IconKey{ "icon" };
static constexpr std::string_view EntriesKey{ "entries" };
static constexpr std::string_view InliningKey{ "inline" };
static constexpr std::string_view AllowEmptyKey{ "allowEmpty" };
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
FolderEntry::FolderEntry() noexcept :
FolderEntry{ winrt::hstring{} }
{
}
FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
FolderEntryT<FolderEntry, NewTabMenuEntry>(NewTabMenuEntryType::Folder),
_Name{ name }
{
}
Json::Value FolderEntry::ToJson() const
{
auto json = NewTabMenuEntry::ToJson();
JsonUtils::SetValueForKey(json, NameKey, _Name);
JsonUtils::SetValueForKey(json, IconKey, _Icon);
JsonUtils::SetValueForKey(json, EntriesKey, _RawEntries);
JsonUtils::SetValueForKey(json, InliningKey, _Inlining);
JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty);
return json;
}
winrt::com_ptr<NewTabMenuEntry> FolderEntry::FromJson(const Json::Value& json)
{
auto entry = winrt::make_self<FolderEntry>();
JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
JsonUtils::GetValueForKey(json, EntriesKey, entry->_RawEntries);
JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining);
JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty);
return entry;
}
// A FolderEntry should only expose the entries to actually render to WinRT,
// to keep the logic for collapsing/expanding more centralised.
IVector<Model::NewTabMenuEntry> FolderEntry::Entries() const
{
// We filter the full list of entries from JSON to just include the
// non-empty ones.
IVector<Model::NewTabMenuEntry> result{ winrt::single_threaded_vector<Model::NewTabMenuEntry>() };
if (_RawEntries == nullptr)
{
return result;
}
for (const auto& entry : _RawEntries)
{
if (entry == nullptr)
{
continue;
}
switch (entry.Type())
{
case NewTabMenuEntryType::Invalid:
continue;
// A profile is filtered out if it is not valid, so if it was not resolved
case NewTabMenuEntryType::Profile:
{
const auto profileEntry = entry.as<Model::ProfileEntry>();
if (profileEntry.Profile() == nullptr)
{
continue;
}
break;
}
// Any profile collection is filtered out if there are no results
case NewTabMenuEntryType::RemainingProfiles:
case NewTabMenuEntryType::MatchProfiles:
{
const auto profileCollectionEntry = entry.as<Model::ProfileCollectionEntry>();
if (profileCollectionEntry.Profiles().Size() == 0)
{
continue;
}
break;
}
// A folder is filtered out if it has an effective size of 0 (calling
// this filtering method recursively), and if it is not allowed to be
// empty, or if it should auto-inline.
case NewTabMenuEntryType::Folder:
{
const auto folderEntry = entry.as<Model::FolderEntry>();
if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
{
continue;
}
break;
}
}
result.Append(entry);
}
return result;
}
Model::NewTabMenuEntry FolderEntry::Copy() const
{
auto entry = winrt::make_self<FolderEntry>();
entry->_Name = _Name;
entry->_Icon = _Icon;
entry->_Inlining = _Inlining;
entry->_AllowEmpty = _AllowEmpty;
if (_RawEntries)
{
entry->_RawEntries = winrt::single_threaded_vector<Model::NewTabMenuEntry>();
for (const auto& e : _RawEntries)
{
entry->_RawEntries.Append(get_self<NewTabMenuEntry>(e)->Copy());
}
}
return *entry;
}
}