diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index c82254bbb..a96139e3c 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -24,6 +24,7 @@ DTDs DWINRT enablewttlogging Intelli +IVisual LKG LOCKFILE Lxss diff --git a/.github/actions/spelling/patterns/patterns.txt b/.github/actions/spelling/patterns/patterns.txt index 882243396..47f902f90 100644 --- a/.github/actions/spelling/patterns/patterns.txt +++ b/.github/actions/spelling/patterns/patterns.txt @@ -24,3 +24,4 @@ VERIFY_ARE_EQUAL\(L"[^"]+" std::memory_order_[\w]+ D2DERR_SHADER_COMPILE_FAILED TIL_FEATURE_[0-9A-Z_]+ +vcvars\w* diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 293b3dd07..ba3387d84 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1,8 +1,8 @@ { "$id": "https://github.com/microsoft/terminal/blob/main/doc/cascadia/profiles.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Microsoft's Windows Terminal Settings Profile Schema", - "definitions": { + "$defs": { "KeyChordSegment": { "pattern": "^(?:(?:ctrl|alt|shift|win)\\+)*(?:app|backspace|browser_(?:back|forward|refresh|stop|search|favorites|home)|comma|delete|down|end|enter|esc|escape|home|insert|left|menu|minus|pagedown|pageup|period|pgdn|pgup|plus|right|space|tab|up|f(?:1\\d?|2[0-4]?|[3-9])|numpad\\d|numpad_(?:\\d|add|decimal|divide|minus|multiply|period|plus|subtract)|(?:vk|sc)\\((?:[1-9]|1?\\d{2}|2[0-4]\\d|25[0-5])\\)|[^\\s+])(?:\\+(?:ctrl|alt|shift|win))*$", "type": "string", @@ -61,28 +61,42 @@ "type": "string" }, "foreground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#cccccc", "description": "Sets the text color when unfocused. Overrides \"foreground\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "background": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#0c0c0c", "description": "Sets the background color of the text when unfocused. Overrides \"background\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "selectionBackground": { "oneOf": [ - {"$ref": "#/definitions/Color"}, - { "type": "null" } + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the background color of selected text when unfocused. Overrides selectionBackground set in the color scheme. Uses hex color format: \"#rrggbb\"." }, "cursorColor": { "oneOf": [ - { "$ref": "#/definitions/Color" }, - {"type": "null"} + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the color of the cursor when unfocused. Overrides the cursor color from the color scheme. Uses hex color format: \"#rrggbb\"." }, @@ -103,14 +117,20 @@ "description": "Sets the percentage height of the cursor (when unfocused) starting from the bottom. Only works when cursorShape is set to \"vintage\". Accepts values from 1-100.", "maximum": 100, "minimum": 1, - "type": ["integer","null"], + "type": [ + "integer", + "null" + ], "default": 25 }, "backgroundImage": { "description": "Sets the file location of the image to draw over the window background when unfocused.", "oneOf": [ { - "type": ["string", null] + "type": [ + "string", + "null" + ] }, { "enum": [ @@ -118,7 +138,10 @@ ] } ], - "type": [ "string", "null" ] + "type": [ + "string", + "null" + ] }, "backgroundImageOpacity": { "default": 1.0, @@ -220,7 +243,9 @@ "description": "Sets the DWrite font features for the given font. For example, { \"ss01\": 1, \"liga\":0 } will enable ss01 and disable ligatures.", "type": "object", "patternProperties": { - "^(([A-Za-z0-9]){4})$": { "type": "integer" } + "^(([A-Za-z0-9]){4})$": { + "type": "integer" + } }, "additionalProperties": false }, @@ -228,7 +253,9 @@ "description": "Sets the DWrite font axes for the given font. For example, { \"wght\": 200 } will set the font weight to 200.", "type": "object", "patternProperties": { - "^([A-Za-z]{4})$": { "type": "number" } + "^([A-Za-z]{4})$": { + "type": "number" + } }, "additionalProperties": false } @@ -319,7 +346,9 @@ "previous", "nextInOrder", "previousInOrder", - "first" + "first", + "parent", + "child" ], "type": "string" }, @@ -420,7 +449,7 @@ "description": "The index of the profile in the new tab dropdown (starting at 0)" }, "tabColor": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": null, "description": "If provided, will set the tab's color to the given value" }, @@ -436,9 +465,11 @@ }, "type": "object" }, - "SwitchToAdjacentTabArgs" : { + "SwitchToAdjacentTabArgs": { "oneOf": [ - { "type": "null" }, + { + "type": "null" + }, { "enum": [ "mru", @@ -453,7 +484,7 @@ "properties": { "action": { "description": "The action to execute", - "$ref": "#/definitions/ShortcutActionName" + "$ref": "#/$defs/ShortcutActionName" } }, "required": [ @@ -464,10 +495,15 @@ "AdjustFontSizeAction": { "description": "Arguments corresponding to an Adjust Font Size Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "adjustFontSize" }, + "action": { + "type": "string", + "pattern": "adjustFontSize" + }, "delta": { "type": "integer", "default": 0, @@ -476,15 +512,22 @@ } } ], - "required": [ "delta" ] + "required": [ + "delta" + ] }, "CopyAction": { "description": "Arguments corresponding to a Copy Text Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "copy" }, + "action": { + "type": "string", + "pattern": "copy" + }, "singleLine": { "type": "boolean", "default": false, @@ -495,7 +538,7 @@ "description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied. Not setting this value inherits the behavior of the `copyFormatting` global setting.", "oneOf": [ { - "$ref": "#/definitions/CopyFormat" + "$ref": "#/$defs/CopyFormat" }, { "type": "null" @@ -509,11 +552,18 @@ "NewTabAction": { "description": "Arguments corresponding to a New Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, - { "$ref": "#/definitions/NewTerminalArgs" }, + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "$ref": "#/$defs/NewTerminalArgs" + }, { "properties": { - "action": { "type":"string", "pattern": "newTab" } + "action": { + "type": "string", + "pattern": "newTab" + } } } ] @@ -521,27 +571,39 @@ "SwitchToTabAction": { "description": "Arguments corresponding to a Switch To Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "switchToTab" }, + "action": { + "type": "string", + "pattern": "switchToTab" + }, "index": { "type": "integer", "default": 0, "description": "Which tab to switch to, with the first being 0" + } } } - } - ], - "required": [ "index" ] - }, - "MovePaneAction": { + ], + "required": [ + "index" + ] + }, + "MovePaneAction": { "description": "Arguments corresponding to a Move Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "movePane" }, + "action": { + "type": "string", + "pattern": "movePane" + }, "index": { "type": "integer", "default": 0, @@ -550,68 +612,94 @@ } } ], - "required": [ "index" ] + "required": [ + "index" + ] }, "MoveFocusAction": { "description": "Arguments corresponding to a Move Focus Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "moveFocus" }, + "action": { + "type": "string", + "pattern": "moveFocus" + }, "direction": { - "$ref": "#/definitions/FocusDirection", + "$ref": "#/$defs/FocusDirection", "default": "left", - "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." + "description": "The direction to move focus in, between panes. Direction can be 'previous' to move to the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, 'first' to focus the first pane, or 'parent' or 'child' to move up and down the tree." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "SwapPaneAction": { "description": "Arguments corresponding to a Swap Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "swapPane" }, + "action": { + "type": "string", + "pattern": "swapPane" + }, "direction": { - "$ref": "#/definitions/FocusDirection", + "$ref": "#/$defs/FocusDirection", "default": "left", - "description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, or 'nextInOrder' or 'previousInOrder' to move to the next or previous pane." + "description": "The direction to move the focus pane in, swapping panes. Direction can be 'previous' to swap with the most recently used pane, 'nextInOrder' or 'previousInOrder' to move to the next or previous pane, or 'first' to swap with the first pane." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "ResizePaneAction": { "description": "Arguments corresponding to a Resize Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "resizePane" }, + "action": { + "type": "string", + "pattern": "resizePane" + }, "direction": { - "$ref": "#/definitions/ResizeDirection", + "$ref": "#/$defs/ResizeDirection", "default": "left", "description": "The direction to move the pane separator in." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "SendInputAction": { "description": "Arguments corresponding to a Send Input Action", "allOf": [ { - "$ref": "#/definitions/ShortcutAction" + "$ref": "#/$defs/ShortcutAction" }, { "properties": { - "action": { "type": "string", "pattern": "sendInput" }, + "action": { + "type": "string", + "pattern": "sendInput" + }, "input": { "type": "string", "default": "", @@ -620,18 +708,27 @@ } } ], - "required": [ "input" ] + "required": [ + "input" + ] }, "SplitPaneAction": { "description": "Arguments corresponding to a Split Pane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, - { "$ref": "#/definitions/NewTerminalArgs" }, + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "$ref": "#/$defs/NewTerminalArgs" + }, { "properties": { - "action": { "type": "string", "pattern": "splitPane" }, + "action": { + "type": "string", + "pattern": "splitPane" + }, "split": { - "$ref": "#/definitions/SplitDirection", + "$ref": "#/$defs/SplitDirection", "default": "auto", "description": "The orientation to split the pane in. Possible values:\n -\"auto\" (splits pane based on remaining space)\n -\"up\" (think [-] and above)\n -\"down\" (think [-] and below)\n -\"left\" (think [|] and to the left)\n -\"right\"(think [|] and to the right)" }, @@ -654,11 +751,14 @@ "description": "Arguments corresponding to a Open Settings Action", "allOf": [ { - "$ref": "#/definitions/ShortcutAction" + "$ref": "#/$defs/ShortcutAction" }, { "properties": { - "action": { "type": "string", "pattern": "openSettings" }, + "action": { + "type": "string", + "pattern": "openSettings" + }, "target": { "type": "string", "default": "settingsUI", @@ -668,7 +768,6 @@ "defaultsFile", "allFiles", "settingsUI" - ] } } @@ -678,12 +777,17 @@ "SetTabColorAction": { "description": "Arguments corresponding to a Set Tab Color Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "setTabColor" }, + "action": { + "type": "string", + "pattern": "setTabColor" + }, "color": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": null, "description": "If provided, will set the tab's color to the given value. If omitted, will reset the tab's color." } @@ -694,10 +798,15 @@ "SetColorSchemeAction": { "description": "Arguments corresponding to a Set Color Scheme Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "setColorScheme" }, + "action": { + "type": "string", + "pattern": "setColorScheme" + }, "colorScheme": { "type": "string", "default": "", @@ -706,15 +815,22 @@ } } ], - "required": [ "colorScheme" ] + "required": [ + "colorScheme" + ] }, "WtAction": { "description": "Arguments corresponding to a wt Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "wt" }, + "action": { + "type": "string", + "pattern": "wt" + }, "commandline": { "type": "string", "default": "", @@ -723,19 +839,30 @@ } } ], - "required": [ "commandline" ] + "required": [ + "commandline" + ] }, "CloseOtherTabsAction": { "description": "Arguments for a closeOtherTabs action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "closeOtherTabs" }, + "action": { + "type": "string", + "pattern": "closeOtherTabs" + }, "index": { "oneOf": [ - { "type": "integer" }, - { "type": "null" } + { + "type": "integer" + }, + { + "type": "null" + } ], "default": null, "description": "Close the tabs other than the one at this index. If no index is provided, use the focused tab's index." @@ -747,14 +874,23 @@ "CloseTabsAfterAction": { "description": "Arguments for a closeTabsAfter action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "closeTabsAfter" }, + "action": { + "type": "string", + "pattern": "closeTabsAfter" + }, "index": { "oneOf": [ - { "type": "integer" }, - { "type": "null" } + { + "type": "integer" + }, + { + "type": "null" + } ], "default": null, "description": "Close the tabs following the tab at this index. If no index is provided, use the focused tab's index." @@ -766,14 +902,23 @@ "CloseTabAction": { "description": "Arguments for a closeTab action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "closeTab" }, + "action": { + "type": "string", + "pattern": "closeTab" + }, "index": { "oneOf": [ - { "type": "integer" }, - { "type": "null" } + { + "type": "integer" + }, + { + "type": "null" + } ], "default": null, "description": "Close the tab at this index. If no index is provided, use the focused tab's index." @@ -785,12 +930,20 @@ "ScrollUpAction": { "description": "Arguments for a scrollUp action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "scrollUp" }, + "action": { + "type": "string", + "pattern": "scrollUp" + }, "rowsToScroll": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Scroll up rowsToScroll lines. If no value is provided, use the system-level defaults." } @@ -801,12 +954,20 @@ "ScrollDownAction": { "description": "Arguments for a scrollDown action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "scrollDown" }, + "action": { + "type": "string", + "pattern": "scrollDown" + }, "rowsToScroll": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Scroll down rowsToScroll lines. If no value is provided, use the system-level defaults." } @@ -817,28 +978,40 @@ "MoveTabAction": { "description": "Arguments for moving a tab", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "moveTab" }, + "action": { + "type": "string", + "pattern": "moveTab" + }, "direction": { - "$ref": "#/definitions/MoveTabDirection", + "$ref": "#/$defs/MoveTabDirection", "description": "The direction to move the tab" } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "MultipleActionsAction": { "description": "Arguments for the multiple actions command", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "multipleActions" }, - "actions" : { - "$ref": "#/definitions/ShortcutAction", + "action": { + "type": "string", + "pattern": "multipleActions" + }, + "actions": { + "$ref": "#/$defs/ShortcutAction", "type": "array", "minItems": 1, "description": "A list of other actions." @@ -846,17 +1019,24 @@ } } ], - "required": [ "actions" ] + "required": [ + "actions" + ] }, "CommandPaletteAction": { "description": "Arguments for a commandPalette action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "commandPalette" }, + "action": { + "type": "string", + "pattern": "commandPalette" + }, "launchMode": { - "$ref": "#/definitions/CommandPaletteLaunchMode", + "$ref": "#/$defs/CommandPaletteLaunchMode", "default": "action", "description": "Toggle command palette in either action or command line mode. If no value is provided, the palette will launch in action mode." } @@ -867,28 +1047,42 @@ "FindMatchAction": { "description": "Arguments corresponding to a Find Match Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "findMatch" }, + "action": { + "type": "string", + "pattern": "findMatch" + }, "direction": { - "$ref": "#/definitions/FindMatchDirection", + "$ref": "#/$defs/FindMatchDirection", "default": "prev", "description": "The direction to search in. \"prev\" will search upwards in the buffer, and \"next\" will search downwards." } } } ], - "required": [ "direction" ] + "required": [ + "direction" + ] }, "NewWindowAction": { "description": "Arguments corresponding to a New Window Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, - { "$ref": "#/definitions/NewTerminalArgs" }, + { + "$ref": "#/$defs/ShortcutAction" + }, + { + "$ref": "#/$defs/NewTerminalArgs" + }, { "properties": { - "action": { "type":"string", "pattern": "newWindow" } + "action": { + "type": "string", + "pattern": "newWindow" + } } } ] @@ -896,12 +1090,17 @@ "PrevTabAction": { "description": "Arguments corresponding to a Previous Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type":"string", "pattern": "prevTab" }, + "action": { + "type": "string", + "pattern": "prevTab" + }, "tabSwitcherMode": { - "$ref": "#/definitions/SwitchToAdjacentTabArgs", + "$ref": "#/$defs/SwitchToAdjacentTabArgs", "default": null, "description": "Move to the previous tab using \"tabSwitcherMode\". If no mode is provided, use the one globally defined one." } @@ -912,12 +1111,17 @@ "NextTabAction": { "description": "Arguments corresponding to a Next Tab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type":"string", "pattern": "nextTab" }, + "action": { + "type": "string", + "pattern": "nextTab" + }, "tabSwitcherMode": { - "$ref": "#/definitions/SwitchToAdjacentTabArgs", + "$ref": "#/$defs/SwitchToAdjacentTabArgs", "default": null, "description": "Move to the next tab using \"tabSwitcherMode\". If no mode is provided, use the one globally defined one." } @@ -928,10 +1132,15 @@ "RenameTabAction": { "description": "Arguments corresponding to a renameTab Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "renameTab" }, + "action": { + "type": "string", + "pattern": "renameTab" + }, "title": { "type": "string", "default": "", @@ -944,10 +1153,15 @@ "RenameWindowAction": { "description": "Arguments corresponding to a renameWindow Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "renameWindow" }, + "action": { + "type": "string", + "pattern": "renameWindow" + }, "name": { "type": "string", "default": "", @@ -960,10 +1174,15 @@ "FocusPaneAction": { "description": "Arguments corresponding to a focusPane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "focusPane" }, + "action": { + "type": "string", + "pattern": "focusPane" + }, "id": { "type": "string", "default": "", @@ -976,10 +1195,15 @@ "GlobalSummonAction": { "description": "This is a special action that works globally in the OS, rather than only in the context of the terminal window. When pressed, this action will summon the terminal window.", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "globalSummon" }, + "action": { + "type": "string", + "pattern": "globalSummon" + }, "desktop": { "type": "string", "default": "toCurrent", @@ -1022,10 +1246,15 @@ "QuakeModeAction": { "description": "This action is a special variation of the globalSummon action. It specifically summons a window called \"_quake\". If you would like to change the behavior of the quakeMode action, we recommended creating a new globalSummon entry.", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "quakeMode" } + "action": { + "type": "string", + "pattern": "quakeMode" + } } } ] @@ -1035,56 +1264,120 @@ "properties": { "command": { "description": "The action executed when the associated key bindings are pressed.", - "oneOf": [ - { "$ref": "#/definitions/AdjustFontSizeAction" }, - { "$ref": "#/definitions/CopyAction" }, - { "$ref": "#/definitions/ShortcutActionName" }, - { "$ref": "#/definitions/NewTabAction" }, - { "$ref": "#/definitions/SwitchToTabAction" }, - { "$ref": "#/definitions/MoveFocusAction" }, - { "$ref": "#/definitions/MovePaneAction" }, - { "$ref": "#/definitions/SwapPaneAction" }, - { "$ref": "#/definitions/ResizePaneAction" }, - { "$ref": "#/definitions/SendInputAction" }, - { "$ref": "#/definitions/SplitPaneAction" }, - { "$ref": "#/definitions/OpenSettingsAction" }, - { "$ref": "#/definitions/SetTabColorAction" }, - { "$ref": "#/definitions/SetColorSchemeAction" }, - { "$ref": "#/definitions/WtAction" }, - { "$ref": "#/definitions/CloseOtherTabsAction" }, - { "$ref": "#/definitions/CloseTabsAfterAction" }, - { "$ref": "#/definitions/CloseTabAction" }, - { "$ref": "#/definitions/ScrollUpAction" }, - { "$ref": "#/definitions/ScrollDownAction" }, - { "$ref": "#/definitions/MoveTabAction" }, - { "$ref": "#/definitions/FindMatchAction" }, - { "$ref": "#/definitions/NewWindowAction" }, - { "$ref": "#/definitions/NextTabAction" }, - { "$ref": "#/definitions/PrevTabAction" }, - { "$ref": "#/definitions/RenameTabAction" }, - { "$ref": "#/definitions/RenameWindowAction" }, - { "$ref": "#/definitions/FocusPaneAction" }, - { "$ref": "#/definitions/GlobalSummonAction" }, - { "$ref": "#/definitions/QuakeModeAction" }, - { "type": "null" } - ] + "oneOf": [ + { + "$ref": "#/$defs/AdjustFontSizeAction" + }, + { + "$ref": "#/$defs/CopyAction" + }, + { + "$ref": "#/$defs/ShortcutActionName" + }, + { + "$ref": "#/$defs/NewTabAction" + }, + { + "$ref": "#/$defs/SwitchToTabAction" + }, + { + "$ref": "#/$defs/MoveFocusAction" + }, + { + "$ref": "#/$defs/MovePaneAction" + }, + { + "$ref": "#/$defs/SwapPaneAction" + }, + { + "$ref": "#/$defs/ResizePaneAction" + }, + { + "$ref": "#/$defs/SendInputAction" + }, + { + "$ref": "#/$defs/SplitPaneAction" + }, + { + "$ref": "#/$defs/OpenSettingsAction" + }, + { + "$ref": "#/$defs/SetTabColorAction" + }, + { + "$ref": "#/$defs/SetColorSchemeAction" + }, + { + "$ref": "#/$defs/WtAction" + }, + { + "$ref": "#/$defs/CloseOtherTabsAction" + }, + { + "$ref": "#/$defs/CloseTabsAfterAction" + }, + { + "$ref": "#/$defs/CloseTabAction" + }, + { + "$ref": "#/$defs/ScrollUpAction" + }, + { + "$ref": "#/$defs/ScrollDownAction" + }, + { + "$ref": "#/$defs/MoveTabAction" + }, + { + "$ref": "#/$defs/FindMatchAction" + }, + { + "$ref": "#/$defs/NewWindowAction" + }, + { + "$ref": "#/$defs/NextTabAction" + }, + { + "$ref": "#/$defs/PrevTabAction" + }, + { + "$ref": "#/$defs/RenameTabAction" + }, + { + "$ref": "#/$defs/RenameWindowAction" + }, + { + "$ref": "#/$defs/FocusPaneAction" + }, + { + "$ref": "#/$defs/GlobalSummonAction" + }, + { + "$ref": "#/$defs/QuakeModeAction" + }, + { + "type": "null" + } + ] }, "keys": { "description": "Defines the key combinations used to call the command. It must be composed of...\n -any number of modifiers (ctrl/alt/shift)\n -a non-modifier key", "oneOf": [ { - "$ref": "#/definitions/KeyChordSegment" + "$ref": "#/$defs/KeyChordSegment" }, { "items": { - "$ref": "#/definitions/KeyChordSegment" + "$ref": "#/$defs/KeyChordSegment" }, "minItems": 1, "type": "array" } ] }, - "icon": { "$ref": "#/definitions/Icon" }, + "icon": { + "$ref": "#/$defs/Icon" + }, "name": { "description": "The name that will appear in the command palette. If one isn't provided, the terminal will attempt to automatically generate a name.\nIf name is a string, it will be the name of the command.\nIf name is a object, the key property of the object will be used to lookup a localized string resource for the command", "properties": { @@ -1109,15 +1402,24 @@ "commands": { "description": "List of commands to execute", "items": { - "$ref": "#/definitions/Keybinding/properties/command" + "$ref": "#/$defs/Keybinding/properties/command" }, "minItems": 1, "type": "array" } }, "anyOf": [ - {"required": ["name","commands"]}, - {"required": ["command"]} + { + "required": [ + "name", + "commands" + ] + }, + { + "required": [ + "command" + ] + } ], "type": "object" }, @@ -1158,7 +1460,7 @@ "copyFormatting": { "default": true, "description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied.", - "$ref": "#/definitions/CopyFormat" + "$ref": "#/$defs/CopyFormat" }, "trimBlockSelection": { "default": false, @@ -1196,7 +1498,7 @@ "disabledProfileSources": { "description": "Disables all the dynamic profile generators in this list, preventing them from adding their profiles to the list of profiles on startup.", "items": { - "$ref": "#/definitions/DynamicProfileSource" + "$ref": "#/$defs/DynamicProfileSource" }, "type": "array" }, @@ -1220,7 +1522,7 @@ "type": "integer" }, "initialPosition": { - "$ref": "#/definitions/Coordinates", + "$ref": "#/$defs/Coordinates", "description": "The position of the top left corner of the window upon first load. On a system with multiple displays, these coordinates are relative to the top left of the primary display. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), the window will be maximized on the monitor specified by those coordinates." }, "initialRows": { @@ -1261,7 +1563,10 @@ "description": "This parameter once allowed you to override the systemwide \"choose how many lines to scroll at one time\" setting. It no longer does so. However, you can customize the number of lines to scroll in \"scrollUp\" and \"scrollDown\" bindings.", "maximum": 999, "minimum": 0, - "type": [ "integer", "string" ], + "type": [ + "integer", + "string" + ], "deprecated": true }, "minimizeToNotificationArea": { @@ -1284,18 +1589,18 @@ "description": "When set to true, the tab row will have an acrylic background with 50% opacity.", "type": "boolean" }, - "actions": { - "description": "Properties are specific to each custom action.", - "items": { - "$ref": "#/definitions/Keybinding" - }, - "type": "array" - }, + "actions": { + "description": "Properties are specific to each custom action.", + "items": { + "$ref": "#/$defs/Keybinding" + }, + "type": "array" + }, "keybindings": { "description": "[deprecated] Use actions instead.", "deprecated": true, "items": { - "$ref": "#/definitions/Keybinding" + "$ref": "#/$defs/Keybinding" }, "type": "array" }, @@ -1423,26 +1728,38 @@ "type": "string" }, "background": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#0c0c0c", "description": "Sets the background color of the text. Overrides \"background\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "unfocusedAppearance": { - "$ref": "#/definitions/AppearanceConfig", + "$ref": "#/$defs/AppearanceConfig", "description": "Sets the appearance of the terminal when it is unfocused.", - "type": ["object", "null"] + "type": [ + "object", + "null" + ] }, "font": { - "$ref": "#/definitions/FontConfig", + "$ref": "#/$defs/FontConfig", "description": "Sets the font options of the terminal.", - "type": ["object", "null"] + "type": [ + "object", + "null" + ] }, "backgroundImage": { "description": "Sets the file location of the image to draw over the window background.", "oneOf": [ { - "type": ["string", null] + "type": [ + "string", + "null" + ] }, { "enum": [ @@ -1450,7 +1767,10 @@ ] } ], - "type": [ "string", "null" ] + "type": [ + "string", + "null" + ] }, "backgroundImageAlignment": { "default": "center", @@ -1489,7 +1809,7 @@ "bellStyle": { "default": "audible", "description": "Controls what happens when the application emits a BEL character. When set to \"all\", the Terminal will play a sound, flash the taskbar icon (if the terminal window is not in focus) and flash the window. An array of specific behaviors can also be used. Supported array values include `audible`, `window` and `taskbar`. When set to \"none\", nothing will happen.", - "$ref": "#/definitions/BellStyle" + "$ref": "#/$defs/BellStyle" }, "closeOnExit": { "default": "graceful", @@ -1519,8 +1839,12 @@ }, "cursorColor": { "oneOf": [ - { "$ref": "#/definitions/Color" }, - {"type": "null"} + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the color of the cursor. Overrides the cursor color from the color scheme. Uses hex color format: \"#rrggbb\"." }, @@ -1528,7 +1852,10 @@ "description": "Sets the percentage height of the cursor starting from the bottom. Only works when cursorShape is set to \"vintage\". Accepts values from 1-100.", "maximum": 100, "minimum": 1, - "type": ["integer","null"], + "type": [ + "integer", + "null" + ], "default": 25 }, "cursorShape": { @@ -1594,13 +1921,16 @@ "deprecated": true }, "foreground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#cccccc", "description": "Sets the text color. Overrides \"foreground\" from the color scheme. Uses hex color format: \"#rrggbb\".", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "guid": { - "$ref": "#/definitions/ProfileGuid", + "$ref": "#/$defs/ProfileGuid", "description": "Unique identifier of the profile. Written in registry format: \"{00000000-0000-0000-0000-000000000000}\"." }, "hidden": { @@ -1614,7 +1944,9 @@ "minimum": -1, "type": "integer" }, - "icon":{ "$ref": "#/definitions/Icon" }, + "icon": { + "$ref": "#/$defs/Icon" + }, "name": { "description": "Name of the profile. Displays in the dropdown menu.", "minLength": 1, @@ -1651,8 +1983,12 @@ }, "selectionBackground": { "oneOf": [ - {"$ref": "#/definitions/Color"}, - { "type": "null" } + { + "$ref": "#/$defs/Color" + }, + { + "type": "null" + } ], "description": "Sets the background color of selected text. Overrides selectionBackground set in the color scheme. Uses hex color format: \"#rrggbb\"." }, @@ -1668,7 +2004,10 @@ }, "source": { "description": "Stores the name of the profile generator that originated this profile.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "startingDirectory": { "description": "The directory the shell starts in when it is loaded.", @@ -1680,13 +2019,19 @@ "default": false }, "tabColor": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color of the profile's tab. Using the tab color picker will override this color.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "tabTitle": { "description": "If set, will replace the name as the title to pass to the shell on startup. Some shells (like bash) may choose to ignore this initial value, while others (cmd, powershell) may use this value over the lifetime of the application.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "useAcrylic": { "default": false, @@ -1699,7 +2044,7 @@ "ProfileList": { "description": "A list of profiles and the properties specific to each.", "items": { - "$ref": "#/definitions/Profile", + "$ref": "#/$defs/Profile", "required": [ "guid", "name" @@ -1711,11 +2056,11 @@ "description": "A list of profiles and default settings that apply to all of them", "properties": { "list": { - "$ref": "#/definitions/ProfileList" + "$ref": "#/$defs/ProfileList" }, "defaults": { "description": "The default settings that apply to every profile.", - "$ref": "#/definitions/Profile" + "$ref": "#/$defs/Profile" } }, "type": "object" @@ -1731,84 +2076,84 @@ "type": "string" }, "background": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the background color of the color scheme." }, "black": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI black." }, "blue": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI blue." }, "brightBlack": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright black." }, "brightBlue": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright blue." }, "brightCyan": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright cyan." }, "brightGreen": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright green." }, "brightPurple": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright purple." }, "brightRed": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright red." }, "brightWhite": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright white." }, "brightYellow": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI bright yellow." }, "cursorColor": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "default": "#FFFFFF", "description": "Sets the cursor color of the color scheme." }, "cyan": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI cyan." }, "foreground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the foreground color of the color scheme." }, "green": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI green." }, "purple": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI purple." }, "red": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI red." }, "selectionBackground": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the selection background color of the color scheme." }, "white": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI white." }, "yellow": { - "$ref": "#/definitions/Color", + "$ref": "#/$defs/Color", "description": "Sets the color used as ANSI yellow." } }, @@ -1818,17 +2163,25 @@ } }, "allOf": [ - { "$ref": "#/definitions/Globals" }, + { + "$ref": "#/$defs/Globals" + }, { "additionalItems": true, "properties": { "profiles": { "oneOf": [ - { "$ref": "#/definitions/ProfileList" }, - { "$ref": "#/definitions/ProfilesObject" } + { + "$ref": "#/$defs/ProfileList" + }, + { + "$ref": "#/$defs/ProfilesObject" + } ] }, - "schemes": { "$ref": "#/definitions/SchemeList" } + "schemes": { + "$ref": "#/$defs/SchemeList" + } }, "required": [ "profiles", diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 4e3da70aa..ecb40c3eb 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -751,7 +751,7 @@ namespace TerminalAppLocalTests }); VERIFY_SUCCEEDED(result); - Log::Comment(L"Move focus. This will cause us to un-zoom."); + Log::Comment(L"Move focus. We should still be zoomed."); result = RunOnUIThread([&page]() { // Set up action MoveFocusArgs args{ FocusDirection::Left }; @@ -761,7 +761,7 @@ namespace TerminalAppLocalTests auto firstTab = page->_GetTerminalTabImpl(page->_tabs.GetAt(0)); VERIFY_ARE_EQUAL(2, firstTab->GetLeafPaneCount()); - VERIFY_IS_FALSE(firstTab->IsZoomed()); + VERIFY_IS_TRUE(firstTab->IsZoomed()); }); VERIFY_SUCCEEDED(result); } @@ -1357,7 +1357,8 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); + // And we should have stored a function to revert the change. + VERIFY_ARE_EQUAL(1u, page->_restorePreviewFuncs.size()); }); TestOnUIThread([&page]() { @@ -1383,7 +1384,8 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(nullptr, page->_originalSettings); + // After preview there should be no more restore functions to execute. + VERIFY_ARE_EQUAL(0u, page->_restorePreviewFuncs.size()); }); } @@ -1428,7 +1430,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); }); TestOnUIThread([&page]() { @@ -1451,7 +1452,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be the same as it originally was"); VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(nullptr, page->_originalSettings); }); } @@ -1498,7 +1498,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); }); TestOnUIThread([&page]() { @@ -1522,7 +1521,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed to the preview"); VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(originalSettings, page->_originalSettings); }); TestOnUIThread([&page]() { @@ -1548,7 +1546,6 @@ namespace TerminalAppLocalTests Log::Comment(L"Color should be changed"); VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground()); - VERIFY_ARE_EQUAL(nullptr, page->_originalSettings); }); } diff --git a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp index f09c97553..3fc4ff2ae 100644 --- a/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp +++ b/src/cascadia/TerminalApp/ActionPreviewHandlers.cpp @@ -67,41 +67,17 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_EndPreviewColorScheme() { - // Get the focused control - if (const auto& activeControl{ _GetActiveControl() }) + for (const auto& f : _restorePreviewFuncs) { - // Get the runtime settings of the focused control - const auto& controlSettings{ activeControl.Settings().as() }; - - // Get the control's root settings, the ones that we actually - // assigned to it. - auto parentSettings{ controlSettings.GetParent() }; - while (parentSettings.GetParent() != nullptr) - { - parentSettings = parentSettings.GetParent(); - } - - // If the root settings are the same as the ones we stashed, - // then reset the parent of the runtime settings to the stashed - // settings. This condition might be false if the settings - // hot-reloaded while the palette was open. In that case, we - // don't want to reset the settings to what they were _before_ - // the hot-reload. - if (_originalSettings == parentSettings) - { - // Set the original settings as the parent of the control's settings - activeControl.Settings().as().SetParent(_originalSettings); - } - - activeControl.UpdateSettings(); + f(); } - _originalSettings = nullptr; + _restorePreviewFuncs.clear(); } // Method Description: // - Preview handler for the SetColorScheme action. - // - This method will stash the settings of the current control in - // _originalSettings. Then it will create a new TerminalSettings object + // - This method will stash functions to reset the settings of the selected controls in + // _restorePreviewFuncs. Then it will create a new TerminalSettings object // with only the properties from the ColorScheme set. It'll _insert_ a // TerminalSettings between the control's root settings (built from // CascadiaSettings) and the control's runtime settings. That'll cause the @@ -112,33 +88,63 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_PreviewColorScheme(const Settings::Model::SetColorSchemeArgs& args) { - // Get the focused control - if (const auto& activeControl{ _GetActiveControl() }) + if (const auto& scheme{ _settings.GlobalSettings().ColorSchemes().TryLookup(args.SchemeName()) }) { - if (const auto& scheme{ _settings.GlobalSettings().ColorSchemes().TryLookup(args.SchemeName()) }) - { + // Clear the saved preview funcs because we don't need to add a restore each time + // the preview color changes, we only need to be able to restore the last one. + _restorePreviewFuncs.clear(); + + _ApplyToActiveControls([&](const auto& control) { // Get the settings of the focused control and stash them - const auto& controlSettings = activeControl.Settings().as(); + const auto& controlSettings = control.Settings().as(); // Make sure to recurse up to the root - if you're doing // this while you're currently previewing a SetColorScheme // action, then the parent of the control's settings is _the // last preview TerminalSettings we inserted! We don't want // to save that one! - _originalSettings = controlSettings.GetParent(); - while (_originalSettings.GetParent() != nullptr) + auto originalSettings = controlSettings.GetParent(); + while (originalSettings.GetParent() != nullptr) { - _originalSettings = _originalSettings.GetParent(); + originalSettings = originalSettings.GetParent(); } // Create a new child for those settings - TerminalSettingsCreateResult fake{ _originalSettings }; + TerminalSettingsCreateResult fake{ originalSettings }; const auto& childStruct = TerminalSettings::CreateWithParent(fake); // Modify the child to have the applied color scheme childStruct.DefaultSettings().ApplyColorScheme(scheme); // Insert that new child as the parent of the control's settings controlSettings.SetParent(childStruct.DefaultSettings()); - activeControl.UpdateSettings(); - } + control.UpdateSettings(); + + // Take a copy of the inputs, since they are pointers anyways. + _restorePreviewFuncs.emplace_back([=]() { + // Get the runtime settings of the focused control + const auto& controlSettings{ control.Settings().as() }; + + // Get the control's root settings, the ones that we actually + // assigned to it. + auto parentSettings{ controlSettings.GetParent() }; + while (parentSettings.GetParent() != nullptr) + { + parentSettings = parentSettings.GetParent(); + } + + // If the root settings are the same as the ones we stashed, + // then reset the parent of the runtime settings to the stashed + // settings. This condition might be false if the settings + // hot-reloaded while the palette was open. In that case, we + // don't want to reset the settings to what they were _before_ + // the hot-reload. + if (originalSettings == parentSettings) + { + // Set the original settings as the parent of the control's settings + control.Settings().as().SetParent(originalSettings); + } + + control.UpdateSettings(); + }); + }); } } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index d99726b39..ae79289fa 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -377,11 +377,10 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - if (const auto& termControl{ _GetActiveControl() }) - { - termControl.AdjustFontSize(realArgs.Delta()); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([&](auto& control) { + control.AdjustFontSize(realArgs.Delta()); + }); + args.Handled(res); } } @@ -395,21 +394,19 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleResetFontSize(const IInspectable& /*sender*/, const ActionEventArgs& args) { - if (const auto& termControl{ _GetActiveControl() }) - { - termControl.ResetFontSize(); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([](auto& control) { + control.ResetFontSize(); + }); + args.Handled(res); } void TerminalPage::_HandleToggleShaderEffects(const IInspectable& /*sender*/, const ActionEventArgs& args) { - if (const auto& termControl{ _GetActiveControl() }) - { - termControl.ToggleShaderEffects(); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([](auto& control) { + control.ToggleShaderEffects(); + }); + args.Handled(res); } void TerminalPage::_HandleToggleFocusMode(const IInspectable& /*sender*/, @@ -452,37 +449,33 @@ namespace winrt::TerminalApp::implementation args.Handled(false); if (const auto& realArgs = args.ActionArgs().try_as()) { - if (const auto activeTab{ _GetFocusedTabImpl() }) + if (const auto scheme = _settings.GlobalSettings().ColorSchemes().TryLookup(realArgs.SchemeName())) { - if (auto activeControl = activeTab->GetActiveTerminalControl()) - { - if (const auto scheme = _settings.GlobalSettings().ColorSchemes().TryLookup(realArgs.SchemeName())) + const auto res = _ApplyToActiveControls([&](auto& control) { + // Start by getting the current settings of the control + auto controlSettings = control.Settings().as(); + auto parentSettings = controlSettings; + // Those are the _runtime_ settings however. What we + // need to do is: + // + // 1. Blow away any colors set in the runtime settings. + // 2. Apply the color scheme to the parent settings. + // + // 1 is important to make sure that the effects of + // something like `colortool` are cleared when setting + // the scheme. + if (controlSettings.GetParent() != nullptr) { - // Start by getting the current settings of the control - auto controlSettings = activeControl.Settings().as(); - auto parentSettings = controlSettings; - // Those are the _runtime_ settings however. What we - // need to do is: - // - // 1. Blow away any colors set in the runtime settings. - // 2. Apply the color scheme to the parent settings. - // - // 1 is important to make sure that the effects of - // something like `colortool` are cleared when setting - // the scheme. - if (controlSettings.GetParent() != nullptr) - { - parentSettings = controlSettings.GetParent(); - } - - // ApplyColorScheme(nullptr) will clear the old color scheme. - controlSettings.ApplyColorScheme(nullptr); - parentSettings.ApplyColorScheme(scheme); - - activeControl.UpdateSettings(); - args.Handled(true); + parentSettings = controlSettings.GetParent(); } - } + + // ApplyColorScheme(nullptr) will clear the old color scheme. + controlSettings.ApplyColorScheme(nullptr); + parentSettings.ApplyColorScheme(scheme); + + control.UpdateSettings(); + }); + args.Handled(res); } } } @@ -896,11 +889,10 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - if (const auto termControl{ _GetActiveControl() }) - { - termControl.ClearBuffer(realArgs.Clear()); - args.Handled(true); - } + const auto res = _ApplyToActiveControls([&](auto& control) { + control.ClearBuffer(realArgs.Clear()); + }); + args.Handled(res); } } } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 616a73cd4..74bc48bb2 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -39,8 +39,8 @@ Pane::Pane(const Profile& profile, const Controls::UserControl& control, const b _lastActive{ lastFocused }, _profile{ profile } { - _root.Children().Append(_border); - _border.Child(_control); + _root.Children().Append(_borderFirst); + _borderFirst.Child(_control); if (const auto& termControl{ _control.try_as() }) { @@ -64,7 +64,52 @@ Pane::Pane(const Profile& profile, const Controls::UserControl& control, const b // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to // Colors::Transparent! The border won't get Tapped events, and they'll fall // through to something else. - _border.Tapped([this](auto&, auto& e) { + _borderFirst.Tapped([this](auto&, auto& e) { + _FocusFirstChild(); + e.Handled(true); + }); + _borderSecond.Tapped([this](auto&, auto& e) { + _FocusFirstChild(); + e.Handled(true); + }); +} + +Pane::Pane(std::shared_ptr first, + std::shared_ptr second, + const SplitState splitState, + const float splitPosition, + const bool lastFocused) : + _firstChild{ first }, + _secondChild{ second }, + _splitState{ splitState }, + _desiredSplitPosition{ splitPosition }, + _lastActive{ lastFocused } +{ + _CreateRowColDefinitions(); + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + // Use the unfocused border color as the pane background, so an actual color + // appears behind panes as we animate them sliding in. + _root.Background(s_unfocusedBorderBrush); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); + + _ApplySplitDefinitions(); + + // Register event handlers on our children to handle their Close events + _SetupChildCloseHandlers(); + + // When our border is tapped, make sure to transfer focus to our control. + // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to + // Colors::Transparent! The border won't get Tapped events, and they'll fall + // through to something else. + _borderFirst.Tapped([this](auto&, auto& e) { + _FocusFirstChild(); + e.Handled(true); + }); + _borderSecond.Tapped([this](auto&, auto& e) { _FocusFirstChild(); e.Handled(true); }); @@ -345,18 +390,18 @@ bool Pane::ResizePane(const ResizeDirection& direction) return false; } - // Check if either our first or second child is the currently focused leaf. + // Check if either our first or second child is the currently focused pane. // If it is, and the requested resize direction matches our separator, then // we're the pane that needs to adjust its separator. // If our separator is the wrong direction, then we can't handle it. - const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; - const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; + const bool firstIsFocused = _firstChild->_lastActive; + const bool secondIsFocused = _secondChild->_lastActive; if (firstIsFocused || secondIsFocused) { return _Resize(direction); } - // If neither of our children were the focused leaf, then recurse into + // If neither of our children were the focused pane, then recurse into // our children and see if they can handle the resize. // For each child, if it has a focused descendant, try having that child // handle the resize. @@ -405,6 +450,36 @@ std::shared_ptr Pane::NavigateDirection(const std::shared_ptr source return nullptr; } + // Check if moving up or down the tree + if (direction == FocusDirection::Parent) + { + if (const auto parent = _FindParentOfPane(sourcePane)) + { + // Keep a reference to which child we came from + parent->_parentChildPath = sourcePane->weak_from_this(); + + return parent; + } + return nullptr; + } + + if (direction == FocusDirection::Child) + { + if (!sourcePane->_IsLeaf()) + { + auto child = sourcePane->_firstChild; + // If we've recorded path try to go back down it + if (const auto prevFocus = sourcePane->_parentChildPath.lock()) + { + child = prevFocus; + } + // clean up references + sourcePane->_parentChildPath.reset(); + return child; + } + return nullptr; + } + // Previous movement relies on the last used panes if (direction == FocusDirection::Previous) { @@ -428,6 +503,7 @@ std::shared_ptr Pane::NavigateDirection(const std::shared_ptr source return PreviousPane(sourcePane); } + // Fixed movement if (direction == FocusDirection::First) { std::shared_ptr firstPane = nullptr; @@ -497,6 +573,11 @@ std::shared_ptr Pane::NextPane(const std::shared_ptr targetPane) bool foundTarget = false; auto foundNext = WalkTree([&](auto pane) { + // If we are a parent pane we don't want to move to one of our children + if (foundTarget && targetPane->_HasChild(pane)) + { + return false; + } // In case the target pane is the last pane in the tree, keep a reference // to the first leaf so we can wrap around. if (firstLeaf == nullptr && pane->_IsLeaf()) @@ -627,6 +708,12 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) return false; } + // Similarly don't swap if we have a circular reference + if (first->_HasChild(second) || second->_HasChild(first)) + { + return false; + } + std::unique_lock lock{ _createCloseLock }; // Recurse through the tree to find the parent panes of each pane that is @@ -640,8 +727,10 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) // after the pointers were found but before we reached this function. if (firstParent && secondParent) { - // Swap size/display information of the two panes. - std::swap(first->_borders, second->_borders); + // Before we swap anything get the borders for the parents so that + // it can be propagated to the swapped child. + firstParent->_borders = firstParent->_GetCommonBorders(); + secondParent->_borders = secondParent->_GetCommonBorders(); // Replace the old child with new one, and revoke appropriate event // handlers. @@ -659,32 +748,30 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) } // Clear now to ensure that we can add the child's grid to us later parent->_root.Children().Clear(); + parent->_borderFirst.Child(nullptr); + parent->_borderSecond.Child(nullptr); }; // Make sure that the right event handlers are set, and the children // are placed in the appropriate locations in the grid. auto updateParent = [](auto& parent) { + // just always revoke the old helpers since we are making new ones. + parent->_firstChild->Closed(parent->_firstClosedToken); + parent->_secondChild->Closed(parent->_secondClosedToken); parent->_SetupChildCloseHandlers(); parent->_root.Children().Clear(); - parent->_root.Children().Append(parent->_firstChild->GetRootElement()); - parent->_root.Children().Append(parent->_secondChild->GetRootElement()); - // Make sure they have the correct borders, and also that they are - // placed in the right location in the grid. - // This mildly reproduces ApplySplitDefinitions, but is different in - // that it does not want to utilize the parent's border to set child - // borders. - if (parent->_splitState == SplitState::Vertical) - { - Controls::Grid::SetColumn(parent->_firstChild->GetRootElement(), 0); - Controls::Grid::SetColumn(parent->_secondChild->GetRootElement(), 1); - } - else if (parent->_splitState == SplitState::Horizontal) - { - Controls::Grid::SetRow(parent->_firstChild->GetRootElement(), 0); - Controls::Grid::SetRow(parent->_secondChild->GetRootElement(), 1); - } - parent->_firstChild->_UpdateBorders(); - parent->_secondChild->_UpdateBorders(); + parent->_borderFirst.Child(nullptr); + parent->_borderSecond.Child(nullptr); + parent->_borderFirst.Child(parent->_firstChild->GetRootElement()); + parent->_borderSecond.Child(parent->_secondChild->GetRootElement()); + + parent->_root.Children().Append(parent->_borderFirst); + parent->_root.Children().Append(parent->_borderSecond); + + // reset split definitions to clear any set row/column + parent->_root.ColumnDefinitions().Clear(); + parent->_root.RowDefinitions().Clear(); + parent->_CreateRowColDefinitions(); }; // If the firstParent and secondParent are the same, then we are just @@ -696,6 +783,7 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) std::swap(firstParent->_firstChild, firstParent->_secondChild); updateParent(firstParent); + firstParent->_ApplySplitDefinitions(); } else { @@ -705,11 +793,45 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) replaceChild(secondParent, second, first); updateParent(firstParent); updateParent(secondParent); + + // If one of the two parents is a child of the other we only want + // to apply the split definitions to the greatest parent to make + // sure that all panes get the correct borders. if this is not done + // and the ordering happens to be bad one parent's children will lose + // a border. + if (firstParent->_HasChild(secondParent)) + { + firstParent->_ApplySplitDefinitions(); + } + else if (secondParent->_HasChild(firstParent)) + { + secondParent->_ApplySplitDefinitions(); + } + else + { + firstParent->_ApplySplitDefinitions(); + secondParent->_ApplySplitDefinitions(); + } } - // For now the first pane is always the focused pane, so re-focus to - // make sure the cursor is still in the terminal since the root was moved. - first->_FocusFirstChild(); + // Refocus the last pane if there was a pane focused + first->WalkTree([](auto p) { + if (p->_lastActive) + { + p->_Focus(); + return true; + } + return false; + }); + + second->WalkTree([](auto p) { + if (p->_lastActive) + { + p->_Focus(); + return true; + } + return false; + }); return true; } @@ -732,7 +854,7 @@ winrt::Windows::UI::Xaml::Controls::UserControl Pane::ReplaceControl(const winrt } _control = control; - _border.Child(_control); + _borderFirst.Child(_control); if (const auto& termControl{ _control.try_as() }) { _connectionStateChangedToken = termControl.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler }); @@ -836,13 +958,13 @@ std::pair Pane::_GetOffsetsForPane(const PaneP if (_splitState == SplitState::Horizontal) { - secondOffset.y += (1 - _desiredSplitPosition) * parentOffset.scaleY; + secondOffset.y += _desiredSplitPosition * parentOffset.scaleY; firstOffset.scaleY *= _desiredSplitPosition; secondOffset.scaleY *= (1 - _desiredSplitPosition); } else { - secondOffset.x += (1 - _desiredSplitPosition) * parentOffset.scaleX; + secondOffset.x += _desiredSplitPosition * parentOffset.scaleX; firstOffset.scaleX *= _desiredSplitPosition; secondOffset.scaleX *= (1 - _desiredSplitPosition); } @@ -1070,10 +1192,15 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect // - // Return Value: // - -void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& /* sender */, +void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const& /* args */) { - _GotFocusHandlers(shared_from_this()); + FocusState f = FocusState::Programmatic; + if (const auto o = sender.try_as()) + { + f = o.FocusState(); + } + _GotFocusHandlers(shared_from_this(), f); } // Event Description: @@ -1135,8 +1262,8 @@ Controls::Grid Pane::GetRootElement() // Method Description: // - If this is the last focused pane, returns itself. Returns nullptr if this -// is a leaf and it's not focused. If it's a parent, it returns nullptr if no -// children of this pane were the last pane to be focused, or the Pane that +// is a leaf and it's not focused. If it's a parent, it returns nullptr if it nor +// any children of this pane were the last pane to be focused, or the Pane that // _was_ the last pane to be focused (if there was one). // - This Pane's control might not currently be focused, if the tab itself is // not currently focused. @@ -1145,9 +1272,13 @@ Controls::Grid Pane::GetRootElement() // `_lastActive`, else returns this std::shared_ptr Pane::GetActivePane() { + if (_lastActive) + { + return shared_from_this(); + } if (_IsLeaf()) { - return _lastActive ? shared_from_this() : nullptr; + return nullptr; } auto firstFocused = _firstChild->GetActivePane(); @@ -1159,7 +1290,38 @@ std::shared_ptr Pane::GetActivePane() } // Method Description: -// - Gets the TermControl of this pane. If this Pane is not a leaf, this will return nullptr. +// - Gets the TermControl of this pane. If this Pane is not a leaf but is +// focused, this will return the control of the last leaf pane that had focus. +// Otherwise, this will return the control of the first child of this pane. +// Arguments: +// - +// Return Value: +// - nullptr if this Pane is an unfocused parent, otherwise the TermControl of this Pane. +TermControl Pane::GetLastFocusedTerminalControl() +{ + if (!_IsLeaf()) + { + if (_lastActive) + { + std::shared_ptr pane = shared_from_this(); + while (const auto p = pane->_parentChildPath.lock()) + { + if (p->_IsLeaf()) + { + return p->GetTerminalControl(); + } + pane = p; + } + // We didn't find our child somehow, they might have closed under us. + } + return _firstChild->GetLastFocusedTerminalControl(); + } + return GetTerminalControl(); +} + +// Method Description: +// - Gets the TermControl of this pane. If this Pane is not a leaf this will +// return the nullptr; // Arguments: // - // Return Value: @@ -1195,7 +1357,7 @@ void Pane::ClearActive() // Method Description: // - Sets the "Active" state on this Pane. Only one Pane in a tree of Panes -// should be "active", and that pane should be a leaf. +// should be "active". // - Updates our visuals to match our new state, including highlighting our borders. // Arguments: // - @@ -1257,7 +1419,7 @@ bool Pane::_HasFocusedChild() const noexcept // We're intentionally making this one giant expression, so the compiler // will skip the following lookups if one of the lookups before it returns // true - return (_control && _lastActive) || + return (_lastActive) || (_firstChild && _firstChild->_HasFocusedChild()) || (_secondChild && _secondChild->_HasFocusedChild()); } @@ -1271,7 +1433,30 @@ bool Pane::_HasFocusedChild() const noexcept // - void Pane::UpdateVisuals() { - _border.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); + // If we are the focused pane, but not a leaf we should add borders + if (!_IsLeaf()) + { + _UpdateBorders(); + } + _borderFirst.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); + _borderSecond.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush); +} + +// Method Description: +// - Focus the current pane. Also trigger focus on the control, or if not a leaf +// the control belonging to the last focused leaf. +// This makes sure that focus exists within the tab (since panes aren't proper controls) +// Arguments: +// - +// Return Value: +// - +void Pane::_Focus() +{ + _GotFocusHandlers(shared_from_this(), FocusState::Programmatic); + if (const auto& control = GetLastFocusedTerminalControl()) + { + control.Focus(FocusState::Programmatic); + } } // Method Description: @@ -1302,10 +1487,7 @@ void Pane::_FocusFirstChild() // // `wtd -w 0 mf down ; sp` // `wtd -w 0 fp -t 1 ; sp` - - _GotFocusHandlers(shared_from_this()); - - _control.Focus(FocusState::Programmatic); + _Focus(); } else { @@ -1365,7 +1547,7 @@ std::shared_ptr Pane::AttachPane(std::shared_ptr pane, SplitDirectio pane->WalkTree([](auto p) { if (p->_lastActive) { - p->_FocusFirstChild(); + p->_Focus(); return true; } return false; @@ -1405,8 +1587,10 @@ std::shared_ptr Pane::DetachPane(std::shared_ptr pane) // other child. _CloseChild(isFirstChild, true); + // Update the borders on this pane and any children to match if we have + // no parent. detached->_borders = Borders::None; - detached->_UpdateBorders(); + detached->_ApplySplitDefinitions(); // Trigger the detached event on each child detached->WalkTree([](auto pane) { @@ -1457,15 +1641,23 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) auto closedChildClosedToken = closeFirst ? _firstClosedToken : _secondClosedToken; auto remainingChildClosedToken = closeFirst ? _secondClosedToken : _firstClosedToken; + // If we were a parent pane, and we pointed into the now closed child + // clear it. We will set it to something else later if + bool usedToFocusClosedChildsTerminal = false; + if (const auto prev = _parentChildPath.lock()) + { + if (closedChild == prev) + { + _parentChildPath.reset(); + usedToFocusClosedChildsTerminal = true; + } + } + // If the only child left is a leaf, that means we're a leaf now. if (remainingChild->_IsLeaf()) { - // When the remaining child is a leaf, that means both our children were - // previously leaves, and the only difference in their borders is the - // border that we gave them. Take a bitwise AND of those two children to - // remove that border. Other borders the children might have, they - // inherited from us, so the flag will be set for both children. - _borders = _firstChild->_borders & _secondChild->_borders; + // Find what borders need to persist after we close the child + _borders = _GetCommonBorders(); // take the control, profile and id of the pane that _wasn't_ closed. _control = remainingChild->_control; @@ -1490,11 +1682,17 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // handlers since it is just getting moved. if (!isDetaching) { - if (const auto& closedControl{ closedChild->_control.try_as() }) - { - closedControl.ConnectionStateChanged(closedChild->_connectionStateChangedToken); - closedControl.WarningBell(closedChild->_warningBellToken); - } + closedChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + if (const auto& closedControl{ p->_control.try_as() }) + { + closedControl.ConnectionStateChanged(p->_connectionStateChangedToken); + closedControl.WarningBell(p->_warningBellToken); + } + } + return false; + }); } closedChild->Closed(closedChildClosedToken); @@ -1506,24 +1704,25 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) remainingControl.WarningBell(remainingChild->_warningBellToken); } - // If either of our children was focused, we want to take that focus from - // them. - _lastActive = _firstChild->_lastActive || _secondChild->_lastActive; + // If we or either of our children was focused, we want to take that + // focus from them. + _lastActive = _lastActive || _firstChild->_lastActive || _secondChild->_lastActive; // Remove all the ui elements of the remaining child. This'll make sure // we can re-attach the UserControl to our Grid. remainingChild->_root.Children().Clear(); - remainingChild->_border.Child(nullptr); + remainingChild->_borderFirst.Child(nullptr); // Reset our UI: _root.Children().Clear(); - _border.Child(nullptr); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); _root.ColumnDefinitions().Clear(); _root.RowDefinitions().Clear(); // Reattach the UserControl to our grid. - _root.Children().Append(_border); - _border.Child(_control); + _root.Children().Append(_borderFirst); + _borderFirst.Child(_control); // Make sure to set our _splitState before focusing the control. If you // fail to do this, when the tab handles the GotFocus event and asks us @@ -1537,7 +1736,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // If we're inheriting the "last active" state from one of our children, // focus our control now. This should trigger our own GotFocus event. - if (_lastActive) + if (usedToFocusClosedChildsTerminal || _lastActive) { _control.Focus(FocusState::Programmatic); @@ -1549,7 +1748,7 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // the control. Because Tab is relying on GotFocus to know who the // active pane in the tree is, without this call, _no one_ will be // the active pane any longer. - _GotFocusHandlers(shared_from_this()); + _GotFocusHandlers(shared_from_this(), FocusState::Programmatic); } _UpdateBorders(); @@ -1580,16 +1779,23 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) closedChild->Closed(closedChildClosedToken); if (!isDetaching) { - if (const auto& closedControl{ closedChild->_control.try_as() }) - { - closedControl.ConnectionStateChanged(closedChild->_connectionStateChangedToken); - closedControl.WarningBell(closedChild->_warningBellToken); - } + closedChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + if (const auto& closedControl{ p->_control.try_as() }) + { + closedControl.ConnectionStateChanged(p->_connectionStateChangedToken); + closedControl.WarningBell(p->_warningBellToken); + } + } + return false; + }); } // Reset our UI: _root.Children().Clear(); - _border.Child(nullptr); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); _root.ColumnDefinitions().Clear(); _root.RowDefinitions().Clear(); @@ -1613,20 +1819,44 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // Remove the child's UI elements from the child's grid, so we can // attach them to us instead. remainingChild->_root.Children().Clear(); - remainingChild->_border.Child(nullptr); + remainingChild->_borderFirst.Child(nullptr); + remainingChild->_borderSecond.Child(nullptr); - _root.Children().Append(_firstChild->GetRootElement()); - _root.Children().Append(_secondChild->GetRootElement()); + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); // Propagate the new borders down to the children. _borders = remainingBorders; _ApplySplitDefinitions(); - // If the closed child was focused, transfer the focus to it's first sibling. - if (closedChild->_lastActive) + // If our child had focus and closed, just transfer to the first remaining + // child + if (closedChild->_HasFocusedChild()) { _FocusFirstChild(); } + // We might not have focus currently, but if our parent does then we + // want to make sure we have a valid path to one of our children. + // We should only update the path if our other child doesn't have focus itself. + else if (usedToFocusClosedChildsTerminal && !_secondChild->_HasFocusedChild()) + { + // update our path to our first remaining leaf + _parentChildPath = _firstChild; + _firstChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + return true; + } + p->_parentChildPath = p->_firstChild; + return false; + }); + // This will focus the first terminal, and will set that leaf pane + // to the active pane if we nor one of our parents is not itself focused. + _FocusFirstChild(); + } // Release the pointers that the child was holding. remainingChild->_firstChild = nullptr; @@ -1676,16 +1906,18 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) }; // Remove both children from the grid - _root.Children().Clear(); - // Add the remaining child back to the grid, in the right place. - _root.Children().Append(remainingChild->GetRootElement()); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); + if (_splitState == SplitState::Vertical) { - Controls::Grid::SetColumn(remainingChild->GetRootElement(), closeFirst ? 1 : 0); + Controls::Grid::SetColumn(_borderFirst, 0); + Controls::Grid::SetColumn(_borderSecond, 1); } else if (_splitState == SplitState::Horizontal) { - Controls::Grid::SetRow(remainingChild->GetRootElement(), closeFirst ? 1 : 0); + Controls::Grid::SetRow(_borderFirst, 0); + Controls::Grid::SetRow(_borderSecond, 1); } // Create the dummy grid. This grid will be the one we actually animate, @@ -1697,17 +1929,9 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst) // It should be the size of the closed pane. dummyGrid.Width(removedOriginalSize.Width); dummyGrid.Height(removedOriginalSize.Height); - // Put it where the removed child is - if (_splitState == SplitState::Vertical) - { - Controls::Grid::SetColumn(dummyGrid, closeFirst ? 0 : 1); - } - else if (_splitState == SplitState::Horizontal) - { - Controls::Grid::SetRow(dummyGrid, closeFirst ? 0 : 1); - } - // Add it to the tree - _root.Children().Append(dummyGrid); + + _borderFirst.Child(closeFirst ? dummyGrid : remainingChild->GetRootElement()); + _borderSecond.Child(closeFirst ? remainingChild->GetRootElement() : dummyGrid); // Set up the rows/cols as auto/auto, so they'll only use the size of // the elements in the grid. @@ -1846,9 +2070,9 @@ void Pane::_UpdateBorders() double top = 0, bottom = 0, left = 0, right = 0; Thickness newBorders{ 0 }; - if (_zoomed) + // Zoomed panes, and focused parents should have full borders + if (_zoomed || (!_IsLeaf() && _lastActive)) { - // When the pane is zoomed, manually show all the borders around the window. top = bottom = right = left = PaneBorderSize; } else @@ -1870,11 +2094,31 @@ void Pane::_UpdateBorders() right = PaneBorderSize; } } - _border.BorderThickness(ThicknessHelper::FromLengths(left, top, right, bottom)); + + if (_IsLeaf()) + { + _borderFirst.BorderThickness(ThicknessHelper::FromLengths(left, top, right, bottom)); + } + else + { + // If we are not a leaf we don't want to duplicate the shared border + // between our children. + if (_splitState == SplitState::Vertical) + { + _borderFirst.BorderThickness(ThicknessHelper::FromLengths(left, top, 0, bottom)); + _borderSecond.BorderThickness(ThicknessHelper::FromLengths(0, top, right, bottom)); + } + else + { + _borderFirst.BorderThickness(ThicknessHelper::FromLengths(left, top, right, 0)); + _borderSecond.BorderThickness(ThicknessHelper::FromLengths(left, 0, right, bottom)); + } + } } // Method Description: // - Find the borders for the leaf pane, or the shared borders for child panes. +// - This deliberately ignores if a focused parent has borders. // Arguments: // - // Return Value: @@ -1901,8 +2145,8 @@ void Pane::_ApplySplitDefinitions() { if (_splitState == SplitState::Vertical) { - Controls::Grid::SetColumn(_firstChild->GetRootElement(), 0); - Controls::Grid::SetColumn(_secondChild->GetRootElement(), 1); + Controls::Grid::SetColumn(_borderFirst, 0); + Controls::Grid::SetColumn(_borderSecond, 1); _firstChild->_borders = _borders | Borders::Right; _secondChild->_borders = _borders | Borders::Left; @@ -1913,8 +2157,8 @@ void Pane::_ApplySplitDefinitions() } else if (_splitState == SplitState::Horizontal) { - Controls::Grid::SetRow(_firstChild->GetRootElement(), 0); - Controls::Grid::SetRow(_secondChild->GetRootElement(), 1); + Controls::Grid::SetRow(_borderFirst, 0); + Controls::Grid::SetRow(_borderSecond, 1); _firstChild->_borders = _borders | Borders::Bottom; _secondChild->_borders = _borders | Borders::Top; @@ -1968,6 +2212,7 @@ void Pane::_SetupEntranceAnimation() auto setupAnimation = [&](const auto& size, const bool isFirstChild) { auto child = isFirstChild ? _firstChild : _secondChild; auto childGrid = child->_root; + // If we are splitting a parent pane this may be null auto control = child->_control; // Build up our animation: // * it'll take as long as our duration (200ms) @@ -2023,16 +2268,22 @@ void Pane::_SetupEntranceAnimation() // the parent pane, otherwise use the bottom/right. This is always // the "outside" of the parent pane. childGrid.HorizontalAlignment(isFirstChild ? HorizontalAlignment::Left : HorizontalAlignment::Right); - control.HorizontalAlignment(HorizontalAlignment::Left); - control.Width(isFirstChild ? totalSize : size); + if (control) + { + control.HorizontalAlignment(HorizontalAlignment::Left); + control.Width(isFirstChild ? totalSize : size); + } // When the animation is completed, undo the trickiness from before, to // restore the controls to the behavior they'd usually have. animation.Completed([childGrid, control, root = _secondChild->_root](auto&&, auto&&) { - control.Width(NAN); childGrid.Width(NAN); childGrid.HorizontalAlignment(HorizontalAlignment::Stretch); - control.HorizontalAlignment(HorizontalAlignment::Stretch); + if (control) + { + control.Width(NAN); + control.HorizontalAlignment(HorizontalAlignment::Stretch); + } root.Background(nullptr); }); } @@ -2042,16 +2293,22 @@ void Pane::_SetupEntranceAnimation() // the parent pane, otherwise use the bottom/right. This is always // the "outside" of the parent pane. childGrid.VerticalAlignment(isFirstChild ? VerticalAlignment::Top : VerticalAlignment::Bottom); - control.VerticalAlignment(VerticalAlignment::Top); - control.Height(isFirstChild ? totalSize : size); + if (control) + { + control.VerticalAlignment(VerticalAlignment::Top); + control.Height(isFirstChild ? totalSize : size); + } // When the animation is completed, undo the trickiness from before, to // restore the controls to the behavior they'd usually have. animation.Completed([childGrid, control, root = _secondChild->_root](auto&&, auto&&) { - control.Height(NAN); childGrid.Height(NAN); childGrid.VerticalAlignment(VerticalAlignment::Stretch); - control.VerticalAlignment(VerticalAlignment::Stretch); + if (control) + { + control.Height(NAN); + control.VerticalAlignment(VerticalAlignment::Stretch); + } root.Background(nullptr); }); } @@ -2099,41 +2356,38 @@ std::optional Pane::PreCalculateCanSplit(const std::shared_ptr targe const float splitSize, const winrt::Windows::Foundation::Size availableSpace) const { - if (_IsLeaf()) + if (target.get() == this) { - if (target.get() == this) + const auto firstPercent = 1.0f - splitSize; + const auto secondPercent = splitSize; + // If this pane is a leaf, and it's the pane we're looking for, use + // the available space to calculate which direction to split in. + const Size minSize = _GetMinSize(); + + if (splitType == SplitDirection::Left || splitType == SplitDirection::Right) { - const auto firstPrecent = 1.0f - splitSize; - const auto secondPercent = splitSize; - // If this pane is a leaf, and it's the pane we're looking for, use - // the available space to calculate which direction to split in. - const Size minSize = _GetMinSize(); + const auto widthMinusSeparator = availableSpace.Width - CombinedPaneBorderSize; + const auto newFirstWidth = widthMinusSeparator * firstPercent; + const auto newSecondWidth = widthMinusSeparator * secondPercent; - if (splitType == SplitDirection::Left || splitType == SplitDirection::Right) - { - const auto widthMinusSeparator = availableSpace.Width - CombinedPaneBorderSize; - const auto newFirstWidth = widthMinusSeparator * firstPrecent; - const auto newSecondWidth = widthMinusSeparator * secondPercent; - - return { newFirstWidth > minSize.Width && newSecondWidth > minSize.Width }; - } - - else if (splitType == SplitDirection::Up || splitType == SplitDirection::Down) - { - const auto heightMinusSeparator = availableSpace.Height - CombinedPaneBorderSize; - const auto newFirstHeight = heightMinusSeparator * firstPrecent; - const auto newSecondHeight = heightMinusSeparator * secondPercent; - - return { newFirstHeight > minSize.Height && newSecondHeight > minSize.Height }; - } + return { newFirstWidth > minSize.Width && newSecondWidth > minSize.Width }; } - else + + else if (splitType == SplitDirection::Up || splitType == SplitDirection::Down) { - // If this pane is _any other leaf_, then just return nullopt, to - // indicate that the `target` Pane is not down this branch. - return std::nullopt; + const auto heightMinusSeparator = availableSpace.Height - CombinedPaneBorderSize; + const auto newFirstHeight = heightMinusSeparator * firstPercent; + const auto newSecondHeight = heightMinusSeparator * secondPercent; + + return { newFirstHeight > minSize.Height && newSecondHeight > minSize.Height }; } } + else if (_IsLeaf()) + { + // If this pane is _any other leaf_, then just return nullopt, to + // indicate that the `target` Pane is not down this branch. + return std::nullopt; + } else { // If this pane is a parent, calculate how much space our children will @@ -2177,13 +2431,13 @@ std::pair, std::shared_ptr> Pane::Split(SplitDirecti const Profile& profile, const Controls::UserControl& control) { - if (!_IsLeaf()) + if (!_lastActive) { - if (_firstChild->_HasFocusedChild()) + if (_firstChild && _firstChild->_HasFocusedChild()) { return _firstChild->Split(splitType, splitSize, profile, control); } - else if (_secondChild->_HasFocusedChild()) + else if (_secondChild && _secondChild->_HasFocusedChild()) { return _secondChild->Split(splitType, splitSize, profile, control); } @@ -2209,11 +2463,11 @@ bool Pane::ToggleSplitOrientation() return false; } - // Check if either our first or second child is the currently focused leaf. - // If they are then switch the split orientation on the current pane. + // If a parent pane is focused, or if one of its children are a leaf and is + // focused then switch the split orientation on the current pane. const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive; const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive; - if (firstIsFocused || secondIsFocused) + if (_lastActive || firstIsFocused || secondIsFocused) { // Switch the split orientation _splitState = _splitState == SplitState::Horizontal ? SplitState::Vertical : SplitState::Horizontal; @@ -2281,49 +2535,69 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect // modify our tree std::unique_lock lock{ _createCloseLock }; - if (const auto& termControl{ _control.try_as() }) + if (_IsLeaf()) { - // revoke our handler - the child will take care of the control now. - termControl.ConnectionStateChanged(_connectionStateChangedToken); - termControl.WarningBell(_warningBellToken); - _connectionStateChangedToken.value = 0; - _warningBellToken.value = 0; + if (const auto& termControl{ _control.try_as() }) + { + // revoke our handler - the child will take care of the control now. + termControl.ConnectionStateChanged(_connectionStateChangedToken); + termControl.WarningBell(_warningBellToken); + _connectionStateChangedToken.value = 0; + _warningBellToken.value = 0; + } + + // Remove our old GotFocus handler from the control. We don't what the + // control telling us that it's now focused, we want it telling its new + // parent. + _gotFocusRevoker.revoke(); + _lostFocusRevoker.revoke(); } - // Remove our old GotFocus handler from the control. We don't what the - // control telling us that it's now focused, we want it telling its new - // parent. - _gotFocusRevoker.revoke(); - _lostFocusRevoker.revoke(); - - _splitState = actualSplitType; - _desiredSplitPosition = 1.0f - splitSize; - // Remove any children we currently have. We can't add the existing // UserControl to a new grid until we do this. _root.Children().Clear(); - _border.Child(nullptr); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); - // Create two new Panes - // Move our control, guid into the first one. - // Move the new guid, control into the second. - _firstChild = std::make_shared(_profile, _control); - _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected); + // Create a new pane from ourself + if (!_IsLeaf()) + { + // Since we are a parent we don't have borders normally, + // so set them temporarily for when we update our split definition. + _borders = _GetCommonBorders(); + _firstChild->Closed(_firstClosedToken); + _secondChild->Closed(_secondClosedToken); + // If we are not a leaf we should create a new pane that contains our children + auto first = std::make_shared(_firstChild, _secondChild, _splitState, _desiredSplitPosition); + _firstChild = first; + } + else + { + // Move our control, guid into the first one. + _firstChild = std::make_shared(_profile, _control); + _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected); + _profile = nullptr; + _control = { nullptr }; + } + + _splitState = actualSplitType; + _desiredSplitPosition = 1.0f - splitSize; _secondChild = newPane; - // If we want the new pane to be the first child, swap the children if (splitType == SplitDirection::Up || splitType == SplitDirection::Left) { std::swap(_firstChild, _secondChild); } - _profile = nullptr; - _control = { nullptr }; - + _root.ColumnDefinitions().Clear(); + _root.RowDefinitions().Clear(); _CreateRowColDefinitions(); - _root.Children().Append(_firstChild->GetRootElement()); - _root.Children().Append(_secondChild->GetRootElement()); + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); _ApplySplitDefinitions(); @@ -2358,12 +2632,9 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect // - void Pane::Maximize(std::shared_ptr zoomedPane) { - if (_IsLeaf()) - { - _zoomed = (zoomedPane == shared_from_this()); - _UpdateBorders(); - } - else + _zoomed = (zoomedPane == shared_from_this()); + _UpdateBorders(); + if (!_IsLeaf()) { if (zoomedPane == _firstChild || zoomedPane == _secondChild) { @@ -2371,6 +2642,8 @@ void Pane::Maximize(std::shared_ptr zoomedPane) // tree. Easy way: just remove both children. We'll re-attach both // when we un-zoom. _root.Children().Clear(); + _borderFirst.Child(nullptr); + _borderSecond.Child(nullptr); } // Always recurse into both children. If the (un)zoomed pane was one of @@ -2393,20 +2666,21 @@ void Pane::Maximize(std::shared_ptr zoomedPane) // - void Pane::Restore(std::shared_ptr zoomedPane) { - if (_IsLeaf()) - { - _zoomed = false; - _UpdateBorders(); - } - else + _zoomed = false; + _UpdateBorders(); + if (!_IsLeaf()) { if (zoomedPane == _firstChild || zoomedPane == _secondChild) { // When we're un-zooming the pane, we'll need to re-add it to our UI // tree where it originally belonged. easy way: just re-add both. _root.Children().Clear(); - _root.Children().Append(_firstChild->GetRootElement()); - _root.Children().Append(_secondChild->GetRootElement()); + + _borderFirst.Child(_firstChild->GetRootElement()); + _borderSecond.Child(_secondChild->GetRootElement()); + + _root.Children().Append(_borderFirst); + _root.Children().Append(_borderSecond); } // Always recurse into both children. If the (un)zoomed pane was one of @@ -2443,6 +2717,8 @@ void Pane::Id(uint32_t id) noexcept // - The ID of the pane we want to focus bool Pane::FocusPane(const uint32_t id) { + // Always clear the parent child path if we are focusing a leaf + _parentChildPath.reset(); if (_IsLeaf() && id == _id) { // Make sure to use _FocusFirstChild here - that'll properly update the @@ -2462,23 +2738,23 @@ bool Pane::FocusPane(const uint32_t id) } // Method Description: // - Focuses the given pane if it is in the tree. -// This deliberately mirrors FocusPane(id) instead of just calling -// _FocusFirstChild directly. +// - This is different than FocusPane(id) in that it allows focusing +// panes that are not leaves. // Arguments: // - the pane to focus // Return Value: // - true if focus was set bool Pane::FocusPane(const std::shared_ptr pane) { - if (_IsLeaf() && this == pane.get()) + if (this == pane.get()) { - // Make sure to use _FocusFirstChild here - that'll properly update the - // focus if we're in startup. - _FocusFirstChild(); + _Focus(); return true; } else { + // clear the parent child path if we are not the pane being focused. + _parentChildPath.reset(); if (_firstChild && _secondChild) { return _firstChild->FocusPane(pane) || @@ -2488,6 +2764,27 @@ bool Pane::FocusPane(const std::shared_ptr pane) return false; } +// Method Description: +// - Check if this pane contains the the argument as a child anywhere along the tree. +// Arguments: +// - child: the child to search for. +// Return Value: +// - true if the child was found. +bool Pane::_HasChild(const std::shared_ptr child) +{ + if (_IsLeaf()) + { + return false; + } + + if (_firstChild == child || _secondChild == child) + { + return true; + } + + return _firstChild->_HasChild(child) || _secondChild->_HasChild(child); +} + // Method Description: // - Recursive function that finds a pane with the given ID // Arguments: @@ -3002,20 +3299,17 @@ int Pane::GetLeafPaneCount() const noexcept std::optional Pane::PreCalculateAutoSplit(const std::shared_ptr target, const winrt::Windows::Foundation::Size availableSpace) const { - if (_IsLeaf()) + if (target.get() == this) { - if (target.get() == this) - { - //If this pane is a leaf, and it's the pane we're looking for, use - //the available space to calculate which direction to split in. - return availableSpace.Width > availableSpace.Height ? SplitDirection::Right : SplitDirection::Down; - } - else - { - // If this pane is _any other leaf_, then just return nullopt, to - // indicate that the `target` Pane is not down this branch. - return std::nullopt; - } + // If this pane is the pane we are looking for, use the available space + // to calculate which direction to split in. + return availableSpace.Width > availableSpace.Height ? SplitDirection::Right : SplitDirection::Down; + } + else if (_IsLeaf()) + { + // If this pane is _any other leaf_, then just return nullopt, to + // indicate that the `target` Pane is not down this branch. + return std::nullopt; } else { @@ -3071,7 +3365,7 @@ void Pane::CollectTaskbarStates(std::vector& s } } -DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate>); +DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, Pane::gotFocusArgs); DEFINE_EVENT(Pane, LostFocus, _LostFocusHandlers, winrt::delegate>); DEFINE_EVENT(Pane, PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); DEFINE_EVENT(Pane, Detached, _PaneDetachedHandlers, winrt::delegate>); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index c57f94d93..8248abd4f 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -40,7 +40,8 @@ enum class Borders : int Top = 0x1, Bottom = 0x2, Left = 0x4, - Right = 0x8 + Right = 0x8, + All = 0xF }; DEFINE_ENUM_FLAG_OPERATORS(Borders); @@ -58,8 +59,15 @@ public: const winrt::Windows::UI::Xaml::Controls::UserControl& control, const bool lastFocused = false); + Pane(std::shared_ptr first, + std::shared_ptr second, + const SplitState splitType, + const float splitPosition, + const bool lastFocused = false); + std::shared_ptr GetActivePane(); winrt::Windows::UI::Xaml::Controls::UserControl GetUserControl() const; + winrt::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl(); winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl() const; winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile(); @@ -145,25 +153,43 @@ public: // - true if the predicate returned true on any pane. template //requires std::predicate> - bool WalkTree(F f) + auto WalkTree(F f) -> decltype(f(shared_from_this())) { - if (f(shared_from_this())) - { - return true; - } + using R = std::invoke_result_t>; + static constexpr auto IsVoid = std::is_void_v; - if (!_IsLeaf()) + if constexpr (IsVoid) { - return _firstChild->WalkTree(f) || _secondChild->WalkTree(f); + f(shared_from_this()); + if (!_IsLeaf()) + { + _firstChild->WalkTree(f); + _secondChild->WalkTree(f); + } } + else + { + if (f(shared_from_this())) + { + return true; + } - return false; + if (!_IsLeaf()) + { + return _firstChild->WalkTree(f) || _secondChild->WalkTree(f); + } + + return false; + } } void CollectTaskbarStates(std::vector& states); WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); - DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); + + using gotFocusArgs = winrt::delegate, winrt::Windows::UI::Xaml::FocusState>; + + DECLARE_EVENT(GotFocus, _GotFocusHandlers, gotFocusArgs); DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate>); DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); DECLARE_EVENT(Detached, _PaneDetachedHandlers, winrt::delegate>); @@ -176,8 +202,10 @@ private: struct LayoutSizeNode; winrt::Windows::UI::Xaml::Controls::Grid _root{}; - winrt::Windows::UI::Xaml::Controls::Border _border{}; + winrt::Windows::UI::Xaml::Controls::Border _borderFirst{}; + winrt::Windows::UI::Xaml::Controls::Border _borderSecond{}; winrt::Windows::UI::Xaml::Controls::UserControl _control{ nullptr }; + winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected }; static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush; static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_unfocusedBorderBrush; @@ -188,6 +216,7 @@ private: float _desiredSplitPosition; std::optional _id; + std::weak_ptr _parentChildPath{}; bool _lastActive{ false }; winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr }; @@ -208,6 +237,7 @@ private: bool _IsLeaf() const noexcept; bool _HasFocusedChild() const noexcept; void _SetupChildCloseHandlers(); + bool _HasChild(const std::shared_ptr child); std::pair, std::shared_ptr> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType, const float splitSize, @@ -235,6 +265,7 @@ private: void _CloseChild(const bool closeFirst, const bool isDetaching); winrt::fire_and_forget _CloseChildRoutine(const bool closeFirst); + void _Focus(); void _FocusFirstChild(); void _ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/); void _ControlWarningBellHandler(winrt::Windows::Foundation::IInspectable const& sender, diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index 64c232b5e..dcf8a262f 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -190,6 +190,9 @@ Do you want to close all tabs? + + Multiple panes + Close... diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index cb1d2f876..9168f3fea 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -748,31 +748,32 @@ namespace winrt::TerminalApp::implementation { _UnZoomIfNeeded(); - auto pane = terminalTab->GetActivePane(); - if (const auto pane{ terminalTab->GetActivePane() }) { - if (const auto control{ pane->GetTerminalControl() }) + if (pane->ContainsReadOnly()) { - if (control.ReadOnly()) + ContentDialogResult warningResult = co_await _ShowCloseReadOnlyDialog(); + + // If the user didn't explicitly click on close tab - leave + if (warningResult != ContentDialogResult::Primary) { - ContentDialogResult warningResult = co_await _ShowCloseReadOnlyDialog(); - - // If the user didn't explicitly click on close tab - leave - if (warningResult != ContentDialogResult::Primary) - { - co_return; - } - - // Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab - if (control.ReadOnly()) - { - control.ToggleReadOnly(); - } + co_return; } - pane->Close(); + // Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab + pane->WalkTree([](auto p) { + if (const auto control{ p->GetTerminalControl() }) + { + if (control.ReadOnly()) + { + control.ToggleReadOnly(); + } + } + return false; + }); } + + pane->Close(); } } else if (auto index{ _GetFocusedTabIndex() }) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 8125029da..5a8fa9799 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -263,6 +263,26 @@ namespace winrt::TerminalApp::implementation bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool _MovePane(const uint32_t tabIdx); + template + bool _ApplyToActiveControls(F f) + { + if (const auto tab{ _GetFocusedTabImpl() }) + { + if (const auto activePane = tab->GetActivePane()) + { + activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + f(control); + } + }); + + return true; + } + } + return false; + } + winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); std::optional _GetFocusedTabIndex() const noexcept; TerminalApp::TabBase _GetFocusedTab() const noexcept; @@ -366,7 +386,7 @@ namespace winrt::TerminalApp::implementation void _EndPreviewColorScheme(); void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); winrt::Microsoft::Terminal::Settings::Model::Command _lastPreviewedCommand{ nullptr }; - winrt::Microsoft::Terminal::Settings::Model::TerminalSettings _originalSettings{ nullptr }; + std::vector> _restorePreviewFuncs{}; HRESULT _OnNewConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index d6ce3eedd..7373d416f 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -67,8 +67,11 @@ namespace winrt::TerminalApp::implementation _rootPane->FocusPane(firstId); _activePane = _rootPane->GetActivePane(); } - // Set the active control - _mruPanes.insert(_mruPanes.begin(), _activePane->Id().value()); + // If the focused pane is a leaf, add it to the MRU panes + if (const auto id = _activePane->Id()) + { + _mruPanes.insert(_mruPanes.begin(), id.value()); + } _Setup(); } @@ -180,8 +183,8 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Returns nullptr if no children of this tab were the last control to be - // focused, or the TermControl that _was_ the last control to be focused (if - // there was one). + // focused, the active control of the current pane, or the last active child control + // of the active pane if it is a parent. // - This control might not currently be focused, if the tab itself is not // currently focused. // Arguments: @@ -193,7 +196,7 @@ namespace winrt::TerminalApp::implementation { if (_activePane) { - return _activePane->GetTerminalControl(); + return _activePane->GetLastFocusedTerminalControl(); } return nullptr; } @@ -390,6 +393,10 @@ namespace winrt::TerminalApp::implementation { return _runtimeTabText; } + if (!_activePane->_IsLeaf()) + { + return RS_(L"MultiplePanes"); + } const auto lastFocusedControl = GetActiveTerminalControl(); return lastFocusedControl ? lastFocusedControl.Title() : L""; } @@ -517,19 +524,14 @@ namespace winrt::TerminalApp::implementation // either the first or second child, but this will always return the // original pane first. auto [original, newPane] = _activePane->Split(splitType, splitSize, profile, control); + // The active pane has an id if it is a leaf if (activePaneId) { original->Id(activePaneId.value()); - newPane->Id(_nextPaneId); - ++_nextPaneId; - } - else - { - original->Id(_nextPaneId); - ++_nextPaneId; - newPane->Id(_nextPaneId); - ++_nextPaneId; } + newPane->Id(_nextPaneId); + ++_nextPaneId; + _activePane = original; // Add a event handlers to the new panes' GotFocus event. When the pane @@ -557,8 +559,8 @@ namespace winrt::TerminalApp::implementation // - The removed pane, if the remove succeeded. std::shared_ptr TerminalTab::DetachPane() { - // if we only have one pane, remove it entirely - // and close this tab + // if we only have one pane, or the focused pane is the root, remove it + // entirely and close this tab if (_rootPane == _activePane) { return DetachRoot(); @@ -633,16 +635,12 @@ namespace winrt::TerminalApp::implementation // Add the new pane as an automatic split on the active pane. auto first = _activePane->AttachPane(pane, SplitDirection::Automatic); - // under current assumptions this condition should always be true. + // This will be true if the original _activePane is a leaf pane. + // If it is a parent pane then we don't want to set an ID on it. if (previousId) { first->Id(previousId.value()); } - else - { - first->Id(_nextPaneId); - ++_nextPaneId; - } // Update with event handlers on the new child. _activePane = first; @@ -718,7 +716,10 @@ namespace winrt::TerminalApp::implementation // throughout the entire tree. if (const auto newFocus = _rootPane->NavigateDirection(_activePane, direction, _mruPanes)) { + // Mark that we want the active pane to changed + _changingActivePane = true; const auto res = _rootPane->FocusPane(newFocus); + _changingActivePane = false; if (_zoomedPane) { @@ -741,11 +742,22 @@ namespace winrt::TerminalApp::implementation // - true if two panes were swapped. bool TerminalTab::SwapPane(const FocusDirection& direction) { + // You cannot swap panes with the parent/child pane because of the + // circular reference. + if (direction == FocusDirection::Parent || direction == FocusDirection::Child) + { + return false; + } // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. if (auto neighbor = _rootPane->NavigateDirection(_activePane, direction, _mruPanes)) { - return _rootPane->SwapPanes(_activePane, neighbor); + // SwapPanes will refocus the terminal to make sure that it has focus + // even after moving. + _changingActivePane = true; + const auto res = _rootPane->SwapPanes(_activePane, neighbor); + _changingActivePane = false; + return res; } return false; @@ -753,7 +765,10 @@ namespace winrt::TerminalApp::implementation bool TerminalTab::FocusPane(const uint32_t id) { - return _rootPane->FocusPane(id); + _changingActivePane = true; + const auto res = _rootPane->FocusPane(id); + _changingActivePane = false; + return res; } // Method Description: @@ -1050,7 +1065,7 @@ namespace winrt::TerminalApp::implementation auto weakThis{ get_weak() }; std::weak_ptr weakPane{ pane }; - auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr sender) { + auto gotFocusToken = pane->GotFocus([weakThis](std::shared_ptr sender, WUX::FocusState focus) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; @@ -1058,8 +1073,20 @@ namespace winrt::TerminalApp::implementation { if (sender != tab->_activePane) { - tab->_UpdateActivePane(sender); - tab->_RecalculateAndApplyTabColor(); + auto senderIsChild = tab->_activePane->_HasChild(sender); + + // Only move focus if we the program moved focus, or the + // user moved with their mouse. This is a problem because a + // pane isn't a control itself, and if we have the parent + // focused we are fine if the terminal control is focused, + // but we don't want to update the active pane. + if (!senderIsChild || + (focus == WUX::FocusState::Programmatic && tab->_changingActivePane) || + focus == WUX::FocusState::Pointer) + { + tab->_UpdateActivePane(sender); + tab->_RecalculateAndApplyTabColor(); + } } tab->_focusState = WUX::FocusState::Programmatic; // This tab has gained focus, remove the bell indicator if it is active @@ -1094,8 +1121,19 @@ namespace winrt::TerminalApp::implementation tab->Content(tab->_rootPane->GetRootElement()); tab->ExitZoom(); } + if (auto pane = weakPane.lock()) { + // When a parent pane is selected, but one of its children + // close out under it we still need to update title/focus information + // but the GotFocus handler will rightly see that the _activePane + // did not actually change. Triggering + if (pane != tab->_activePane && !tab->_activePane->_IsLeaf()) + { + co_await winrt::resume_foreground(tab->Content().Dispatcher()); + tab->_UpdateActivePane(tab->_activePane); + } + for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i) { if (*i == pane->Id()) @@ -1313,16 +1351,13 @@ namespace winrt::TerminalApp::implementation // - The tab's color, if any std::optional TerminalTab::GetTabColor() { - const auto& termControl{ GetActiveTerminalControl() }; - if (!termControl) - { - return std::nullopt; - } - const auto currControlColor{ termControl.TabColor() }; std::optional controlTabColor; - if (currControlColor != nullptr) + if (const auto& control = GetActiveTerminalControl()) { - controlTabColor = currControlColor.Value(); + if (const auto color = control.TabColor()) + { + controlTabColor = color.Value(); + } } // A Tab's color will be the result of layering a variety of sources, @@ -1435,7 +1470,16 @@ namespace winrt::TerminalApp::implementation // TabViewItem().Background() only sets the color of the tab background // when the TabViewItem is unselected. So we still need to set the other // properties ourselves. - TabViewItem().Background(deselectedTabBrush); + // + // GH#11294: DESPITE the fact that there's a Background() API that we + // could just call like: + // + // TabViewItem().Background(deselectedTabBrush); + // + // We actually can't, because it will make the part of the tab that + // doesn't contain the text totally transparent to hit tests. So we + // actually _do_ still need to set TabViewItemHeaderBackground manually. + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackground"), deselectedTabBrush); TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), selectedTabBrush); TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPointerOver"), hoverTabBrush); TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPressed"), selectedTabBrush); @@ -1481,6 +1525,7 @@ namespace winrt::TerminalApp::implementation void TerminalTab::_ClearTabBackgroundColor() { winrt::hstring keys[] = { + L"TabViewItemHeaderBackground", L"TabViewItemHeaderBackgroundSelected", L"TabViewItemHeaderBackgroundPointerOver", L"TabViewItemHeaderForeground", @@ -1501,9 +1546,6 @@ namespace winrt::TerminalApp::implementation } } - // Clear out the Background. - TabViewItem().Background(nullptr); - _RefreshVisualState(); _colorCleared(); } @@ -1613,6 +1655,11 @@ namespace winrt::TerminalApp::implementation void TerminalTab::EnterZoom() { + // Clear the content first, because with parent focusing it is possible + // to zoom the root pane, but setting the content will not trigger the + // property changed event since it is the same and you would end up with + // an empty tab. + Content(nullptr); _zoomedPane = _activePane; _rootPane->Maximize(_zoomedPane); // Update the tab header to show the magnifying glass @@ -1621,6 +1668,7 @@ namespace winrt::TerminalApp::implementation } void TerminalTab::ExitZoom() { + Content(nullptr); _rootPane->Restore(_zoomedPane); _zoomedPane = nullptr; // Update the tab header to hide the magnifying glass @@ -1635,13 +1683,34 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Toggle read-only mode on the active pane + // - If a parent pane is selected, this will ensure that all children have + // the same read-only status. void TerminalTab::TogglePaneReadOnly() { - auto control = GetActiveTerminalControl(); - if (control) - { - control.ToggleReadOnly(); - } + auto hasReadOnly = false; + auto allReadOnly = true; + _activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + hasReadOnly |= control.ReadOnly(); + allReadOnly &= control.ReadOnly(); + } + }); + _activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + // If all controls have the same read only state then just toggle + if (allReadOnly || !hasReadOnly) + { + control.ToggleReadOnly(); + } + // otherwise set to all read only. + else if (!control.ReadOnly()) + { + control.ToggleReadOnly(); + } + } + }); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 1fae344d1..d4af27076 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -140,6 +140,7 @@ namespace winrt::TerminalApp::implementation bool _receivedKeyDown{ false }; bool _iconHidden{ false }; + bool _changingActivePane{ false }; winrt::hstring _runtimeTabText{}; bool _inRename{ false }; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 1e41fdb5c..efcce89c8 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -274,10 +274,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation _updateAntiAliasingMode(_renderEngine.get()); // GH#5098: Inform the engine of the opacity of the default text background. - if (_settings.UseAcrylic()) - { - _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.Opacity())); - } + // GH#11315: Always do this, even if they don't have acrylic on. + _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.Opacity())); THROW_IF_FAILED(_renderEngine->Enable()); @@ -469,6 +467,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _renderEngine->ToggleShaderEffects(); } + // Always redraw after toggling effects. This way even if the control + // does not have focus it will update immediately. + _renderer->TriggerRedrawAll(); } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 30bffe174..937dd98b5 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -459,7 +459,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // GH#5098: Inform the engine of the new opacity of the default text background. - _core.SetBackgroundOpacity(::base::saturated_cast(appearance.Opacity())); + _core.SetBackgroundOpacity(appearance.Opacity()); } else { diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml index 7765d6626..e1556e80d 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml +++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml @@ -72,13 +72,15 @@ two font lists causes a crash within the ComboBox code. As a workaround, introduce two ComboBox controls and only display one at a time. --> - - - - - diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.xaml b/src/cascadia/TerminalSettingsEditor/Profiles.xaml index 24e6a7fe1..0208a8065 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles.xaml @@ -81,7 +81,8 @@ SettingOverrideSource="{x:Bind State.Profile.CommandlineOverrideSource, Mode=OneWay}" Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(State.Profile.IsBaseLayer), Mode=OneWay}"> -