terminal/doc/specs/#1571 - New Tab Menu Custom.../#1571 - New Tab Menu Custom...

12 KiB

author created on last updated issue id
Mike Griese @zadjii-msft 2020-5-13 2020-08-04 1571

New Tab Menu Customization

Abstract

Many users have lots and lots of profiles that they use. Some of these profiles the user might not use that frequently. When that happens, the new tab dropdown can become quite cluttered.

A common ask is for the ability to reorder and reorganize this dropdown. This spec provides a design for how the user might be able to specify the customization in their settings.

Inspiration

Largely, this spec was inspired by discussion in #1571 and the many linked threads.

Solution Design

This design proposes adding a new setting "newTabMenu". When unset, (the default), the new tab menu is populated with all the profiles, in the order they appear in the users settings file. When set, this enables the user to control the appearance of the new tab dropdown. Let's take a look at an example:

{
    "profiles":{ ... },
    "newTabMenu": [
        { "type":"profile", "profile": "cmd" },
        { "type":"profile", "profile": "Windows PowerShell" },
        { "type":"separator" },
        {
            "type":"folder",
            "name": "ssh",
            "icon": "C:\\path\\to\\icon.png",
            "entries":[
                { "type":"profile", "profile": "Host 1" },
                { "type":"profile", "profile": "8.8.8.8" },
                { "type":"profile", "profile": "Host 2" }
            ]
        },
        { "type":"separator" },
        { "type":"profile", "profile": "Ubuntu-18.04" },
        { "type":"profile", "profile": "Fedora" }
    ]
}

If a user were to use this as their new tab menu, that they would get is a menu that looks like this:

fig 1

fig 1: A very rough mockup of what this feature might look like

There are five types of objects in this menu:

  • "type":"profile": This is a profile. Clicking on this entry will open a new tab, with that profile. The profile is identified with the "profile" parameter, which accepts either a profile name or GUID. The icon for this entry will be the profile's icon, and the text on the entry will be the profile's name.
  • "type":"separator": This represents a XAML MenuFlyoutSeparator, enabling the user to visually space out entries.
  • "type":"folder": This represents a nested menu of entries.
    • The "name" property provides a string of text to display for the group.
    • The "icon" property provides a path to a image to use as the icon. This property is optional.
    • The "entries" property specifies a list of menu entries that will appear nested under this entry. This can contain other "type":"folder" groups as well!
  • "type":"action": This represents a menu entry that should execute a specific ShortcutAction.
    • the id property will specify the global action ID (see #6899, #7175) to identify the action to perform when the user selects the entry. Actions with invalid IDs will be ignored and omitted from the list.
    • The text for this entry will be the action's label (which is either provided as the "name" in the global list of actions, or the generated name if no name was provided)
    • The icon for this entry will similarly re-use the action's icon.
  • "type":"remainingProfiles": This is a special type of entry that will be expanded to contain one "type":"profile" entry for every profile that was not already listed in the menu. This will allow users to add one entry for just "all the profiles they haven't manually added to the menu".
    • This type of entry can only be specified once - trying to add it to the menu twice will raise a warning, and ignore all but the first remainingProfiles entry.
    • This type of entry can also be set inside a folder entry, allowing users to highlight only a couple profiles in the top-level of the menu, but enabling all other profiles to also be accessible.
    • The "name" of these entries will simply be the name of the profile
    • The "icon" of these entries will simply be the profile's icon

The "default" new tab menu could be imagined as the following blob of json:

{
    "newTabMenu": [
        { "type":"remainingProfiles" }
    ]
}

Other considerations

Also considered during the investigation for this feature was re-using the list of profiles to expose the structure of the new tab menu. For example, doing something like:

"profiles": {
  "defaults": {},
  "list":
  [
    { "name": "cmd" },
    { "name": "powershell" },
    { "type": "separator" },
    {
      "type": "folder" ,
      "profiles": [
        { "name": "ubuntu" }
      ]
    }
  ]
}

This option was not pursued because we felt that it needlessly complicated the contents of the list of profiles objects. We'd rather have the profiles list exclusively contain Profile objects, and have other elements of the json refer to those profiles. What if someone would like to have an action that opened a new tab with profile index 4, and then they set that action as entry 4 in the profile's list? That would certainly be some sort of unexpected behavior.

Additionally, what if someone wants to have an entry that opens a tab with one pane with one profile in it, and another pane with different profile in it? Or what if they want the same profile to appear twice in the menu?

By overloading the structure of the profiles list, we're forcing all other consumers of the list of profiles to care about the structure of the elements of the list. These other consumers should only really care about the list of profiles, and not necessarily how they're structured in the new tab dropdown. Furthermore, it complicates the list of profiles, by adding actions intermixed with the profiles.

The design chosen in this spec more cleanly separates the responsibilities of the list of profiles and the contents of the new tab menu. This way, each object can be defined independent of the structure of the other.

UI/UX Design

See the above figure 1.

The profile's icon will also appear as the icon on profile entries. If there's a keybinding bound to open a new tab with that profile, then that will also be added to the MenuFlyoutItem as the accelerator text, similar to the text we have nowadays.

Beneath the list of profiles will always be the same "Settings", "Feedback" and "About" entries, separated by a MenuFlyoutSeparator. This is consistent with the UI as it exists with no customization. These entries cannot be removed with this feature, only the list of profiles customized.

Capabilities

Accessibility

This menu will be added to the XAML tree in the same fashion as the current new tab flyout, so there should be no dramatic change here.

Security

(no change expected)

Reliability

(no change expected)

Compatibility

(no change expected)

Performance, Power, and Efficiency

(no change expected)

Potential Issues

Currently, the openTab and splitPane keybindings will accept a index parameter to say either:

  • "Create a new tab/pane with the N'th profile"
  • "Create a new tab/pane with the profile at index N in the new tab dropdown".

These two were previously synonymous, as the N'th profile was always the N'th in the dropdown. However, with this change, we'll be changing the meaning of that argument to mean explicitly the first option - "Open a tab/pane with the N'th profile".

A previous version of this spec considered changing the meaning of that parameter to mean "open the entry at index N", the second option. However, in Command Palette, Addendum 1, we found that naming that command would become unnecessarily complex.

To cover that above scenario, we could consider adding an index parameter to the openNewTabDropdown action. If specified, that would open either the N'th action in the dropdown (ignoring separators), or open the dropdown with the n'th item selected.

The N'th entry in the menu won't always be a profile: it might be a folder with more options, or it might be an action (that might not be opening a new tab/pane at all).

Given all the above scenarios, openNewTabDropdown with an "index":N parameter will behave in the following ways. If the Nth top-level entry in the new tab menu is a:

  • "type":"profile": perform the newTab or splitPane action with that profile.
  • "type":"folder": Focus the first element in the sub menu, so the user could navigate it with the keyboard.
  • "type":"separator": Ignore these when counting top-level entries.
  • "type":"action": Perform the action.

So for example:

New Tab Button ▽
├─ Folder 1
│  └─ Profile A
│  └─ Action B
├─ Separator
├─ Folder 2
│  └─ Profile C
│  └─ Profile D
├─ Action E
└─ Profile F

And assuming the user has bound:

{
  "bindings":
  [
    { "command": { "action": "openNewTabDropdown", "index": 0 }, "keys": "ctrl+shift+1" },
    { "command": { "action": "openNewTabDropdown", "index": 1 }, "keys": "ctrl+shift+2" },
    { "command": { "action": "openNewTabDropdown", "index": 2 }, "keys": "ctrl+shift+3" },
    { "command": { "action": "openNewTabDropdown", "index": 3 }, "keys": "ctrl+shift+4" },
  ]
}
  • ctrl+shift+1 focuses "Profile A", but the user needs to press enter/space to creates a new tab/split
  • ctrl+shift+2 focuses "Profile C", but the user needs to press enter/space to creates a new tab/split
  • ctrl+shift+3 performs Action E
  • ctrl+shift+4 Creates a new tab/split with Profile F

Future considerations

  • The user could set a "name"/"text", or "icon" property to these menu items manually, to override the value from the profile or action. These settings would be totally optional, but it's not unreasonable that someone might want this.
  • We may want to consider adding a default icon for all folders or actions in the menu. For example, a folder (like 📁) for folder entries, or something like for actions. We'll leave these unset by default, and evaluate setting these icons by default in the future.
  • Something considered during review was a way to specify "All my WSL profiles". Maybe the user wants to have all their profiles generated by the WSL Distro Generator appear in a "WSL" folder. This would likely require a more elaborate filtering syntax, to be able to select only profiles where a certain property has a specific value. Consider the user who has multiple "SSH me@<some host>.com" profiles, and they want all their "SSH*" profiles to appear in an "SSH" folder. This feels out-of-scope for this spec.
  • A similar structure could potentially also be used for customizing the context menu within a control, or the context menu for the tab. (see #3337)
    • In both of those cases, it might be important to somehow refer to the context of the current tab or control in the json. Think for example about "Close tab" or "Close other tabs" - currently, those work by knowing which tab the "action" is specified for, not by actually using a closeTab action. In the future, they might need to be implemented as something like
      • Close Tab: { "action": "closeTab", "index": "${selectedTab.index}" }
      • Close Other Tabs: { "action": "closeTabs", "otherThan": "${selectedTab.index}" }
      • Close Tabs to the Right: { "action": "closeTabs", "after": "${selectedTab.index}" }