diff --git a/.editorconfig b/.editorconfig index 40de22df5..7ffdc78df 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,7 @@ root = true [*] +charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/.gitattributes b/.gitattributes index bf0e54a56..6aa281e5b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,8 @@ ############################################################################### * -text +*.inc linguist-language=cpp + ############################################################################### # Set default behavior for command prompt diff. # diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index e4abdb7d8..35742a611 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,6 +1,7 @@ apc calt ccmp +changelog cybersecurity Apc clickable @@ -41,6 +42,7 @@ Lorigin maxed mkmk mru +noreply nje ogonek ok'd diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 4b6fbe4f5..19f5fb487 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -25,6 +25,7 @@ DERR dlldata DONTADDTORECENT DWORDLONG +enumset environstrings EXPCMDFLAGS EXPCMDSTATE diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 87d7a3d8c..a96139e3c 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -24,7 +24,9 @@ DTDs DWINRT enablewttlogging Intelli +IVisual LKG +LOCKFILE Lxss mfcribbon microsoft diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index 27ba53635..3635d3723 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -53,6 +53,7 @@ oldnewthing opengl osgwiki pabhojwa +panos paulcam pauldotknopf PGP diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index efd6edff8..c12609802 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -194,6 +194,8 @@ cascadia cassert castsi catid +carlos +zamora cazamor CBash cbegin @@ -2690,6 +2692,7 @@ WINDOWPOSCHANGING windowproc windowrect windowsapp +windowsdeveloper windowsinternalstring WINDOWSIZE windowsx 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/building.md b/doc/building.md index b00d7c45f..5a5b46ff0 100644 --- a/doc/building.md +++ b/doc/building.md @@ -15,7 +15,7 @@ Import-Module .\tools\OpenConsole.psm1 Set-MsBuildDevEnvironment Get-Format ``` -After, go to Tools > Options > Text Editor > C++ > Formatting and checking "Use custom clang-format.exe file" in Visual Studio and choose the clang-format.exe in the repository at /packages/clang-format.win-x86.10.0.0/tools/clang-format.exe by clicking "browse" right under the check box. +After, go to Tools > Options > Text Editor > C++ > Formatting and check "Use custom clang-format.exe file" in Visual Studio and choose the clang-format.exe in the repository at /packages/clang-format.win-x86.10.0.0/tools/clang-format.exe by clicking "browse" right under the check box. ### Building in PowerShell diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 319e0bae7..4f55d576b 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,13 +1174,19 @@ "FocusPaneAction": { "description": "Arguments corresponding to a focusPane Action", "allOf": [ - { "$ref": "#/definitions/ShortcutAction" }, + { + "$ref": "#/$defs/ShortcutAction" + }, { "properties": { - "action": { "type": "string", "pattern": "focusPane" }, - "id": { + "action": { "type": "string", - "default": "", + "pattern": "focusPane" + }, + "id": { + "type": "integer", + "minimum": 0, + "default": 0, "description": "The ID of the pane to focus" } } @@ -976,10 +1196,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", @@ -1005,7 +1230,7 @@ "description": "When provided, summon the window whose name or ID matches the given name value. If no such window exists, then create a new window with that name." }, "dropdownDuration": { - "type": "number", + "type": "integer", "minimum": 0, "default": 0, "description": "When provided with a positive number, \"slide\" the window in from the top of the screen using an animation that lasts dropdownDuration milliseconds." @@ -1022,10 +1247,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 +1265,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 +1403,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 +1461,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 +1499,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 +1523,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 +1564,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": { @@ -1274,23 +1580,28 @@ "description": "When set to true, the Terminal's notification icon will always be shown in the notification area.", "type": "boolean" }, + "showAdminShield": { + "default": "true", + "description": "When set to true, the Terminal's tab row will display a shield icon when the Terminal is running with administrator privileges", + "type": "boolean" + }, "useAcrylicInTabRow": { "default": "false", "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" }, @@ -1418,26 +1729,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": [ @@ -1445,7 +1768,10 @@ ] } ], - "type": [ "string", "null" ] + "type": [ + "string", + "null" + ] }, "backgroundImageAlignment": { "default": "center", @@ -1484,7 +1810,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", @@ -1514,8 +1840,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\"." }, @@ -1523,7 +1853,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": { @@ -1589,13 +1922,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": { @@ -1609,7 +1945,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, @@ -1646,8 +1984,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\"." }, @@ -1663,7 +2005,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.", @@ -1675,13 +2020,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, @@ -1694,7 +2045,7 @@ "ProfileList": { "description": "A list of profiles and the properties specific to each.", "items": { - "$ref": "#/definitions/Profile", + "$ref": "#/$defs/Profile", "required": [ "guid", "name" @@ -1706,11 +2057,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" @@ -1726,84 +2077,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." } }, @@ -1813,17 +2164,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/doc/specs/Keyboard-Selection.md b/doc/specs/Keyboard-Selection.md new file mode 100644 index 000000000..f9954fca7 --- /dev/null +++ b/doc/specs/Keyboard-Selection.md @@ -0,0 +1,171 @@ +--- +author: Carlos Zamora @carlos-zamora +created on: 2019-08-30 +last updated: 2021-09-17 +issue id: 715 +--- + +# Keyboard Selection + +## Abstract + +This spec describes a new set of non-configurable keybindings that allows the user to update a selection without the use of a mouse or stylus. + +## Inspiration + +ConHost allows the user to modify a selection using the keyboard. Holding `Shift` allows the user to move the second selection endpoint in accordance with the arrow keys. The selection endpoint updates by one cell per key event, allowing the user to refine the selected region. + +Mark mode allows the user to create a selection using only the keyboard, then edit it as mentioned above. + + +## Solution Design + +The fundamental solution design for keyboard selection is that the responsibilities between the Terminal Control and Terminal Core must be very distinct. The Terminal Control is responsible for handling user interaction and directing the Terminal Core to update the selection. The Terminal Core will need to update the selection according to the preferences of the Terminal Control. + +Relatively recently, TerminalControl was split into `TerminalControl`, `ControlInteractivity`, and `ControlCore`. Changes made to `ControlInteractivity`, `ControlCore`, and below propagate functionality to all consumers, meaning that the WPF terminal would benefit from these changes with no additional work required. + +### Fundamental Terminal Control Changes + +`ControlCore::TrySendKeyEvent()` is responsible for handling the key events after key bindings are dealt with in `TermControl`. At the time of writing this spec, there are 2 cases handled in this order: +- Clear the selection (except in a few key scenarios) +- Send Key Event + +The first branch will be updated to _modify_ the selection instead of usually _clearing_ it. This will happen by converting the key event into parameters to forward to `TerminalCore`, which then updates the selection appropriately. + +#### Idea: Make keyboard selection a collection of standard keybindings +One idea is to introduce an `updateSelection` action that conditionally works if a selection is active (similar to the `copy` action). For these key bindings, if there is no selection, the key events are forwarded to the application. + +Thanks to Keybinding Args, there would only be 1 new command: +| Action | Keybinding Args | Description | +|--|--|--| +| `updateSelection` | | If a selection exists, moves the last selection endpoint. | +| | `Enum direction { up, down, left, right }` | The direction the selection will be moved in. | +| | `Enum mode { char, word, view, buffer }` | The context for which to move the selection endpoint to. (defaults to `char`) | + + +By default, the following keybindings will be set: +```JS +// Character Selection +{ "command": {"action": "updateSelection", "direction": "left", "mode": "char" }, "keys": "shift+left" }, +{ "command": {"action": "updateSelection", "direction": "right", "mode": "char" }, "keys": "shift+right" }, +{ "command": {"action": "updateSelection", "direction": "up", "mode": "char" }, "keys": "shift+up" }, +{ "command": {"action": "updateSelection", "direction": "down", "mode": "char" }, "keys": "shift+down" }, + +// Word Selection +{ "command": {"action": "updateSelection", "direction": "left", "mode": "word" }, "keys": "ctrl+shift+left" }, +{ "command": {"action": "updateSelection", "direction": "right", "mode": "word" }, "keys": "ctrl+shift+right" }, + +// Viewport Selection +{ "command": {"action": "updateSelection", "direction": "left", "mode": "view" }, "keys": "shift+home" }, +{ "command": {"action": "updateSelection", "direction": "right", "mode": "view" }, "keys": "shift+end" }, +{ "command": {"action": "updateSelection", "direction": "up", "mode": "view" }, "keys": "shift+pgup" }, +{ "command": {"action": "updateSelection", "direction": "down", "mode": "view" }, "keys": "shift+pgdn" }, + +// Buffer Corner Selection +{ "command": {"action": "updateSelection", "direction": "up", "mode": "buffer" }, "keys": "ctrl+shift+home" }, +{ "command": {"action": "updateSelection", "direction": "down", "mode": "buffer" }, "keys": "ctrl+shift+end" }, +``` +These are in accordance with ConHost's keyboard selection model. + +This idea was abandoned due to several reasons: +1. Keyboard selection should be a standard way to interact with a terminal across all consumers (i.e. WPF control, etc.) +2. There isn't really another set of key bindings that makes sense for this. We already hardcoded ESC as a way to clear the selection. This is just an extension of that. +3. Adding 12 conditionally effective key bindings takes the spot of 12 potential non-conditional key bindings. It would be nice if a different key binding could be set when the selection is not active, but that makes the settings design much more complicated. +4. 12 new items in the command palette is also pretty excessive. +5. If proven wrong when this is in WT Preview, we can revisit this and make them customizable then. It's better to add the ability to customize it later than take it away. + +#### Idea: Make keyboard selection a simulation of mouse selection +It may seem that some effort can be saved by making the keyboard selection act as a simulation of mouse selection. There is a union of mouse and keyboard activity that can be represented in a single set of selection motion interfaces that are commanded by the TermControl's Mouse/Keyboard handler and adapted into appropriate motions in the Terminal Core. + +However, the mouse handler operates by translating a pixel coordinate on the screen to a text buffer coordinate. This would have to be rewritten and the approach was deemed unworthy. + + +### Fundamental Terminal Core Changes + +The Terminal Core will need to expose a `UpdateSelection()` function that is called by the keybinding handler. The following parameters will need to be passed in: +- `enum SelectionDirection`: the direction that the selection endpoint will attempt to move to. Possible values include `Up`, `Down`, `Left`, and `Right`. +- `enum SelectionExpansion`: the selection expansion mode that the selection endpoint will adhere to. Possible values include `Char`, `Word`, `View`, `Buffer`. + +#### Moving by Cell +For `SelectionExpansion = Char`, the selection endpoint will be updated according to the buffer's output pattern. For **horizontal movements**, the selection endpoint will attempt to move left or right. If a viewport boundary is hit, the endpoint will wrap appropriately (i.e.: hitting the left boundary moves it to the last cell of the line above it). + +For **vertical movements**, the selection endpoint will attempt to move up or down. If a **viewport boundary** is hit and there is a scroll buffer, the endpoint will move and scroll accordingly by a line. + +If a **buffer boundary** is hit, the endpoint will not move. In this case, however, the event will still be considered handled. + +**NOTE**: An important thing to handle properly in all cases is wide glyphs. The user should not be allowed to select a portion of a wide glyph; it should be all or none of it. When calling `_ExpandWideGlyphSelection` functions, the result must be saved to the endpoint. + +#### Moving by Word +For `SelectionExpansion = Word`, the selection endpoint will also be updated according to the buffer's output pattern, as above. However, the selection will be updated in accordance with "chunk selection" (performing a double-click and dragging the mouse to expand the selection). For **horizontal movements**, the selection endpoint will be updated according to the `_ExpandDoubleClickSelection` functions. The result must be saved to the endpoint. As before, if a boundary is hit, the endpoint will wrap appropriately. See [Future Considerations](#FutureConsiderations) for how this will interact with line wrapping. + +For **vertical movements**, the movement is a little more complicated than before. The selection will still respond to buffer and viewport boundaries as before. If the user is trying to move up, the selection endpoint will attempt to move up by one line, then selection will be expanded leftwards. Alternatively, if the user is trying to move down, the selection endpoint will attempt to move down by one line, then the selection will be expanded rightwards. + +#### Moving by Viewport +For `SelectionExpansion = View`, the selection endpoint will be updated according to the viewport's height. Horizontal movements will be updated according to the viewport's width, thus resulting in the endpoint being moved to the left/right boundary of the viewport. + +#### Moving by Buffer + +For `SelectionExpansion = Buffer`, the selection endpoint will be moved to the beginning or end of all the text within the buffer. If moving up or left, set the position to 0,0 (the origin of the buffer). If moving down or right, set the position to the last character in the buffer. + + +**NOTE**: In all cases, horizontal movements attempting to move past the left/right viewport boundaries result in a wrap. Vertical movements attempting to move past the top/bottom viewport boundaries will scroll such that the selection is at the edge of the screen. Vertical movements attempting to move past the top/bottom buffer boundaries will be clamped to be within buffer boundaries. + +Every combination of the `SelectionDirection` and `SelectionExpansion` will map to a keybinding. These pairings are shown below in the UI/UX Design --> Keybindings section. + +**NOTE**: If `copyOnSelect` is enabled, we need to make sure we **DO NOT** update the clipboard on every change in selection. The user must explicitly choose to copy the selected text from the buffer. + + +## UI/UX Design + +### Key Bindings + +There will only be 1 new command that needs to be added: +| Action | Keybinding Args | Description | +|--|--|--| +| `selectAll` | | Select the entire text buffer. + +By default, the following key binding will be set: +```JS +{ "command": "selectAll", "keys": "ctrl+shift+a" }, +``` + +## Capabilities + +### Accessibility + +Using the keyboard is generally a more accessible experience than using the mouse. Being able to modify a selection by using the keyboard is a good first step towards making selecting text more accessible. + +### Security + +N/A + +### Reliability + +With regards to the Terminal Core, the newly introduced code should rely on already existing and tested code. Thus no crash-related bugs are expected. + +With regards to Terminal Control and the settings model, crash-related bugs are not expected. However, ensuring that the selection is updated and cleared in general use-case scenarios must be ensured. + +### Compatibility + +N/A + +### Performance, Power, and Efficiency + +## Potential Issues + +### Grapheme Clusters +When grapheme cluster support is inevitably added to the Text Buffer, moving by "cell" is expected to move by "character" or "cluster". This is similar to how wide glyphs are handled today. Either all of it is selected, or none of it. + +## Future considerations + +### Word Selection Wrap +At the time of writing this spec, expanding or moving by word is interrupted by the beginning or end of the line, regardless of the wrap flag being set. In the future, selection and the accessibility models will respect the wrap flag on the text buffer. + +## Mark Mode + +This functionality will be expanded to create a feature similar to Mark Mode. This will allow a user to create a selection using only the keyboard. + + +## Resources + +- https://blogs.windows.com/windowsdeveloper/2014/10/07/console-improvements-in-the-windows-10-technical-preview/ diff --git a/src/Terminal.wprp b/src/Terminal.wprp index d37c749c9..80a6e730b 100644 --- a/src/Terminal.wprp +++ b/src/Terminal.wprp @@ -12,6 +12,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 466e38ecf..3fba7b082 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1430,12 +1430,13 @@ const til::point TextBuffer::GetGlyphStart(const til::point pos, std::optional limitOptional) const +const til::point TextBuffer::GetGlyphEnd(const til::point pos, bool accessibilityMode, std::optional limitOptional) const { COORD resultPos = pos; const auto bufferSize = GetSize(); @@ -1453,7 +1454,10 @@ const til::point TextBuffer::GetGlyphEnd(const til::point pos, std::optional limitOptional = std::nullopt) const; - const til::point GetGlyphEnd(const til::point pos, std::optional limitOptional = std::nullopt) const; + const til::point GetGlyphEnd(const til::point pos, bool accessibilityMode = false, std::optional limitOptional = std::nullopt) const; bool MoveToNextGlyph(til::point& pos, bool allowBottomExclusive = false, std::optional limitOptional = std::nullopt) const; bool MoveToPreviousGlyph(til::point& pos, std::optional limitOptional = std::nullopt) const; diff --git a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp index b25d476e7..da5e3916c 100644 --- a/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/CommandTests.cpp @@ -465,6 +465,10 @@ namespace SettingsModelLocalTests "name":"action7_startingDirectoryWithTrailingSlash", "command": { "action": "newWindow", "startingDirectory":"C:\\", "commandline": "bar.exe" } }, + { + "name":"action8_tabTitleEscaping", + "command": { "action": "newWindow", "tabTitle":"\\\";foo\\" } + } ])" }; const auto commands0Json = VerifyParseSucceeded(commands0String); @@ -473,7 +477,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, commands.Size()); auto warnings = implementation::Command::LayerJson(commands, commands0Json); VERIFY_ARE_EQUAL(0u, warnings.size()); - VERIFY_ARE_EQUAL(8u, commands.Size()); + VERIFY_ARE_EQUAL(9u, commands.Size()); { auto command = commands.Lookup(L"action0"); @@ -586,5 +590,20 @@ namespace SettingsModelLocalTests L"cmdline: \"%s\"", cmdline.c_str())); VERIFY_ARE_EQUAL(L"--startingDirectory \"C:\\\\\" -- \"bar.exe\"", terminalArgs.ToCommandline()); } + + { + auto command = commands.Lookup(L"action8_tabTitleEscaping"); + VERIFY_IS_NOT_NULL(command); + VERIFY_IS_NOT_NULL(command.ActionAndArgs()); + VERIFY_ARE_EQUAL(ShortcutAction::NewWindow, command.ActionAndArgs().Action()); + const auto& realArgs = command.ActionAndArgs().Args().try_as(); + VERIFY_IS_NOT_NULL(realArgs); + const auto& terminalArgs = realArgs.TerminalArgs(); + VERIFY_IS_NOT_NULL(terminalArgs); + auto cmdline = terminalArgs.ToCommandline(); + Log::Comment(NoThrowString().Format( + L"cmdline: \"%s\"", cmdline.c_str())); + VERIFY_ARE_EQUAL(LR"-(--title "\\\"\;foo\\")-", terminalArgs.ToCommandline()); + } } } 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/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index c86f5d621..02025be81 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -549,11 +549,11 @@ try if (multiClickMapper == 3) { - _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansionMode::Line); + _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Line); } else if (multiClickMapper == 2) { - _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansionMode::Word); + _terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Word); } else { diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.cpp b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp new file mode 100644 index 000000000..f2cc01df4 --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "GetWindowLayoutArgs.h" +#include "GetWindowLayoutArgs.g.cpp" diff --git a/src/cascadia/Remoting/GetWindowLayoutArgs.h b/src/cascadia/Remoting/GetWindowLayoutArgs.h new file mode 100644 index 000000000..06706f60b --- /dev/null +++ b/src/cascadia/Remoting/GetWindowLayoutArgs.h @@ -0,0 +1,32 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- GetWindowLayoutArgs.h + +Abstract: +- This is a helper class for getting the window layout from a peasant. + Depending on if we are running on the monarch or on a peasant we might need + to switch what thread we are executing on. This gives us the option of + either returning the json result synchronously, or as a promise. +--*/ + +#pragma once + +#include "GetWindowLayoutArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct GetWindowLayoutArgs : public GetWindowLayoutArgsT + { + WINRT_PROPERTY(winrt::hstring, WindowLayoutJson, L""); + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncOperation, WindowLayoutJsonAsync, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(GetWindowLayoutArgs); +} diff --git a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj index a2d6e8ae6..517c2f56f 100644 --- a/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj +++ b/src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj @@ -12,7 +12,6 @@ - @@ -36,6 +35,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + @@ -71,6 +76,12 @@ Peasant.idl + + Peasant.idl + + + Monarch.idl + Create @@ -128,6 +139,5 @@ - - + \ No newline at end of file diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index c16eb52e9..eed247ab4 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -6,6 +6,7 @@ #include "Monarch.h" #include "CommandlineArgs.h" #include "FindTargetWindowArgs.h" +#include "QuitAllRequestedArgs.h" #include "ProposeCommandlineResult.h" #include "Monarch.g.cpp" @@ -135,12 +136,18 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // - used // Return Value: // - - void Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::Foundation::IInspectable& /*args*/) + winrt::fire_and_forget Monarch::_handleQuitAll(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::Foundation::IInspectable& /*args*/) { // Let the process hosting the monarch run any needed logic before // closing all windows. - _QuitAllRequestedHandlers(*this, nullptr); + auto args = winrt::make_self(); + _QuitAllRequestedHandlers(*this, *args); + + if (const auto action = args->BeforeQuitAllAction()) + { + co_await action; + } _quitting.store(true); // Tell all peasants to exit. @@ -994,4 +1001,28 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _forEachPeasant(func, onError); } + + // Method Description: + // - Ask all peasants to return their window layout as json + // Arguments: + // - + // Return Value: + // - The collection of window layouts from each peasant. + Windows::Foundation::Collections::IVector Monarch::GetAllWindowLayouts() + { + std::vector vec; + auto callback = [&](const auto& /*id*/, const auto& p) { + vec.emplace_back(p.GetWindowLayout()); + }; + auto onError = [](auto&& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_GetAllWindowLayouts_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not get a window layout from"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + _forEachPeasant(callback, onError); + + return winrt::single_threaded_vector(std::move(vec)); + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 7ec904499..b965d1d2a 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -59,13 +59,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void SummonAllWindows(); bool DoesQuakeWindowExist(); Windows::Foundation::Collections::IVectorView GetPeasantInfos(); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); private: uint64_t _ourPID; @@ -103,8 +104,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void _renameRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); - void _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, - const winrt::Windows::Foundation::IInspectable& args); + winrt::fire_and_forget _handleQuitAll(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::Foundation::IInspectable& args); // Method Description: // - Helper for doing something on each and every peasant. @@ -177,6 +178,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } _clearOldMruEntries(peasantsToErase); + + // A peasant died, let the app host know that the number of + // windows has changed. + _WindowClosedHandlers(nullptr, nullptr); } } diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index 4eb77695a..f60b3997a 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -31,6 +31,12 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.IReference WindowID; } + [default_interface] runtimeclass QuitAllRequestedArgs + { + QuitAllRequestedArgs(); + Windows.Foundation.IAsyncAction BeforeQuitAllAction; + } + struct PeasantInfo { UInt64 Id; @@ -52,12 +58,13 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); Boolean DoesQuakeWindowExist(); Windows.Foundation.Collections.IVectorView GetPeasantInfos { get; }; + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; - event Windows.Foundation.TypedEventHandler QuitAllRequested; + event Windows.Foundation.TypedEventHandler QuitAllRequested; }; } diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index a8cb749d6..46fd7ce2e 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -5,6 +5,7 @@ #include "Peasant.h" #include "CommandlineArgs.h" #include "SummonWindowBehavior.h" +#include "GetWindowLayoutArgs.h" #include "Peasant.g.cpp" #include "../../types/inc/utils.hpp" @@ -289,4 +290,24 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + + // Method Description: + // - Request and return the window layout from the current TerminalPage + // Arguments: + // - + // Return Value: + // - the window layout as a json string + hstring Peasant::GetWindowLayout() + { + auto args = winrt::make_self(); + _GetWindowLayoutRequestedHandlers(nullptr, *args); + if (const auto op = args->WindowLayoutJsonAsync()) + { + // This will fail if called on the UI thread, so the monarch should + // never set WindowLayoutJsonAsync. + auto str = op.get(); + return str; + } + return args->WindowLayoutJson(); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index f6f884491..fdb20d942 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -36,6 +36,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs(); + + winrt::hstring GetWindowLayout(); + WINRT_PROPERTY(winrt::hstring, WindowName); WINRT_PROPERTY(winrt::hstring, ActiveTabTitle); @@ -49,6 +52,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); private: Peasant(const uint64_t testPID); diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index 80e24cb2c..ec87c8518 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -30,6 +30,11 @@ namespace Microsoft.Terminal.Remoting Windows.Foundation.DateTime ActivatedTime { get; }; }; + [default_interface] runtimeclass GetWindowLayoutArgs { + GetWindowLayoutArgs(); + String WindowLayoutJson; + Windows.Foundation.IAsyncOperation WindowLayoutJsonAsync; + } enum MonitorBehavior { @@ -69,6 +74,7 @@ namespace Microsoft.Terminal.Remoting void RequestHideNotificationIcon(); void RequestQuitAll(); void Quit(); + String GetWindowLayout(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; @@ -78,6 +84,7 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler SummonRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; + event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler QuitAllRequested; event Windows.Foundation.TypedEventHandler QuitRequested; }; diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.cpp b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp new file mode 100644 index 000000000..ed5c39dcf --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "pch.h" +#include "QuitAllRequestedArgs.h" +#include "QuitAllRequestedArgs.g.cpp" diff --git a/src/cascadia/Remoting/QuitAllRequestedArgs.h b/src/cascadia/Remoting/QuitAllRequestedArgs.h new file mode 100644 index 000000000..8c9c26fd2 --- /dev/null +++ b/src/cascadia/Remoting/QuitAllRequestedArgs.h @@ -0,0 +1,30 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Class Name: +- QuitAllRequestedArgs.h + +Abstract: +- This is a helper class for allowing the monarch to run code before telling all + peasants to quit. This way the monarch can raise an event and get back a future + to wait for before continuing. +--*/ + +#pragma once + +#include "QuitAllRequestedArgs.g.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::Remoting::implementation +{ + struct QuitAllRequestedArgs : public QuitAllRequestedArgsT + { + WINRT_PROPERTY(winrt::Windows::Foundation::IAsyncAction, BeforeQuitAllAction, nullptr) + }; +} + +namespace winrt::Microsoft::Terminal::Remoting::factory_implementation +{ + BASIC_FACTORY(QuitAllRequestedArgs); +} diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 9a71fcef0..4cafee145 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -271,7 +271,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); _monarch.ShowNotificationIconRequested([this](auto&&, auto&&) { _ShowNotificationIconRequestedHandlers(*this, nullptr); }); _monarch.HideNotificationIconRequested([this](auto&&, auto&&) { _HideNotificationIconRequestedHandlers(*this, nullptr); }); - _monarch.QuitAllRequested([this](auto&&, auto&&) { _QuitAllRequestedHandlers(*this, nullptr); }); + _monarch.QuitAllRequested({ get_weak(), &WindowManager::_QuitAllRequestedHandlers }); _BecameMonarchHandlers(*this, nullptr); } @@ -318,6 +318,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation } } + _peasant.GetWindowLayoutRequested({ get_weak(), &WindowManager::_GetWindowLayoutRequestedHandlers }); + TraceLoggingWrite(g_hRemotingProvider, "WindowManager_CreateOurPeasant", TraceLoggingUInt64(_peasant.GetID(), "peasantID", "The ID of our new peasant"), @@ -610,4 +612,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { winrt::get_self(_peasant)->ActiveTabTitle(title); } + + Windows::Foundation::Collections::IVector WindowManager::GetAllWindowLayouts() + { + if (_monarch) + { + try + { + return _monarch.GetAllWindowLayouts(); + } + CATCH_LOG() + } + return nullptr; + } } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 3d2eaf6c7..379038750 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -50,6 +50,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation winrt::fire_and_forget RequestQuitAll(); bool DoesQuakeWindowExist(); void UpdateActiveTabTitle(winrt::hstring title); + Windows::Foundation::Collections::IVector GetAllWindowLayouts(); TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); @@ -57,7 +58,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(WindowClosed, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(HideNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); - TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(QuitAllRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::QuitAllRequestedArgs); + TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs); private: bool _shouldCreateWindow{ false }; diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index cf15fbb42..2fdfd7e34 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -16,6 +16,8 @@ namespace Microsoft.Terminal.Remoting void SummonAllWindows(); void RequestShowNotificationIcon(); void RequestHideNotificationIcon(); + Windows.Foundation.Collections.IVector GetAllWindowLayouts(); + UInt64 GetNumberOfPeasants(); void RequestQuitAll(); void UpdateActiveTabTitle(String title); @@ -25,8 +27,9 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler BecameMonarch; event Windows.Foundation.TypedEventHandler WindowCreated; event Windows.Foundation.TypedEventHandler WindowClosed; + event Windows.Foundation.TypedEventHandler QuitAllRequested; + event Windows.Foundation.TypedEventHandler GetWindowLayoutRequested; event Windows.Foundation.TypedEventHandler ShowNotificationIconRequested; event Windows.Foundation.TypedEventHandler HideNotificationIconRequested; - event Windows.Foundation.TypedEventHandler QuitAllRequested; }; } diff --git a/src/cascadia/ShellExtension/OpenTerminalHere.cpp b/src/cascadia/ShellExtension/OpenTerminalHere.cpp index a92609e70..5f472d72f 100644 --- a/src/cascadia/ShellExtension/OpenTerminalHere.cpp +++ b/src/cascadia/ShellExtension/OpenTerminalHere.cpp @@ -55,9 +55,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, STARTUPINFOEX siEx{ 0 }; siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); - // Append a "\." to the given path, so that this will work in "C:\" - auto path{ wil::str_printf(LR"-(%s\.)-", pszName.get()) }; - auto cmdline{ wil::str_printf(LR"-("%s" -d "%s")-", GetWtExePath().c_str(), path.c_str()) }; + auto cmdline{ wil::str_printf(LR"-("%s" -d %s)-", GetWtExePath().c_str(), QuoteAndEscapeCommandlineArg(pszName.get()).c_str()) }; RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW( nullptr, // lpApplicationName cmdline.data(), @@ -66,7 +64,7 @@ HRESULT OpenTerminalHere::Invoke(IShellItemArray* psiItemArray, false, // bInheritHandles EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags nullptr, // lpEnvironment - path.data(), + pszName.get(), &siEx.StartupInfo, // lpStartupInfo &_piClient // lpProcessInformation )); 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 1ef4c0c8a..ae79289fa 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -78,7 +78,7 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleCloseWindow(const IInspectable& /*sender*/, const ActionEventArgs& args) { - CloseWindow(false); + _CloseRequestedHandlers(nullptr, nullptr); args.Handled(true); } @@ -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/AppCommandlineArgs.cpp b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp index 7f2f919a4..3baad1aa0 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.cpp +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.cpp @@ -187,6 +187,10 @@ void AppCommandlineArgs::_buildParser() _windowTarget, RS_A(L"CmdWindowTargetArgDesc")); + _app.add_option("-s,--saved", + _loadPersistedLayoutIdx, + RS_A(L"CmdSavedLayoutArgDesc")); + // Subcommands _buildNewTabParser(); _buildSplitPaneParser(); @@ -700,6 +704,7 @@ void AppCommandlineArgs::_resetStateToDefault() _swapPaneDirection = FocusDirection::None; _focusPaneTarget = -1; + _loadPersistedLayoutIdx = -1; // DON'T clear _launchMode here! This will get called once for every // subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out @@ -915,6 +920,12 @@ void AppCommandlineArgs::ValidateStartupCommands() } } } +std::optional AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept +{ + return _loadPersistedLayoutIdx >= 0 ? + std::optional{ static_cast(_loadPersistedLayoutIdx) } : + std::nullopt; +} std::optional AppCommandlineArgs::GetLaunchMode() const noexcept { diff --git a/src/cascadia/TerminalApp/AppCommandlineArgs.h b/src/cascadia/TerminalApp/AppCommandlineArgs.h index 598c3b8ed..de076ec99 100644 --- a/src/cascadia/TerminalApp/AppCommandlineArgs.h +++ b/src/cascadia/TerminalApp/AppCommandlineArgs.h @@ -39,6 +39,7 @@ public: const std::string& GetExitMessage(); bool ShouldExitEarly() const noexcept; + std::optional GetPersistedLayoutIdx() const noexcept; std::optional GetLaunchMode() const noexcept; int ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -123,6 +124,7 @@ private: std::string _exitMessage; bool _shouldExitEarly{ false }; + int _loadPersistedLayoutIdx{}; std::string _windowTarget{}; // Are you adding more args or attributes here? If they are not reset in _resetStateToDefault, make sure to reset them in FullResetState diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 08032f1c8..72f25b83e 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -600,13 +600,11 @@ namespace winrt::TerminalApp::implementation winrt::Windows::Foundation::Size proposedSize{}; const float scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialSize()) + if (layout.InitialSize()) { - proposedSize = layouts.GetAt(0).InitialSize().Value(); + proposedSize = layout.InitialSize().Value(); // The size is saved as a non-scaled real pixel size, // so we need to scale it appropriately. proposedSize.Height = proposedSize.Height * scale; @@ -704,13 +702,11 @@ namespace winrt::TerminalApp::implementation auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; - if (_root->ShouldUsePersistedLayout(_settings)) + if (const auto layout = _root->LoadPersistedLayout(_settings)) { - const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).InitialPosition()) + if (layout.InitialPosition()) { - initialPosition = layouts.GetAt(0).InitialPosition().Value(); + initialPosition = layout.InitialPosition().Value(); } } @@ -1151,10 +1147,22 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - void AppLogic::WindowCloseButtonClicked() + void AppLogic::CloseWindow(LaunchPosition pos) { if (_root) { + // If persisted layout is enabled and we are the last window closing + // we should save our state. + if (_root->ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(pos); + const auto state = ApplicationState::SharedInstance(); + state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + } + } + _root->CloseWindow(false); } } @@ -1168,6 +1176,16 @@ namespace winrt::TerminalApp::implementation return {}; } + bool AppLogic::HasCommandlineArguments() const noexcept + { + return _hasCommandLineArguments; + } + + bool AppLogic::HasSettingsStartupActions() const noexcept + { + return _hasSettingsStartupActions; + } + // Method Description: // - Sets the initial commandline to process on startup, and attempts to // parse it. Commands will be parsed into a list of ShortcutActions that @@ -1191,6 +1209,10 @@ namespace winrt::TerminalApp::implementation // then it contains only the executable name and no other arguments. _hasCommandLineArguments = args.size() > 1; _appArgs.ValidateStartupCommands(); + if (const auto idx = _appArgs.GetPersistedLayoutIdx()) + { + _root->SetPersistedLayoutIdx(idx.value()); + } _root->SetStartupActions(_appArgs.GetStartupActions()); // Check if we were started as a COM server for inbound connections of console sessions @@ -1428,6 +1450,40 @@ namespace winrt::TerminalApp::implementation return _settings.GlobalSettings().ActionMap().GlobalHotkeys(); } + bool AppLogic::ShouldUsePersistedLayout() + { + return _root != nullptr ? _root->ShouldUsePersistedLayout(_settings) : false; + } + + void AppLogic::SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts) + { + std::vector converted; + converted.reserve(layouts.Size()); + + for (const auto& json : layouts) + { + if (json != L"") + { + converted.emplace_back(WindowLayout::FromJson(json)); + } + } + + ApplicationState::SharedInstance().PersistedWindowLayouts(winrt::single_threaded_vector(std::move(converted))); + } + + hstring AppLogic::GetWindowLayoutJson(LaunchPosition position) + { + if (_root != nullptr) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(position); + return WindowLayout::ToJson(layout); + } + } + return L""; + } + void AppLogic::IdentifyWindow() { if (_root) @@ -1459,8 +1515,17 @@ namespace winrt::TerminalApp::implementation } } + void AppLogic::SetPersistedLayoutIdx(const uint32_t idx) + { + if (_root) + { + _root->SetPersistedLayoutIdx(idx); + } + } + void AppLogic::SetNumberOfOpenWindows(const uint64_t num) { + _numOpenWindows = num; if (_root) { _root->SetNumberOfOpenWindows(num); diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 08dc77007..17bd61aaa 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -55,6 +55,8 @@ namespace winrt::TerminalApp::implementation void Quit(); + bool HasCommandlineArguments() const noexcept; + bool HasSettingsStartupActions() const noexcept; int32_t SetStartupCommandline(array_view actions); int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd); TerminalApp::FindTargetWindowResult FindTargetWindow(array_view actions); @@ -65,12 +67,16 @@ namespace winrt::TerminalApp::implementation bool Fullscreen() const; bool AlwaysOnTop() const; + bool ShouldUsePersistedLayout(); + hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position); + void SaveWindowLayoutJsons(const Windows::Foundation::Collections::IVector& layouts); void IdentifyWindow(); void RenameFailed(); winrt::hstring WindowName(); void WindowName(const winrt::hstring& name); uint64_t WindowId(); void WindowId(const uint64_t& id); + void SetPersistedLayoutIdx(const uint32_t idx); void SetNumberOfOpenWindows(const uint64_t num); bool IsQuakeWindow() const noexcept; @@ -91,7 +97,7 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft::Terminal::Settings::Model::LaunchPosition position); winrt::TerminalApp::TaskbarState TaskbarState(); @@ -123,6 +129,8 @@ namespace winrt::TerminalApp::implementation HRESULT _settingsLoadedResult = S_OK; bool _loadedInitialSettings = false; + uint64_t _numOpenWindows{ 0 }; + std::shared_mutex _dialogLock; ::TerminalApp::AppCommandlineArgs _appArgs; @@ -175,6 +183,7 @@ namespace winrt::TerminalApp::implementation FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested); FORWARDED_TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IsQuakeWindowChanged); FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested); + FORWARDED_TYPED_EVENT(CloseRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, CloseRequested); FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu); FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index a600891c4..cfe93321a 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -34,6 +34,8 @@ namespace TerminalApp void RunAsUwp(); Boolean IsElevated(); + Boolean HasCommandlineArguments(); + Boolean HasSettingsStartupActions(); Int32 SetStartupCommandline(String[] commands); Int32 ExecuteCommandline(String[] commands, String cwd); String ParseCommandlineMessage { get; }; @@ -55,6 +57,7 @@ namespace TerminalApp void IdentifyWindow(); String WindowName; UInt64 WindowId; + void SetPersistedLayoutIdx(UInt32 idx); void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -69,10 +72,14 @@ namespace TerminalApp Boolean GetInitialAlwaysOnTop(); Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); - void WindowCloseButtonClicked(); + void CloseWindow(Microsoft.Terminal.Settings.Model.LaunchPosition position); TaskbarState TaskbarState{ get; }; + Boolean ShouldUsePersistedLayout(); + String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position); + void SaveWindowLayoutJsons(Windows.Foundation.Collections.IVector layouts); + Boolean GetMinimizeToNotificationArea(); Boolean GetAlwaysShowNotificationIcon(); Boolean GetShowTitleInTitlebar(); @@ -99,6 +106,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler SettingsChanged; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; event Windows.Foundation.TypedEventHandler QuitRequested; } diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 0c5705a7b..e04f558f6 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -39,8 +39,8 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo _lastActive{ lastFocused }, _profile{ profile } { - _root.Children().Append(_border); - _border.Child(_control); + _root.Children().Append(_borderFirst); + _borderFirst.Child(_control); _connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler }); _warningBellToken = _control.WarningBell({ this, &Pane::_ControlWarningBellHandler }); @@ -61,7 +61,52 @@ Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFo // 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); }); @@ -83,7 +128,15 @@ NewTerminalArgs Pane::GetTerminalArgsForPane() const auto controlSettings = _control.Settings().as(); args.Profile(controlSettings.ProfileName()); - args.StartingDirectory(controlSettings.StartingDirectory()); + // If we know the user's working directory use it instead of the profile. + if (const auto dir = _control.WorkingDirectory(); !dir.empty()) + { + args.StartingDirectory(dir); + } + else + { + args.StartingDirectory(controlSettings.StartingDirectory()); + } args.TabTitle(controlSettings.StartingTitle()); args.Commandline(controlSettings.Commandline()); args.SuppressApplicationTitle(controlSettings.SuppressApplicationTitle()); @@ -106,7 +159,12 @@ NewTerminalArgs Pane::GetTerminalArgsForPane() const if (controlSettings.AppliedColorScheme()) { auto name = controlSettings.AppliedColorScheme().Name(); - args.ColorScheme(name); + // Only save the color scheme if it is different than the profile color + // scheme to not override any other profile appearance choices. + if (_profile.DefaultAppearance().ColorSchemeName() != name) + { + args.ColorScheme(name); + } } return args; @@ -325,18 +383,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. @@ -385,6 +443,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) { @@ -408,6 +496,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; @@ -477,6 +566,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()) @@ -607,6 +701,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 @@ -620,8 +720,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. @@ -639,32 +741,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 @@ -676,6 +776,7 @@ bool Pane::SwapPanes(std::shared_ptr first, std::shared_ptr second) std::swap(firstParent->_firstChild, firstParent->_secondChild); updateParent(firstParent); + firstParent->_ApplySplitDefinitions(); } else { @@ -685,11 +786,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; } @@ -791,13 +926,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); } @@ -1019,10 +1154,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: @@ -1080,8 +1220,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. @@ -1090,9 +1230,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(); @@ -1104,7 +1248,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->_control; + } + pane = p; + } + // We didn't find our child somehow, they might have closed under us. + } + return _firstChild->GetLastFocusedTerminalControl(); + } + return _control; +} + +// Method Description: +// - Gets the TermControl of this pane. If this Pane is not a leaf this will +// return the nullptr; // Arguments: // - // Return Value: @@ -1134,7 +1309,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: // - @@ -1196,7 +1371,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()); } @@ -1210,7 +1385,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: @@ -1241,10 +1439,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 { @@ -1300,7 +1495,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; @@ -1340,8 +1535,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) { @@ -1392,15 +1589,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; @@ -1421,8 +1626,14 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) // handlers since it is just getting moved. if (!isDetaching) { - closedChild->_control.ConnectionStateChanged(closedChild->_connectionStateChangedToken); - closedChild->_control.WarningBell(closedChild->_warningBellToken); + closedChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + p->_control.ConnectionStateChanged(p->_connectionStateChangedToken); + p->_control.WarningBell(p->_warningBellToken); + } + return false; + }); } closedChild->Closed(closedChildClosedToken); @@ -1430,24 +1641,25 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) remainingChild->_control.ConnectionStateChanged(remainingChild->_connectionStateChangedToken); remainingChild->_control.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 TermControl 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 TermControl 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 @@ -1461,7 +1673,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); @@ -1473,7 +1685,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(); @@ -1504,13 +1716,20 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching) closedChild->Closed(closedChildClosedToken); if (!isDetaching) { - closedChild->_control.ConnectionStateChanged(closedChild->_connectionStateChangedToken); - closedChild->_control.WarningBell(closedChild->_warningBellToken); + closedChild->WalkTree([](auto p) { + if (p->_IsLeaf()) + { + p->_control.ConnectionStateChanged(p->_connectionStateChangedToken); + p->_control.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(); @@ -1534,20 +1753,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; @@ -1597,16 +1840,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, @@ -1618,17 +1863,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. @@ -1767,9 +2004,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 @@ -1791,11 +2028,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: @@ -1822,8 +2079,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; @@ -1834,8 +2091,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; @@ -1889,6 +2146,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) @@ -1944,16 +2202,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); }); } @@ -1963,16 +2227,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); }); } @@ -2020,41 +2290,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 @@ -2098,13 +2365,13 @@ std::pair, std::shared_ptr> Pane::Split(SplitDirecti const Profile& profile, const TermControl& 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); } @@ -2130,11 +2397,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; @@ -2202,46 +2469,66 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect // modify our tree std::unique_lock lock{ _createCloseLock }; - // revoke our handler - the child will take care of the control now. - _control.ConnectionStateChanged(_connectionStateChangedToken); - _connectionStateChangedToken.value = 0; - _control.WarningBell(_warningBellToken); - _warningBellToken.value = 0; + if (_IsLeaf()) + { + // revoke our handler - the child will take care of the control now. + _control.ConnectionStateChanged(_connectionStateChangedToken); + _connectionStateChangedToken.value = 0; + _control.WarningBell(_warningBellToken); + _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(); - - _splitState = actualSplitType; - _desiredSplitPosition = 1.0f - splitSize; + // Remove our old GotFocus handler from the control. We don't want the + // control telling us that it's now focused, we want it telling its new + // parent. + _gotFocusRevoker.revoke(); + _lostFocusRevoker.revoke(); + } // Remove any children we currently have. We can't add the existing // TermControl 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(); @@ -2276,12 +2563,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) { @@ -2289,6 +2573,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 @@ -2311,20 +2597,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 @@ -2361,6 +2648,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 @@ -2380,23 +2669,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) || @@ -2406,6 +2695,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: @@ -2901,20 +3211,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 { @@ -2966,7 +3273,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 ab1309f40..9892a6e8a 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,7 +59,14 @@ public: const winrt::Microsoft::Terminal::Control::TermControl& 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::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl(); winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl(); winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile(); @@ -142,25 +150,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>); @@ -173,7 +199,8 @@ 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::Microsoft::Terminal::Control::TermControl _control{ nullptr }; winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected }; static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush; @@ -185,6 +212,7 @@ private: float _desiredSplitPosition; std::optional _id; + std::weak_ptr _parentChildPath{}; bool _lastActive{ false }; winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr }; @@ -205,6 +233,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, @@ -232,6 +261,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 86b0b407f..55e51abbb 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... @@ -381,6 +384,9 @@ Launch the window in focus mode + + This parameter is an internal implementation detail and should not be used. + Specify a terminal window to run the given commandline in. "0" always refers to the current window. @@ -715,4 +721,7 @@ Don't show again + + This Terminal window is running as Admin + diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index f57e3fe9b..b99af3236 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -19,6 +19,9 @@ #include "ColorHelper.h" #include "DebugTapConnection.h" #include "SettingsTab.h" +#include "..\TerminalSettingsModel\FileUtils.h" + +#include using namespace winrt; using namespace winrt::Windows::Foundation::Collections; @@ -410,33 +413,45 @@ namespace winrt::TerminalApp::implementation // - tab: tab to export winrt::fire_and_forget TerminalPage::_ExportTab(const TerminalTab& tab) { + // This will be used to set up the file picker "filter", to select .txt + // files by default. + static constexpr COMDLG_FILTERSPEC supportedFileTypes[] = { + { L"Text Files (*.txt)", L"*.txt" }, + { L"All Files (*.*)", L"*.*" } + }; + // An arbitrary GUID to associate with all instances of this + // dialog, so they all re-open in the same path as they were + // open before: + static constexpr winrt::guid clientGuidExportFile{ 0xF6AF20BB, 0x0800, 0x48E6, { 0xB0, 0x17, 0xA1, 0x4C, 0xD8, 0x73, 0xDD, 0x58 } }; + try { if (const auto control{ tab.GetActiveTerminalControl() }) { - const FileSavePicker savePicker; - savePicker.as()->Initialize(*_hostingHwnd); - savePicker.SuggestedStartLocation(PickerLocationId::Downloads); - const auto fileChoices = single_threaded_vector({ L".txt" }); - savePicker.FileTypeChoices().Insert(RS_(L"PlainText"), fileChoices); - savePicker.SuggestedFileName(control.Title()); + // GH#11356 - we can't use the UWP apis for writing the file, + // because they don't work elevated (shocker) So just use the + // shell32 file picker manually. + auto path = co_await SaveFilePicker(*_hostingHwnd, [control](auto&& dialog) { + THROW_IF_FAILED(dialog->SetClientGuid(clientGuidExportFile)); + try + { + // Default to the Downloads folder + auto folderShellItem{ winrt::capture(&SHGetKnownFolderItem, FOLDERID_Downloads, KF_FLAG_DEFAULT, nullptr) }; + dialog->SetDefaultFolder(folderShellItem.get()); + } + CATCH_LOG(); // non-fatal + THROW_IF_FAILED(dialog->SetFileTypes(ARRAYSIZE(supportedFileTypes), supportedFileTypes)); + THROW_IF_FAILED(dialog->SetFileTypeIndex(1)); // the array is 1-indexed + THROW_IF_FAILED(dialog->SetDefaultExtension(L"txt")); - const StorageFile file = co_await savePicker.PickSaveFileAsync(); - if (file != nullptr) + // Default to using the tab title as the file name + THROW_IF_FAILED(dialog->SetFileName((control.Title() + L".txt").c_str())); + }); + + if (!path.empty()) { const auto buffer = control.ReadEntireBuffer(); - CachedFileManager::DeferUpdates(file); - co_await FileIO::WriteTextAsync(file, buffer); - const auto status = co_await CachedFileManager::CompleteUpdatesAsync(file); - switch (status) - { - case FileUpdateStatus::Complete: - case FileUpdateStatus::CompleteAndRenamed: - _ShowControlNoticeDialog(RS_(L"NoticeInfo"), RS_(L"ExportSuccess")); - break; - default: - _ShowControlNoticeDialog(RS_(L"NoticeError"), RS_(L"ExportFailure")); - } + CascadiaSettings::ExportFile(path, buffer); } } } @@ -501,7 +516,9 @@ namespace winrt::TerminalApp::implementation { // If we are supposed to save state, make sure we clear it out // if the user manually closed all tabs. - if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings)) + // Do this only if we are the last window; the monarch will notice + // we are missing and remove us that way otherwise. + if (!_maintainStateOnTabClose && ShouldUsePersistedLayout(_settings) && _numOpenWindows == 1) { auto state = ApplicationState::SharedInstance(); state.PersistedWindowLayouts(nullptr); @@ -734,31 +751,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/TabRowControl.h b/src/cascadia/TerminalApp/TabRowControl.h index 6ffa60532..1e4bd573f 100644 --- a/src/cascadia/TerminalApp/TabRowControl.h +++ b/src/cascadia/TerminalApp/TabRowControl.h @@ -4,6 +4,7 @@ #pragma once #include "winrt/Microsoft.UI.Xaml.Controls.h" +#include "../../cascadia/inc/cppwinrt_utils.h" #include "TabRowControl.g.h" @@ -16,6 +17,9 @@ namespace winrt::TerminalApp::implementation void OnNewTabButtonClick(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const& args); void OnNewTabButtonDrop(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e); void OnNewTabButtonDragOver(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::DragEventArgs const& e); + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + WINRT_OBSERVABLE_PROPERTY(bool, ShowElevationShield, _PropertyChangedHandlers, false); }; } diff --git a/src/cascadia/TerminalApp/TabRowControl.idl b/src/cascadia/TerminalApp/TabRowControl.idl index 1415df9a2..4dd1d37bd 100644 --- a/src/cascadia/TerminalApp/TabRowControl.idl +++ b/src/cascadia/TerminalApp/TabRowControl.idl @@ -3,9 +3,11 @@ namespace TerminalApp { - [default_interface] runtimeclass TabRowControl : Windows.UI.Xaml.Controls.ContentPresenter + [default_interface] runtimeclass TabRowControl : Windows.UI.Xaml.Controls.ContentPresenter, + Windows.UI.Xaml.Data.INotifyPropertyChanged { TabRowControl(); Microsoft.UI.Xaml.Controls.TabView TabView { get; }; + Boolean ShowElevationShield; } } diff --git a/src/cascadia/TerminalApp/TabRowControl.xaml b/src/cascadia/TerminalApp/TabRowControl.xaml index 802e7c529..b3b0f58c3 100644 --- a/src/cascadia/TerminalApp/TabRowControl.xaml +++ b/src/cascadia/TerminalApp/TabRowControl.xaml @@ -20,6 +20,17 @@ IsAddTabButtonVisible="false" TabWidthMode="Equal"> + + + + + ().Logic().IsElevated(); + } + CATCH_LOG(); + return result; + }(); + + return isElevated; + } + void TerminalPage::Create() { // Hookup the key bindings @@ -128,19 +151,7 @@ namespace winrt::TerminalApp::implementation _tabView = _tabRow.TabView(); _rearranging = false; - // GH#2455 - Make sure to try/catch calls to Application::Current, - // because that _won't_ be an instance of TerminalApp::App in the - // LocalTests - auto isElevated = false; - try - { - // GH#3581 - There's a platform limitation that causes us to crash when we rearrange tabs. - // Xaml tries to send a drag visual (to wit: a screenshot) to the drag hosting process, - // but that process is running at a different IL than us. - // For now, we're disabling elevated drag. - isElevated = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsElevated(); - } - CATCH_LOG(); + const auto isElevated = IsElevated(); if (_settings.GlobalSettings().UseAcrylicInTabRow()) { @@ -267,6 +278,8 @@ namespace winrt::TerminalApp::implementation // Setup mouse vanish attributes SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &_shouldMouseVanish, false); + _tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield()); + // Store cursor, so we can restore it, e.g., after mouse vanishing // (we'll need to adapt this logic once we make cursor context aware) try @@ -285,10 +298,37 @@ namespace winrt::TerminalApp::implementation // - true if the ApplicationState should be used. bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const { - // If the setting is enabled, and we are the only window. + // GH#5000 Until there is a separate state file for elevated sessions we should just not + // save at all while in an elevated window. return Feature_PersistedWindowLayout::IsEnabled() && - settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout && - _numOpenWindows == 1; + !IsElevated() && + settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout; + } + + // Method Description; + // - Checks if the current window is configured to load a particular layout + // Arguments: + // - settings: The settings to use as this may be called before the page is + // fully initialized. + // Return Value: + // - non-null if there is a particular saved layout to use + std::optional TerminalPage::LoadPersistedLayoutIdx(CascadiaSettings& settings) const + { + return ShouldUsePersistedLayout(settings) ? _loadFromPersistedLayoutIdx : std::nullopt; + } + + WindowLayout TerminalPage::LoadPersistedLayout(CascadiaSettings& settings) const + { + if (const auto idx = LoadPersistedLayoutIdx(settings)) + { + const auto i = idx.value(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (layouts && layouts.Size() > i) + { + return layouts.GetAt(i); + } + } + return nullptr; } winrt::fire_and_forget TerminalPage::NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e) @@ -374,30 +414,13 @@ namespace winrt::TerminalApp::implementation { _startupState = StartupState::InStartup; - // If the user selected to save their tab layout, we are the first - // window opened, and wt was not run with any other arguments, then - // we should use the saved settings. - auto firstActionIsDefault = [](ActionAndArgs action) { - if (action.Action() != ShortcutAction::NewTab) - { - return false; - } - - // If no commands were given, we will have default args - if (const auto args = action.Args().try_as()) - { - NewTerminalArgs defaultArgs{}; - return args.TerminalArgs() == nullptr || args.TerminalArgs().Equals(defaultArgs); - } - - return false; - }; - if (ShouldUsePersistedLayout(_settings) && _startupActions.Size() == 1 && firstActionIsDefault(_startupActions.GetAt(0))) + // If we are provided with an index, the cases where we have + // commandline args and startup actions are already handled. + if (const auto layout = LoadPersistedLayout(_settings)) { - auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); - if (layouts && layouts.Size() > 0 && layouts.GetAt(0).TabLayout() && layouts.GetAt(0).TabLayout().Size() > 0) + if (layout.TabLayout().Size() > 0) { - _startupActions = layouts.GetAt(0).TabLayout(); + _startupActions = layout.TabLayout(); } } @@ -1276,12 +1299,19 @@ namespace winrt::TerminalApp::implementation // Method Description: // - Saves the window position and tab layout to the application state + // - This does not create the InitialPosition field, that needs to be + // added externally. // Arguments: // - // Return Value: - // - - void TerminalPage::PersistWindowLayout() + // - the window layout + WindowLayout TerminalPage::GetWindowLayout() { + if (_startupState != StartupState::Initialized) + { + return nullptr; + } + std::vector actions; for (auto tab : _tabs) @@ -1289,7 +1319,7 @@ namespace winrt::TerminalApp::implementation if (auto terminalTab = _GetTerminalTabImpl(tab)) { auto tabActions = terminalTab->BuildStartupActions(); - actions.insert(actions.end(), tabActions.begin(), tabActions.end()); + actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); } else if (tab.try_as()) { @@ -1298,7 +1328,7 @@ namespace winrt::TerminalApp::implementation OpenSettingsArgs args{ SettingsTarget::SettingsUI }; action.Args(args); - actions.push_back(action); + actions.emplace_back(std::move(action)); } } @@ -1311,7 +1341,18 @@ namespace winrt::TerminalApp::implementation SwitchToTabArgs switchToTabArgs{ idx.value() }; action.Args(switchToTabArgs); - actions.push_back(action); + actions.emplace_back(std::move(action)); + } + + // If the user set a custom name, save it + if (_WindowName != L"") + { + ActionAndArgs action; + action.Action(ShortcutAction::RenameWindow); + RenameWindowArgs args{ _WindowName }; + action.Args(args); + + actions.emplace_back(std::move(action)); } WindowLayout layout{}; @@ -1324,33 +1365,7 @@ namespace winrt::TerminalApp::implementation layout.InitialSize(windowSize); - if (_hostingHwnd) - { - // Get the position of the current window. This includes the - // non-client already. - RECT window{}; - GetWindowRect(_hostingHwnd.value(), &window); - - // We want to remove the non-client area so calculate that. - // We don't have access to the (NonClient)IslandWindow directly so - // just replicate the logic. - const auto windowStyle = static_cast(GetWindowLong(_hostingHwnd.value(), GWL_STYLE)); - - auto dpi = GetDpiForWindow(_hostingHwnd.value()); - RECT nonClientArea{}; - LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&nonClientArea, windowStyle, false, 0, dpi)); - - // The nonClientArea adjustment is negative, so subtract that out. - // This way we save the user-visible location of the terminal. - LaunchPosition pos{}; - pos.X = window.left - nonClientArea.left; - pos.Y = window.top; - - layout.InitialPosition(pos); - } - - auto state = ApplicationState::SharedInstance(); - state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + return layout; } // Method Description: @@ -1379,8 +1394,9 @@ namespace winrt::TerminalApp::implementation if (ShouldUsePersistedLayout(_settings)) { - PersistWindowLayout(); - // don't delete the ApplicationState when all of the tabs are removed. + // Don't delete the ApplicationState when all of the tabs are removed. + // If there is still a monarch living they will get the event that + // a window closed and trigger a new save without this window. _maintainStateOnTabClose = true; } @@ -2242,6 +2258,8 @@ namespace winrt::TerminalApp::implementation // enabled application-wide, so we don't need to check it each time we // want to create an animation. WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); + + _tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield()); } // This is a helper to aid in sorting commands by their `Name`s, alphabetically. @@ -3091,6 +3109,11 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::SetPersistedLayoutIdx(const uint32_t idx) + { + _loadFromPersistedLayoutIdx = idx; + } + void TerminalPage::SetNumberOfOpenWindows(const uint64_t num) { _numOpenWindows = num; diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 7302cf6ed..26332453a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -59,6 +59,9 @@ namespace winrt::TerminalApp::implementation void Create(); bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + std::optional LoadPersistedLayoutIdx(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + Microsoft::Terminal::Settings::Model::WindowLayout GetWindowLayout(); winrt::fire_and_forget NewTerminalByDrop(winrt::Windows::UI::Xaml::DragEventArgs& e); @@ -82,7 +85,6 @@ namespace winrt::TerminalApp::implementation bool AlwaysOnTop() const; void SetStartupActions(std::vector& actions); - void PersistWindowLayout(); void SetInboundListener(bool isEmbedding); static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); @@ -111,10 +113,12 @@ namespace winrt::TerminalApp::implementation void WindowId(const uint64_t& value); void SetNumberOfOpenWindows(const uint64_t value); + void SetPersistedLayoutIdx(const uint32_t value); winrt::hstring WindowIdForDisplay() const noexcept; winrt::hstring WindowNameForDisplay() const noexcept; bool IsQuakeWindow() const noexcept; + bool IsElevated() const noexcept; WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); @@ -132,6 +136,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs); TYPED_EVENT(IsQuakeWindowChanged, IInspectable, IInspectable); TYPED_EVENT(SummonWindowRequested, IInspectable, IInspectable); + TYPED_EVENT(CloseRequested, IInspectable, IInspectable); TYPED_EVENT(OpenSystemMenu, IInspectable, IInspectable); TYPED_EVENT(QuitRequested, IInspectable, IInspectable); @@ -165,6 +170,7 @@ namespace winrt::TerminalApp::implementation bool _isAlwaysOnTop{ false }; winrt::hstring _WindowName{}; uint64_t _WindowId{ 0 }; + std::optional _loadFromPersistedLayoutIdx{}; uint64_t _numOpenWindows{ 0 }; bool _maintainStateOnTabClose{ false }; @@ -256,6 +262,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; @@ -359,7 +385,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/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index dd2777add..979483d0a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -33,7 +33,6 @@ namespace TerminalApp UInt64 WindowId; String WindowNameForDisplay { get; }; String WindowIdForDisplay { get; }; - void SetNumberOfOpenWindows(UInt64 num); void RenameFailed(); Boolean IsQuakeWindow(); @@ -58,6 +57,7 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RenameWindowRequested; event Windows.Foundation.TypedEventHandler IsQuakeWindowChanged; event Windows.Foundation.TypedEventHandler SummonWindowRequested; + event Windows.Foundation.TypedEventHandler CloseRequested; event Windows.Foundation.TypedEventHandler OpenSystemMenu; } } diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 53c75eb6f..aaf8768cd 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""; } @@ -450,12 +457,25 @@ namespace winrt::TerminalApp::implementation // 1 for the child after the first split. auto state = _rootPane->BuildStartupActions(0, 1); - ActionAndArgs newTabAction{}; - newTabAction.Action(ShortcutAction::NewTab); - NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; - newTabAction.Args(newTabArgs); + { + ActionAndArgs newTabAction{}; + newTabAction.Action(ShortcutAction::NewTab); + NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; + newTabAction.Args(newTabArgs); - state.args.emplace(state.args.begin(), std::move(newTabAction)); + state.args.emplace(state.args.begin(), std::move(newTabAction)); + } + + if (_runtimeTabColor) + { + ActionAndArgs setColorAction{}; + setColorAction.Action(ShortcutAction::SetTabColor); + + SetTabColorArgs setColorArgs{ _runtimeTabColor.value() }; + setColorAction.Args(setColorArgs); + + state.args.emplace_back(std::move(setColorAction)); + } // If we only have one arg, we only have 1 pane so we don't need any // special focus logic @@ -501,19 +521,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 @@ -538,8 +553,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(); @@ -614,16 +629,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; @@ -699,7 +710,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) { @@ -722,11 +736,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; @@ -734,7 +759,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: @@ -1027,7 +1055,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() }; @@ -1035,8 +1063,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 @@ -1071,8 +1111,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()) @@ -1290,11 +1341,13 @@ namespace winrt::TerminalApp::implementation // - The tab's color, if any std::optional TerminalTab::GetTabColor() { - const auto currControlColor{ GetActiveTerminalControl().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, @@ -1407,7 +1460,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); @@ -1453,6 +1515,7 @@ namespace winrt::TerminalApp::implementation void TerminalTab::_ClearTabBackgroundColor() { winrt::hstring keys[] = { + L"TabViewItemHeaderBackground", L"TabViewItemHeaderBackgroundSelected", L"TabViewItemHeaderBackgroundPointerOver", L"TabViewItemHeaderForeground", @@ -1473,9 +1536,6 @@ namespace winrt::TerminalApp::implementation } } - // Clear out the Background. - TabViewItem().Background(nullptr); - _RefreshVisualState(); _colorCleared(); } @@ -1585,6 +1645,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 @@ -1593,6 +1658,7 @@ namespace winrt::TerminalApp::implementation } void TerminalTab::ExitZoom() { + Content(nullptr); _rootPane->Restore(_zoomedPane); _zoomedPane = nullptr; // Update the tab header to hide the magnifying glass @@ -1607,13 +1673,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 3dbeaa5aa..4c071c0b0 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -139,6 +139,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 e09eb93ae..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()); @@ -359,21 +357,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation const ControlKeyStates modifiers, const bool keyDown) { - // When there is a selection active, escape should clear it and NOT flow through - // to the terminal. With any other keypress, it should clear the selection AND - // flow through to the terminal. + // Update the selection, if it's present // GH#6423 - don't dismiss selection if the key that was pressed was a // modifier key. We'll wait for a real keystroke to dismiss the // GH #7395 - don't dismiss selection when taking PrintScreen // selection. - // GH#8522, GH#3758 - Only dismiss the selection on key _down_. If we - // dismiss on key up, then there's chance that we'll immediately dismiss + // GH#8522, GH#3758 - Only modify the selection on key _down_. If we + // modify on key up, then there's chance that we'll immediately dismiss // a selection created by an action bound to a keydown. if (HasSelection() && !KeyEvent::IsModifierKey(vkey) && vkey != VK_SNAPSHOT && keyDown) { + // try to update the selection + if (const auto updateSlnParams{ ::Terminal::ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) }) + { + auto lock = _terminal->LockForWriting(); + _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second); + _renderer->TriggerSelection(); + return true; + } + // GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination. if (!modifiers.IsWinPressed()) { @@ -381,6 +386,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderer->TriggerSelection(); } + // When there is a selection active, escape should clear it and NOT flow through + // to the terminal. With any other keypress, it should clear the selection AND + // flow through to the terminal. if (vkey == VK_ESCAPE) { return true; @@ -459,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: @@ -1399,18 +1410,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation // handle ALT key _terminal->SetBlockSelection(altEnabled); - ::Terminal::SelectionExpansionMode mode = ::Terminal::SelectionExpansionMode::Cell; + ::Terminal::SelectionExpansion mode = ::Terminal::SelectionExpansion::Char; if (numberOfClicks == 1) { - mode = ::Terminal::SelectionExpansionMode::Cell; + mode = ::Terminal::SelectionExpansion::Char; } else if (numberOfClicks == 2) { - mode = ::Terminal::SelectionExpansionMode::Word; + mode = ::Terminal::SelectionExpansion::Word; } else if (numberOfClicks == 3) { - mode = ::Terminal::SelectionExpansionMode::Line; + mode = ::Terminal::SelectionExpansion::Line; } // Update the selection appropriately @@ -1435,7 +1446,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetSelectionEnd(terminalPosition, mode); selectionNeedsToBeCopied = true; } - else if (mode != ::Terminal::SelectionExpansionMode::Cell || shiftEnabled) + else if (mode != ::Terminal::SelectionExpansion::Char || shiftEnabled) { // If we are handling a double / triple-click or shift+single click // we establish selection using the selected mode @@ -1534,5 +1545,4 @@ namespace winrt::Microsoft::Terminal::Control::implementation return hstring(ss.str()); } - } diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 55d389869..6991d1b40 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -214,12 +214,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (_canSendVTMouseInput(modifiers)) { - const auto adjustment = _core->ScrollOffset() > 0 ? _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight() : 0; - // If the click happened outside the active region, just don't send any mouse event - if (const auto adjustedY = terminalPosition.y() - adjustment; adjustedY >= 0) - { - _core->SendMouseEvent({ terminalPosition.x(), adjustedY }, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); - } + _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); } else if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown)) { @@ -287,7 +282,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Short-circuit isReadOnly check to avoid warning dialog if (focused && !_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); + _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); } // GH#4603 - don't modify the selection if the pointer press didn't // actually start _in_ the control bounds. Case in point - someone drags @@ -370,7 +365,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Short-circuit isReadOnly check to avoid warning dialog if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) { - _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, toInternalMouseState(buttonState)); + _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); return; } @@ -420,11 +415,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation // here with a PointerPoint. However, as of #979, we don't have a // PointerPoint to work with. So, we're just going to do a // mousewheel event manually - return _core->SendMouseEvent(terminalPosition, + return _sendMouseEventHelper(terminalPosition, WM_MOUSEWHEEL, modifiers, ::base::saturated_cast(delta), - toInternalMouseState(buttonState)); + buttonState); } const auto ctrlPressed = modifiers.IsCtrlPressed(); @@ -600,6 +595,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation return til::point{ pixelPosition / fontSize }; } + bool ControlInteractivity::_sendMouseEventHelper(const til::point terminalPosition, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const SHORT wheelDelta, + Control::MouseButtonState buttonState) + { + const auto adjustment = _core->ScrollOffset() > 0 ? _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight() : 0; + // If the click happened outside the active region, just don't send any mouse event + if (const auto adjustedY = terminalPosition.y() - adjustment; adjustedY >= 0) + { + return _core->SendMouseEvent({ terminalPosition.x(), adjustedY }, pointerUpdateKind, modifiers, wheelDelta, toInternalMouseState(buttonState)); + } + return false; + } + // Method Description: // - Creates an automation peer for the Terminal Control, enabling // accessibility on our control. diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index 937534838..71312afc3 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -142,6 +142,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _sendPastedTextToConnection(std::wstring_view wstr); til::point _getTerminalPosition(const til::point& pixelPosition); + bool _sendMouseEventHelper(const til::point terminalPosition, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const SHORT wheelDelta, + Control::MouseButtonState buttonState); + friend class ControlUnitTests::ControlCoreTests; friend class ControlUnitTests::ControlInteractivityTests; }; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index fc74dd0a9..32bd45f6b 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 { @@ -848,6 +848,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } + const auto keyStatus = e.KeyStatus(); + const auto vkey = gsl::narrow_cast(e.OriginalKey()); + const auto scanCode = gsl::narrow_cast(keyStatus.ScanCode); + auto modifiers = _GetPressedModifierKeys(); + + // GH#11076: + // For some weird reason we sometimes receive a WM_KEYDOWN + // message without vkey or scanCode if a user drags a tab. + // The KeyChord constructor has a debug assertion ensuring that all KeyChord + // either have a valid vkey/scanCode. This is important, because this prevents + // accidential insertion of invalid KeyChords into classes like ActionMap. + if (!vkey && !scanCode) + { + e.Handled(true); + return; + } + // Mark the event as handled and do nothing if we're closing, or the key // was the Windows key. // @@ -856,19 +873,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // win32-input-mode, then we'll send all these keystrokes to the // terminal - it's smart enough to ignore the keys it doesn't care // about. - if (_IsClosing() || - e.OriginalKey() == VirtualKey::LeftWindows || - e.OriginalKey() == VirtualKey::RightWindows) - + if (_IsClosing() || vkey == VK_LWIN || vkey == VK_RWIN) { e.Handled(true); return; } - auto modifiers = _GetPressedModifierKeys(); - const auto vkey = gsl::narrow_cast(e.OriginalKey()); - const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); - // Short-circuit isReadOnly check to avoid warning dialog if (_core.IsInReadOnlyMode()) { @@ -876,7 +886,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - if (e.KeyStatus().IsExtendedKey) + if (keyStatus.IsExtendedKey) { modifiers |= ControlKeyStates::EnhancedKey; } @@ -886,8 +896,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // will be sent through the TSFInputControl. See GH#1401 for more // details if (modifiers.IsAltPressed() && - (e.OriginalKey() >= VirtualKey::NumberPad0 && e.OriginalKey() <= VirtualKey::NumberPad9)) - + (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)) { e.Handled(true); return; @@ -917,7 +926,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Manually prevent keyboard navigation with tab. We want to send tab to // the terminal, and we don't want to be able to escape focus of the // control with tab. - e.Handled(e.OriginalKey() == VirtualKey::Tab); + e.Handled(vkey == VK_TAB); } // Method Description: diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index aba4d68de..1ecf6158c 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -849,7 +849,13 @@ WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept // will release this lock when it's destructed. [[nodiscard]] std::unique_lock Terminal::LockForReading() { +#ifdef NDEBUG return std::unique_lock{ _readWriteLock }; +#else + auto lock = std::unique_lock{ _readWriteLock }; + _lastLocker = GetCurrentThreadId(); + return lock; +#endif } // Method Description: @@ -859,7 +865,13 @@ WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept // will release this lock when it's destructed. [[nodiscard]] std::unique_lock Terminal::LockForWriting() { +#ifdef NDEBUG return std::unique_lock{ _readWriteLock }; +#else + auto lock = std::unique_lock{ _readWriteLock }; + _lastLocker = GetCurrentThreadId(); + return lock; +#endif } Viewport Terminal::_GetMutableViewport() const noexcept diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 27e26b7f2..0e105b1e1 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -199,6 +199,7 @@ public: const COORD GetSelectionEnd() const noexcept override; const std::wstring_view GetConsoleTitle() const noexcept override; void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute) override; + const bool IsUiaDataInitialized() const noexcept override; #pragma endregion void SetWriteInputCallback(std::function pfn) noexcept; @@ -227,16 +228,30 @@ public: #pragma region TextSelection // These methods are defined in TerminalSelection.cpp - enum class SelectionExpansionMode + enum class SelectionDirection { - Cell, - Word, - Line + Left, + Right, + Up, + Down }; - void MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode); + + enum class SelectionExpansion + { + Char, + Word, + Line, // Mouse selection only! + Viewport, + Buffer + }; + void MultiClickSelection(const COORD viewportPos, SelectionExpansion expansionMode); void SetSelectionAnchor(const COORD position); - void SetSelectionEnd(const COORD position, std::optional newExpansionMode = std::nullopt); + void SetSelectionEnd(const COORD position, std::optional newExpansionMode = std::nullopt); void SetBlockSelection(const bool isEnabled) noexcept; + void UpdateSelection(SelectionDirection direction, SelectionExpansion mode); + + using UpdateSelectionParams = std::optional>; + static UpdateSelectionParams ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey); const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace); #pragma endregion @@ -254,6 +269,9 @@ private: // But we can abuse the fact that the surrounding members rarely change and are huge // (std::function is like 64 bytes) to create some natural padding without wasting space. til::ticket_lock _readWriteLock; +#ifndef NDEBUG + DWORD _lastLocker; +#endif std::function _pfnScrollPositionChanged; std::function _pfnBackgroundColorChanged; @@ -308,7 +326,7 @@ private: std::optional _selection; bool _blockSelection; std::wstring _wordDelimiters; - SelectionExpansionMode _multiClickSelectionMode; + SelectionExpansion _multiClickSelectionMode; #pragma endregion // TODO: These members are not shared by an alt-buffer. They should be @@ -375,6 +393,10 @@ private: std::pair _PivotSelection(const COORD targetPos, bool& targetStart) const; std::pair _ExpandSelectionAnchors(std::pair anchors) const; COORD _ConvertToBufferCell(const COORD viewportPos) const; + void _MoveByChar(SelectionDirection direction, COORD& pos); + void _MoveByWord(SelectionDirection direction, COORD& pos); + void _MoveByViewport(SelectionDirection direction, COORD& pos); + void _MoveByBuffer(SelectionDirection direction, COORD& pos); #pragma endregion Microsoft::Console::VirtualTerminal::SgrStack _sgrStack; diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index c8c877032..367a8365a 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -100,8 +100,8 @@ const bool Terminal::IsBlockSelection() const noexcept // - Perform a multi-click selection at viewportPos expanding according to the expansionMode // Arguments: // - viewportPos: the (x,y) coordinate on the visible viewport -// - expansionMode: the SelectionExpansionMode to dictate the boundaries of the selection anchors -void Terminal::MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode) +// - expansionMode: the SelectionExpansion to dictate the boundaries of the selection anchors +void Terminal::MultiClickSelection(const COORD viewportPos, SelectionExpansion expansionMode) { // set the selection pivot to expand the selection using SetSelectionEnd() _selection = SelectionAnchors{}; @@ -124,7 +124,7 @@ void Terminal::SetSelectionAnchor(const COORD viewportPos) _selection = SelectionAnchors{}; _selection->pivot = _ConvertToBufferCell(viewportPos); - _multiClickSelectionMode = SelectionExpansionMode::Cell; + _multiClickSelectionMode = SelectionExpansion::Char; SetSelectionEnd(viewportPos); _selection->start = _selection->pivot; @@ -136,7 +136,7 @@ void Terminal::SetSelectionAnchor(const COORD viewportPos) // Arguments: // - viewportPos: the (x,y) coordinate on the visible viewport // - newExpansionMode: overwrites the _multiClickSelectionMode for this function call. Used for ShiftClick -void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional newExpansionMode) +void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional newExpansionMode) { if (!_selection.has_value()) { @@ -210,15 +210,15 @@ std::pair Terminal::_ExpandSelectionAnchors(std::pairGetSize(); switch (_multiClickSelectionMode) { - case SelectionExpansionMode::Line: + case SelectionExpansion::Line: start = { bufferSize.Left(), start.Y }; end = { bufferSize.RightInclusive(), end.Y }; break; - case SelectionExpansionMode::Word: + case SelectionExpansion::Word: start = _buffer->GetWordStart(start, _wordDelimiters); end = _buffer->GetWordEnd(end, _wordDelimiters); break; - case SelectionExpansionMode::Cell: + case SelectionExpansion::Char: default: // no expansion is necessary break; @@ -235,6 +235,229 @@ void Terminal::SetBlockSelection(const bool isEnabled) noexcept _blockSelection = isEnabled; } +Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) +{ + if (mods.IsShiftPressed() && !mods.IsAltPressed()) + { + if (mods.IsCtrlPressed()) + { + // Ctrl + Shift + _ + switch (vkey) + { + case VK_LEFT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Word }; + case VK_RIGHT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Word }; + case VK_HOME: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Buffer }; + case VK_END: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Buffer }; + } + } + else + { + // Shift + _ + switch (vkey) + { + case VK_HOME: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Viewport }; + case VK_END: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Viewport }; + case VK_PRIOR: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Up, SelectionExpansion::Viewport }; + case VK_NEXT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Down, SelectionExpansion::Viewport }; + case VK_LEFT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Left, SelectionExpansion::Char }; + case VK_RIGHT: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Right, SelectionExpansion::Char }; + case VK_UP: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Up, SelectionExpansion::Char }; + case VK_DOWN: + return UpdateSelectionParams{ std::in_place, SelectionDirection::Down, SelectionExpansion::Char }; + } + } + } + return std::nullopt; +} + +void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion mode) +{ + // 1. Figure out which endpoint to update + // One of the endpoints is the pivot, signifying that the other endpoint is the one we want to move. + const bool movingEnd{ _selection->start == _selection->pivot }; + auto targetPos{ movingEnd ? _selection->end : _selection->start }; + + // 2. Perform the movement + switch (mode) + { + case SelectionExpansion::Char: + _MoveByChar(direction, targetPos); + break; + case SelectionExpansion::Word: + _MoveByWord(direction, targetPos); + break; + case SelectionExpansion::Viewport: + _MoveByViewport(direction, targetPos); + break; + case SelectionExpansion::Buffer: + _MoveByBuffer(direction, targetPos); + break; + } + + // 3. Actually modify the selection + // NOTE: targetStart doesn't matter here + bool targetStart = false; + std::tie(_selection->start, _selection->end) = _PivotSelection(targetPos, targetStart); + + // 4. Scroll (if necessary) + if (const auto viewport = _GetVisibleViewport(); !viewport.IsInBounds(targetPos)) + { + if (const auto amtAboveView = viewport.Top() - targetPos.Y; amtAboveView > 0) + { + // anchor is above visible viewport, scroll by that amount + _scrollOffset += amtAboveView; + } + else + { + // anchor is below visible viewport, scroll by that amount + const auto amtBelowView = targetPos.Y - viewport.BottomInclusive(); + _scrollOffset -= amtBelowView; + } + _NotifyScrollEvent(); + _buffer->GetRenderTarget().TriggerScroll(); + } +} + +void Terminal::_MoveByChar(SelectionDirection direction, COORD& pos) +{ + switch (direction) + { + case SelectionDirection::Left: + _buffer->GetSize().DecrementInBounds(pos); + pos = _buffer->GetGlyphStart(pos); + break; + case SelectionDirection::Right: + _buffer->GetSize().IncrementInBounds(pos); + pos = _buffer->GetGlyphEnd(pos); + break; + case SelectionDirection::Up: + { + const auto bufferSize{ _buffer->GetSize() }; + pos = { pos.X, std::clamp(base::ClampSub(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) }; + break; + } + case SelectionDirection::Down: + { + const auto bufferSize{ _buffer->GetSize() }; + pos = { pos.X, std::clamp(base::ClampAdd(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) }; + break; + } + } +} + +void Terminal::_MoveByWord(SelectionDirection direction, COORD& pos) +{ + switch (direction) + { + case SelectionDirection::Left: + const auto wordStartPos{ _buffer->GetWordStart(pos, _wordDelimiters) }; + if (_buffer->GetSize().CompareInBounds(_selection->pivot, pos) < 0) + { + // If we're moving towards the pivot, move one more cell + pos = wordStartPos; + _buffer->GetSize().DecrementInBounds(pos); + } + else if (wordStartPos == pos) + { + // already at the beginning of the current word, + // move to the beginning of the previous word + _buffer->GetSize().DecrementInBounds(pos); + pos = _buffer->GetWordStart(pos, _wordDelimiters); + } + else + { + // move to the beginning of the current word + pos = wordStartPos; + } + break; + case SelectionDirection::Right: + const auto wordEndPos{ _buffer->GetWordEnd(pos, _wordDelimiters) }; + if (_buffer->GetSize().CompareInBounds(pos, _selection->pivot) < 0) + { + // If we're moving towards the pivot, move one more cell + pos = _buffer->GetWordEnd(pos, _wordDelimiters); + _buffer->GetSize().IncrementInBounds(pos); + } + else if (wordEndPos == pos) + { + // already at the end of the current word, + // move to the end of the next word + _buffer->GetSize().IncrementInBounds(pos); + pos = _buffer->GetWordEnd(pos, _wordDelimiters); + } + else + { + // move to the end of the current word + pos = wordEndPos; + } + break; + case SelectionDirection::Up: + _MoveByChar(direction, pos); + pos = _buffer->GetWordStart(pos, _wordDelimiters); + break; + case SelectionDirection::Down: + _MoveByChar(direction, pos); + pos = _buffer->GetWordEnd(pos, _wordDelimiters); + break; + } +} + +void Terminal::_MoveByViewport(SelectionDirection direction, COORD& pos) +{ + const auto bufferSize{ _buffer->GetSize() }; + switch (direction) + { + case SelectionDirection::Left: + pos = { bufferSize.Left(), pos.Y }; + break; + case SelectionDirection::Right: + pos = { bufferSize.RightInclusive(), pos.Y }; + break; + case SelectionDirection::Up: + { + const auto viewportHeight{ _mutableViewport.Height() }; + const auto newY{ base::ClampSub(pos.Y, viewportHeight) }; + pos = newY < bufferSize.Top() ? bufferSize.Origin() : COORD{ pos.X, newY }; + break; + } + case SelectionDirection::Down: + { + const auto viewportHeight{ _mutableViewport.Height() }; + const auto mutableBottom{ _mutableViewport.BottomInclusive() }; + const auto newY{ base::ClampAdd(pos.Y, viewportHeight) }; + pos = newY > mutableBottom ? COORD{ bufferSize.RightInclusive(), mutableBottom } : COORD{ pos.X, newY }; + break; + } + } +} + +void Terminal::_MoveByBuffer(SelectionDirection direction, COORD& pos) +{ + const auto bufferSize{ _buffer->GetSize() }; + switch (direction) + { + case SelectionDirection::Left: + case SelectionDirection::Up: + pos = bufferSize.Origin(); + break; + case SelectionDirection::Right: + case SelectionDirection::Down: + pos = { bufferSize.RightInclusive(), _mutableViewport.BottomInclusive() }; + break; + } +} + // Method Description: // - clear selection data and disable rendering it #pragma warning(disable : 26440) // changing this to noexcept would require a change to ConHost's selection model diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 84b00eadf..fe1fee627 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -206,7 +206,7 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd) realCoordEnd.Y -= gsl::narrow(_VisibleStartIndex()); SetSelectionAnchor(realCoordStart); - SetSelectionEnd(realCoordEnd, SelectionExpansionMode::Cell); + SetSelectionEnd(realCoordEnd, SelectionExpansion::Char); } const std::wstring_view Terminal::GetConsoleTitle() const noexcept @@ -233,6 +233,9 @@ catch (...) void Terminal::LockConsole() noexcept { _readWriteLock.lock(); +#ifndef NDEBUG + _lastLocker = GetCurrentThreadId(); +#endif } // Method Description: @@ -250,3 +253,12 @@ bool Terminal::IsScreenReversed() const noexcept { return _screenReversed; } + +const bool Terminal::IsUiaDataInitialized() const noexcept +{ + // GH#11135: Windows Terminal needs to create and return an automation peer + // when a screen reader requests it. However, the terminal might not be fully + // initialized yet. So we use this to check if any crucial components of + // UiaData are not yet initialized. + return !!_buffer; +} diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.cpp b/src/cascadia/TerminalSettingsEditor/Appearances.cpp index db7f1c16b..0eb24bb05 100644 --- a/src/cascadia/TerminalSettingsEditor/Appearances.cpp +++ b/src/cascadia/TerminalSettingsEditor/Appearances.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" @@ -7,6 +7,7 @@ #include "EnumEntry.h" #include +#include "..\WinRTUtils\inc\Utils.h" using namespace winrt::Windows::UI::Text; using namespace winrt::Windows::UI::Xaml; 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.cpp b/src/cascadia/TerminalSettingsEditor/Profiles.cpp index 94b49c2fa..8e43f9b10 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" @@ -8,6 +8,7 @@ #include "EnumEntry.h" #include +#include "..\WinRTUtils\inc\Utils.h" using namespace winrt::Windows::UI::Text; using namespace winrt::Windows::UI::Xaml; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.h b/src/cascadia/TerminalSettingsEditor/Profiles.h index 54d7d4108..74c31e49a 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles.h +++ b/src/cascadia/TerminalSettingsEditor/Profiles.h @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #pragma once @@ -22,7 +22,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void SetAcrylicOpacityPercentageValue(double value) { - _profile.DefaultAppearance().Opacity(winrt::Microsoft::Terminal::Settings::Editor::Converters::PercentageValueToPercentage(value)); + Opacity(winrt::Microsoft::Terminal::Settings::Editor::Converters::PercentageValueToPercentage(value)); }; void SetPadding(double value) diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.xaml b/src/cascadia/TerminalSettingsEditor/Profiles.xaml index 24e6a7fe1..945001efe 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}"> -