diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index ce36d8d2a2b..c55d71ad4f6 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -34,7 +34,6 @@ DNE DONTADDTORECENT DWMSBT DWMWA -DWMWA DWORDLONG endfor ENDSESSION @@ -160,6 +159,7 @@ rcx REGCLS RETURNCMD rfind +RLO ROOTOWNER roundf RSHIFT @@ -251,3 +251,4 @@ xtree xutility YIcon YMax +zwstring diff --git a/.github/actions/spelling/allow/microsoft.txt b/.github/actions/spelling/allow/microsoft.txt index 2088e15b861..b79f49c3781 100644 --- a/.github/actions/spelling/allow/microsoft.txt +++ b/.github/actions/spelling/allow/microsoft.txt @@ -9,9 +9,11 @@ appxbundle appxerror appxmanifest ATL +autoexec backplating bitmaps BOMs +COMPUTERNAME CPLs cpptools cppvsdbg @@ -26,6 +28,7 @@ dotnetfeed DTDs DWINRT enablewttlogging +HOMESHARE Intelli IVisual libucrt @@ -33,6 +36,7 @@ libucrtd LKG LOCKFILE Lxss +makepri mfcribbon microsoft microsoftonline @@ -50,15 +54,19 @@ pgo pgosweep powerrename powershell +priconfig +PRIINFO propkey pscustomobject QWORD regedit +resfiles robocopy SACLs segoe sdkddkver Shobjidl +sid Skype SRW sxs @@ -71,6 +79,7 @@ tdbuildteamid ucrt ucrtd unvirtualized +USERDNSDOMAIN VCRT vcruntime Virtualization diff --git a/.github/actions/spelling/expect/alphabet.txt b/.github/actions/spelling/expect/alphabet.txt index 23933713a40..68b264a0b14 100644 --- a/.github/actions/spelling/expect/alphabet.txt +++ b/.github/actions/spelling/expect/alphabet.txt @@ -18,6 +18,7 @@ BBBBBBBB BBBBBCCC BBBBCCCCC BBGGRR +efg EFG EFGh QQQQQQQQQQABCDEFGHIJ diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 0c46903dbe8..6eda39c7185 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -2,6 +2,7 @@ aabbcc ABANDONFONT abbcc ABCDEFGHIJKLMNOPQRSTUVWXY +ABCF abgr abi ABORTIFHUNG @@ -193,7 +194,6 @@ chh chk CHT Cic -CLA Clcompile CLE cleartype @@ -408,6 +408,7 @@ DECAUPSS DECAWM DECBKM DECCARA +DECCIR DECCKM DECCKSR DECCOLM @@ -437,9 +438,12 @@ DECRC DECREQTPARM DECRLM DECRPM +DECRQCRA DECRQM +DECRQPSR DECRQSS DECRQTSR +DECRSPS decrst DECSACE DECSASD @@ -460,6 +464,7 @@ DECSTBM DECSTGLT DECSTR DECSWL +DECTABSR DECTCEM DECXCPR DEFAPP @@ -479,7 +484,6 @@ defterm DELAYLOAD DELETEONRELEASE Delt -demoable depersist deprioritized deserializers @@ -536,7 +540,6 @@ DSSCL DSwap DTest DTTERM -DUMMYUNIONNAME dup'ed dvi dwl @@ -589,7 +592,6 @@ ETW EUDC EVENTID eventing -everytime evflags evt execd @@ -611,8 +613,12 @@ FACESIZE FAILIFTHERE fastlink fcharset +FDEA fdw +FECF +FEEF fesb +FFAF FFDE FFrom fgbg @@ -797,7 +803,6 @@ HIBYTE hicon HIDEWINDOW hinst -Hirots HISTORYBUFS HISTORYNODUP HISTORYSIZE @@ -809,6 +814,8 @@ hkl HKLM hlocal hlsl +HMB +HMK hmod hmodule hmon @@ -898,7 +905,6 @@ INSERTMODE INTERACTIVITYBASE INTERCEPTCOPYPASTE INTERNALNAME -inthread intsafe INVALIDARG INVALIDATERECT @@ -1255,14 +1261,12 @@ ntm nto ntrtl ntstatus -ntsubauth NTSYSCALLAPI nttree nturtl ntuser NTVDM ntverp -NTWIN nugetversions nullability nullness @@ -1301,8 +1305,6 @@ opencode opencon openconsole openconsoleproxy -OPENIF -OPENLINK openps openvt ORIGINALFILENAME @@ -1355,9 +1357,7 @@ pcg pch PCIDLIST PCIS -PCLIENT PCLONG -PCOBJECT pcon PCONSOLE PCONSOLEENDTASK @@ -1369,7 +1369,6 @@ pcshell PCSHORT PCSR PCSTR -PCUNICODE PCWCH PCWCHAR PCWSTR @@ -1418,7 +1417,6 @@ PLOGICAL pnm PNMLINK pntm -PNTSTATUS POBJECT Podcast POINTSLIST @@ -1437,7 +1435,6 @@ ppf ppguid ppidl PPROC -PPROCESS ppropvar ppsi ppsl @@ -1501,7 +1498,6 @@ ptrs ptsz PTYIn PUCHAR -PUNICODE pwch PWDDMCONSOLECONTEXT pws @@ -1563,7 +1559,6 @@ REGISTEROS REGISTERVDM regkey REGSTR -reingest RELBINPATH remoting renamer @@ -1575,6 +1570,7 @@ replatformed Replymessage repositorypath Requiresx +rerasterize rescap Resequence RESETCONTENT @@ -1775,6 +1771,7 @@ srv srvinit srvpipe ssa +startdir STARTF STARTUPINFO STARTUPINFOEX @@ -1857,6 +1854,7 @@ TDP TEAMPROJECT tearoff Teb +Techo tellp teraflop terminalcore @@ -1948,6 +1946,7 @@ trx tsattrs tsf tsgr +tsm TStr TSTRFORMAT TSub @@ -1999,7 +1998,6 @@ unittesting unittests unk unknwn -unmark UNORM unparseable unregistering @@ -2283,8 +2281,6 @@ xunit xutr XVIRTUALSCREEN XWalk -xwwyzz -xxyyzz yact YCast YCENTER diff --git a/.github/actions/spelling/patterns/patterns.txt b/.github/actions/spelling/patterns/patterns.txt index a0e1931f36f..5acf5e9bfa6 100644 --- a/.github/actions/spelling/patterns/patterns.txt +++ b/.github/actions/spelling/patterns/patterns.txt @@ -35,7 +35,7 @@ ROY\sG\.\sBIV # hit-count: 71 file-count: 35 # Compiler flags (?:^|[\t ,"'`=(])-[D](?=[A-Z]{2,}|[A-Z][a-z]) -(?:^|[\t ,"'`=(])-[X](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}) +(?:^|[\t ,"'`=(])-[X](?!aml)(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}) # hit-count: 41 file-count: 28 # version suffix v# diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 00000000000..63464ea4fee --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,24 @@ +name: Publish to Winget + +on: + release: + types: [published] + +env: + REGEX: 'Microsoft\.WindowsTerminal(?:Preview)?_([\d.]+)_8wekyb3d8bbwe\.msixbundle$' + +jobs: + publish: + runs-on: windows-latest # Action can only run on Windows + steps: + - name: Publish Windows Terminal ${{ github.event.release.prerelease && 'Preview' || 'Stable' }} + run: | + $assets = '${{ toJSON(github.event.release.assets) }}' | ConvertFrom-Json + $wingetRelevantAsset = $assets | Where-Object { $_.name -like '*.msixbundle' } | Select-Object -First 1 + $regex = [Regex]::New($env:REGEX) + $version = $regex.Match($wingetRelevantAsset.name).Groups[1].Value + + $wingetPackage = "Microsoft.WindowsTerminal${{ github.event.release.prerelease && '.Preview' || '' }}" + + & curl.exe -JLO https://aka.ms/wingetcreate/latest + & .\wingetcreate.exe update $wingetPackage -s -v $version -u $wingetRelevantAsset.browser_download_url -t "${{ secrets.WINGET_TOKEN }}" diff --git a/.vscode/settings.json b/.vscode/settings.json index 604f91797d9..bb2f304e54a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "C_Cpp.loggingLevel": "None", "files.associations": { "xstring": "cpp", - "*.idl": "cpp", + "*.idl": "midl3", "array": "cpp", "future": "cpp", "istream": "cpp", @@ -106,4 +106,4 @@ "**/packages/**": true, "**/Generated Files/**": true } -} \ No newline at end of file +} diff --git a/OpenConsole.sln b/OpenConsole.sln index 10062c82f12..cd5fcddd42d 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -326,6 +326,9 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "winconpty.Tests.Feature", "src\winconpty\ft_pty\winconpty.FeatureTests.vcxproj", "{024052DE-83FB-4653-AEA4-90790D29D5BD}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalAzBridge", "src\cascadia\TerminalAzBridge\TerminalAzBridge.vcxproj", "{067F0A06-FCB7-472C-96E9-B03B54E8E18D}" + ProjectSection(ProjectDependencies) = postProject + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} = {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} + EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fmt", "src\dep\fmt\fmt.vcxproj", "{6BAE5851-50D5-4934-8D5E-30361A8A40F3}" EndProject diff --git a/build/packages.config b/build/packages.config index 96481841311..ceadd04ac2c 100644 --- a/build/packages.config +++ b/build/packages.config @@ -3,8 +3,6 @@ - - diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index 2fd0f8757db..834df750d9d 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -56,11 +56,6 @@ parameters: - x64 - x86 - arm64 - - name: buildWindowsVersions - type: object - default: - - Win10 - - Win11 variables: MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\MakeAppx.exe' @@ -87,13 +82,6 @@ variables: NuGetPackBetaVersion: preview ${{ elseif eq(variables['Build.SourceBranchName'], 'main') }}: NuGetPackBetaVersion: experimental - # The NuGet packages have to use *somebody's* DLLs. We used to force them to - # use the Win10 build outputs, but if there isn't a Win10 build we should use - # the Win11 one. - ${{ if containsValue(parameters.buildWindowsVersions, 'Win10') }}: - TerminalBestVersionForNuGetPackages: Win10 - ${{ else }}: - TerminalBestVersionForNuGetPackages: Win11 name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) resources: @@ -107,11 +95,9 @@ jobs: matrix: ${{ each config in parameters.buildConfigurations }}: ${{ each platform in parameters.buildPlatforms }}: - ${{ each windowsVersion in parameters.buildWindowsVersions }}: - ${{ config }}_${{ platform }}_${{ windowsVersion }}: - BuildConfiguration: ${{ config }} - BuildPlatform: ${{ platform }} - TerminalTargetWindowsVersion: ${{ windowsVersion }} + ${{ config }}_${{ platform }}: + BuildConfiguration: ${{ config }} + BuildPlatform: ${{ platform }} displayName: Build timeoutInMinutes: 240 cancelTimeoutInMinutes: 1 @@ -185,10 +171,6 @@ jobs: arguments: -MarkdownNoticePath .\NOTICE.md -OutputPath .\src\cascadia\CascadiaPackage\NOTICE.html pwsh: true - ${{ if eq(parameters.buildTerminal, true) }}: - - pwsh: |- - ./build/scripts/Patch-ManifestsToWindowsVersion.ps1 -NewWindowsVersion "10.0.22000.0" - displayName: Update manifest target version to Win11 (if necessary) - condition: and(succeeded(), eq(variables['TerminalTargetWindowsVersion'], 'Win11')) - task: VSBuild@1 displayName: Build solution **\OpenConsole.sln condition: true @@ -205,7 +187,7 @@ jobs: continueOnError: True inputs: PathtoPublish: $(Build.SourcesDirectory)\msbuild.binlog - ArtifactName: binlog-$(BuildPlatform)-$(TerminalTargetWindowsVersion) + ArtifactName: binlog-$(BuildPlatform) - task: PowerShell@2 displayName: Check MSIX for common regressions inputs: @@ -254,16 +236,12 @@ jobs: arguments: -MatchPattern '*feature.test*.dll' -Platform '$(RationalizedBuildPlatform)' -Configuration '$(BuildConfiguration)' - ${{ if eq(parameters.buildTerminal, true) }}: - task: CopyFiles@2 - displayName: Copy *.appx/*.msix to Artifacts + displayName: Copy *.msix and symbols to Artifacts inputs: Contents: >- - **/*.appx - **/*.msix **/*.appxsym - - !**/Microsoft.VCLibs*.appx TargetFolder: $(Build.ArtifactStagingDirectory)/appx OverWrite: true flattenFolders: true @@ -297,7 +275,17 @@ jobs: displayName: Publish Artifact (appx) inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/appx - ArtifactName: appx-$(BuildPlatform)-$(BuildConfiguration)-$(TerminalTargetWindowsVersion) + ArtifactName: appx-$(BuildPlatform)-$(BuildConfiguration) + + - pwsh: |- + $XamlAppxPath = (Get-Item "src\cascadia\CascadiaPackage\AppPackages\*\Dependencies\$(BuildPlatform)\Microsoft.UI.Xaml*.appx").FullName + & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 -TerminalAppX $(WindowsTerminalPackagePath) -XamlAppX $XamlAppxPath -Destination "$(Build.ArtifactStagingDirectory)/unpackaged" + displayName: Build Unpackaged Distribution + + - publish: $(Build.ArtifactStagingDirectory)/unpackaged + artifact: unpackaged-$(BuildPlatform)-$(BuildConfiguration) + displayName: Publish Artifact (unpackaged) + - ${{ if eq(parameters.buildConPTY, true) }}: - task: CopyFiles@2 displayName: Copy ConPTY to Artifacts @@ -315,7 +303,7 @@ jobs: displayName: Publish Artifact (ConPTY) inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/conpty - ArtifactName: conpty-dll-$(BuildPlatform)-$(BuildConfiguration)-$(TerminalTargetWindowsVersion) + ArtifactName: conpty-dll-$(BuildPlatform)-$(BuildConfiguration) - ${{ if eq(parameters.buildWPF, true) }}: - task: CopyFiles@2 displayName: Copy PublicTerminalCore.dll to Artifacts @@ -329,7 +317,7 @@ jobs: displayName: Publish Artifact (PublicTerminalCore) inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/wpf - ArtifactName: wpf-dll-$(BuildPlatform)-$(BuildConfiguration)-$(TerminalTargetWindowsVersion) + ArtifactName: wpf-dll-$(BuildPlatform)-$(BuildConfiguration) - task: PublishSymbols@2 displayName: Publish symbols path @@ -347,11 +335,6 @@ jobs: - ${{ if eq(parameters.buildTerminal, true) }}: - job: BundleAndSign - strategy: - matrix: - ${{ each windowsVersion in parameters.buildWindowsVersions }}: - ${{ windowsVersion }}: - TerminalTargetWindowsVersion: ${{ windowsVersion }} displayName: Create and sign AppX/MSIX bundles variables: ${{ if eq(parameters.branding, 'Release') }}: @@ -373,9 +356,9 @@ jobs: disableOutputRedirect: true - ${{ each platform in parameters.buildPlatforms }}: - task: DownloadBuildArtifacts@0 - displayName: Download Artifacts ${{ platform }} $(TerminalTargetWindowsVersion) + displayName: Download Artifacts ${{ platform }} inputs: - artifactName: appx-${{ platform }}-Release-$(TerminalTargetWindowsVersion) + artifactName: appx-${{ platform }}-Release # Add 3000 to the major version component, but only for the bundle. # This is to ensure that it is newer than "2022.xx.yy.zz" or whatever the original bundle versions were before # we switched to uniform naming. @@ -385,7 +368,7 @@ jobs: $Components[0] = ([int]$Components[0] + $VersionEpoch) $BundleVersion = $Components -Join "." New-Item -Type Directory "$(System.ArtifactsDirectory)\bundle" - .\build\scripts\Create-AppxBundle.ps1 -InputPath "$(System.ArtifactsDirectory)" -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(TerminalTargetWindowsVersion)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" + .\build\scripts\Create-AppxBundle.ps1 -InputPath "$(System.ArtifactsDirectory)" -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle" displayName: Create WindowsTerminal*.msixbundle - task: EsrpCodeSigning@1 displayName: Submit *.msixbundle to ESRP for code signing @@ -426,7 +409,7 @@ jobs: displayName: 'Publish Artifact: appxbundle-signed' inputs: PathtoPublish: $(System.ArtifactsDirectory)\bundle - ArtifactName: appxbundle-signed-$(TerminalTargetWindowsVersion) + ArtifactName: appxbundle-signed - ${{ if eq(parameters.buildConPTY, true) }}: - job: PackageAndSignConPTY @@ -451,7 +434,7 @@ jobs: - task: DownloadBuildArtifacts@0 displayName: Download ${{ platform }} ConPTY binaries inputs: - artifactName: conpty-dll-${{ platform }}-$(BuildConfiguration)-$(TerminalBestVersionForNuGetPackages) + artifactName: conpty-dll-${{ platform }}-$(BuildConfiguration) downloadPath: bin\${{ platform }}\$(BuildConfiguration)\ extractTars: false - task: PowerShell@2 @@ -542,7 +525,7 @@ jobs: - task: DownloadBuildArtifacts@0 displayName: Download ${{ platform }} PublicTerminalCore inputs: - artifactName: wpf-dll-${{ platform }}-$(BuildConfiguration)-$(TerminalBestVersionForNuGetPackages) + artifactName: wpf-dll-${{ platform }}-$(BuildConfiguration) itemPattern: '**/*.dll' downloadPath: bin\${{ platform }}\$(BuildConfiguration)\ extractTars: false @@ -640,11 +623,10 @@ jobs: # Download the appx-PLATFORM-CONFIG-VERSION artifact for every platform/version combo - ${{ each platform in parameters.buildPlatforms }}: - - ${{ each windowsVersion in parameters.buildWindowsVersions }}: - - task: DownloadBuildArtifacts@0 - displayName: Download Symbols ${{ platform }} ${{ windowsVersion }} - inputs: - artifactName: appx-${{ platform }}-Release-${{ windowsVersion }} + - task: DownloadBuildArtifacts@0 + displayName: Download Symbols ${{ platform }} + inputs: + artifactName: appx-${{ platform }}-Release # It seems easier to do this -- download every appxsym -- then enumerate all the PDBs in the build directory for the # public symbol push. Otherwise, we would have to list all of the PDB files one by one. @@ -704,7 +686,7 @@ jobs: - task: DownloadBuildArtifacts@0 displayName: Download Build Artifacts inputs: - artifactName: appxbundle-signed-Win11 + artifactName: appxbundle-signed extractTars: false - task: PowerShell@2 displayName: Rename and stage packages for vpack @@ -713,7 +695,7 @@ jobs: script: >- # Rename to known/fixed name for Windows build system - Get-ChildItem Microsoft.WindowsTerminal_Win11_*.msixbundle | Rename-Item -NewName { 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle' } + Get-ChildItem Microsoft.WindowsTerminal_*.msixbundle | Rename-Item -NewName { 'Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle' } # Create vpack directory and place item inside @@ -721,13 +703,13 @@ jobs: mkdir WindowsTerminal.app mv Microsoft.WindowsTerminal_8wekyb3d8bbwe.msixbundle .\WindowsTerminal.app\ - workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed-Win11 + workingDirectory: $(System.ArtifactsDirectory)\appxbundle-signed - task: PkgESVPack@12 displayName: 'Package ES - VPack' env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) inputs: - sourceDirectory: $(System.ArtifactsDirectory)\appxbundle-signed-Win11\WindowsTerminal.app + sourceDirectory: $(System.ArtifactsDirectory)\appxbundle-signed\WindowsTerminal.app description: VPack for the Windows Terminal Application pushPkgName: WindowsTerminal.app owner: conhost diff --git a/build/pipelines/templates/build-console-steps.yml b/build/pipelines/templates/build-console-steps.yml index 05cdf667c7c..947dd353553 100644 --- a/build/pipelines/templates/build-console-steps.yml +++ b/build/pipelines/templates/build-console-steps.yml @@ -64,17 +64,24 @@ steps: Write-Host "##vso[task.setvariable variable=RationalizedBuildPlatform]${Arch}" - task: CopyFiles@2 - displayName: 'Copy *.appx/*.msix to Artifacts (Non-PR builds only)' + displayName: 'Copy *.msix to Artifacts' inputs: Contents: | - **/*.appx **/*.msix **/*.appxsym - !**/Microsoft.VCLibs*.appx TargetFolder: '$(Build.ArtifactStagingDirectory)/appx' OverWrite: true flattenFolders: true - condition: succeeded() + +- pwsh: |- + $TerminalMsixPath = (Get-Item "$(Build.ArtifactStagingDirectory)\appx\Cascadia*.msix").FullName + $XamlAppxPath = (Get-Item "src\cascadia\CascadiaPackage\AppPackages\*\Dependencies\$(BuildPlatform)\Microsoft.UI.Xaml*.appx").FullName + & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 -TerminalAppX $TerminalMsixPath -XamlAppX $XamlAppxPath -Destination "$(Build.ArtifactStagingDirectory)/unpackaged" + displayName: Build Unpackaged Distribution + +- publish: $(Build.ArtifactStagingDirectory)/unpackaged + artifact: unpackaged-$(BuildPlatform)-$(BuildConfiguration) + displayName: Publish Artifact (unpackaged) - task: CopyFiles@2 displayName: 'Copy outputs needed for test runs to Artifacts' diff --git a/build/pipelines/templates/helix-runtests-job.yml b/build/pipelines/templates/helix-runtests-job.yml index 2036857cac8..0e7c22efff0 100644 --- a/build/pipelines/templates/helix-runtests-job.yml +++ b/build/pipelines/templates/helix-runtests-job.yml @@ -14,8 +14,8 @@ parameters: platform: '' # if 'useBuildOutputFromBuildId' is set, we will default to using a build from this pipeline: useBuildOutputFromPipeline: $(System.DefinitionId) - openHelixTargetQueues: 'windows.10.amd64.client21h1.open.xaml' - closedHelixTargetQueues: 'windows.10.amd64.client21h1.xaml' + openHelixTargetQueues: 'windows.11.amd64.client.open.reunion' + closedHelixTargetQueues: 'windows.11.amd64.client.reunion' jobs: - job: ${{ parameters.name }} diff --git a/build/scripts/Merge-PriFiles.ps1 b/build/scripts/Merge-PriFiles.ps1 new file mode 100644 index 00000000000..3a9985936aa --- /dev/null +++ b/build/scripts/Merge-PriFiles.ps1 @@ -0,0 +1,78 @@ +Param( + [Parameter(Mandatory, + HelpMessage="List of PRI files or XML dumps (detailed only) to merge")] + [string[]] + $Path, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath, + + [Parameter(HelpMessage="Name of index in output file; defaults to 'Application'")] + [string] + $IndexName = "Application", + + [Parameter(HelpMessage="Path to makepri.exe")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakePriPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakePri.exe" +) + +$ErrorActionPreference = 'Stop' + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "tmp$([Convert]::ToString((Get-Random 65535),16).PadLeft(4,'0')).tmp" +New-Item -ItemType Directory -Path $tempDir | Out-Null +$priConfig = Join-Path $tempDir "priconfig.xml" +$priListFile = Join-Path $tempDir "pri.resfiles" +$dumpListFile = Join-Path $tempDir "dump.resfiles" + +@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@ | Out-File -Encoding:utf8NoBOM $priConfig + +$Path | Where { $_ -Like "*.pri" } | ForEach-Object { + Get-Item $_ | Select -Expand FullName +} | Out-File -Encoding:utf8NoBOM $priListFile + +$Path | Where { $_ -Like "*.xml" } | ForEach-Object { + Get-Item $_ | Select -Expand FullName +} | Out-File -Encoding:utf8NoBOM $dumpListFile + +& $MakePriPath new /pr $tempDir /cf $priConfig /o /in $IndexName /of $OutputPath + +Remove-Item -Recurse -Force $tempDir diff --git a/build/scripts/Merge-TerminalAndXamlResources.ps1 b/build/scripts/Merge-TerminalAndXamlResources.ps1 new file mode 100644 index 00000000000..92c8bad5565 --- /dev/null +++ b/build/scripts/Merge-TerminalAndXamlResources.ps1 @@ -0,0 +1,47 @@ +Param( + [Parameter(Mandatory, + HelpMessage="Root directory of extracted Terminal AppX")] + [string[]] + $TerminalRoot, + + [Parameter(Mandatory, + HelpMessage="Root directory of extracted Xaml AppX")] + [string[]] + $XamlRoot, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath, + + [Parameter(HelpMessage="Path to makepri.exe")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakePriPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakePri.exe" +) + +$ErrorActionPreference = 'Stop' + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "tmp$([Convert]::ToString((Get-Random 65535),16).PadLeft(4,'0')).tmp" +New-Item -ItemType Directory -Path $tempDir | Out-Null + +$terminalDump = Join-Path $tempDir "terminal.pri.xml" + +& $MakePriPath dump /if (Join-Path $TerminalRoot "resources.pri") /of $terminalDump /dt detailed + +Write-Verbose "Removing Microsoft.UI.Xaml node from Terminal to prevent a collision with XAML" +$terminalXMLDocument = [xml](Get-Content $terminalDump) +$resourceMap = $terminalXMLDocument.PriInfo.ResourceMap +$fileSubtree = $resourceMap.ResourceMapSubtree | Where-Object { $_.Name -eq "Files" } +$subtrees = $fileSubtree.ResourceMapSubtree +$xamlSubtreeChild = ($subtrees | Where-Object { $_.Name -eq "Microsoft.UI.Xaml" }) +if ($Null -Ne $xamlSubtreeChild) { + $null = $fileSubtree.RemoveChild($xamlSubtreeChild) + $terminalXMLDocument.Save($terminalDump) +} + +$indexName = $terminalXMLDocument.PriInfo.ResourceMap.name + +& (Join-Path $PSScriptRoot "Merge-PriFiles.ps1") -Path $terminalDump, (Join-Path $XamlRoot "resources.pri") -IndexName $indexName -OutputPath $OutputPath -MakePriPath $MakePriPath + +Remove-Item -Recurse -Force $tempDir diff --git a/build/scripts/New-UnpackagedTerminalDistribution.ps1 b/build/scripts/New-UnpackagedTerminalDistribution.ps1 new file mode 100644 index 00000000000..834f81bbd02 --- /dev/null +++ b/build/scripts/New-UnpackagedTerminalDistribution.ps1 @@ -0,0 +1,117 @@ +Param( + [Parameter(Mandatory, + HelpMessage="Path to Terminal AppX")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $TerminalAppX, + + [Parameter(Mandatory, + HelpMessage="Path to Xaml AppX")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $XamlAppX, + + [Parameter(HelpMessage="Output Directory")] + [string] + $Destination = ".", + + [Parameter(HelpMessage="Path to makeappx.exe")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakeAppxPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\MakeAppx.exe" +) + +$filesToRemove = @("*.xml", "*.winmd", "Appx*", "Images/*Tile*", "Images/*Logo*") # Remove from Terminal +$filesToKeep = @("Microsoft.Terminal.Remoting.winmd") # ... except for these +$filesToCopyFromXaml = @("Microsoft.UI.Xaml.dll", "Microsoft.UI.Xaml") # We don't need the .winmd + +$ErrorActionPreference = 'Stop' + +If ($null -Eq (Get-Item $MakeAppxPath -EA:SilentlyContinue)) { + Write-Error "Could not find MakeAppx.exe at `"$MakeAppxPath`".`nMake sure that -MakeAppxPath points to a valid SDK." + Exit 1 +} + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "tmp$([Convert]::ToString((Get-Random 65535),16).PadLeft(4,'0')).tmp" +New-Item -ItemType Directory -Path $tempDir | Out-Null + +$XamlAppX = Get-Item $XamlAppX | Select-Object -Expand FullName +$TerminalAppX = Get-Item $TerminalAppX | Select-Object -Expand FullName + +######## +# Reading the AppX Manifest for preliminary info +######## + +$appxManifestPath = Join-Path $tempDir AppxManifest.xml +& tar.exe -x -f "$TerminalAppX" -C $tempDir AppxManifest.xml +$manifest = [xml](Get-Content $appxManifestPath) +$pfn = $manifest.Package.Identity.Name +$version = $manifest.Package.Identity.Version +$architecture = $manifest.Package.Identity.ProcessorArchitecture + +$distributionName = "{0}_{1}_{2}" -f ($pfn, $version, $architecture) +$terminalDir = "terminal-{0}" -f ($version) + +######## +# Unpacking Terminal and XAML +######## + +$terminalAppPath = Join-Path $tempdir $terminalDir +$xamlAppPath = Join-Path $tempdir "xaml" +New-Item -ItemType Directory -Path $terminalAppPath | Out-Null +New-Item -ItemType Directory -Path $xamlAppPath | Out-Null +& $MakeAppxPath unpack /p $TerminalAppX /d $terminalAppPath /o | Out-Null +If ($LASTEXITCODE -Ne 0) { + Throw "Unpacking $TerminalAppX failed" +} +& $MakeAppxPath unpack /p $XamlAppX /d $xamlAppPath /o | Out-Null +If ($LASTEXITCODE -Ne 0) { + Throw "Unpacking $XamlAppX failed" +} + +######## +# Some sanity checking +######## + +$xamlManifest = [xml](Get-Content (Join-Path $xamlAppPath "AppxManifest.xml")) +If ($xamlManifest.Package.Identity.Name -NotLike "Microsoft.UI.Xaml*") { + Throw "$XamlAppX is not a XAML package (instead, it looks like $($xamlManifest.Package.Identity.Name))" +} +If ($xamlManifest.Package.Identity.ProcessorArchitecture -Ne $architecture) { + Throw "$XamlAppX is not built for $architecture (instead, it is built for $($xamlManifest.Package.Identity.ProcessorArchitecture))" +} + +######## +# Preparation of source files +######## + +$itemsToRemove = $filesToRemove | ForEach-Object { + Get-Item (Join-Path $terminalAppPath $_) -EA:SilentlyContinue | Where-Object { + $filesToKeep -NotContains $_.Name + } +} | Sort-Object FullName -Unique +$itemsToRemove | Remove-Item -Recurse + +$filesToCopyFromXaml | ForEach-Object { + Get-Item (Join-Path $xamlAppPath $_) +} | Copy-Item -Recurse -Destination $terminalAppPath + +######## +# Resource Management +######## + +$finalTerminalPriFile = Join-Path $terminalAppPath "resources.pri" +& (Join-Path $PSScriptRoot "Merge-TerminalAndXamlResources.ps1") ` + -TerminalRoot $terminalAppPath ` + -XamlRoot $xamlAppPath ` + -OutputPath $finalTerminalPriFile ` + -Verbose:$Verbose + +######## +# Packaging +######## + +New-Item -ItemType Directory -Path $Destination -ErrorAction:SilentlyContinue | Out-Null +$outputZip = (Join-Path $Destination ("{0}.zip" -f ($distributionName))) +& tar -c --format=zip -f $outputZip -C $tempDir $terminalDir +Get-Item $outputZip diff --git a/build/scripts/Patch-ManifestsToWindowsVersion.ps1 b/build/scripts/Patch-ManifestsToWindowsVersion.ps1 deleted file mode 100644 index fe86f24fd8c..00000000000 --- a/build/scripts/Patch-ManifestsToWindowsVersion.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -Param( - [string]$NewWindowsVersion = "10.0.22000.0" -) - -Get-ChildItem src/cascadia/CascadiaPackage -Recurse -Filter *.appxmanifest | ForEach-Object { - $xml = [xml](Get-Content $_.FullName) - $xml.Package.Dependencies.TargetDeviceFamily | Where-Object Name -Like "Windows*" | ForEach-Object { - $_.MinVersion = $NewWindowsVersion - } - $xml.Save($_.FullName) -} diff --git a/common.openconsole.props b/common.openconsole.props index f6eb4c7c94d..c8f43e3602f 100644 --- a/common.openconsole.props +++ b/common.openconsole.props @@ -10,22 +10,4 @@ $(MSBuildThisFileDirectory) - - - 2.7.3-prerelease.220816001 - - 2.7.3 - - diff --git a/custom.props b/custom.props index 0983ed7a6f5..93c4f49ff7a 100644 --- a/custom.props +++ b/custom.props @@ -2,18 +2,6 @@ - - $([MSBuild]::Add($(VersionBuildRevision), 1)) - true 2023 1 diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index 3b6f70b1cec..0438fdebf17 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -4,11 +4,11 @@ - + - + diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 36b20927a6b..2c62802115a 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -24,6 +24,13 @@ "pattern": "^(-?\\d+)?(,\\s?(-?\\d+)?)?$", "type": "string" }, + "CSSLengthPercentage": { + "pattern": "^[+-]?\\d+(?:\\.\\d+)?(?:%|ch|pt|px)?$", + "type": [ + "string", + "null" + ] + }, "DynamicProfileSource": { "enum": [ "Windows.Terminal.Wsl", @@ -314,6 +321,14 @@ } }, "additionalProperties": false + }, + "cellWidth": { + "$ref": "#/$defs/CSSLengthPercentage", + "description": "Override the width of the terminal's cells. The override works similar to CSS' letter-spacing. It defaults to the natural glyph advance width of the primary font rounded to the nearest pixel." + }, + "cellHeight": { + "$ref": "#/$defs/CSSLengthPercentage", + "description": "Override the height of the terminal's cells. The override works similar to CSS' line-height. Defaults to the sum of the natural glyph ascend, descend and line-gap of the primary font rounded to the nearest pixel. The default is usually quite close to setting this to 1.2." } }, "type": "object" @@ -1807,7 +1822,7 @@ "name": { "type": "string", "description": "The name of the theme. This will be displayed in the settings UI.", - "not": { + "not": { "enum": [ "light", "dark", "system" ] } }, @@ -2077,6 +2092,11 @@ "description": "When set to true, the terminal will focus the pane on mouse hover.", "type": "boolean" }, + "compatibility.isolatedMode": { + "default": false, + "description": "When set to true, Terminal windows will not be able to interact with each other (including global hotkeys, tab drag/drop, running commandlines in existing windows, etc.). This is a compatibility escape hatch for users who are running into certain windowing issues.", + "type": "boolean" + }, "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.", @@ -2149,6 +2169,11 @@ "description": "When set to true, the background image for the currently focused profile is expanded to encompass the entire window, beneath other panes.", "type": "boolean" }, + "compatibility.reloadEnvironmentVariables": { + "default": true, + "description": "When set to true, when opening a new tab or pane it will get reloaded environment variables.", + "type": "boolean" + }, "initialCols": { "default": 120, "description": "The number of columns displayed in the window upon first load. If \"launchMode\" is set to \"maximized\" (or \"maximizedFocus\"), this property is ignored.", diff --git a/doc/specs/portable-mode-spec.md b/doc/specs/portable-mode-spec.md new file mode 100644 index 00000000000..c75f6e9f7d8 --- /dev/null +++ b/doc/specs/portable-mode-spec.md @@ -0,0 +1,90 @@ +--- +author: Dustin L. Howett @DHowett +created on: 2023-03-22 +last updated: 2023-03-22 +issue id: none +--- + +# Windows Terminal "Portable" Mode + +## Abstract + +Since we are planning on officially supporting unpackaged execution, I propose a special mode where Terminal stores its +settings in a `settings` folder next to `WindowsTerminal.exe`. + +## Inspiration + +- [PortableApps](https://portableapps.com) +- "Embeddable" Python, which relies on the deployment of a specific file to the Python root + +## Solution Design + +- _If running without package identity,_ `CascadiaSettings` will look for the presence of a file called `.portable` next + to `Microsoft.Terminal.Settings.Model.dll`. +- If that file is present, it will change the settings and state paths to be rooted in a subfolder named `settings` next + to `Microsoft.Terminal.Settings.Model.dll`. + +Right now, _the only thing_ that makes Terminal not work in a "portable" manner is that it saves settings to +`%LOCALAPPDATA%`. + +## UI/UX Design + +_No UI/UX impact is expected._ + +## Capabilities + +- Distributors could ship a self-contained and preconfigured Terminal installation. +- Users could archive fully-working preconfigured versions of Terminal. +- Developers (such as those on the team) could easily test multiple versions of Terminal without worrying about global + settings pollution. + +### Accessibility + +_No change is expected._ + +### Security + +_No change is expected._ + +### Reliability + +More code always bears a risk. + +### Compatibility + +This is a net new feature, and it does not break any existing features. A distributor (or a user) can opt in (or out) by +adding (or removing) the `.portable` file. + +The following features may be impacted. + +- **Dynamic Profiles** and **Fragment Extensions** + - _No impact expected._ Dynamic profiles will still be generated. If a portable installation is moved to a machine without the dynamic profile source, that profile will disappear. +- `firstWindowPreference` and `state.json` + - _No impact expected._ + - State is stored next to settings, even for portable installations. + - If a dynamic profile was saved in `state` and has been removed, Terminal will proceed as in non-portable mode. +- Moving an install from Windows 10 to Windows 11 and back + - _No impact expected._ +- "Machine-specific" settings, like those about rendering and repainting + - _No impact expected._ + - Terminal does not distinguish settings that are specific to a machine. These settings will move along with the portable install. +- The shell extension + - _No impact expected._ + - The shell extension will not be registered with Windows. + - If we choose to register the shell extension, it is already prepared for running a version of WT from the same directory. Registering the portable shell extension will make it launch portable Terminal. + +### Performance, Power, and Efficiency + +_No change is expected._ + +## Potential Issues + +- User confusion around where settings are stored. + +## Future considerations + +- In the future, perhaps `.portable` could itself contain a directory path into which we would store settings. +- We could consider adding an indicator in the Settings UI. +- Because we are using the module path of the Settings Model DLL, a future unpackaged version of the shell extension + that supports profile loading would read the right settings file (assuming it used the settings model.) +- If we choose to store the shell extension cache in the registry, we would need to avoid doing so in portable mode. diff --git a/samples/PixelShaders/Outlines.hlsl b/samples/PixelShaders/Outlines.hlsl index 789f5dc2d22..ecee3adcca4 100644 --- a/samples/PixelShaders/Outlines.hlsl +++ b/samples/PixelShaders/Outlines.hlsl @@ -6,14 +6,14 @@ SamplerState samplerState; // Terminal settings such as the resolution of the texture cbuffer PixelShaderSettings { - // The number of seconds since the pixel shader was enabled - float Time; - // UI Scale - float Scale; - // Resolution of the shaderTexture - float2 Resolution; - // Background color as rgba - float4 Background; + // The number of seconds since the pixel shader was enabled + float Time; + // UI Scale + float Scale; + // Resolution of the shaderTexture + float2 Resolution; + // Background color as rgba + float4 Background; }; // A pixel shader is a program that given a texture coordinate (tex) produces a color. @@ -29,38 +29,19 @@ float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET // effect, read the colors offset on the left, right, top, bottom of this // fragment, as well as on the corners of this fragment. // - // You could get away with fewer samples, but the resulting outlines will be - // blurrier. - - //left, right, top, bottom: - float4 leftColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 1.0, 0.0)/Resolution.y); - float4 rightColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2(-1.0, 0.0)/Resolution.y); - float4 topColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 0.0, 1.0)/Resolution.y); - float4 bottomColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 0.0, -1.0)/Resolution.y); - - // Corners - float4 topLeftColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 1.0, 1.0)/Resolution.y); - float4 topRightColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2(-1.0, 1.0)/Resolution.y); - float4 bottomLeftColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2( 1.0, -1.0)/Resolution.y); - float4 bottomRightColor = shaderTexture.Sample(samplerState, tex+1.0*Scale*float2(-1.0, -1.0)/Resolution.y); - - // Now, if any of those adjacent cells has text in it, then the *color vec4 // will have a non-zero .w (which is used for alpha). Use that alpha value // to add some black to the current fragment. // // This will result in only coloring fragments adjacent to text, but leaving // background images (for example) untouched. - float3 outlineColor = float3(0, 0, 0); - float4 result = color; - result = result + float4(outlineColor, leftColor.w); - result = result + float4(outlineColor, rightColor.w); - result = result + float4(outlineColor, topColor.w); - result = result + float4(outlineColor, bottomColor.w); - result = result + float4(outlineColor, topLeftColor.w); - result = result + float4(outlineColor, topRightColor.w); - result = result + float4(outlineColor, bottomLeftColor.w); - result = result + float4(outlineColor, bottomRightColor.w); - return result; + for (int dy = -2; dy <= 2; dy += 2) { + for (int dx = -2; dx <= 2; dx += 2) { + float4 neighbor = shaderTexture.Sample(samplerState, tex, int2(dx, dy)); + color.a += neighbor.a; + } + } + + return color; } diff --git a/scratch/ScratchIslandApp/SampleApp/packages.config b/scratch/ScratchIslandApp/SampleApp/packages.config index 5df52911dab..857f429dacd 100644 --- a/scratch/ScratchIslandApp/SampleApp/packages.config +++ b/scratch/ScratchIslandApp/SampleApp/packages.config @@ -2,5 +2,5 @@ - + diff --git a/scratch/ScratchIslandApp/WindowExe/packages.config b/scratch/ScratchIslandApp/WindowExe/packages.config index 595507a802b..040e8f88beb 100644 --- a/scratch/ScratchIslandApp/WindowExe/packages.config +++ b/scratch/ScratchIslandApp/WindowExe/packages.config @@ -1,6 +1,6 @@ - + diff --git a/src/buffer/out/OutputCellIterator.cpp b/src/buffer/out/OutputCellIterator.cpp index 8c5247bb410..6e332ee6d15 100644 --- a/src/buffer/out/OutputCellIterator.cpp +++ b/src/buffer/out/OutputCellIterator.cpp @@ -198,6 +198,11 @@ OutputCellIterator::operator bool() const noexcept CATCH_FAIL_FAST(); } +size_t OutputCellIterator::Position() const noexcept +{ + return _pos; +} + // Routine Description: // - Advances the iterator one position over the underlying data source. // Return Value: diff --git a/src/buffer/out/OutputCellIterator.hpp b/src/buffer/out/OutputCellIterator.hpp index 8f59ac2eb81..0199b7c4aa5 100644 --- a/src/buffer/out/OutputCellIterator.hpp +++ b/src/buffer/out/OutputCellIterator.hpp @@ -48,6 +48,7 @@ class OutputCellIterator final operator bool() const noexcept; + size_t Position() const noexcept; til::CoordType GetCellDistance(OutputCellIterator other) const noexcept; til::CoordType GetInputDistance(OutputCellIterator other) const noexcept; friend til::CoordType operator-(OutputCellIterator one, OutputCellIterator two) = delete; diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index de6bc7e8e0b..38a82857cf2 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -4,7 +4,10 @@ #include "precomp.h" #include "Row.hpp" +#include + #include "textBuffer.hpp" +#include "../../types/inc/GlyphWidth.hpp" // The STL is missing a std::iota_n analogue for std::iota, so I made my own. template @@ -229,6 +232,40 @@ void ROW::TransferAttributes(const til::small_rle& a _attr.resize_trailing_extent(gsl::narrow(newWidth)); } +// Returns the previous possible cursor position, preceding the given column. +// Returns 0 if column is less than or equal to 0. +til::CoordType ROW::NavigateToPrevious(til::CoordType column) const noexcept +{ + return _adjustBackward(_clampedColumn(column - 1)); +} + +// Returns the next possible cursor position, following the given column. +// Returns the row width if column is beyond the width of the row. +til::CoordType ROW::NavigateToNext(til::CoordType column) const noexcept +{ + return _adjustForward(_clampedColumn(column + 1)); +} + +uint16_t ROW::_adjustBackward(uint16_t column) const noexcept +{ + // Safety: This is a little bit more dangerous. The first column is supposed + // to never be a trailer and so this loop should exit if column == 0. + for (; _uncheckedIsTrailer(column); --column) + { + } + return column; +} + +uint16_t ROW::_adjustForward(uint16_t column) const noexcept +{ + // Safety: This is a little bit more dangerous. The last column is supposed + // to never be a trailer and so this loop should exit if column == _columnCount. + for (; _uncheckedIsTrailer(column); ++column) + { + } + return column; +} + // Routine Description: // - clears char data in column in row // Arguments: @@ -311,16 +348,20 @@ OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType c } break; case DbcsAttribute::Trailing: - // Handling the trailing half of wide chars ensures that we correctly restore - // wide characters when a user backs up and restores the viewport via CHAR_INFOs. if (fillingFirstColumn) { // The wide char doesn't fit. Pad with whitespace. // Ignore the character. There's no correct alternative way to handle this situation. ClearCell(currentIndex); } - else + else if (it.Position() == 0) { + // A common way to back up and restore the buffer is via `ReadConsoleOutputW` and + // `WriteConsoleOutputW` respectively. But the area might bisect/intersect/clip wide characters and + // only backup either their leading or trailing half. In general, in the rest of conhost, we're + // throwing away the trailing half of all `CHAR_INFO`s (during text rendering, as well as during + // `ReadConsoleOutputW`), so to make this code behave the same and prevent surprises, we need to + // make sure to only look at the trailer if it's the first `CHAR_INFO` the user is trying to write. ReplaceCharacters(currentIndex - 1, 2, chars); } ++it; @@ -371,90 +412,245 @@ void ROW::ReplaceAttributes(const til::CoordType beginIndex, const til::CoordTyp _attr.replace(_clampedColumnInclusive(beginIndex), _clampedColumnInclusive(endIndex), newAttr); } +[[msvc::forceinline]] ROW::WriteHelper::WriteHelper(ROW& row, til::CoordType columnBegin, til::CoordType columnLimit, const std::wstring_view& chars) noexcept : + row{ row }, + chars{ chars } +{ + colBeg = row._clampedColumnInclusive(columnBegin); + colLimit = row._clampedColumnInclusive(columnLimit); + chBegDirty = row._uncheckedCharOffset(colBeg); + colBegDirty = row._adjustBackward(colBeg); + leadingSpaces = colBeg - colBegDirty; + chBeg = chBegDirty + leadingSpaces; + colEnd = colBeg; + colEndDirty = 0; + charsConsumed = 0; +} + +[[msvc::forceinline]] bool ROW::WriteHelper::IsValid() const noexcept +{ + return colBeg < colLimit && !chars.empty(); +} + void ROW::ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars) +try { - const auto colBeg = _clampedUint16(columnBegin); - const auto colEnd = _clampedUint16(columnBegin + width); + WriteHelper h{ *this, columnBegin, _columnCount, chars }; + if (!h.IsValid()) + { + return; + } + h.ReplaceCharacters(width); + h.Finish(); +} +catch (...) +{ + // Due to this function writing _charOffsets first, then calling _resizeChars (which may throw) and only then finally + // filling in _chars, we might end up in a situation were _charOffsets contains offsets outside of the _chars array. + // --> Restore this row to a known "okay"-state. + Reset(TextAttribute{}); + throw; +} - if (colBeg >= colEnd || colEnd > _columnCount || chars.empty()) +[[msvc::forceinline]] void ROW::WriteHelper::ReplaceCharacters(til::CoordType width) noexcept +{ + const auto colEndNew = gsl::narrow_cast(colEnd + width); + if (colEndNew > colLimit) + { + colEndDirty = colLimit; + } + else { + til::at(row._charOffsets, colEnd++) = chBeg; + for (; colEnd < colEndNew; ++colEnd) + { + til::at(row._charOffsets, colEnd) = gsl::narrow_cast(chBeg | CharOffsetsTrailer); + } + + colEndDirty = colEnd; + charsConsumed = chars.size(); + } +} + +void ROW::ReplaceText(RowWriteState& state) +try +{ + WriteHelper h{ *this, state.columnBegin, state.columnLimit, state.text }; + if (!h.IsValid()) + { + state.columnEnd = h.colBeg; + state.columnBeginDirty = h.colBeg; + state.columnEndDirty = h.colBeg; return; } + h.ReplaceText(); + h.Finish(); - // Safety: - // * colBeg is now [0, _columnCount) - // * colEnd is now (colBeg, _columnCount] + state.text = state.text.substr(h.charsConsumed); + // Here's why we set `state.columnEnd` to `colLimit` if there's remaining text: + // Callers should be able to use `state.columnEnd` as the next cursor position, as well as the parameter for a + // follow-up call to ReplaceAttributes(). But if we fail to insert a wide glyph into the last column of a row, + // that last cell (which now contains padding whitespace) should get the same attributes as the rest of the + // string so that the row looks consistent. This requires us to return `colLimit` instead of `colLimit - 1`. + // Additionally, this has the benefit that callers can detect line wrapping by checking `columnEnd >= columnLimit`. + state.columnEnd = state.text.empty() ? h.colEnd : h.colLimit; + state.columnBeginDirty = h.colBegDirty; + state.columnEndDirty = h.colEndDirty; +} +catch (...) +{ + Reset(TextAttribute{}); + throw; +} - // Algorithm explanation - // - // Task: - // Replace the characters in cells [colBeg, colEnd) with a single `width`-wide glyph consisting of `chars`. - // - // Problem: - // Imagine that we have the following ROW contents: - // "xxyyzz" - // xx, yy, zz are 2 cell wide glyphs. We want to insert a 2 cell wide glyph ww at colBeg 1: - // ^^ - // ww - // An incorrect result would be: - // "xwwyzz" - // The half cut off x and y glyph wouldn't make much sense, so we need to fill them with whitespace: - // " ww zz" - // - // Solution: - // Given the range we want to replace [colBeg, colEnd), we "extend" it to encompass leading (preceding) - // and trailing wide glyphs we partially overwrite resulting in the range [colExtBeg, colExtEnd), where - // colExtBeg <= colBeg and colExtEnd >= colEnd. In other words, the to be replaced range has been "extended". - // The amount of leading whitespace we need to insert is thus colBeg - colExtBeg - // and the amount of trailing whitespace colExtEnd - colEnd. +[[msvc::forceinline]] void ROW::WriteHelper::ReplaceText() noexcept +{ + size_t ch = chBeg; - // Extend range downwards (leading whitespace) - uint16_t colExtBeg = colBeg; - // Safety: colExtBeg is [0, _columnCount], because colBeg is. - const uint16_t chExtBeg = _uncheckedCharOffset(colExtBeg); - // Safety: colExtBeg remains [0, _columnCount] due to colExtBeg != 0. - for (; colExtBeg != 0 && _uncheckedIsTrailer(colExtBeg); --colExtBeg) + for (const auto& s : til::utf16_iterator{ chars }) { + const auto wide = til::at(s, 0) < 0x80 ? false : IsGlyphFullWidth(s); + const auto colEndNew = gsl::narrow_cast(colEnd + 1u + wide); + if (colEndNew > colLimit) + { + colEndDirty = colLimit; + break; + } + + til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(ch); + if (wide) + { + til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(ch | CharOffsetsTrailer); + } + + colEndDirty = colEnd; + ch += s.size(); } - // Extend range upwards (trailing whitespace) - uint16_t colExtEnd = colEnd; - // Safety: colExtEnd cannot be incremented past _columnCount, because the last - // _charOffset at index _columnCount will never get the CharOffsetsTrailer flag. - for (; _uncheckedIsTrailer(colExtEnd); ++colExtEnd) + charsConsumed = ch - chBeg; +} + +til::CoordType ROW::CopyRangeFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit) +try +{ + const auto otherColBeg = other._clampedColumnInclusive(otherBegin); + const auto otherColLimit = other._clampedColumnInclusive(otherLimit); + std::span charOffsets; + std::wstring_view chars; + + if (otherColBeg < otherColLimit) + { + charOffsets = other._charOffsets.subspan(otherColBeg, static_cast(otherColLimit) - otherColBeg + 1); + const auto charsOffset = charOffsets.front() & CharOffsetsMask; + // We _are_ using span. But C++ decided that string_view and span aren't convertible. + // _chars is a std::span for performance and because it refers to raw, shared memory. +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). + chars = { other._chars.data() + charsOffset, other._chars.size() - charsOffset }; + } + + WriteHelper h{ *this, columnBegin, columnLimit, chars }; + if (!h.IsValid()) + { + return h.colBeg; + } + // Any valid charOffsets array is at least 2 elements long (the 1st element is the start offset and the 2nd + // element is the length of the first glyph) and begins/ends with a non-trailer offset. We don't really + // need to test for the end offset, since `WriteHelper::WriteWithOffsets` already takes care of that. + if (charOffsets.size() < 2 || WI_IsFlagSet(charOffsets.front(), CharOffsetsTrailer)) { + assert(false); + otherBegin = other.size(); + return h.colBeg; } - // Safety: After the previous loop colExtEnd is [0, _columnCount]. - const uint16_t chExtEnd = _uncheckedCharOffset(colExtEnd); + h.CopyRangeFrom(charOffsets); + h.Finish(); + + otherBegin += h.colEnd - h.colBeg; + return h.colEndDirty; +} +catch (...) +{ + Reset(TextAttribute{}); + throw; +} - const uint16_t leadingSpaces = colBeg - colExtBeg; - const uint16_t trailingSpaces = colExtEnd - colEnd; - const size_t chExtEndNew = chars.size() + leadingSpaces + trailingSpaces + chExtBeg; +[[msvc::forceinline]] void ROW::WriteHelper::CopyRangeFrom(const std::span& charOffsets) noexcept +{ + // Since our `charOffsets` input is already in columns (just like the `ROW::_charOffsets`), + // we can directly look up the end char-offset, but... + const auto colEndDirtyInput = std::min(gsl::narrow_cast(colLimit - colBeg), gsl::narrow(charOffsets.size() - 1)); - if (chExtEndNew != chExtEnd) + // ...since the colLimit might intersect with a wide glyph in `charOffset`, we need to adjust our input-colEnd. + auto colEndInput = colEndDirtyInput; + for (; WI_IsFlagSet(til::at(charOffsets, colEndInput), CharOffsetsTrailer); --colEndInput) { - _resizeChars(colExtEnd, chExtBeg, chExtEnd, chExtEndNew); } - // Add leading/trailing whitespace and copy chars + const auto baseOffset = til::at(charOffsets, 0); + const auto endOffset = til::at(charOffsets, colEndInput); + const auto inToOutOffset = gsl::narrow_cast(chBeg - baseOffset); + + // Now with the `colEndInput` figured out, we can easily copy the `charOffsets` into the `_charOffsets`. + // It's possible to use SIMD for this loop for extra perf gains. Something like this for SSE2 (~8x faster): + // const auto in = _mm_loadu_si128(...); + // const auto off = _mm_and_epi32(in, _mm_set1_epi16(CharOffsetsMask)); + // const auto trailer = _mm_and_epi32(in, _mm_set1_epi16(CharOffsetsTrailer)); + // const auto out = _mm_or_epi32(_mm_add_epi16(off, _mm_set1_epi16(inToOutOffset)), trailer); + // _mm_store_si128(..., out); + for (uint16_t i = 0; i < colEndInput; ++i, ++colEnd) { - auto it = _chars.begin() + chExtBeg; - it = fill_n_small(it, leadingSpaces, L' '); - it = copy_n_small(chars.begin(), chars.size(), it); - it = fill_n_small(it, trailingSpaces, L' '); + const auto ch = til::at(charOffsets, i); + const auto off = ch & CharOffsetsMask; + const auto trailer = ch & CharOffsetsTrailer; + til::at(row._charOffsets, colEnd) = gsl::narrow_cast((off + inToOutOffset) | trailer); } - // Update char offsets with leading/trailing whitespace and the chars columns. + + colEndDirty = gsl::narrow_cast(colBeg + colEndDirtyInput); + charsConsumed = endOffset - baseOffset; +} + +[[msvc::forceinline]] void ROW::WriteHelper::Finish() +{ + colEndDirty = row._adjustForward(colEndDirty); + + const uint16_t trailingSpaces = colEndDirty - colEnd; + const auto chEndDirtyOld = row._uncheckedCharOffset(colEndDirty); + const auto chEndDirty = chBegDirty + charsConsumed + leadingSpaces + trailingSpaces; + + if (chEndDirty != chEndDirtyOld) { - auto chPos = chExtBeg; - auto it = _charOffsets.begin() + colExtBeg; + row._resizeChars(colEndDirty, chBegDirty, chEndDirty, chEndDirtyOld); + } - it = iota_n_mut(it, leadingSpaces, chPos); + { + // std::copy_n compiles to memmove. We can do better. It also gets rid of an extra branch, + // because std::copy_n avoids calling memmove if the count is 0. It's never 0 for us. + const auto itBeg = row._chars.begin() + chBeg; + memcpy(&*itBeg, chars.data(), charsConsumed * sizeof(wchar_t)); - *it++ = chPos; - it = fill_small(it, _charOffsets.begin() + colEnd, gsl::narrow_cast(chPos | CharOffsetsTrailer)); - chPos = gsl::narrow_cast(chPos + chars.size()); + if (leadingSpaces) + { + fill_n_small(row._chars.begin() + chBegDirty, leadingSpaces, L' '); + iota_n(row._charOffsets.begin() + colBegDirty, leadingSpaces, chBegDirty); + } + if (trailingSpaces) + { + fill_n_small(itBeg + charsConsumed, trailingSpaces, L' '); + iota_n(row._charOffsets.begin() + colEnd, trailingSpaces, gsl::narrow_cast(chBeg + charsConsumed)); + } + } - it = iota_n_mut(it, trailingSpaces, chPos); + // This updates `_doubleBytePadded` whenever we write the last column in the row. `_doubleBytePadded` tells our text + // reflow algorithm whether it should ignore the last column. This is important when writing wide characters into + // the terminal: If the last wide character in a row only fits partially, we should render whitespace, but + // during text reflow pretend as if no whitespace exists. After all, the user didn't write any whitespace there. + // + // The way this is written, it'll set `_doubleBytePadded` to `true` no matter whether a wide character didn't fit, + // or if the last 2 columns contain a wide character and a narrow character got written into the left half of it. + // In both cases `trailingSpaces` is 1 and fills the last column and `_doubleBytePadded` will be `true`. + if (colEndDirty == row._columnCount) + { + row.SetDoubleBytePadded(colEnd < row._columnCount); } } @@ -462,15 +658,15 @@ void ROW::ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, co // as it reallocates the backing buffer and shifts the char offsets. // The parameters are difficult to explain, but their names are identical to // local variables in ReplaceCharacters() which I've attempted to document there. -void ROW::_resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEnd, size_t chExtEndNew) +void ROW::_resizeChars(uint16_t colEndDirty, uint16_t chBegDirty, size_t chEndDirty, uint16_t chEndDirtyOld) { - const auto diff = chExtEndNew - chExtEnd; + const auto diff = chEndDirty - chEndDirtyOld; const auto currentLength = _charSize(); const auto newLength = currentLength + diff; if (newLength <= _chars.size()) { - std::copy_n(_chars.begin() + chExtEnd, currentLength - chExtEnd, _chars.begin() + chExtEndNew); + std::copy_n(_chars.begin() + chEndDirtyOld, currentLength - chEndDirtyOld, _chars.begin() + chEndDirty); } else { @@ -480,14 +676,14 @@ void ROW::_resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEnd, auto charsHeap = std::make_unique_for_overwrite(newCapacity); const std::span chars{ charsHeap.get(), newCapacity }; - std::copy_n(_chars.begin(), chExtBeg, chars.begin()); - std::copy_n(_chars.begin() + chExtEnd, currentLength - chExtEnd, chars.begin() + chExtEndNew); + std::copy_n(_chars.begin(), chBegDirty, chars.begin()); + std::copy_n(_chars.begin() + chEndDirtyOld, currentLength - chEndDirtyOld, chars.begin() + chEndDirty); _charsHeap = std::move(charsHeap); _chars = chars; } - auto it = _charOffsets.begin() + colExtEnd; + auto it = _charOffsets.begin() + colEndDirty; const auto end = _charOffsets.end(); for (; it != end; ++it) { @@ -495,6 +691,11 @@ void ROW::_resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEnd, } } +til::small_rle& ROW::Attributes() noexcept +{ + return _attr; +} + const til::small_rle& ROW::Attributes() const noexcept { return _attr; @@ -523,6 +724,12 @@ uint16_t ROW::size() const noexcept return _columnCount; } +til::CoordType ROW::LineRenditionColumns() const noexcept +{ + const auto scale = _lineRendition != LineRendition::SingleWidth ? 1 : 0; + return _columnCount >> scale; +} + til::CoordType ROW::MeasureLeft() const noexcept { const auto text = GetText(); @@ -677,11 +884,13 @@ uint16_t ROW::_charSize() const noexcept // Safety: col must be [0, _columnCount]. uint16_t ROW::_uncheckedCharOffset(size_t col) const noexcept { + assert(col < _charOffsets.size()); return til::at(_charOffsets, col) & CharOffsetsMask; } // Safety: col must be [0, _columnCount]. bool ROW::_uncheckedIsTrailer(size_t col) const noexcept { + assert(col < _charOffsets.size()); return WI_IsFlagSet(til::at(_charOffsets, col), CharOffsetsTrailer); } diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index 7393bf67097..1e14c382fc5 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -20,8 +20,6 @@ Revision History: #pragma once -#include - #include #include "LineRendition.hpp" @@ -37,6 +35,28 @@ enum class DelimiterClass RegularChar }; +struct RowWriteState +{ + // The text you want to write into the given ROW. When ReplaceText() returns, + // this is updated to remove all text from the beginning that was successfully written. + std::wstring_view text; // IN/OUT + // The column at which to start writing. + til::CoordType columnBegin = 0; // IN + // The first column which should not be written to anymore. + til::CoordType columnLimit = 0; // IN + + // The column 1 past the last glyph that was successfully written into the row. If you need to call + // ReplaceAttributes() to colorize the written range, etc., this is the columnEnd parameter you want. + // If you want to continue writing where you left off, this is also the next columnBegin parameter. + til::CoordType columnEnd = 0; // OUT + // The first column that got modified by this write operation. In case that the first glyph we write overwrites + // the trailing half of a wide glyph, leadingSpaces will be 1 and this value will be 1 less than colBeg. + til::CoordType columnBeginDirty = 0; // OUT + // This is 1 past the last column that was modified and will be 1 past columnEnd if we overwrote + // the leading half of a wide glyph and had to fill the trailing half with whitespace. + til::CoordType columnEndDirty = 0; // OUT +}; + class ROW final { public: @@ -62,16 +82,23 @@ class ROW final void Resize(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute); void TransferAttributes(const til::small_rle& attr, til::CoordType newWidth); + til::CoordType NavigateToPrevious(til::CoordType column) const noexcept; + til::CoordType NavigateToNext(til::CoordType column) const noexcept; + void ClearCell(til::CoordType column); OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional wrap = std::nullopt, std::optional limitRight = std::nullopt); bool SetAttrToEnd(til::CoordType columnBegin, TextAttribute attr); void ReplaceAttributes(til::CoordType beginIndex, til::CoordType endIndex, const TextAttribute& newAttr); void ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars); + void ReplaceText(RowWriteState& state); + til::CoordType CopyRangeFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit); + til::small_rle& Attributes() noexcept; const til::small_rle& Attributes() const noexcept; TextAttribute GetAttrByColumn(til::CoordType column) const; std::vector GetHyperlinks() const; uint16_t size() const noexcept; + til::CoordType LineRenditionColumns() const noexcept; til::CoordType MeasureLeft() const noexcept; til::CoordType MeasureRight() const noexcept; bool ContainsText() const noexcept; @@ -89,6 +116,50 @@ class ROW final #endif private: + // WriteHelper exists because other forms of abstracting this functionality away (like templates with lambdas) + // where only very poorly optimized by MSVC as it failed to inline the templates. + struct WriteHelper + { + explicit WriteHelper(ROW& row, til::CoordType columnBegin, til::CoordType columnLimit, const std::wstring_view& chars) noexcept; + bool IsValid() const noexcept; + void ReplaceCharacters(til::CoordType width) noexcept; + void ReplaceText() noexcept; + void CopyRangeFrom(const std::span& charOffsets) noexcept; + void Finish(); + + // Parent pointer. + ROW& row; + // The text given by the caller. + const std::wstring_view& chars; + + // This is the same as the columnBegin parameter for ReplaceText(), etc., + // but clamped to a valid range via _clampedColumnInclusive. + uint16_t colBeg; + // This is the same as the columnLimit parameter for ReplaceText(), etc., + // but clamped to a valid range via _clampedColumnInclusive. + uint16_t colLimit; + + // The column 1 past the last glyph that was successfully written into the row. If you need to call + // ReplaceAttributes() to colorize the written range, etc., this is the columnEnd parameter you want. + // If you want to continue writing where you left off, this is also the next columnBegin parameter. + uint16_t colEnd; + // The first column that got modified by this write operation. In case that the first glyph we write overwrites + // the trailing half of a wide glyph, leadingSpaces will be 1 and this value will be 1 less than colBeg. + uint16_t colBegDirty; + // Similar to dirtyBeg, this is 1 past the last column that was modified and will be 1 past colEnd if + // we overwrote the leading half of a wide glyph and had to fill the trailing half with whitespace. + uint16_t colEndDirty; + // The offset in ROW::chars at which we start writing the contents of WriteHelper::chars. + uint16_t chBeg; + // The offset at which we start writing leadingSpaces-many whitespaces. + uint16_t chBegDirty; + // The same as `colBeg - colBegDirty`. This is the amount of whitespace + // we write at chBegDirty, before the actual WriteHelper::chars content. + uint16_t leadingSpaces; + // The amount of characters copied from WriteHelper::chars. + size_t charsConsumed; + }; + // To simplify the detection of wide glyphs, we don't just store the simple character offset as described // for _charOffsets. Instead we use the most significant bit to indicate whether any column is the // trailing half of a wide glyph. This simplifies many implementation details via _uncheckedIsTrailer. @@ -102,13 +173,16 @@ class ROW final template constexpr uint16_t _clampedColumnInclusive(T v) const noexcept; + uint16_t _adjustBackward(uint16_t column) const noexcept; + uint16_t _adjustForward(uint16_t column) const noexcept; + wchar_t _uncheckedChar(size_t off) const noexcept; uint16_t _charSize() const noexcept; uint16_t _uncheckedCharOffset(size_t col) const noexcept; bool _uncheckedIsTrailer(size_t col) const noexcept; void _init() noexcept; - void _resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEnd, size_t chExtEndNew); + void _resizeChars(uint16_t colEndDirty, uint16_t chBegDirty, size_t chEndDirty, uint16_t chEndDirtyOld); // These fields are a bit "wasteful", but it makes all this a bit more robust against // programming errors during initial development (which is when this comment was written). diff --git a/src/buffer/out/cursor.cpp b/src/buffer/out/cursor.cpp index 485f2dbc829..9084a60003d 100644 --- a/src/buffer/out/cursor.cpp +++ b/src/buffer/out/cursor.cpp @@ -286,9 +286,9 @@ void Cursor::CopyProperties(const Cursor& OtherCursor) noexcept _cursorType = OtherCursor._cursorType; } -void Cursor::DelayEOLWrap(const til::point coordDelayedAt) noexcept +void Cursor::DelayEOLWrap() noexcept { - _coordDelayedAt = coordDelayedAt; + _coordDelayedAt = _cPosition; _fDelayedEolWrap = true; } diff --git a/src/buffer/out/cursor.h b/src/buffer/out/cursor.h index bbd908eb3e7..3c377e61b2a 100644 --- a/src/buffer/out/cursor.h +++ b/src/buffer/out/cursor.h @@ -76,7 +76,7 @@ class Cursor final void CopyProperties(const Cursor& OtherCursor) noexcept; - void DelayEOLWrap(const til::point coordDelayedAt) noexcept; + void DelayEOLWrap() noexcept; void ResetDelayEOLWrap() noexcept; til::point GetDelayedAtPosition() const noexcept; bool IsDelayedEOLWrap() const noexcept; diff --git a/src/buffer/out/precomp.h b/src/buffer/out/precomp.h index 3ae7439f781..2ecc2c00d19 100644 --- a/src/buffer/out/precomp.h +++ b/src/buffer/out/precomp.h @@ -1,41 +1,7 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- precomp.h - -Abstract: -- Contains external headers to include in the precompile phase of console build process. -- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). ---*/ - -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once -// clang-format off - -// This includes support libraries from the CRT, STL, WIL, and GSL -#include "LibraryIncludes.h" - -#pragma warning(push) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers -#define NOMCX -#define NOHELP -#define NOCOMM -#endif - -// Windows Header Files: -#include -#include - -// private dependencies -#include "../inc/unicode.hpp" -#pragma warning(pop) +#include -// clang-format on +#include diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 2bbc2e20721..8b8f9d1f79a 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -376,6 +376,32 @@ bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute return fSuccess; } +void TextBuffer::ConsumeGrapheme(std::wstring_view& chars) noexcept +{ + // This function is supposed to mirror the behavior of ROW::Write, when it reads characters off of `chars`. + // (I know that a UTF-16 code point is not a grapheme, but that's what we're working towards.) + chars = til::utf16_pop(chars); +} + +// This function is intended for writing regular "lines" of text and only the `state.text` and`state.columnBegin` +// fields are being used, whereas `state.columnLimit` is automatically overwritten by the line width of the given row. +// This allows this function to automatically set the wrap-forced field of the row, which is also the return value. +// The return value indicates to the caller whether the cursor should be moved to the next line. +void TextBuffer::WriteLine(til::CoordType row, bool wrapAtEOL, const TextAttribute& attributes, RowWriteState& state) +{ + auto& r = GetRowByOffset(row); + + r.ReplaceText(state); + r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes); + + if (state.columnEnd >= state.columnLimit) + { + r.SetWrapForced(wrapAtEOL); + } + + TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 })); +} + // Routine Description: // - Writes cells to the output buffer. Writes at the cursor. // Arguments: diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index c7e57da058b..fe27d4ef007 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -89,6 +89,9 @@ class TextBuffer final TextBufferTextIterator GetTextDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const; // Text insertion functions + static void ConsumeGrapheme(std::wstring_view& chars) noexcept; + void WriteLine(til::CoordType row, bool wrapAtEOL, const TextAttribute& attributes, RowWriteState& state); + OutputCellIterator Write(const OutputCellIterator givenIt); OutputCellIterator Write(const OutputCellIterator givenIt, diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index 59e6667daf7..4a0735bb206 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -10,9 +10,11 @@ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" + xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" + xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" - IgnorableNamespaces="uap mp rescap uap3"> + IgnorableNamespaces="uap mp rescap uap3 desktop6 virtualization"> ms-resource:AppStoreNameDev A Lone Developer Images\StoreLogo.png + + disabled + + + + HKEY_CURRENT_USER\Console\%%Startup + + @@ -136,5 +146,6 @@ + diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index 03d484c67d1..98fb12b9455 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -12,8 +12,10 @@ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" + xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" - IgnorableNamespaces="uap mp rescap uap3"> + xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10" + IgnorableNamespaces="uap mp rescap uap3 desktop6 virtualization"> ms-resource:AppStoreNamePre Microsoft Corporation Images\StoreLogo.png + + disabled + + + + HKEY_CURRENT_USER\Console\%%Startup + + @@ -225,6 +235,7 @@ + diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index 742e74c9d22..c123778d1e9 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -12,8 +12,10 @@ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" + xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" - IgnorableNamespaces="uap mp rescap uap3"> + xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10" + IgnorableNamespaces="uap mp rescap uap3 desktop6 virtualization"> ms-resource:AppStoreName Microsoft Corporation Images\StoreLogo.png + + disabled + + + + HKEY_CURRENT_USER\Console\%%Startup + + @@ -225,6 +235,7 @@ + diff --git a/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h index 4e9650c8831..e1a235239b0 100644 --- a/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h +++ b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h @@ -20,7 +20,7 @@ class JsonTestClass public: static Json::Value VerifyParseSucceeded(const std::string_view& content) { - static const std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; + static const std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; Json::Value root; std::string errs; @@ -31,7 +31,7 @@ class JsonTestClass static std::string toString(const Json::Value& json) { - static const std::unique_ptr writer{ Json::StreamWriterBuilder::StreamWriterBuilder().newStreamWriter() }; + static const std::unique_ptr writer{ Json::StreamWriterBuilder{}.newStreamWriter() }; std::stringstream s; writer->write(json, &s); diff --git a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj index 771e5e33d52..dbd279005be 100644 --- a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj @@ -20,6 +20,7 @@ DynamicLibrary true + true @@ -73,8 +74,6 @@ ..;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\jsoncpp\json;$(OpenConsoleDir)src\inc;$(OpenConsoleDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettingsModel\Generated Files";%(AdditionalIncludeDirectories) pch.h - - %(AdditionalOptions) /Zc:twoPhase- 4702;%(DisableSpecificWarnings) @@ -99,15 +98,4 @@ - - - - x86 - $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.$(TerminalMUXVersion)\runtimes\win10-$(Native-Platform)\native\" - - - - - diff --git a/src/cascadia/LocalTests_SettingsModel/pch.h b/src/cascadia/LocalTests_SettingsModel/pch.h index b2e15b5a7b1..96727252489 100644 --- a/src/cascadia/LocalTests_SettingsModel/pch.h +++ b/src/cascadia/LocalTests_SettingsModel/pch.h @@ -18,7 +18,7 @@ Author(s): // Manually include til after we include Windows.Foundation to give it winrt superpowers #define BLOCK_TIL // This includes support libraries from the CRT, STL, WIL, and GSL -#include "LibraryIncludes.h" +#include // This is inexplicable, but for whatever reason, cppwinrt conflicts with the // SDK definition of this function, so the only fix is to undef it. // from WinBase.h @@ -28,8 +28,9 @@ Author(s): #endif #include -#include +#include #include +#include #include #include diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index a3dcbb45f4e..a940cc915f3 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -98,6 +98,26 @@ namespace TerminalAppLocalTests } } } + void _logCommands(winrt::Windows::Foundation::Collections::IVector commands, const int indentation = 1) + { + if (indentation == 1) + { + Log::Comment((commands.Size() == 0) ? L"Commands:\n " : L"Commands:"); + } + for (const auto& cmd : commands) + { + Log::Comment(fmt::format(L"{0:>{1}}* {2}", + L"", + indentation, + cmd.Name()) + .c_str()); + + if (cmd.HasNestedCommands()) + { + _logCommandNames(cmd.NestedCommands(), indentation + 2); + } + } + } }; void SettingsTests::TestIterateCommands() @@ -164,14 +184,15 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"iterable command profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command profile0", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -189,7 +210,8 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile1"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command profile1", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -207,7 +229,8 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command profile2", command.Name()); VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -287,14 +310,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile0", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -312,7 +337,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile1"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile1", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -330,7 +357,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"Split pane, profile: profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"Split pane, profile: profile2", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -412,14 +441,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${profile.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); { - auto command = expandedCommands.Lookup(L"iterable command profile0"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command profile0", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -437,7 +468,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile1\""); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command profile1\"", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -455,7 +488,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command profile2"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command profile2", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -527,14 +562,15 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"Connect to ssh..."); + auto rootCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"Connect to ssh...", rootCommand.Name()); auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, rootActionAndArgs.Action()); @@ -621,14 +657,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto grandparentCommand = expandedCommands.Lookup(L"grandparent"); + auto grandparentCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(grandparentCommand); + VERIFY_ARE_EQUAL(L"grandparent", grandparentCommand.Name()); + auto grandparentActionAndArgs = grandparentCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(grandparentActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, grandparentActionAndArgs.Action()); @@ -744,17 +782,22 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); - for (auto name : std::vector({ L"profile0", L"profile1", L"profile2" })) + const std::vector profileNames{ L"profile0", L"profile1", L"profile2" }; + for (auto i = 0u; i < profileNames.size(); i++) { - winrt::hstring commandName{ name + L"..." }; - auto command = expandedCommands.Lookup(commandName); + const auto& name{ profileNames[i] }; + winrt::hstring commandName{ profileNames[i] + L"..." }; + + auto command = expandedCommands.GetAt(i); + VERIFY_ARE_EQUAL(commandName, command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -880,14 +923,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"New Tab With Profile..."); + auto rootCommand = expandedCommands.GetAt(0); VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"New Tab With Profile...", rootCommand.Name()); + auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::Invalid, rootActionAndArgs.Action()); @@ -982,13 +1027,16 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings.ActiveProfiles().Size()); - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(settings.ActionMap().NameMap(), settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(0u, settings.Warnings().Size()); VERIFY_ARE_EQUAL(1u, expandedCommands.Size()); - auto rootCommand = expandedCommands.Lookup(L"New Pane..."); + auto rootCommand = expandedCommands.GetAt(0); + VERIFY_IS_NOT_NULL(rootCommand); + VERIFY_ARE_EQUAL(L"New Pane...", rootCommand.Name()); + VERIFY_IS_NOT_NULL(rootCommand); auto rootActionAndArgs = rootCommand.ActionAndArgs(); VERIFY_IS_NOT_NULL(rootActionAndArgs); @@ -1205,8 +1253,8 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(L"${scheme.name}", realArgs.TerminalArgs().Profile()); } - auto expandedCommands = winrt::TerminalApp::implementation::TerminalPage::_ExpandCommands(nameMap, settings.ActiveProfiles().GetView(), settings.GlobalSettings().ColorSchemes()); - _logCommandNames(expandedCommands.GetView()); + const auto& expandedCommands{ settings.GlobalSettings().ActionMap().ExpandedCommands() }; + _logCommands(expandedCommands); VERIFY_ARE_EQUAL(3u, expandedCommands.Size()); @@ -1215,7 +1263,9 @@ namespace TerminalAppLocalTests // just easy tests to write. { - auto command = expandedCommands.Lookup(L"iterable command Campbell"); + auto command = expandedCommands.GetAt(0); + VERIFY_ARE_EQUAL(L"iterable command Campbell", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -1233,7 +1283,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command Campbell PowerShell"); + auto command = expandedCommands.GetAt(1); + VERIFY_ARE_EQUAL(L"iterable command Campbell PowerShell", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); @@ -1251,7 +1303,9 @@ namespace TerminalAppLocalTests } { - auto command = expandedCommands.Lookup(L"iterable command Vintage"); + auto command = expandedCommands.GetAt(2); + VERIFY_ARE_EQUAL(L"iterable command Vintage", command.Name()); + VERIFY_IS_NOT_NULL(command); auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 53013f93721..44f1a9c2db8 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -4,11 +4,13 @@ #include "pch.h" #include "../TerminalApp/TerminalPage.h" +#include "../TerminalApp/TerminalWindow.h" #include "../TerminalApp/MinMaxCloseControl.h" #include "../TerminalApp/TabRowControl.h" #include "../TerminalApp/ShortcutActionDispatch.h" #include "../TerminalApp/TerminalTab.h" #include "../TerminalApp/CommandPalette.h" +#include "../TerminalApp/ContentManager.h" #include "CppWinrtTailored.h" using namespace Microsoft::Console; @@ -110,6 +112,8 @@ namespace TerminalAppLocalTests void _initializeTerminalPage(winrt::com_ptr& page, CascadiaSettings initialSettings); winrt::com_ptr _commonSetup(); + winrt::com_ptr _windowProperties; + winrt::com_ptr _contentManager; }; template @@ -194,8 +198,14 @@ namespace TerminalAppLocalTests { winrt::com_ptr page{ nullptr }; - auto result = RunOnUIThread([&page]() { - page = winrt::make_self(); + _windowProperties = winrt::make_self(); + winrt::TerminalApp::WindowProperties props = *_windowProperties; + + _contentManager = winrt::make_self(); + winrt::TerminalApp::ContentManager contentManager = *_contentManager; + + auto result = RunOnUIThread([&page, props, contentManager]() { + page = winrt::make_self(props, contentManager); VERIFY_IS_NOT_NULL(page); }); VERIFY_SUCCEEDED(result); @@ -239,9 +249,13 @@ namespace TerminalAppLocalTests // it's weird. winrt::TerminalApp::TerminalPage projectedPage{ nullptr }; + _windowProperties = winrt::make_self(); + winrt::TerminalApp::WindowProperties props = *_windowProperties; + _contentManager = winrt::make_self(); + winrt::TerminalApp::ContentManager contentManager = *_contentManager; Log::Comment(NoThrowString().Format(L"Construct the TerminalPage")); - auto result = RunOnUIThread([&projectedPage, &page, initialSettings]() { - projectedPage = winrt::TerminalApp::TerminalPage(); + auto result = RunOnUIThread([&projectedPage, &page, initialSettings, props, contentManager]() { + projectedPage = winrt::TerminalApp::TerminalPage(props, contentManager); page.copy_from(winrt::get_self(projectedPage)); page->_settings = initialSettings; }); @@ -1242,14 +1256,16 @@ namespace TerminalAppLocalTests END_TEST_METHOD_PROPERTIES() auto page = _commonSetup(); - page->RenameWindowRequested([&page](auto&&, const winrt::TerminalApp::RenameWindowRequestedArgs args) { + page->RenameWindowRequested([&page, this](auto&&, const winrt::TerminalApp::RenameWindowRequestedArgs args) { // In the real terminal, this would bounce up to the monarch and // come back down. Instead, immediately call back and set the name. - page->WindowName(args.ProposedName()); + // + // This replicates how TerminalWindow works + _windowProperties->WindowName(args.ProposedName()); }); auto windowNameChanged = false; - page->PropertyChanged([&page, &windowNameChanged](auto&&, const winrt::WUX::Data::PropertyChangedEventArgs& args) mutable { + _windowProperties->PropertyChanged([&page, &windowNameChanged](auto&&, const winrt::WUX::Data::PropertyChangedEventArgs& args) mutable { if (args.PropertyName() == L"WindowNameForDisplay") { windowNameChanged = true; @@ -1260,7 +1276,7 @@ namespace TerminalAppLocalTests page->_RequestWindowRename(winrt::hstring{ L"Foo" }); }); TestOnUIThread([&]() { - VERIFY_ARE_EQUAL(L"Foo", page->_WindowName); + VERIFY_ARE_EQUAL(L"Foo", page->WindowProperties().WindowName()); VERIFY_IS_TRUE(windowNameChanged, L"The window name should have changed, and we should have raised a notification that WindowNameForDisplay changed"); }); diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj index f60d2a4f679..e5a00d2d892 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj @@ -23,6 +23,7 @@ + true @@ -85,15 +86,4 @@ - - - - x86 - $(Platform) - <_MUXBinRoot>"$(OpenConsoleDir)packages\Microsoft.UI.Xaml.$(TerminalMUXVersion)\runtimes\win10-$(Native-Platform)\native\" - - - - - diff --git a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj index d51a96986af..9e2029c9bcc 100644 --- a/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TestHostApp/TestHostApp.vcxproj @@ -16,6 +16,8 @@ true false + true + @@ -231,6 +240,12 @@ AppLogic.idl + + TerminalPage.idl + + + TerminalWindow.idl + @@ -252,6 +267,7 @@ + MinMaxCloseControl.xaml Code @@ -300,7 +316,6 @@ - diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 94e9865e63a..9b0abe8904a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -5,7 +5,10 @@ #include "pch.h" #include "TerminalPage.h" #include "TerminalPage.g.cpp" +#include "LastTabClosedEventArgs.g.cpp" #include "RenameWindowRequestedArgs.g.cpp" +#include "RequestMoveContentArgs.g.cpp" +#include "RequestReceiveContentArgs.g.cpp" #include @@ -30,10 +33,12 @@ using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::System; using namespace winrt::Windows::System; +using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::UI::Text; using namespace winrt::Windows::UI::Xaml::Controls; using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Media; using namespace ::TerminalApp; using namespace ::Microsoft::Console; using namespace ::Microsoft::Terminal::Core; @@ -51,13 +56,17 @@ namespace winrt namespace winrt::TerminalApp::implementation { - TerminalPage::TerminalPage() : + TerminalPage::TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager) : _tabs{ winrt::single_threaded_observable_vector() }, _mruTabs{ winrt::single_threaded_observable_vector() }, _startupActions{ winrt::single_threaded_vector() }, - _hostingHwnd{} + _manager{ manager }, + _hostingHwnd{}, + _WindowProperties{ std::move(properties) } { InitializeComponent(); + + _WindowProperties.PropertyChanged({ get_weak(), &TerminalPage::_windowPropertyChanged }); } // Method Description: @@ -97,38 +106,11 @@ namespace winrt::TerminalApp::implementation return S_OK; } - // Function Description: - // - Recursively check our commands to see if there's a keybinding for - // exactly their action. If there is, label that command with the text - // corresponding to that key chord. - // - Will recurse into nested commands as well. - // Arguments: - // - settings: The settings who's keybindings we should use to look up the key chords from - // - commands: The list of commands to label. - static void _recursiveUpdateCommandKeybindingLabels(CascadiaSettings settings, - IMapView commands) - { - for (const auto& nameAndCmd : commands) - { - const auto& command = nameAndCmd.Value(); - if (command.HasNestedCommands()) - { - _recursiveUpdateCommandKeybindingLabels(settings, command.NestedCommands()); - } - else - { - // If there's a keybinding that's bound to exactly this command, - // then get the keychord and display it as a - // part of the command in the UI. - // We specifically need to do this for nested commands. - const auto keyChord{ settings.ActionMap().GetKeyBindingForAction(command.ActionAndArgs().Action(), command.ActionAndArgs().Args()) }; - command.RegisterKey(keyChord); - } - } - } - + // INVARIANT: This needs to be called on OUR UI thread! void TerminalPage::SetSettings(CascadiaSettings settings, bool needRefreshUI) { + assert(Dispatcher().HasThreadAccess()); + _settings = settings; // Make sure to _UpdateCommandsForPalette before @@ -149,27 +131,26 @@ namespace winrt::TerminalApp::implementation _systemRowsToScroll = _ReadSystemRowsToScroll(); } - bool TerminalPage::IsElevated() const noexcept + bool TerminalPage::IsRunningElevated() const noexcept { - // use C++11 magic statics to make sure we only do this once. - // This won't change over the lifetime of the application - - static const auto isElevated = []() { - // *** THIS IS A SINGLETON *** - auto result = 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 - try - { - result = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsElevated(); - } - CATCH_LOG(); - return result; - }(); - - return isElevated; + // GH#2455 - Make sure to try/catch calls to Application::Current, + // because that _won't_ be an instance of TerminalApp::App in the + // LocalTests + try + { + return Application::Current().as().Logic().IsRunningElevated(); + } + CATCH_LOG(); + return false; + } + bool TerminalPage::CanDragDrop() const noexcept + { + try + { + return Application::Current().as().Logic().CanDragDrop(); + } + CATCH_LOG(); + return true; } void TerminalPage::Create() @@ -182,11 +163,11 @@ namespace winrt::TerminalApp::implementation _tabView = _tabRow.TabView(); _rearranging = false; - const auto isElevated = IsElevated(); + const auto canDragDrop = CanDragDrop(); _tabRow.PointerMoved({ get_weak(), &TerminalPage::_RestorePointerCursorHandler }); - _tabView.CanReorderTabs(!isElevated); - _tabView.CanDragTabs(!isElevated); + _tabView.CanReorderTabs(canDragDrop); + _tabView.CanDragTabs(canDragDrop); _tabView.TabDragStarting({ get_weak(), &TerminalPage::_TabDragStarted }); _tabView.TabDragCompleted({ get_weak(), &TerminalPage::_TabDragCompleted }); @@ -249,9 +230,6 @@ namespace winrt::TerminalApp::implementation // Hookup our event handlers to the ShortcutActionDispatch _RegisterActionCallbacks(); - // Hook up inbound connection event handler - ConptyConnection::NewConnection({ this, &TerminalPage::_OnNewConnection }); - //Event Bindings (Early) _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { if (auto page{ weakThis.get() }) @@ -269,6 +247,11 @@ namespace winrt::TerminalApp::implementation _tabView.TabCloseRequested({ this, &TerminalPage::_OnTabCloseRequested }); _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged }); + _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting }); + _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver }); + _tabView.TabStripDrop({ this, &TerminalPage::_onTabStripDrop }); + _tabView.TabDroppedOutside({ this, &TerminalPage::_onTabDroppedOutside }); + _CreateNewTabFlyout(); _UpdateTabWidthMode(); @@ -308,7 +291,7 @@ namespace winrt::TerminalApp::implementation // Setup mouse vanish attributes SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &_shouldMouseVanish, false); - _tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield()); + _tabRow.ShowElevationShield(IsRunningElevated() && _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) @@ -321,18 +304,6 @@ namespace winrt::TerminalApp::implementation ShowSetAsDefaultInfoBar(); } - // Method Description; - // - Checks if the current terminal window should load or save its layout information. - // Arguments: - // - settings: The settings to use as this may be called before the page is - // fully initialized. - // Return Value: - // - true if the ApplicationState should be used. - bool TerminalPage::ShouldUsePersistedLayout(CascadiaSettings& settings) const - { - return settings.GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedWindowLayout; - } - // Method Description: // - This is a bit of trickiness: If we're running unelevated, and the user // passed in only --elevate actions, the we don't _actually_ want to @@ -347,7 +318,7 @@ namespace winrt::TerminalApp::implementation // GH#12267: Don't forget about defterm handoff here. If we're being // created for embedding, then _yea_, we don't need to handoff to an // elevated window. - if (!_startupActions || IsElevated() || _shouldStartInboundListener) + if (!_startupActions || IsRunningElevated() || _shouldStartInboundListener || _startupActions.Size() == 0) { // there aren't startup actions, or we're elevated. In that case, go for it. return false; @@ -444,32 +415,6 @@ namespace winrt::TerminalApp::implementation } } - // 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) { Windows::Foundation::Collections::IVectorView items; @@ -553,16 +498,6 @@ namespace winrt::TerminalApp::implementation { _startupState = StartupState::InStartup; - // 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)) - { - if (layout.TabLayout().Size() > 0) - { - _startupActions = layout.TabLayout(); - } - } - ProcessStartupActions(_startupActions, true); // If we were told that the COM server needs to be started to listen for incoming @@ -588,6 +523,9 @@ namespace winrt::TerminalApp::implementation { _shouldStartInboundListener = false; + // Hook up inbound connection event handler + _newConnectionRevoker = ConptyConnection::NewConnection(winrt::auto_revoke, { this, &TerminalPage::_OnNewConnection }); + try { winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::StartInboundListener(); @@ -704,7 +642,7 @@ namespace winrt::TerminalApp::implementation // have a tab yet, but will once we're initialized. if (_tabs.Size() == 0 && !(_shouldStartInboundListener || _isEmbeddingInboundListener)) { - _LastTabClosedHandlers(*this, nullptr); + _LastTabClosedHandlers(*this, winrt::make(false)); } else { @@ -1172,7 +1110,7 @@ namespace winrt::TerminalApp::implementation WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - const auto dispatchToElevatedWindow = ctrlPressed && !IsElevated(); + const auto dispatchToElevatedWindow = ctrlPressed && !IsRunningElevated(); if ((shiftPressed || dispatchToElevatedWindow) && !debugTap) { @@ -1318,6 +1256,8 @@ namespace winrt::TerminalApp::implementation winrt::guid()); valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough())); + valueSet.Insert(L"reloadEnvironmentVariables", + Windows::Foundation::PropertyValue::CreateBoolean(_settings.GlobalSettings().ReloadEnvironmentVariables())); conhostConn.Initialize(valueSet); @@ -1660,6 +1600,9 @@ namespace winrt::TerminalApp::implementation }); term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); + + term.ContextMenu().Opening({ this, &TerminalPage::_ContextMenuOpened }); + term.SelectionContextMenu().Opening({ this, &TerminalPage::_SelectionMenuOpened }); } // Method Description: @@ -1847,11 +1790,11 @@ namespace winrt::TerminalApp::implementation } // If the user set a custom name, save it - if (_WindowName != L"") + if (const auto& windowName{ _WindowProperties.WindowName() }; !windowName.empty()) { ActionAndArgs action; action.Action(ShortcutAction::RenameWindow); - RenameWindowArgs args{ _WindowName }; + RenameWindowArgs args{ windowName }; action.Args(args); actions.emplace_back(std::move(action)); @@ -1901,7 +1844,7 @@ namespace winrt::TerminalApp::implementation } } - if (ShouldUsePersistedLayout(_settings)) + if (_settings.GlobalSettings().ShouldUsePersistedLayout()) { // 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 @@ -1947,12 +1890,15 @@ namespace winrt::TerminalApp::implementation // is the last remaining pane on a tab, that tab will be closed upon moving. // - No move will occur if the tabIdx is the same as the current tab, or if // the specified tab is not a host of terminals (such as the settings tab). - // Arguments: - // - tabIdx: The target tab index. + // - If the Window is specified, the pane will instead be detached and moved + // to the window with the given name/id. // Return Value: // - true if the pane was successfully moved to the new tab. - bool TerminalPage::_MovePane(const uint32_t tabIdx) + bool TerminalPage::_MovePane(MovePaneArgs args) { + const auto tabIdx{ args.TabIndex() }; + const auto windowId{ args.Window() }; + auto focusedTab{ _GetFocusedTabImpl() }; if (!focusedTab) @@ -1960,6 +1906,23 @@ namespace winrt::TerminalApp::implementation return false; } + // If there was a windowId in the action, try to move it to the + // specified window instead of moving it in our tab row. + if (!windowId.empty()) + { + if (const auto terminalTab{ _GetFocusedTabImpl() }) + { + if (const auto pane{ terminalTab->GetActivePane() }) + { + auto startupActions = pane->BuildStartupActions(0, 1, true, true); + _DetachPaneFromWindow(pane); + _MoveContent(std::move(startupActions.args), args.Window(), args.TabIndex()); + focusedTab->DetachPane(); + return true; + } + } + } + // If we are trying to move from the current tab to the current tab do nothing. if (_GetFocusedTabIndex() == tabIdx) { @@ -1990,6 +1953,152 @@ namespace winrt::TerminalApp::implementation return true; } + // Detach a tree of panes from this terminal. Helper used for moving panes + // and tabs to other windows. + void TerminalPage::_DetachPaneFromWindow(std::shared_ptr pane) + { + pane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + _manager.Detach(control); + } + }); + } + + void TerminalPage::_DetachTabFromWindow(const winrt::com_ptr& tab) + { + if (const auto terminalTab = tab.try_as()) + { + // Detach the root pane, which will act like the whole tab got detached. + if (const auto rootPane = terminalTab->GetRootPane()) + { + _DetachPaneFromWindow(rootPane); + } + } + } + + // Method Description: + // - Serialize these actions to json, and raise them as a RequestMoveContent + // event. Our Window will raise that to the window manager / monarch, who + // will dispatch this blob of json back to the window that should handle + // this. + // - `actions` will be emptied into a winrt IVector as a part of this method + // and should be expected to be empty after this call. + void TerminalPage::_MoveContent(std::vector&& actions, + const winrt::hstring& windowName, + const uint32_t tabIndex, + const std::optional& dragPoint) + { + const auto winRtActions{ winrt::single_threaded_vector(std::move(actions)) }; + const auto str{ ActionAndArgs::Serialize(winRtActions) }; + const auto request = winrt::make_self(windowName, + str, + tabIndex); + if (dragPoint.has_value()) + { + request->WindowPosition(dragPoint->to_winrt_point()); + } + _RequestMoveContentHandlers(*this, *request); + } + + bool TerminalPage::_MoveTab(MoveTabArgs args) + { + // If there was a windowId in the action, try to move it to the + // specified window instead of moving it in our tab row. + const auto windowId{ args.Window() }; + if (!windowId.empty()) + { + if (const auto terminalTab{ _GetFocusedTabImpl() }) + { + auto startupActions = terminalTab->BuildStartupActions(true); + _DetachTabFromWindow(terminalTab); + _MoveContent(std::move(startupActions), args.Window(), 0); + _RemoveTab(*terminalTab); + return true; + } + } + + const auto direction = args.Direction(); + if (direction != MoveTabDirection::None) + { + if (auto focusedTabIndex = _GetFocusedTabIndex()) + { + const auto currentTabIndex = focusedTabIndex.value(); + const auto delta = direction == MoveTabDirection::Forward ? 1 : -1; + _TryMoveTab(currentTabIndex, currentTabIndex + delta); + } + } + + return true; + } + + uint32_t TerminalPage::NumberOfTabs() const + { + return _tabs.Size(); + } + + // Method Description: + // - Called when it is determined that an existing tab or pane should be + // attached to our window. content represents a blob of JSON describing + // some startup actions for rebuilding the specified panes. They will + // include `__content` properties with the GUID of the existing + // ControlInteractivity's we should use, rather than starting new ones. + // - _MakePane is already enlightened to use the ContentId property to + // reattach instead of create new content, so this method simply needs to + // parse the JSON and pump it into our action handler. Almost the same as + // doing something like `wt -w 0 nt`. + winrt::fire_and_forget TerminalPage::AttachContent(IVector args, + uint32_t tabIndex) + { + if (args == nullptr || + args.Size() == 0) + { + co_return; + } + + // Switch to the UI thread before selecting a tab or dispatching actions. + co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::High); + + const auto& firstAction = args.GetAt(0); + const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane }; + + // `splitPane` allows the user to specify which tab to split. In that + // case, split specifically the requested pane. + // + // If there's not enough tabs, then just turn this pane into a new tab. + // + // If the first action is `newTab`, the index is always going to be 0, + // so don't do anything in that case. + if (firstIsSplitPane && tabIndex < _tabs.Size()) + { + _SelectTab(tabIndex); + } + + for (const auto& action : args) + { + _actionDispatch->DoAction(action); + } + + // After handling all the actions, then re-check the tabIndex. We might + // have been called as a part of a tab drag/drop. In that case, the + // tabIndex is actually relevant, and we need to move the tab we just + // made into position. + if (!firstIsSplitPane && tabIndex != -1) + { + // Move the currently active tab to the requested index Use the + // currently focused tab index, because we don't know if the new tab + // opened at the end of the list, or adjacent to the previously + // active tab. This is affected by the user's "newTabPosition" + // setting. + if (const auto focusedTabIndex = _GetFocusedTabIndex()) + { + const auto source = *focusedTabIndex; + _TryMoveTab(source, tabIndex); + } + // else: This shouldn't really be possible, because the tab we _just_ opened should be active. + } + } + // Method Description: // - Split the focused pane either horizontally or vertically, and place the // given pane accordingly in the tree @@ -2473,14 +2582,27 @@ namespace winrt::TerminalApp::implementation { return true; } + + // GH#10188: WSL paths are okay. We'll let those through. + if (host == L"wsl$" || host == L"wsl.localhost") + { + return true; + } + // TODO: by the OSC 8 spec, if a hostname (other than localhost) is provided, we _should_ be // comparing that value against what is returned by GetComputerNameExW and making sure they match. // However, ShellExecute does not seem to be happy with file URIs of the form // file://{hostname}/path/to/file.ext // and so while we could do the hostname matching, we do not know how to actually open the URI // if its given in that form. So for now we ignore all hostnames other than localhost + return false; } - return false; + + // In this case, the app manually output a URI other than file:// or + // http(s)://. We'll trust the user knows what they're doing when + // clicking on those sorts of links. + // See discussion in GH#7562 for more details. + return true; } // Important! Don't take this eventArgs by reference, we need to extend the @@ -2640,23 +2762,46 @@ namespace winrt::TerminalApp::implementation } } - TermControl TerminalPage::_InitControl(const TerminalSettingsCreateResult& settings, const ITerminalConnection& connection) + TermControl TerminalPage::_CreateNewControlAndContent(const TerminalSettingsCreateResult& settings, const ITerminalConnection& connection) { // Do any initialization that needs to apply to _every_ TermControl we // create here. // TermControl will copy the settings out of the settings passed to it. - TermControl term{ settings.DefaultSettings(), settings.UnfocusedSettings(), connection }; + const auto content = _manager.CreateCore(settings.DefaultSettings(), settings.UnfocusedSettings(), connection); + return _SetupControl(TermControl{ content }); + } + + TermControl TerminalPage::_AttachControlToContent(const uint64_t& contentId) + { + if (const auto& content{ _manager.TryLookupCore(contentId) }) + { + // We have to pass in our current keybindings, because that's an + // object that belongs to this TerminalPage, on this thread. If we + // don't, then when we move the content to another thread, and it + // tries to handle a key, it'll callback on the original page's + // stack, inevitably resulting in a wrong_thread + return _SetupControl(TermControl::NewControlByAttachingContent(content, *_bindings)); + } + return nullptr; + } + + TermControl TerminalPage::_SetupControl(const TermControl& term) + { // GH#12515: ConPTY assumes it's hidden at the start. If we're not, let it know now. if (_visible) { term.WindowVisibilityChanged(_visible); } + // Even in the case of re-attaching content from another window, this + // will correctly update the control's owning HWND if (_hostingHwnd.has_value()) { term.OwningHwnd(reinterpret_cast(*_hostingHwnd)); } + + _RegisterTerminalEvents(term); return term; } @@ -2680,6 +2825,19 @@ namespace winrt::TerminalApp::implementation const winrt::TerminalApp::TabBase& sourceTab, TerminalConnection::ITerminalConnection existingConnection) { + // First things first - Check for making a pane from content ID. + if (newTerminalArgs && + newTerminalArgs.ContentId() != 0) + { + // Don't need to worry about duplicating or anything - we'll + // serialize the actual profile's GUID along with the content guid. + const auto& profile = _settings.GetProfileForArgs(newTerminalArgs); + + const auto control = _AttachControlToContent(newTerminalArgs.ContentId()); + + return std::make_shared(profile, control); + } + TerminalSettingsCreateResult controlSettings{ nullptr }; Profile profile{ nullptr }; @@ -2731,15 +2889,13 @@ namespace winrt::TerminalApp::implementation } } - const auto control = _InitControl(controlSettings, connection); - _RegisterTerminalEvents(control); + const auto control = _CreateNewControlAndContent(controlSettings, connection); auto resultPane = std::make_shared(profile, control); if (debugConnection) // this will only be set if global debugging is on and tap is active { - auto newControl = _InitControl(controlSettings, debugConnection); - _RegisterTerminalEvents(newControl); + auto newControl = _CreateNewControlAndContent(controlSettings, debugConnection); // Split (auto) with the debug tap. auto debugPane = std::make_shared(profile, newControl); @@ -2920,7 +3076,7 @@ namespace winrt::TerminalApp::implementation // want to create an animation. WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); - _tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield()); + _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; _tabView.Background(transparent); @@ -2972,50 +3128,6 @@ namespace winrt::TerminalApp::implementation } } - // This is a helper to aid in sorting commands by their `Name`s, alphabetically. - static bool _compareSchemeNames(const ColorScheme& lhs, const ColorScheme& rhs) - { - std::wstring leftName{ lhs.Name() }; - std::wstring rightName{ rhs.Name() }; - return leftName.compare(rightName) < 0; - } - - // Method Description: - // - Takes a mapping of names->commands and expands them - // Arguments: - // - - // Return Value: - // - - IMap TerminalPage::_ExpandCommands(IMapView commandsToExpand, - IVectorView profiles, - IMapView schemes) - { - auto warnings{ winrt::single_threaded_vector() }; - - std::vector sortedSchemes; - sortedSchemes.reserve(schemes.Size()); - - for (const auto& nameAndScheme : schemes) - { - sortedSchemes.push_back(nameAndScheme.Value()); - } - std::sort(sortedSchemes.begin(), - sortedSchemes.end(), - _compareSchemeNames); - - auto copyOfCommands = winrt::single_threaded_map(); - for (const auto& nameAndCommand : commandsToExpand) - { - copyOfCommands.Insert(nameAndCommand.Key(), nameAndCommand.Value()); - } - - Command::ExpandCommands(copyOfCommands, - profiles, - { sortedSchemes }, - warnings); - - return copyOfCommands; - } // Method Description: // - Repopulates the list of commands in the command palette with the // current commands in the settings. Also updates the keybinding labels to @@ -3026,20 +3138,9 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_UpdateCommandsForPalette() { - auto copyOfCommands = _ExpandCommands(_settings.GlobalSettings().ActionMap().NameMap(), - _settings.ActiveProfiles().GetView(), - _settings.GlobalSettings().ColorSchemes()); - - _recursiveUpdateCommandKeybindingLabels(_settings, copyOfCommands.GetView()); - // Update the command palette when settings reload - auto commandsCollection = winrt::single_threaded_vector(); - for (const auto& nameAndCommand : copyOfCommands) - { - commandsCollection.Append(nameAndCommand.Value()); - } - - CommandPalette().SetCommands(commandsCollection); + const auto& expanded{ _settings.GlobalSettings().ActionMap().ExpandedCommands() }; + CommandPalette().SetCommands(expanded); } // Method Description: @@ -3450,6 +3551,8 @@ namespace winrt::TerminalApp::implementation HRESULT TerminalPage::_OnNewConnection(const ConptyConnection& connection) { + _newConnectionRevoker.revoke(); + // We need to be on the UI thread in order for _OpenNewTab to run successfully. // HasThreadAccess will return true if we're currently on a UI thread and false otherwise. // When we're on a COM thread, we'll need to dispatch the calls to the UI thread @@ -3856,105 +3959,6 @@ namespace winrt::TerminalApp::implementation } } - // WindowName is a otherwise generic WINRT_OBSERVABLE_PROPERTY, but it needs - // to raise a PropertyChanged for WindowNameForDisplay, instead of - // WindowName. - winrt::hstring TerminalPage::WindowName() const noexcept - { - return _WindowName; - } - - winrt::fire_and_forget TerminalPage::WindowName(const winrt::hstring& value) - { - const auto oldIsQuakeMode = IsQuakeWindow(); - const auto changed = _WindowName != value; - if (changed) - { - _WindowName = value; - } - auto weakThis{ get_weak() }; - // On the foreground thread, raise property changed notifications, and - // display the success toast. - co_await wil::resume_foreground(Dispatcher()); - if (auto page{ weakThis.get() }) - { - if (changed) - { - page->_PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"WindowName" }); - page->_PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"WindowNameForDisplay" }); - - // DON'T display the confirmation if this is the name we were - // given on startup! - if (page->_startupState == StartupState::Initialized) - { - page->IdentifyWindow(); - - // If we're entering quake mode, or leaving it - if (IsQuakeWindow() != oldIsQuakeMode) - { - // If we're entering Quake Mode from ~Focus Mode, then this will enter Focus Mode - // If we're entering Quake Mode from Focus Mode, then this will do nothing - // If we're leaving Quake Mode (we're already in Focus Mode), then this will do nothing - SetFocusMode(true); - _IsQuakeWindowChangedHandlers(*this, nullptr); - } - } - } - } - } - - // WindowId is a otherwise generic WINRT_OBSERVABLE_PROPERTY, but it needs - // to raise a PropertyChanged for WindowIdForDisplay, instead of - // WindowId. - uint64_t TerminalPage::WindowId() const noexcept - { - return _WindowId; - } - void TerminalPage::WindowId(const uint64_t& value) - { - if (_WindowId != value) - { - _WindowId = value; - _PropertyChangedHandlers(*this, WUX::Data::PropertyChangedEventArgs{ L"WindowIdForDisplay" }); - } - } - - void TerminalPage::SetPersistedLayoutIdx(const uint32_t idx) - { - _loadFromPersistedLayoutIdx = idx; - } - - void TerminalPage::SetNumberOfOpenWindows(const uint64_t num) - { - _numOpenWindows = num; - } - - // Method Description: - // - Returns a label like "Window: 1234" for the ID of this window - // Arguments: - // - - // Return Value: - // - a string for displaying the name of the window. - winrt::hstring TerminalPage::WindowIdForDisplay() const noexcept - { - return winrt::hstring{ fmt::format(L"{}: {}", - std::wstring_view(RS_(L"WindowIdLabel")), - _WindowId) }; - } - - // Method Description: - // - Returns a label like "" when the window has no name, or the name of the window. - // Arguments: - // - - // Return Value: - // - a string for displaying the name of the window. - winrt::hstring TerminalPage::WindowNameForDisplay() const noexcept - { - return _WindowName.empty() ? - winrt::hstring{ fmt::format(L"<{}>", RS_(L"UnnamedWindowName")) } : - _WindowName; - } - // Method Description: // - Called when an attempt to rename the window has failed. This will open // the toast displaying a message to the user that the attempt to rename @@ -4066,17 +4070,12 @@ namespace winrt::TerminalApp::implementation else if (key == Windows::System::VirtualKey::Escape) { // User wants to discard the changes they made - WindowRenamerTextBox().Text(WindowName()); + WindowRenamerTextBox().Text(_WindowProperties.WindowName()); WindowRenamer().IsOpen(false); _renamerPressedEnter = false; } } - bool TerminalPage::IsQuakeWindow() const noexcept - { - return WindowName() == QuakeWindowName; - } - // Method Description: // - This function stops people from duplicating the base profile, because // it gets ~ ~ weird ~ ~ when they do. Remove when TODO GH#5047 is done. @@ -4164,7 +4163,7 @@ namespace winrt::TerminalApp::implementation { // Try to handle auto-elevation const auto requestedElevation = controlSettings.DefaultSettings().Elevate(); - const auto currentlyElevated = IsElevated(); + const auto currentlyElevated = IsRunningElevated(); // We aren't elevated, but we want to be. if (requestedElevation && !currentlyElevated) @@ -4328,17 +4327,17 @@ namespace winrt::TerminalApp::implementation auto requestedTheme{ theme.RequestedTheme() }; { - // Update the brushes that Pane's use... - Pane::SetupResources(requestedTheme); - // ... then trigger a visual update for all the pane borders to - // apply the new ones. + _updatePaneResources(requestedTheme); + for (const auto& tab : _tabs) { if (auto terminalTab{ _GetTerminalTabImpl(tab) }) { - terminalTab->GetRootPane()->WalkTree([&](auto&& pane) { - pane->UpdateVisuals(); - }); + // The root pane will propagate the theme change to all its children. + if (const auto& rootPane{ terminalTab->GetRootPane() }) + { + rootPane->UpdateResources(_paneResources); + } } } } @@ -4445,6 +4444,54 @@ namespace winrt::TerminalApp::implementation } } + // Function Description: + // - Attempts to load some XAML resources that Panes will need. This includes: + // * The Color they'll use for active Panes's borders - SystemAccentColor + // * The Brush they'll use for inactive Panes - TabViewBackground (to match the + // color of the titlebar) + // Arguments: + // - requestedTheme: this should be the currently active Theme for the app + // Return Value: + // - + void TerminalPage::_updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) + { + const auto res = Application::Current().Resources(); + const auto accentColorKey = winrt::box_value(L"SystemAccentColor"); + if (res.HasKey(accentColorKey)) + { + const auto colorFromResources = ThemeLookup(res, requestedTheme, accentColorKey); + // If SystemAccentColor is _not_ a Color for some reason, use + // Transparent as the color, so we don't do this process again on + // the next pane (by leaving s_focusedBorderBrush nullptr) + auto actualColor = winrt::unbox_value_or(colorFromResources, Colors::Black()); + _paneResources.focusedBorderBrush = SolidColorBrush(actualColor); + } + else + { + // DON'T use Transparent here - if it's "Transparent", then it won't + // be able to hittest for clicks, and then clicking on the border + // will eat focus. + _paneResources.focusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + + const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); + if (res.HasKey(unfocusedBorderBrushKey)) + { + // MAKE SURE TO USE ThemeLookup, so that we get the correct resource for + // the requestedTheme, not just the value from the resources (which + // might not respect the settings' requested theme) + auto obj = ThemeLookup(res, requestedTheme, unfocusedBorderBrushKey); + _paneResources.unfocusedBorderBrush = obj.try_as(); + } + else + { + // DON'T use Transparent here - if it's "Transparent", then it won't + // be able to hittest for clicks, and then clicking on the border + // will eat focus. + _paneResources.unfocusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + } + void TerminalPage::WindowActivated(const bool activated) { // Stash if we're activated. Use that when we reload @@ -4452,4 +4499,320 @@ namespace winrt::TerminalApp::implementation _activated = activated; _updateThemeColors(); } + + void TerminalPage::_ContextMenuOpened(const IInspectable& sender, + const IInspectable& /*args*/) + { + _PopulateContextMenu(sender, false /*withSelection*/); + } + void TerminalPage::_SelectionMenuOpened(const IInspectable& sender, + const IInspectable& /*args*/) + { + _PopulateContextMenu(sender, true /*withSelection*/); + } + + void TerminalPage::_PopulateContextMenu(const IInspectable& sender, + const bool /*withSelection*/) + { + // withSelection can be used to add actions that only appear if there's + // selected text, like "search the web". In this initial draft, it's not + // actually augmented by the TerminalPage, so it's left commented out. + + const auto& menu{ sender.try_as() }; + if (!menu) + { + return; + } + + // Helper lambda for dispatching an ActionAndArgs onto the + // ShortcutActionDispatch. Used below to wire up each menu entry to the + // respective action. + + auto weak = get_weak(); + auto makeCallback = [weak](const ActionAndArgs& actionAndArgs) { + return [weak, actionAndArgs](auto&&, auto&&) { + if (auto page{ weak.get() }) + { + page->_actionDispatch->DoAction(actionAndArgs); + } + }; + }; + + auto makeItem = [&menu, &makeCallback](const winrt::hstring& label, + const winrt::hstring& icon, + const auto& action) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Click(makeCallback(action)); + menu.SecondaryCommands().Append(button); + }; + + // Wire up each item to the action that should be performed. By actually + // connecting these to actions, we ensure the implementation is + // consistent. This also leaves room for customizing this menu with + // actions in the future. + + makeItem(RS_(L"SplitPaneText"), L"\xF246", ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate } }); + makeItem(RS_(L"DuplicateTabText"), L"\xF5ED", ActionAndArgs{ ShortcutAction::DuplicateTab, nullptr }); + + // Only wire up "Close Pane" if there's multiple panes. + if (_GetFocusedTabImpl()->GetLeafPaneCount() > 1) + { + makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }); + } + + makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } }); + } + + // Handler for our WindowProperties's PropertyChanged event. We'll use this + // to pop the "Identify Window" toast when the user renames our window. + winrt::fire_and_forget TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, + const WUX::Data::PropertyChangedEventArgs& args) + { + if (args.PropertyName() != L"WindowName") + { + co_return; + } + auto weakThis{ get_weak() }; + // On the foreground thread, raise property changed notifications, and + // display the success toast. + co_await wil::resume_foreground(Dispatcher()); + if (auto page{ weakThis.get() }) + { + // DON'T display the confirmation if this is the name we were + // given on startup! + if (page->_startupState == StartupState::Initialized) + { + page->IdentifyWindow(); + } + } + } + + void TerminalPage::_onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView&, + const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e) + { + // Get the tab impl from this event. + const auto eventTab = e.Tab(); + const auto tabBase = _GetTabByTabViewItem(eventTab); + winrt::com_ptr tabImpl; + tabImpl.copy_from(winrt::get_self(tabBase)); + if (tabImpl) + { + // First: stash the tab we started dragging. + // We're going to be asked for this. + _stashed.draggedTab = tabImpl; + + // Stash the offset from where we started the drag to the + // tab's origin. We'll use that offset in the future to help + // position the dropped window. + + // First, the position of the pointer, from the CoreWindow + const til::point pointerPosition{ til::math::rounding, CoreWindow::GetForCurrentThread().PointerPosition() }; + // Next, the position of the tab itself: + const til::point tabPosition{ til::math::rounding, eventTab.TransformToVisual(nullptr).TransformPoint({ 0, 0 }) }; + // Now, we need to add the origin of our CoreWindow to the tab + // position. + const auto& coreWindowBounds{ CoreWindow::GetForCurrentThread().Bounds() }; + const til::point windowOrigin{ til::math::rounding, coreWindowBounds.X, coreWindowBounds.Y }; + const auto realTabPosition = windowOrigin + tabPosition; + // Subtract the two to get the offset. + _stashed.dragOffset = til::point{ pointerPosition - realTabPosition }; + + // Into the DataPackage, let's stash our own window ID. + const auto id{ _WindowProperties.WindowId() }; + + // Get our PID + const auto pid{ GetCurrentProcessId() }; + + e.Data().Properties().Insert(L"windowId", winrt::box_value(id)); + e.Data().Properties().Insert(L"pid", winrt::box_value(pid)); + e.Data().RequestedOperation(DataPackageOperation::Move); + + // The next thing that will happen: + // * Another TerminalPage will get a TabStripDragOver, then get a + // TabStripDrop + // * This will be handled by the _other_ page asking the monarch + // to ask us to send our content to them. + // * We'll get a TabDroppedOutside to indicate that this tab was + // dropped _not_ on a TabView. + // * This will be handled by _onTabDroppedOutside, which will + // raise a MoveContent (to a new window) event. + } + } + + void TerminalPage::_onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::UI::Xaml::DragEventArgs& e) + { + // We must mark that we can accept the drag/drop. The system will never + // call TabStripDrop on us if we don't indicate that we're willing. + const auto& props{ e.DataView().Properties() }; + if (props.HasKey(L"windowId") && + props.HasKey(L"pid") && + (winrt::unbox_value_or(props.TryLookup(L"pid"), 0u) == GetCurrentProcessId())) + { + e.AcceptedOperation(DataPackageOperation::Move); + } + + // You may think to yourself, this is a great place to increase the + // width of the TabView artificially, to make room for the new tab item. + // However, we'll never get a message that the tab left the tab view + // (without being dropped). So there's no good way to resize back down. + } + + // Method Description: + // - Called on the TARGET of a tab drag/drop. We'll unpack the DataPackage + // to find who the tab came from. We'll then ask the Monarch to ask the + // sender to move that tab to us. + winrt::fire_and_forget TerminalPage::_onTabStripDrop(winrt::Windows::Foundation::IInspectable /*sender*/, + winrt::Windows::UI::Xaml::DragEventArgs e) + { + // Get the PID and make sure it is the same as ours. + if (const auto& pidObj{ e.DataView().Properties().TryLookup(L"pid") }) + { + const auto pid{ winrt::unbox_value_or(pidObj, 0u) }; + if (pid != GetCurrentProcessId()) + { + // The PID doesn't match ours. We can't handle this drop. + co_return; + } + } + else + { + // No PID? We can't handle this drop. Bail. + co_return; + } + + const auto& windowIdObj{ e.DataView().Properties().TryLookup(L"windowId") }; + if (windowIdObj == nullptr) + { + // No windowId? Bail. + co_return; + } + const uint64_t src{ winrt::unbox_value(windowIdObj) }; + + // Figure out where in the tab strip we're dropping this tab. Add that + // index to the request. This is largely taken from the WinUI sample + // app. + + // We need to be on OUR UI thread to figure out where we dropped + auto weakThis{ get_weak() }; + co_await wil::resume_foreground(Dispatcher()); + if (const auto& page{ weakThis.get() }) + { + // First we need to get the position in the List to drop to + auto index = -1; + + // Determine which items in the list our pointer is between. + for (auto i = 0u; i < _tabView.TabItems().Size(); i++) + { + if (const auto& item{ _tabView.ContainerFromIndex(i).try_as() }) + { + const auto posX{ e.GetPosition(item).X }; // The point of the drop, relative to the tab + const auto itemWidth{ item.ActualWidth() }; // The right of the tab + // If the drag point is on the left half of the tab, then insert here. + if (posX < itemWidth / 2) + { + index = i; + break; + } + } + } + + // `this` is safe to use + const auto request = winrt::make_self(src, _WindowProperties.WindowId(), index); + + // This will go up to the monarch, who will then dispatch the request + // back down to the source TerminalPage, who will then perform a + // RequestMoveContent to move their tab to us. + _RequestReceiveContentHandlers(*this, *request); + } + } + + // Method Description: + // - This is called on the drag/drop SOURCE TerminalPage, when the monarch has + // requested that we send our tab to another window. We'll need to + // serialize the tab, and send it to the monarch, who will then send it to + // the destination window. + // - Fortunately, sending the tab is basically just a MoveTab action, so we + // can largely reuse that. + winrt::fire_and_forget TerminalPage::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args) + { + // validate that we're the source window of the tab in this request + if (args.SourceWindow() != _WindowProperties.WindowId()) + { + co_return; + } + if (!_stashed.draggedTab) + { + co_return; + } + + // must do the work of adding/removing tabs on the UI thread. + auto weakThis{ get_weak() }; + co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Normal); + if (const auto& page{ weakThis.get() }) + { + // `this` is safe to use in here. + + _sendDraggedTabToWindow(winrt::hstring{ fmt::format(L"{}", args.TargetWindow()) }, + args.TabIndex(), + std::nullopt); + } + } + + winrt::fire_and_forget TerminalPage::_onTabDroppedOutside(winrt::IInspectable sender, + winrt::MUX::Controls::TabViewTabDroppedOutsideEventArgs e) + { + // Get the current pointer point from the CoreWindow + const auto& pointerPoint{ CoreWindow::GetForCurrentThread().PointerPosition() }; + + // This is called when a tab FROM OUR WINDOW was dropped outside the + // tabview. We already know which tab was being dragged. We'll just + // invoke a moveTab action with the target window being -1. That will + // force the creation of a new window. + + if (!_stashed.draggedTab) + { + co_return; + } + + // must do the work of adding/removing tabs on the UI thread. + auto weakThis{ get_weak() }; + co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Normal); + if (const auto& page{ weakThis.get() }) + { + // `this` is safe to use in here. + + // We need to convert the pointer point to a point that we can use + // to position the new window. We'll use the drag offset from before + // so that the tab in the new window is positioned so that it's + // basically still directly under the cursor. + + // -1 is the magic number for "new window" + // 0 as the tab index, because we don't care. It's making a new window. It'll be the only tab. + const til::point adjusted = til::point{ til::math::rounding, pointerPoint } - _stashed.dragOffset; + _sendDraggedTabToWindow(winrt::hstring{ L"-1" }, 0, adjusted); + } + } + + void TerminalPage::_sendDraggedTabToWindow(const winrt::hstring& windowId, + const uint32_t tabIndex, + std::optional dragPoint) + { + auto startupActions = _stashed.draggedTab->BuildStartupActions(true); + _DetachTabFromWindow(_stashed.draggedTab); + + _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint); + // _RemoveTab will make sure to null out the _stashed.draggedTab + _RemoveTab(*_stashed.draggedTab); + } + } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 9c1705706a0..efc68f5f6b2 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -7,7 +7,10 @@ #include "TerminalTab.h" #include "AppKeyBindings.h" #include "AppCommandlineArgs.h" +#include "LastTabClosedEventArgs.g.h" #include "RenameWindowRequestedArgs.g.h" +#include "RequestMoveContentArgs.g.h" +#include "RequestReceiveContentArgs.g.h" #include "Toast.h" #define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); @@ -41,6 +44,15 @@ namespace winrt::TerminalApp::implementation ScrollDown = 1 }; + struct LastTabClosedEventArgs : LastTabClosedEventArgsT + { + WINRT_PROPERTY(bool, ClearPersistedState); + + public: + LastTabClosedEventArgs(const bool& shouldClear) : + _ClearPersistedState{ shouldClear } {}; + }; + struct RenameWindowRequestedArgs : RenameWindowRequestedArgsT { WINRT_PROPERTY(winrt::hstring, ProposedName); @@ -50,10 +62,37 @@ namespace winrt::TerminalApp::implementation _ProposedName{ name } {}; }; + struct RequestMoveContentArgs : RequestMoveContentArgsT + { + WINRT_PROPERTY(winrt::hstring, Window); + WINRT_PROPERTY(winrt::hstring, Content); + WINRT_PROPERTY(uint32_t, TabIndex); + WINRT_PROPERTY(Windows::Foundation::IReference, WindowPosition); + + public: + RequestMoveContentArgs(const winrt::hstring window, const winrt::hstring content, uint32_t tabIndex) : + _Window{ window }, + _Content{ content }, + _TabIndex{ tabIndex } {}; + }; + + struct RequestReceiveContentArgs : RequestReceiveContentArgsT + { + WINRT_PROPERTY(uint64_t, SourceWindow); + WINRT_PROPERTY(uint64_t, TargetWindow); + WINRT_PROPERTY(uint32_t, TabIndex); + + public: + RequestReceiveContentArgs(const uint64_t src, const uint64_t tgt, const uint32_t tabIndex) : + _SourceWindow{ src }, + _TargetWindow{ tgt }, + _TabIndex{ tabIndex } {}; + }; + struct TerminalPage : TerminalPageT { public: - TerminalPage(); + TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager); // This implements shobjidl's IInitializeWithWindow, but due to a XAML Compiler bug we cannot // put it in our inheritance graph. https://github.com/microsoft/microsoft-ui-xaml/issues/3331 @@ -63,11 +102,8 @@ namespace winrt::TerminalApp::implementation void Create(); - bool ShouldUsePersistedLayout(Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; bool ShouldImmediatelyHandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; void HandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); - 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); @@ -117,27 +153,21 @@ namespace winrt::TerminalApp::implementation const bool initial, const winrt::hstring cwd = L""); - // Normally, WindowName and WindowId would be - // WINRT_OBSERVABLE_PROPERTY's, but we want them to raise - // WindowNameForDisplay and WindowIdForDisplay instead - winrt::hstring WindowName() const noexcept; - winrt::fire_and_forget WindowName(const winrt::hstring& value); - uint64_t WindowId() const noexcept; - void WindowId(const uint64_t& value); + TerminalApp::WindowProperties WindowProperties() const noexcept { return _WindowProperties; }; - 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; + bool CanDragDrop() const noexcept; + bool IsRunningElevated() const noexcept; void OpenSettingsUI(); void WindowActivated(const bool activated); bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + winrt::fire_and_forget AttachContent(Windows::Foundation::Collections::IVector args, uint32_t tabIndex); + winrt::fire_and_forget SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); + + uint32_t NumberOfTabs() const; + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); // -------------------------------- WinRT Events --------------------------------- @@ -153,13 +183,16 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(Initialized, IInspectable, winrt::Windows::UI::Xaml::RoutedEventArgs); TYPED_EVENT(IdentifyWindowsRequested, IInspectable, IInspectable); 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); TYPED_EVENT(ShowWindowChanged, IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs) + TYPED_EVENT(RequestMoveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestMoveContentArgs); + TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs); + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, _PropertyChangedHandlers, nullptr); private: @@ -192,10 +225,8 @@ namespace winrt::TerminalApp::implementation bool _isFullscreen{ false }; bool _isMaximized{ false }; bool _isAlwaysOnTop{ false }; - winrt::hstring _WindowName{}; - uint64_t _WindowId{ 0 }; + std::optional _loadFromPersistedLayoutIdx{}; - uint64_t _numOpenWindows{ 0 }; bool _maintainStateOnTabClose{ false }; bool _rearranging{ false }; @@ -230,6 +261,19 @@ namespace winrt::TerminalApp::implementation int _renamerLayoutCount{ 0 }; bool _renamerPressedEnter{ false }; + TerminalApp::WindowProperties _WindowProperties{ nullptr }; + PaneResources _paneResources; + + TerminalApp::ContentManager _manager{ nullptr }; + + struct StashedDragData + { + winrt::com_ptr draggedTab{ nullptr }; + til::point dragOffset{ 0, 0 }; + } _stashed; + + winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::NewConnection_revoker _newConnectionRevoker; + winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); void _ShowAboutDialog(); @@ -273,10 +317,6 @@ namespace winrt::TerminalApp::implementation void _UpdateCommandsForPalette(); void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance); - static winrt::Windows::Foundation::Collections::IMap _ExpandCommands(Windows::Foundation::Collections::IMapView commandsToExpand, - Windows::Foundation::Collections::IVectorView profiles, - Windows::Foundation::Collections::IMapView schemes); - void _DuplicateFocusedTab(); void _DuplicateTab(const TerminalTab& tab); @@ -301,7 +341,8 @@ namespace winrt::TerminalApp::implementation bool _SelectTab(uint32_t tabIndex); bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); - bool _MovePane(const uint32_t tabIdx); + bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args); + bool _MoveTab(const Microsoft::Terminal::Settings::Model::MoveTabArgs args); template bool _ApplyToActiveControls(F f) @@ -390,8 +431,10 @@ namespace winrt::TerminalApp::implementation void _Find(const TerminalTab& tab); - winrt::Microsoft::Terminal::Control::TermControl _InitControl(const winrt::Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, - const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection); + winrt::Microsoft::Terminal::Control::TermControl _CreateNewControlAndContent(const winrt::Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings, + const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection); + winrt::Microsoft::Terminal::Control::TermControl _SetupControl(const winrt::Microsoft::Terminal::Control::TermControl& term); + winrt::Microsoft::Terminal::Control::TermControl _AttachControlToContent(const uint64_t& contentGuid); std::shared_ptr _MakePane(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr, const winrt::TerminalApp::TabBase& sourceTab = nullptr, @@ -459,8 +502,27 @@ namespace winrt::TerminalApp::implementation void _updateThemeColors(); void _updateTabCloseButton(const winrt::Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem); + void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); winrt::fire_and_forget _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); + winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); + + void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e); + void _onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); + winrt::fire_and_forget _onTabStripDrop(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::DragEventArgs e); + winrt::fire_and_forget _onTabDroppedOutside(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::UI::Xaml::Controls::TabViewTabDroppedOutsideEventArgs e); + + void _DetachPaneFromWindow(std::shared_ptr pane); + void _DetachTabFromWindow(const winrt::com_ptr& terminalTab); + void _MoveContent(std::vector&& actions, + const winrt::hstring& windowName, + const uint32_t tabIndex, + const std::optional& dragPoint = std::nullopt); + void _sendDraggedTabToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); + + void _ContextMenuOpened(const IInspectable& sender, const IInspectable& args); + void _SelectionMenuOpened(const IInspectable& sender, const IInspectable& args); + void _PopulateContextMenu(const IInspectable& sender, const bool withSelection); #pragma region ActionHandlers // These are all defined in AppActionHandlers.cpp @@ -477,4 +539,5 @@ namespace winrt::TerminalApp::implementation namespace winrt::TerminalApp::factory_implementation { BASIC_FACTORY(TerminalPage); + BASIC_FACTORY(RequestReceiveContentArgs); } diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 5535c527a21..5c8a151b909 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -5,21 +5,58 @@ import "IDirectKeyListener.idl"; namespace TerminalApp { - delegate void LastTabClosedEventArgs(); + [default_interface] runtimeclass ContentManager + { + Microsoft.Terminal.Control.ControlInteractivity CreateCore(Microsoft.Terminal.Control.IControlSettings settings, + Microsoft.Terminal.Control.IControlAppearance unfocusedAppearance, + Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); + + Microsoft.Terminal.Control.ControlInteractivity TryLookupCore(UInt64 id); + void Detach(Microsoft.Terminal.Control.TermControl control); + } + + [default_interface] runtimeclass LastTabClosedEventArgs + { + Boolean ClearPersistedState { get; }; + }; [default_interface] runtimeclass RenameWindowRequestedArgs { String ProposedName { get; }; }; + [default_interface] runtimeclass RequestMoveContentArgs + { + String Window { get; }; + String Content { get; }; + UInt32 TabIndex { get; }; + Windows.Foundation.IReference WindowPosition { get; }; + }; + [default_interface] runtimeclass RequestReceiveContentArgs { + RequestReceiveContentArgs(UInt64 src, UInt64 tgt, UInt32 tabIndex); + + UInt64 SourceWindow { get; }; + UInt64 TargetWindow { get; }; + UInt32 TabIndex { get; }; + }; interface IDialogPresenter { Windows.Foundation.IAsyncOperation ShowDialog(Windows.UI.Xaml.Controls.ContentDialog dialog); }; + [default_interface] runtimeclass WindowProperties : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + String WindowName { get; }; + UInt64 WindowId { get; }; + String WindowNameForDisplay { get; }; + String WindowIdForDisplay { get; }; + + Boolean IsQuakeWindow(); + }; + [default_interface] runtimeclass TerminalPage : Windows.UI.Xaml.Controls.Page, Windows.UI.Xaml.Data.INotifyPropertyChanged, IDirectKeyListener { - TerminalPage(); + TerminalPage(WindowProperties properties, ContentManager manager); // XAML bound properties String ApplicationDisplayName { get; }; @@ -29,13 +66,9 @@ namespace TerminalApp Boolean Fullscreen { get; }; Boolean AlwaysOnTop { get; }; + WindowProperties WindowProperties { get; }; void IdentifyWindow(); - String WindowName; - UInt64 WindowId; - String WindowNameForDisplay { get; }; - String WindowIdForDisplay { get; }; void RenameFailed(); - Boolean IsQuakeWindow(); // We cannot use the default XAML APIs because we want to make sure // that there's only one application-global dialog visible at a time, @@ -48,6 +81,7 @@ namespace TerminalApp Windows.UI.Xaml.Media.Brush TitlebarBrush { get; }; void WindowActivated(Boolean activated); + void SendContentToOther(RequestReceiveContentArgs args); event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler LastTabClosed; @@ -59,10 +93,13 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler SetTaskbarProgress; event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; 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; event Windows.Foundation.TypedEventHandler ShowWindowChanged; + + event Windows.Foundation.TypedEventHandler RequestMoveContent; + event Windows.Foundation.TypedEventHandler RequestReceiveContent; } } diff --git a/src/cascadia/TerminalApp/TerminalPage.xaml b/src/cascadia/TerminalApp/TerminalPage.xaml index 3434cd868ee..38d7c0f69f7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.xaml +++ b/src/cascadia/TerminalApp/TerminalPage.xaml @@ -201,10 +201,10 @@ tracked by MUX#4382 --> + Subtitle="{x:Bind WindowProperties.WindowNameForDisplay, Mode=OneWay}" /> + Text="{x:Bind WindowProperties.WindowName, Mode=OneWay}" /> diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 8cd4ae86acd..446f74097f8 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -438,16 +438,16 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - A vector of commands - std::vector TerminalTab::BuildStartupActions() const + std::vector TerminalTab::BuildStartupActions(const bool asContent) const { // Give initial ids (0 for the child created with this tab, // 1 for the child after the first split. - auto state = _rootPane->BuildStartupActions(0, 1); + auto state = _rootPane->BuildStartupActions(0, 1, asContent); { ActionAndArgs newTabAction{}; newTabAction.Action(ShortcutAction::NewTab); - NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane() }; + NewTabArgs newTabArgs{ state.firstPane->GetTerminalArgsForPane(asContent) }; newTabAction.Args(newTabArgs); state.args.emplace(state.args.begin(), std::move(newTabAction)); @@ -783,6 +783,10 @@ namespace winrt::TerminalApp::implementation bool TerminalTab::FocusPane(const uint32_t id) { + if (_rootPane == nullptr) + { + return false; + } _changingActivePane = true; const auto res = _rootPane->FocusPane(id); _changingActivePane = false; @@ -1563,14 +1567,14 @@ namespace winrt::TerminalApp::implementation { auto hasReadOnly = false; auto allReadOnly = true; - _activePane->WalkTree([&](auto p) { + _activePane->WalkTree([&](const auto& p) { if (const auto& control{ p->GetTerminalControl() }) { hasReadOnly |= control.ReadOnly(); allReadOnly &= control.ReadOnly(); } }); - _activePane->WalkTree([&](auto p) { + _activePane->WalkTree([&](const auto& p) { if (const auto& control{ p->GetTerminalControl() }) { // If all controls have the same read only state then just toggle @@ -1587,6 +1591,38 @@ namespace winrt::TerminalApp::implementation }); } + // Method Description: + // - Set 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::SetPaneReadOnly(const bool readOnlyState) + { + auto hasReadOnly = false; + auto allReadOnly = true; + _activePane->WalkTree([&](const auto& p) { + if (const auto& control{ p->GetTerminalControl() }) + { + hasReadOnly |= control.ReadOnly(); + allReadOnly &= control.ReadOnly(); + } + }); + _activePane->WalkTree([&](const auto& p) { + if (const auto& control{ p->GetTerminalControl() }) + { + // If all controls have the same read only state then just disable + if (allReadOnly || !hasReadOnly) + { + control.SetReadOnly(readOnlyState); + } + // otherwise set to all read only. + else if (!control.ReadOnly()) + { + control.SetReadOnly(readOnlyState); + } + } + }); + } + // Method Description: // - Calculates if the tab is read-only. // The tab is considered read-only if one of the panes is read-only. diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 53492095db1..66ae9e63c8b 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -82,11 +82,13 @@ namespace winrt::TerminalApp::implementation void EnterZoom(); void ExitZoom(); - std::vector BuildStartupActions() const override; + std::vector BuildStartupActions(const bool asContent = false) const override; int GetLeafPaneCount() const noexcept; void TogglePaneReadOnly(); + void SetPaneReadOnly(const bool readOnlyState); + std::shared_ptr GetActivePane() const; winrt::TerminalApp::TaskbarState GetCombinedTaskbarState() const; diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp new file mode 100644 index 00000000000..dd984c6c126 --- /dev/null +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -0,0 +1,1393 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TerminalWindow.h" +#include "../inc/WindowingBehavior.h" +#include "TerminalWindow.g.cpp" +#include "SettingsLoadEventArgs.g.cpp" +#include "WindowProperties.g.cpp" + +#include +#include +#include + +#include "../../types/inc/utils.hpp" + +using namespace winrt::Windows::ApplicationModel; +using namespace winrt::Windows::ApplicationModel::DataTransfer; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::System; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Windows::Foundation::Collections; +using namespace ::TerminalApp; + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +// !!! IMPORTANT !!! +// Make sure that these keys are in the same order as the +// SettingsLoadWarnings/Errors enum is! +static const std::array settingsLoadWarningsLabels{ + USES_RESOURCE(L"MissingDefaultProfileText"), + USES_RESOURCE(L"DuplicateProfileText"), + USES_RESOURCE(L"UnknownColorSchemeText"), + USES_RESOURCE(L"InvalidBackgroundImage"), + USES_RESOURCE(L"InvalidIcon"), + USES_RESOURCE(L"AtLeastOneKeybindingWarning"), + USES_RESOURCE(L"TooManyKeysForChord"), + USES_RESOURCE(L"MissingRequiredParameter"), + USES_RESOURCE(L"FailedToParseCommandJson"), + USES_RESOURCE(L"FailedToWriteToSettings"), + USES_RESOURCE(L"InvalidColorSchemeInCmd"), + USES_RESOURCE(L"InvalidSplitSize"), + USES_RESOURCE(L"FailedToParseStartupActions"), + USES_RESOURCE(L"FailedToParseSubCommands"), + USES_RESOURCE(L"UnknownTheme"), + USES_RESOURCE(L"DuplicateRemainingProfilesEntry"), + USES_RESOURCE(L"InvalidUseOfContent"), +}; + +static_assert(settingsLoadWarningsLabels.size() == static_cast(SettingsLoadWarnings::WARNINGS_SIZE)); +// Errors are defined in AppLogic.cpp + +// Function Description: +// - General-purpose helper for looking up a localized string for a +// warning/error. First will look for the given key in the provided map of +// keys->strings, where the values in the map are ResourceKeys. If it finds +// one, it will lookup the localized string from that ResourceKey. +// - If it does not find a key, it'll return an empty string +// Arguments: +// - key: the value to use to look for a resource key in the given map +// - map: A map of keys->Resource keys. +// Return Value: +// - the localized string for the given type, if it exists. +template +winrt::hstring _GetMessageText(uint32_t index, const T& keys) +{ + if (index < keys.size()) + { + return GetLibraryResourceString(til::at(keys, index)); + } + return {}; +} + +// Function Description: +// - Gets the text from our ResourceDictionary for the given +// SettingsLoadWarning. If there is no such text, we'll return nullptr. +// - The warning should have an entry in settingsLoadWarningsLabels. +// Arguments: +// - warning: the SettingsLoadWarnings value to get the localized text for. +// Return Value: +// - localized text for the given warning +static winrt::hstring _GetWarningText(SettingsLoadWarnings warning) +{ + return _GetMessageText(static_cast(warning), settingsLoadWarningsLabels); +} + +// Function Description: +// - Creates a Run of text to display an error message. The text is yellow or +// red for dark/light theme, respectively. +// Arguments: +// - text: The text of the error message. +// - resources: The application's resource loader. +// Return Value: +// - The fully styled text run. +static Documents::Run _BuildErrorRun(const winrt::hstring& text, const ResourceDictionary& resources) +{ + Documents::Run textRun; + textRun.Text(text); + + // Color the text red (light theme) or yellow (dark theme) based on the system theme + auto key = winrt::box_value(L"ErrorTextBrush"); + if (resources.HasKey(key)) + { + auto g = resources.Lookup(key); + auto brush = g.try_as(); + textRun.Foreground(brush); + } + + return textRun; +} + +namespace winrt::TerminalApp::implementation +{ + TerminalWindow::TerminalWindow(const TerminalApp::SettingsLoadEventArgs& settingsLoadedResult, + const TerminalApp::ContentManager& manager) : + _settings{ settingsLoadedResult.NewSettings() }, + _manager{ manager }, + _initialLoadResult{ settingsLoadedResult }, + _WindowProperties{ winrt::make_self() } + { + // The TerminalPage has to ABSOLUTELY NOT BE constructed during our + // construction. We can't do ANY xaml till Initialize() is called. + + // For your own sanity, it's better to do setup outside the ctor. + // If you do any setup in the ctor that ends up throwing an exception, + // then it might look like App just failed to activate, which will + // cause you to chase down the rabbit hole of "why is App not + // registered?" when it definitely is. + } + + // Method Description: + // - Implements the IInitializeWithWindow interface from shobjidl_core. + HRESULT TerminalWindow::Initialize(HWND hwnd) + { + // Now that we know we can do XAML, build our page. + _root = winrt::make_self(*_WindowProperties, _manager); + _dialog = ContentDialog{}; + + // Pass in information about the initial state of the window. + // * If we were supposed to start from serialized "content", do that, + // * If we were supposed to load from a persisted layout, do that + // instead. + // * if we have commandline arguments, Pass commandline args into the + // TerminalPage. + if (!_initialContentArgs.empty()) + { + _root->SetStartupActions(_initialContentArgs); + } + else + { + // layout will only ever be non-null if there were >0 tabs persisted in + // .TabLayout(). We can re-evaluate that as a part of TODO: GH#12633 + if (const auto& layout = LoadPersistedLayout()) + { + std::vector actions; + for (const auto& a : layout.TabLayout()) + { + actions.emplace_back(a); + } + _root->SetStartupActions(actions); + } + else + { + _root->SetStartupActions(_appArgs.GetStartupActions()); + } + } + + // Check if we were started as a COM server for inbound connections of console sessions + // coming out of the operating system default application feature. If so, + // tell TerminalPage to start the listener as we have to make sure it has the chance + // to register a handler to hear about the requests first and is all ready to receive + // them before the COM server registers itself. Otherwise, the request might come + // in and be routed to an event with no handlers or a non-ready Page. + if (_appArgs.IsHandoffListener()) + { + _root->SetInboundListener(true); + } + + return _root->Initialize(hwnd); + } + + // Method Description: + // - Called around the codebase to discover if this is a UWP where we need to turn off specific settings. + // Arguments: + // - - reports internal state + // Return Value: + // - True if UWP, false otherwise. + bool TerminalWindow::IsUwp() const noexcept + { + // use C++11 magic statics to make sure we only do this once. + // This won't change over the lifetime of the application + + static const auto isUwp = []() { + // *** THIS IS A SINGLETON *** + auto result = 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 + try + { + result = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsUwp(); + } + CATCH_LOG(); + return result; + }(); + + return isUwp; + } + + // Method Description: + // - Build the UI for the terminal app. Before this method is called, it + // should not be assumed that the TerminalApp is usable. The Settings + // should be loaded before this is called, either with LoadSettings or + // GetLaunchDimensions (which will call LoadSettings) + // Arguments: + // - + // Return Value: + // - + void TerminalWindow::Create() + { + _root->DialogPresenter(*this); + + // Pay attention, that even if some command line arguments were parsed (like launch mode), + // we will not use the startup actions from settings. + // While this simplifies the logic, we might want to reconsider this behavior in the future. + if (!_hasCommandLineArguments && _gotSettingsStartupActions) + { + _root->SetStartupActions(_settingsStartupArgs); + } + + _root->SetSettings(_settings, false); // We're on our UI thread right now, so this is safe + _root->Loaded({ get_weak(), &TerminalWindow::_OnLoaded }); + + _root->Initialized([this](auto&&, auto&&) { + // GH#288 - When we finish initialization, if the user wanted us + // launched _fullscreen_, toggle fullscreen mode. This will make sure + // that the window size is _first_ set up as something sensible, so + // leaving fullscreen returns to a reasonable size. + const auto launchMode = this->GetLaunchMode(); + if (_WindowProperties->IsQuakeWindow() || WI_IsFlagSet(launchMode, LaunchMode::FocusMode)) + { + _root->SetFocusMode(true); + } + + // The IslandWindow handles (creating) the maximized state + // we just want to record it here on the page as well. + if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode)) + { + _root->Maximized(true); + } + + if (WI_IsFlagSet(launchMode, LaunchMode::FullscreenMode) && !_WindowProperties->IsQuakeWindow()) + { + _root->SetFullscreen(true); + } + }); + _root->Create(); + + _RefreshThemeRoutine(); + + auto args = winrt::make_self(RS_(L"SettingsMenuItem"), + SystemMenuChangeAction::Add, + SystemMenuItemHandler(this, &TerminalWindow::_OpenSettingsUI)); + _SystemMenuChangeRequestedHandlers(*this, *args); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "WindowCreated", + TraceLoggingDescription("Event emitted when the window is started"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + void TerminalWindow::Quit() + { + if (_root) + { + _root->CloseWindow(true); + } + } + + winrt::Windows::UI::Xaml::ElementTheme TerminalWindow::GetRequestedTheme() + { + return Theme().RequestedTheme(); + } + + bool TerminalWindow::GetShowTabsInTitlebar() + { + return _settings.GlobalSettings().ShowTabsInTitlebar(); + } + + bool TerminalWindow::GetInitialAlwaysOnTop() + { + return _settings.GlobalSettings().AlwaysOnTop(); + } + + bool TerminalWindow::GetMinimizeToNotificationArea() + { + return _settings.GlobalSettings().MinimizeToNotificationArea(); + } + + bool TerminalWindow::GetAlwaysShowNotificationIcon() + { + return _settings.GlobalSettings().AlwaysShowNotificationIcon(); + } + + bool TerminalWindow::GetShowTitleInTitlebar() + { + return _settings.GlobalSettings().ShowTitleInTitlebar(); + } + + Microsoft::Terminal::Settings::Model::Theme TerminalWindow::Theme() + { + return _settings.GlobalSettings().CurrentTheme(); + } + // Method Description: + // - Show a ContentDialog with buttons to take further action. Uses the + // FrameworkElements provided as the title and content of this dialog, and + // displays buttons (or a single button). Two buttons (primary and secondary) + // will be displayed if this is an warning dialog for closing the terminal, + // this allows the users to abandon the closing action. Otherwise, a single + // close button will be displayed. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. + // Arguments: + // - dialog: the dialog object that is going to show up + // Return value: + // - an IAsyncOperation with the dialog result + winrt::Windows::Foundation::IAsyncOperation TerminalWindow::ShowDialog(winrt::WUX::Controls::ContentDialog dialog) + { + // DON'T release this lock in a wil::scope_exit. The scope_exit will get + // called when we await, which is not what we want. + std::unique_lock lock{ _dialogLock, std::try_to_lock }; + if (!lock) + { + // Another dialog is visible. + co_return ContentDialogResult::None; + } + + _dialog = dialog; + + // IMPORTANT: This is necessary as documented in the ContentDialog MSDN docs. + // Since we're hosting the dialog in a Xaml island, we need to connect it to the + // xaml tree somehow. + dialog.XamlRoot(_root->XamlRoot()); + + // IMPORTANT: Set the requested theme of the dialog, because the + // PopupRoot isn't directly in the Xaml tree of our root. So the dialog + // won't inherit our RequestedTheme automagically. + // GH#5195, GH#3654 Because we cannot set RequestedTheme at the application level, + // we occasionally run into issues where parts of our UI end up themed incorrectly. + // Dialogs, for example, live under a different Xaml root element than the rest of + // our application. This makes our popup menus and buttons "disappear" when the + // user wants Terminal to be in a different theme than the rest of the system. + // This hack---and it _is_ a hack--walks up a dialog's ancestry and forces the + // theme on each element up to the root. We're relying a bit on Xaml's implementation + // details here, but it does have the desired effect. + // It's not enough to set the theme on the dialog alone. + auto themingLambda{ [this](const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&) { + auto theme{ _settings.GlobalSettings().CurrentTheme() }; + auto requestedTheme{ theme.RequestedTheme() }; + auto element{ sender.try_as() }; + while (element) + { + element.RequestedTheme(requestedTheme); + element = element.Parent().try_as(); + } + } }; + + themingLambda(dialog, nullptr); // if it's already in the tree + auto loadedRevoker{ dialog.Loaded(winrt::auto_revoke, themingLambda) }; // if it's not yet in the tree + + // Display the dialog. + co_return co_await dialog.ShowAsync(Controls::ContentDialogPlacement::Popup); + + // After the dialog is dismissed, the dialog lock (held by `lock`) will + // be released so another can be shown + } + + // Method Description: + // - Dismiss the (only) visible ContentDialog + void TerminalWindow::DismissDialog() + { + if (auto localDialog = std::exchange(_dialog, nullptr)) + { + localDialog.Hide(); + } + } + + // Method Description: + // - Displays a dialog for errors found while loading or validating the + // settings. Uses the resources under the provided title and content keys + // as the title and first content of the dialog, then also displays a + // message for whatever exception was found while validating the settings. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See ShowDialog for details + // Arguments: + // - titleKey: The key to use to lookup the title text from our resources. + // - contentKey: The key to use to lookup the content text from our resources. + void TerminalWindow::_ShowLoadErrorsDialog(const winrt::hstring& titleKey, + const winrt::hstring& contentKey, + HRESULT settingsLoadedResult, + const winrt::hstring& exceptionText) + { + auto title = GetLibraryResourceString(titleKey); + auto buttonText = RS_(L"Ok"); + + Controls::TextBlock warningsTextBlock; + // Make sure you can copy-paste + warningsTextBlock.IsTextSelectionEnabled(true); + // Make sure the lines of text wrap + warningsTextBlock.TextWrapping(TextWrapping::Wrap); + + winrt::Windows::UI::Xaml::Documents::Run errorRun; + const auto errorLabel = GetLibraryResourceString(contentKey); + errorRun.Text(errorLabel); + warningsTextBlock.Inlines().Append(errorRun); + warningsTextBlock.Inlines().Append(Documents::LineBreak{}); + + if (FAILED(settingsLoadedResult)) + { + if (!exceptionText.empty()) + { + warningsTextBlock.Inlines().Append(_BuildErrorRun(exceptionText, + winrt::WUX::Application::Current().as<::winrt::TerminalApp::App>().Resources())); + warningsTextBlock.Inlines().Append(Documents::LineBreak{}); + } + } + + // Add a note that we're using the default settings in this case. + winrt::Windows::UI::Xaml::Documents::Run usingDefaultsRun; + const auto usingDefaultsText = RS_(L"UsingDefaultSettingsText"); + usingDefaultsRun.Text(usingDefaultsText); + warningsTextBlock.Inlines().Append(Documents::LineBreak{}); + warningsTextBlock.Inlines().Append(usingDefaultsRun); + + Controls::ContentDialog dialog; + dialog.Title(winrt::box_value(title)); + dialog.Content(winrt::box_value(warningsTextBlock)); + dialog.CloseButtonText(buttonText); + dialog.DefaultButton(Controls::ContentDialogButton::Close); + + ShowDialog(dialog); + } + + // Method Description: + // - Displays a dialog for warnings found while loading or validating the + // settings. Displays messages for whatever warnings were found while + // validating the settings. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See ShowDialog for details + void TerminalWindow::_ShowLoadWarningsDialog(const Windows::Foundation::Collections::IVector& warnings) + { + auto title = RS_(L"SettingsValidateErrorTitle"); + auto buttonText = RS_(L"Ok"); + + Controls::TextBlock warningsTextBlock; + // Make sure you can copy-paste + warningsTextBlock.IsTextSelectionEnabled(true); + // Make sure the lines of text wrap + warningsTextBlock.TextWrapping(TextWrapping::Wrap); + + for (const auto& warning : warnings) + { + // Try looking up the warning message key for each warning. + const auto warningText = _GetWarningText(warning); + if (!warningText.empty()) + { + warningsTextBlock.Inlines().Append(_BuildErrorRun(warningText, winrt::WUX::Application::Current().as<::winrt::TerminalApp::App>().Resources())); + warningsTextBlock.Inlines().Append(Documents::LineBreak{}); + } + } + + Controls::ContentDialog dialog; + dialog.Title(winrt::box_value(title)); + dialog.Content(winrt::box_value(warningsTextBlock)); + dialog.CloseButtonText(buttonText); + dialog.DefaultButton(Controls::ContentDialogButton::Close); + + ShowDialog(dialog); + } + + // Method Description: + // - Triggered when the application is finished loading. If we failed to load + // the settings, then this will display the error dialog. This is done + // here instead of when loading the settings, because we need our UI to be + // visible to display the dialog, and when we're loading the settings, + // the UI might not be visible yet. + // Arguments: + // - + void TerminalWindow::_OnLoaded(const IInspectable& /*sender*/, + const RoutedEventArgs& /*eventArgs*/) + { + if (_settings.GlobalSettings().InputServiceWarning()) + { + const auto keyboardServiceIsDisabled = !_IsKeyboardServiceEnabled(); + if (keyboardServiceIsDisabled) + { + _root->ShowKeyboardServiceWarning(); + } + } + + const auto& settingsLoadedResult = gsl::narrow_cast(_initialLoadResult.Result()); + if (FAILED(settingsLoadedResult)) + { + const winrt::hstring titleKey = USES_RESOURCE(L"InitialJsonParseErrorTitle"); + const winrt::hstring textKey = USES_RESOURCE(L"InitialJsonParseErrorText"); + _ShowLoadErrorsDialog(titleKey, textKey, settingsLoadedResult, _initialLoadResult.ExceptionText()); + } + else if (settingsLoadedResult == S_FALSE) + { + _ShowLoadWarningsDialog(_initialLoadResult.Warnings()); + } + } + + // Method Description: + // - Helper for determining if the "Touch Keyboard and Handwriting Panel + // Service" is enabled. If it isn't, we want to be able to display a + // warning to the user, because they won't be able to type in the + // Terminal. + // Return Value: + // - true if the service is enabled, or if we fail to query the service. We + // return true in that case, to be less noisy (though, that is unexpected) + bool TerminalWindow::_IsKeyboardServiceEnabled() + { + if (IsUwp()) + { + return true; + } + + // If at any point we fail to open the service manager, the service, + // etc, then just quick return true to disable the dialog. We'd rather + // not be noisy with this dialog if we failed for some reason. + + // Open the service manager. This will return 0 if it failed. + wil::unique_schandle hManager{ OpenSCManagerW(nullptr, nullptr, 0) }; + + if (LOG_LAST_ERROR_IF(!hManager.is_valid())) + { + return true; + } + + // Get a handle to the keyboard service + wil::unique_schandle hService{ OpenServiceW(hManager.get(), TabletInputServiceKey.data(), SERVICE_QUERY_STATUS) }; + + // Windows 11 doesn't have a TabletInputService. + // (It was renamed to TextInputManagementService, because people kept thinking that a + // service called "tablet-something" is system-irrelevant on PCs and can be disabled.) + if (!hService.is_valid()) + { + return true; + } + + // Get the current state of the service + SERVICE_STATUS status{ 0 }; + if (!LOG_IF_WIN32_BOOL_FALSE(QueryServiceStatus(hService.get(), &status))) + { + return true; + } + + const auto state = status.dwCurrentState; + return (state == SERVICE_RUNNING || state == SERVICE_START_PENDING); + } + + // Method Description: + // - Get the size in pixels of the client area we'll need to launch this + // terminal app. This method will use the default profile's settings to do + // this calculation, as well as the _system_ dpi scaling. See also + // TermControl::GetProposedDimensions. + // Arguments: + // - + // Return Value: + // - a point containing the requested dimensions in pixels. + winrt::Windows::Foundation::Size TerminalWindow::GetLaunchDimensions(uint32_t dpi) + { + winrt::Windows::Foundation::Size proposedSize{}; + + const auto scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + if (const auto layout = LoadPersistedLayout()) + { + if (layout.InitialSize()) + { + 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; + proposedSize.Width = proposedSize.Width * scale; + } + } + + if (_appArgs.GetSize().has_value() || (proposedSize.Width == 0 && proposedSize.Height == 0)) + { + // Use the default profile to determine how big of a window we need. + const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) }; + + const til::size emptySize{}; + const auto commandlineSize = _appArgs.GetSize().value_or(emptySize); + proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(), + dpi, + commandlineSize.width, + commandlineSize.height); + } + + if (_contentBounds) + { + // If we've been created as a torn-out window, then we'll need to + // use that size instead. _contentBounds is in raw pixels. Huzzah! + // Just return that. + return { + _contentBounds.Value().Width, + _contentBounds.Value().Height + }; + } + + // GH#2061 - If the global setting "Always show tab bar" is + // set or if "Show tabs in title bar" is set, then we'll need to add + // the height of the tab bar here. + if (_settings.GlobalSettings().ShowTabsInTitlebar()) + { + // In the past, we used to actually instantiate a TitlebarControl + // and use Measure() to determine the DesiredSize of the control, to + // reserve exactly what we'd need. + // + // We can't do that anymore, because this is now called _before_ + // we've initialized XAML for this thread. We can't start XAML till + // we have an HWND, and we can't finish creating the window till we + // know how big it should be. + // + // Instead, we'll just hardcode how big the titlebar should be. If + // the titlebar / tab row ever change size, these numbers will have + // to change accordingly. + + static constexpr auto titlebarHeight = 40; + proposedSize.Height += (titlebarHeight)*scale; + } + else if (_settings.GlobalSettings().AlwaysShowTabs()) + { + // Same comment as above, but with a TabRowControl. + // + // A note from before: For whatever reason, there's about 10px of + // unaccounted-for space in the application. I couldn't tell you + // where these 10px are coming from, but they need to be included in + // this math. + static constexpr auto tabRowHeight = 32; + proposedSize.Height += (tabRowHeight + 10) * scale; + } + + return proposedSize; + } + + // Method Description: + // - Get the launch mode in json settings file. Now there + // two launch mode: default, maximized. Default means the window + // will launch according to the launch dimensions provided. Maximized + // means the window will launch as a maximized window + // Arguments: + // - + // Return Value: + // - LaunchMode enum that indicates the launch mode + LaunchMode TerminalWindow::GetLaunchMode() + { + if (_contentBounds) + { + return LaunchMode::DefaultMode; + } + + // GH#4620/#5801 - If the user passed --maximized or --fullscreen on the + // commandline, then use that to override the value from the settings. + const auto valueFromSettings = _settings.GlobalSettings().LaunchMode(); + const auto valueFromCommandlineArgs = _appArgs.GetLaunchMode(); + if (const auto layout = LoadPersistedLayout()) + { + if (layout.LaunchMode()) + { + return layout.LaunchMode().Value(); + } + } + return valueFromCommandlineArgs.has_value() ? + valueFromCommandlineArgs.value() : + valueFromSettings; + } + + // Method Description: + // - Get the user defined initial position from Json settings file. + // This position represents the top left corner of the Terminal window. + // This setting is optional, if not provided, we will use the system + // default size, which is provided in IslandWindow::MakeWindow. + // Arguments: + // - defaultInitialX: the system default x coordinate value + // - defaultInitialY: the system default y coordinate value + // Return Value: + // - a point containing the requested initial position in pixels. + TerminalApp::InitialPosition TerminalWindow::GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY) + { + auto initialPosition{ _settings.GlobalSettings().InitialPosition() }; + + if (const auto layout = LoadPersistedLayout()) + { + if (layout.InitialPosition()) + { + initialPosition = layout.InitialPosition().Value(); + } + } + + // Commandline args trump everything except for content bounds (tear-out) + if (_appArgs.GetPosition().has_value()) + { + initialPosition = _appArgs.GetPosition().value(); + } + + if (_contentBounds) + { + // If the user has specified a contentBounds, then we should use + // that to determine the initial position of the window. This is + // used when the user is dragging a tab out of the window, to create + // a new window. + // + // contentBounds is in screen pixels, but that's okay! we want to + // return screen pixels out of here. Nailed it. + const til::rect bounds = { til::math::rounding, _contentBounds.Value() }; + initialPosition = { bounds.left, bounds.top }; + } + return { + initialPosition.X ? initialPosition.X.Value() : defaultInitialX, + initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY + }; + } + + bool TerminalWindow::CenterOnLaunch() + { + // If + // * the position has been specified on the commandline, + // * We're opening the window as a part of tear out (and _contentBounds were set) + // then don't center on launch + return !_contentBounds && _settings.GlobalSettings().CenterOnLaunch() && !_appArgs.GetPosition().has_value(); + } + + // Method Description: + // - See Pane::CalcSnappedDimension + float TerminalWindow::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const + { + return _root->CalcSnappedDimension(widthOrHeight, dimension); + } + + // Method Description: + // - Update the current theme of the application. This will trigger our + // RequestedThemeChanged event, to have our host change the theme of the + // root of the application. + // Arguments: + // - newTheme: The ElementTheme to apply to our elements. + void TerminalWindow::_RefreshThemeRoutine() + { + // Propagate the event to the host layer, so it can update its own UI + _RequestedThemeChangedHandlers(*this, Theme()); + } + + // This may be called on a background thread, or the main thread, but almost + // definitely not on OUR UI thread. + winrt::fire_and_forget TerminalWindow::UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args) + { + _settings = args.NewSettings(); + + const auto weakThis{ get_weak() }; + co_await wil::resume_foreground(_root->Dispatcher()); + // Back on our UI thread... + if (auto logic{ weakThis.get() }) + { + // Update the settings in TerminalPage + // We're on our UI thread right now, so this is safe + _root->SetSettings(_settings, true); + + // Bubble the notification up to the AppHost, now that we've updated our _settings. + _SettingsChangedHandlers(*this, args); + + if (FAILED(args.Result())) + { + const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle"); + const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText"); + _ShowLoadErrorsDialog(titleKey, + textKey, + gsl::narrow_cast(args.Result()), + args.ExceptionText()); + co_return; + } + else if (args.Result() == S_FALSE) + { + _ShowLoadWarningsDialog(args.Warnings()); + } + _RefreshThemeRoutine(); + } + } + + void TerminalWindow::_OpenSettingsUI() + { + _root->OpenSettingsUI(); + } + UIElement TerminalWindow::GetRoot() noexcept + { + return _root.as(); + } + + // Method Description: + // - Gets the title of the currently focused terminal control. If there + // isn't a control selected for any reason, returns "Terminal" + // Arguments: + // - + // Return Value: + // - the title of the focused control if there is one, else "Terminal" + hstring TerminalWindow::Title() + { + if (_root) + { + return _root->Title(); + } + return { L"Terminal" }; + } + + // Method Description: + // - Used to tell the app that the titlebar has been clicked. The App won't + // actually receive any clicks in the titlebar area, so this is a helper + // to clue the app in that a click has happened. The App will use this as + // a indicator that it needs to dismiss any open flyouts. + // Arguments: + // - + // Return Value: + // - + void TerminalWindow::TitlebarClicked() + { + if (_root) + { + _root->TitlebarClicked(); + } + } + + // Method Description: + // - Used to tell the PTY connection that the window visibility has changed. + // The underlying PTY might need to expose window visibility status to the + // client application for the `::GetConsoleWindow()` API. + // Arguments: + // - showOrHide - True is show; false is hide. + // Return Value: + // - + void TerminalWindow::WindowVisibilityChanged(const bool showOrHide) + { + if (_root) + { + _root->WindowVisibilityChanged(showOrHide); + } + } + + // Method Description: + // - Implements the F7 handler (per GH#638) + // - Implements the Alt handler (per GH#6421) + // Return value: + // - whether the key was handled + bool TerminalWindow::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) + { + if (_root) + { + // Manually bubble the OnDirectKeyEvent event up through the focus tree. + auto xamlRoot{ _root->XamlRoot() }; + auto focusedObject{ Windows::UI::Xaml::Input::FocusManager::GetFocusedElement(xamlRoot) }; + do + { + if (auto keyListener{ focusedObject.try_as() }) + { + if (keyListener.OnDirectKeyEvent(vkey, scanCode, down)) + { + return true; + } + // otherwise, keep walking. bubble the event manually. + } + + if (auto focusedElement{ focusedObject.try_as() }) + { + focusedObject = focusedElement.Parent(); + + // Parent() seems to return null when the focusedElement is created from an ItemTemplate. + // Use the VisualTreeHelper's GetParent as a fallback. + if (!focusedObject) + { + focusedObject = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(focusedElement); + } + } + else + { + break; // we hit a non-FE object, stop bubbling. + } + } while (focusedObject); + } + return false; + } + + // Method Description: + // - Used to tell the app that the 'X' button has been clicked and + // the user wants to close the app. We kick off the close warning + // experience. + // Arguments: + // - + // Return Value: + // - + void TerminalWindow::CloseWindow(LaunchPosition pos, const bool isLastWindow) + { + if (_root) + { + // If persisted layout is enabled and we are the last window closing + // we should save our state. + if (_settings.GlobalSettings().ShouldUsePersistedLayout() && isLastWindow) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(pos); + const auto state = ApplicationState::SharedInstance(); + state.PersistedWindowLayouts(winrt::single_threaded_vector({ layout })); + } + } + + _root->CloseWindow(false); + } + } + + void TerminalWindow::ClearPersistedWindowState() + { + if (_settings.GlobalSettings().ShouldUsePersistedLayout()) + { + auto state = ApplicationState::SharedInstance(); + state.PersistedWindowLayouts(nullptr); + } + } + + winrt::TerminalApp::TaskbarState TerminalWindow::TaskbarState() + { + if (_root) + { + return _root->TaskbarState(); + } + return {}; + } + + winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::TitlebarBrush() + { + if (_root) + { + return _root->TitlebarBrush(); + } + return { nullptr }; + } + void TerminalWindow::WindowActivated(const bool activated) + { + if (_root) + { + _root->WindowActivated(activated); + } + } + + // Method Description: + // - Returns true if we should exit the application before even starting the + // window. We might want to do this if we're displaying an error message or + // the version string, or if we want to open the settings file. + // Arguments: + // - + // Return Value: + // - true iff we should exit the application before even starting the window + bool TerminalWindow::ShouldExitEarly() + { + return _appArgs.ShouldExitEarly(); + } + + bool TerminalWindow::FocusMode() const + { + return _root ? _root->FocusMode() : false; + } + + bool TerminalWindow::Fullscreen() const + { + return _root ? _root->Fullscreen() : false; + } + + void TerminalWindow::Maximized(bool newMaximized) + { + if (_root) + { + _root->Maximized(newMaximized); + } + } + + bool TerminalWindow::AlwaysOnTop() const + { + return _root ? _root->AlwaysOnTop() : false; + } + + void TerminalWindow::SetSettingsStartupArgs(const std::vector& actions) + { + for (const auto& action : actions) + { + _settingsStartupArgs.push_back(action); + } + _gotSettingsStartupActions = true; + } + + bool TerminalWindow::HasCommandlineArguments() const noexcept + { + return _hasCommandLineArguments; + } + + // 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 + // will be processed on TerminalPage::Create(). + // - This function will have no effective result after Create() is called. + // - This function returns 0, unless a there was a non-zero result from + // trying to parse one of the commands provided. In that case, no commands + // after the failing command will be parsed, and the non-zero code + // returned. + // Arguments: + // - args: an array of strings to process as a commandline. These args can contain spaces + // Return Value: + // - the result of the first command who's parsing returned a non-zero code, + // or 0. (see TerminalWindow::_ParseArgs) + int32_t TerminalWindow::SetStartupCommandline(array_view args) + { + // This is called in AppHost::ctor(), before we've created the window + // (or called TerminalWindow::Initialize) + const auto result = _appArgs.ParseArgs(args); + if (result == 0) + { + // If the size of the arguments list is 1, + // then it contains only the executable name and no other arguments. + _hasCommandLineArguments = args.size() > 1; + _appArgs.ValidateStartupCommands(); + + // DON'T pass the args into the page yet. It doesn't exist yet. + // Instead, we'll handle that in Initialize, when we first instantiate the page. + } + + // If we have a -s param passed to us to load a saved layout, cache that now. + if (const auto idx = _appArgs.GetPersistedLayoutIdx()) + { + SetPersistedLayoutIdx(idx.value()); + } + + return result; + } + + void TerminalWindow::SetStartupContent(const winrt::hstring& content, + const Windows::Foundation::IReference& bounds) + { + _contentBounds = bounds; + + const auto& args = _contentStringToActions(content, true); + + for (const auto& action : args) + { + _initialContentArgs.push_back(action); + } + } + + // Method Description: + // - Parse the provided commandline arguments into actions, and try to + // perform them immediately. + // - This function returns 0, unless a there was a non-zero result from + // trying to parse one of the commands provided. In that case, no commands + // after the failing command will be parsed, and the non-zero code + // returned. + // - If a non-empty cwd is provided, the entire terminal exe will switch to + // that CWD while we handle these actions, then return to the original + // CWD. + // Arguments: + // - args: an array of strings to process as a commandline. These args can contain spaces + // - cwd: The directory to use as the CWD while performing these actions. + // Return Value: + // - the result of the first command who's parsing returned a non-zero code, + // or 0. (see TerminalWindow::_ParseArgs) + int32_t TerminalWindow::ExecuteCommandline(array_view args, + const winrt::hstring& cwd) + { + ::TerminalApp::AppCommandlineArgs appArgs; + auto result = appArgs.ParseArgs(args); + if (result == 0) + { + auto actions = winrt::single_threaded_vector(std::move(appArgs.GetStartupActions())); + + _root->ProcessStartupActions(actions, false, cwd); + + if (appArgs.IsHandoffListener()) + { + _root->SetInboundListener(true); + } + } + // Return the result of parsing with commandline, though it may or may not be used. + return result; + } + + // Method Description: + // - If there were any errors parsing the commandline that was used to + // initialize the terminal, this will return a string containing that + // message. If there were no errors, this message will be blank. + // - If the user requested help on any command (using --help), this will + // contain the help message. + // - If the user requested the version number (using --version), this will + // contain the version string. + // Arguments: + // - + // Return Value: + // - the help text or error message for the provided commandline, if one + // exists, otherwise the empty string. + winrt::hstring TerminalWindow::ParseCommandlineMessage() + { + return winrt::to_hstring(_appArgs.GetExitMessage()); + } + + hstring TerminalWindow::GetWindowLayoutJson(LaunchPosition position) + { + if (_root != nullptr) + { + if (const auto layout = _root->GetWindowLayout()) + { + layout.InitialPosition(position); + return WindowLayout::ToJson(layout); + } + } + return L""; + } + + void TerminalWindow::SetPersistedLayoutIdx(const uint32_t idx) + { + _loadFromPersistedLayoutIdx = idx; + _cachedLayout = std::nullopt; + } + + // 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 TerminalWindow::LoadPersistedLayoutIdx() const + { + return _settings.GlobalSettings().ShouldUsePersistedLayout() ? _loadFromPersistedLayoutIdx : std::nullopt; + } + + WindowLayout TerminalWindow::LoadPersistedLayout() + { + if (_cachedLayout.has_value()) + { + return *_cachedLayout; + } + + if (const auto idx = LoadPersistedLayoutIdx()) + { + const auto i = idx.value(); + const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts(); + if (layouts && layouts.Size() > i) + { + auto layout = layouts.GetAt(i); + + // TODO: GH#12633: Right now, we're manually making sure that we + // have at least one tab to restore. If we ever want to come + // back and make it so that you can persist position and size, + // but not the tabs themselves, we can revisit this assumption. + _cachedLayout = (layout.TabLayout() && layout.TabLayout().Size() > 0) ? layout : nullptr; + return *_cachedLayout; + } + } + _cachedLayout = nullptr; + return *_cachedLayout; + } + + void TerminalWindow::RequestExitFullscreen() + { + _root->SetFullscreen(false); + } + + bool TerminalWindow::AutoHideWindow() + { + return _settings.GlobalSettings().AutoHideWindow(); + } + + void TerminalWindow::UpdateSettingsHandler(const winrt::IInspectable& /*sender*/, + const winrt::TerminalApp::SettingsLoadEventArgs& args) + { + UpdateSettings(args); + } + + void TerminalWindow::IdentifyWindow() + { + if (_root) + { + _root->IdentifyWindow(); + } + } + + void TerminalWindow::RenameFailed() + { + if (_root) + { + _root->RenameFailed(); + } + } + + void TerminalWindow::WindowName(const winrt::hstring& name) + { + const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow(); + _WindowProperties->WindowName(name); + if (!_root) + { + return; + } + const auto newIsQuakeMode = _WindowProperties->IsQuakeWindow(); + if (newIsQuakeMode != oldIsQuakeMode) + { + // If we're entering Quake Mode from ~Focus Mode, then this will enter Focus Mode + // If we're entering Quake Mode from Focus Mode, then this will do nothing + // If we're leaving Quake Mode (we're already in Focus Mode), then this will do nothing + _root->SetFocusMode(true); + _IsQuakeWindowChangedHandlers(*this, nullptr); + } + } + void TerminalWindow::WindowId(const uint64_t& id) + { + _WindowProperties->WindowId(id); + } + + // Method Description: + // - Deserialize this string of content into a list of actions to perform. + // If replaceFirstWithNewTab is true and the first serialized action is a + // `splitPane` action, we'll attempt to replace that action with the + // equivalent `newTab` action. + IVector TerminalWindow::_contentStringToActions(const winrt::hstring& content, + const bool replaceFirstWithNewTab) + { + try + { + const auto& args = ActionAndArgs::Deserialize(content); + if (args == nullptr || + args.Size() == 0) + { + return args; + } + + const auto& firstAction = args.GetAt(0); + const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane }; + if (replaceFirstWithNewTab && + firstIsSplitPane) + { + // Create the equivalent NewTab action. + const auto newAction = Settings::Model::ActionAndArgs{ Settings::Model::ShortcutAction::NewTab, + Settings::Model::NewTabArgs(firstAction.Args() ? + firstAction.Args().try_as().TerminalArgs() : + nullptr) }; + args.SetAt(0, newAction); + } + + return args; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + + return nullptr; + } + + void TerminalWindow::AttachContent(winrt::hstring content, uint32_t tabIndex) + { + if (_root) + { + // `splitPane` allows the user to specify which tab to split. In that + // case, split specifically the requested pane. + // + // If there's not enough tabs, then just turn this pane into a new tab. + // + // If the first action is `newTab`, the index is always going to be 0, + // so don't do anything in that case. + + const bool replaceFirstWithNewTab = tabIndex >= _root->NumberOfTabs(); + + const auto& args = _contentStringToActions(content, replaceFirstWithNewTab); + + _root->AttachContent(args, tabIndex); + } + } + void TerminalWindow::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args) + { + if (_root) + { + _root->SendContentToOther(args); + } + } + + bool TerminalWindow::ShouldImmediatelyHandoffToElevated() + { + return _root != nullptr ? _root->ShouldImmediatelyHandoffToElevated(_settings) : false; + } + + // Method Description: + // - Escape hatch for immediately dispatching requests to elevated windows + // when first launched. At this point in startup, the window doesn't exist + // yet, XAML hasn't been started, but we need to dispatch these actions. + // We can't just go through ProcessStartupActions, because that processes + // the actions async using the XAML dispatcher (which doesn't exist yet) + // - DON'T CALL THIS if you haven't already checked + // ShouldImmediatelyHandoffToElevated. If you're thinking about calling + // this outside of the one place it's used, that's probably the wrong + // solution. + // Arguments: + // - settings: the settings we should use for dispatching these actions. At + // this point in startup, we hadn't otherwise been initialized with these, + // so use them now. + // Return Value: + // - + void TerminalWindow::HandoffToElevated() + { + if (_root) + { + _root->HandoffToElevated(_settings); + return; + } + } + + winrt::hstring WindowProperties::WindowName() const noexcept + { + return _WindowName; + } + + void WindowProperties::WindowName(const winrt::hstring& value) + { + if (_WindowName != value) + { + _WindowName = value; + // If we get initialized with a window name, this will be called + // before XAML is stood up, and constructing a + // PropertyChangedEventArgs will throw. + try + { + _PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowName" }); + _PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowNameForDisplay" }); + } + CATCH_LOG(); + } + } + uint64_t WindowProperties::WindowId() const noexcept + { + return _WindowId; + } + + void WindowProperties::WindowId(const uint64_t& value) + { + _WindowId = value; + } + + // Method Description: + // - Returns a label like "Window: 1234" for the ID of this window + // Arguments: + // - + // Return Value: + // - a string for displaying the name of the window. + winrt::hstring WindowProperties::WindowIdForDisplay() const noexcept + { + return winrt::hstring{ fmt::format(L"{}: {}", + std::wstring_view(RS_(L"WindowIdLabel")), + _WindowId) }; + } + + // Method Description: + // - Returns a label like "" when the window has no name, or the name of the window. + // Arguments: + // - + // Return Value: + // - a string for displaying the name of the window. + winrt::hstring WindowProperties::WindowNameForDisplay() const noexcept + { + return _WindowName.empty() ? + winrt::hstring{ fmt::format(L"<{}>", RS_(L"UnnamedWindowName")) } : + _WindowName; + } + + bool WindowProperties::IsQuakeWindow() const noexcept + { + return _WindowName == QuakeWindowName; + } + +}; diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h new file mode 100644 index 00000000000..adb2c9b6638 --- /dev/null +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "TerminalWindow.g.h" +#include "SystemMenuChangeArgs.g.h" +#include "WindowProperties.g.h" + +#include "SettingsLoadEventArgs.h" +#include "TerminalPage.h" +#include "SettingsLoadEventArgs.h" + +#include +#include + +#ifdef UNIT_TESTING +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class CommandlineTest; +}; +#endif + +namespace winrt::TerminalApp::implementation +{ + struct SystemMenuChangeArgs : SystemMenuChangeArgsT + { + WINRT_PROPERTY(winrt::hstring, Name, L""); + WINRT_PROPERTY(SystemMenuChangeAction, Action, SystemMenuChangeAction::Add); + WINRT_PROPERTY(SystemMenuItemHandler, Handler, nullptr); + + public: + SystemMenuChangeArgs(const winrt::hstring& name, SystemMenuChangeAction action, SystemMenuItemHandler handler = nullptr) : + _Name{ name }, _Action{ action }, _Handler{ handler } {}; + }; + + struct WindowProperties : WindowPropertiesT + { + // Normally, WindowName and WindowId would be + // WINRT_OBSERVABLE_PROPERTY's, but we want them to raise + // WindowNameForDisplay and WindowIdForDisplay instead + winrt::hstring WindowName() const noexcept; + void WindowName(const winrt::hstring& value); + uint64_t WindowId() const noexcept; + void WindowId(const uint64_t& value); + winrt::hstring WindowIdForDisplay() const noexcept; + winrt::hstring WindowNameForDisplay() const noexcept; + bool IsQuakeWindow() const noexcept; + + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + + private: + winrt::hstring _WindowName{}; + uint64_t _WindowId{ 0 }; + }; + + struct TerminalWindow : TerminalWindowT + { + public: + TerminalWindow(const TerminalApp::SettingsLoadEventArgs& settingsLoadedResult, const TerminalApp::ContentManager& manager); + ~TerminalWindow() = default; + + STDMETHODIMP Initialize(HWND hwnd); + + void Create(); + + bool IsUwp() const noexcept; + + void Quit(); + + winrt::fire_and_forget UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); + + bool HasCommandlineArguments() const noexcept; + + int32_t SetStartupCommandline(array_view actions); + void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference& contentBounds); + int32_t ExecuteCommandline(array_view actions, const winrt::hstring& cwd); + void SetSettingsStartupArgs(const std::vector& actions); + winrt::hstring ParseCommandlineMessage(); + bool ShouldExitEarly(); + + bool ShouldImmediatelyHandoffToElevated(); + void HandoffToElevated(); + + bool FocusMode() const; + bool Fullscreen() const; + void Maximized(bool newMaximized); + bool AlwaysOnTop() const; + bool AutoHideWindow(); + + hstring GetWindowLayoutJson(Microsoft::Terminal::Settings::Model::LaunchPosition position); + + void IdentifyWindow(); + void RenameFailed(); + + std::optional LoadPersistedLayoutIdx() const; + winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout(); + + void SetPersistedLayoutIdx(const uint32_t idx); + void SetNumberOfOpenWindows(const uint64_t num); + bool ShouldUsePersistedLayout() const; + void ClearPersistedWindowState(); + + void RequestExitFullscreen(); + + Windows::Foundation::Size GetLaunchDimensions(uint32_t dpi); + bool CenterOnLaunch(); + TerminalApp::InitialPosition GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY); + winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); + Microsoft::Terminal::Settings::Model::LaunchMode GetLaunchMode(); + bool GetShowTabsInTitlebar(); + bool GetInitialAlwaysOnTop(); + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; + + Windows::UI::Xaml::UIElement GetRoot() noexcept; + + hstring Title(); + void TitlebarClicked(); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + + void CloseWindow(Microsoft::Terminal::Settings::Model::LaunchPosition position, const bool isLastWindow); + void WindowVisibilityChanged(const bool showOrHide); + + winrt::TerminalApp::TaskbarState TaskbarState(); + winrt::Windows::UI::Xaml::Media::Brush TitlebarBrush(); + void WindowActivated(const bool activated); + + bool GetMinimizeToNotificationArea(); + bool GetAlwaysShowNotificationIcon(); + + bool GetShowTitleInTitlebar(); + + winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); + void DismissDialog(); + + Microsoft::Terminal::Settings::Model::Theme Theme(); + void UpdateSettingsHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::SettingsLoadEventArgs& arg); + + void WindowName(const winrt::hstring& value); + void WindowId(const uint64_t& value); + + bool IsQuakeWindow() const noexcept { return _WindowProperties->IsQuakeWindow(); } + TerminalApp::WindowProperties WindowProperties() { return *_WindowProperties; } + + void AttachContent(winrt::hstring content, uint32_t tabIndex); + void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); + + // -------------------------------- WinRT Events --------------------------------- + // PropertyChanged is surprisingly not a typed event, so we'll define that one manually. + // Usually we'd just do + // WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + // + // But what we're doing here is exposing the Page's PropertyChanged _as + // our own event_. It's a FORWARDED_CALLBACK, essentially. + winrt::event_token PropertyChanged(Windows::UI::Xaml::Data::PropertyChangedEventHandler const& handler) { return _root->PropertyChanged(handler); } + void PropertyChanged(winrt::event_token const& token) { _root->PropertyChanged(token); } + + TYPED_EVENT(RequestedThemeChanged, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Settings::Model::Theme); + + private: + // If you add controls here, but forget to null them either here or in + // the ctor, you're going to have a bad time. It'll mysteriously fail to + // activate the AppLogic. + // ALSO: If you add any UIElements as roots here, make sure they're + // updated in _ApplyTheme. The root currently is _root. + winrt::com_ptr _root{ nullptr }; + winrt::Windows::UI::Xaml::Controls::ContentDialog _dialog{ nullptr }; + std::shared_mutex _dialogLock; + + bool _hasCommandLineArguments{ false }; + ::TerminalApp::AppCommandlineArgs _appArgs; + bool _gotSettingsStartupActions{ false }; + std::vector _settingsStartupArgs{}; + Windows::Foundation::IReference _contentBounds{ nullptr }; + + winrt::com_ptr _WindowProperties{ nullptr }; + + std::optional _loadFromPersistedLayoutIdx{}; + std::optional _cachedLayout{ std::nullopt }; + + Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; + TerminalApp::SettingsLoadEventArgs _initialLoadResult{ nullptr }; + + TerminalApp::ContentManager _manager{ nullptr }; + std::vector _initialContentArgs; + + void _ShowLoadErrorsDialog(const winrt::hstring& titleKey, + const winrt::hstring& contentKey, + HRESULT settingsLoadedResult, + const winrt::hstring& exceptionText); + void _ShowLoadWarningsDialog(const Windows::Foundation::Collections::IVector& warnings); + + bool _IsKeyboardServiceEnabled(); + + void _RefreshThemeRoutine(); + void _OnLoaded(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _OpenSettingsUI(); + + winrt::Windows::Foundation::Collections::IVector _contentStringToActions(const winrt::hstring& content, + const bool replaceFirstWithNewTab); + + // These are events that are handled by the TerminalPage, but are + // exposed through the AppLogic. This macro is used to forward the event + // directly to them. + FORWARDED_TYPED_EVENT(SetTitleBarContent, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::UIElement, _root, SetTitleBarContent); + FORWARDED_TYPED_EVENT(TitleChanged, winrt::Windows::Foundation::IInspectable, winrt::hstring, _root, TitleChanged); + FORWARDED_TYPED_EVENT(LastTabClosed, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::LastTabClosedEventArgs, _root, LastTabClosed); + FORWARDED_TYPED_EVENT(FocusModeChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, FocusModeChanged); + FORWARDED_TYPED_EVENT(FullscreenChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, FullscreenChanged); + FORWARDED_TYPED_EVENT(ChangeMaximizeRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, ChangeMaximizeRequested); + FORWARDED_TYPED_EVENT(AlwaysOnTopChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, AlwaysOnTopChanged); + FORWARDED_TYPED_EVENT(RaiseVisualBell, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, RaiseVisualBell); + FORWARDED_TYPED_EVENT(SetTaskbarProgress, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, SetTaskbarProgress); + FORWARDED_TYPED_EVENT(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested); + FORWARDED_TYPED_EVENT(RenameWindowRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs, _root, RenameWindowRequested); + 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); + FORWARDED_TYPED_EVENT(ShowWindowChanged, Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs, _root, ShowWindowChanged); + + TYPED_EVENT(IsQuakeWindowChanged, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable); + + TYPED_EVENT(SystemMenuChangeRequested, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SystemMenuChangeArgs); + + TYPED_EVENT(SettingsChanged, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SettingsLoadEventArgs); + + FORWARDED_TYPED_EVENT(RequestMoveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestMoveContentArgs, _root, RequestMoveContent); + FORWARDED_TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs, _root, RequestReceiveContent); + +#ifdef UNIT_TESTING + friend class TerminalAppLocalTests::CommandlineTest; +#endif + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(TerminalWindow); +} diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl new file mode 100644 index 00000000000..825afa44c4a --- /dev/null +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "TerminalPage.idl"; +import "ShortcutActionDispatch.idl"; +import "IDirectKeyListener.idl"; + +namespace TerminalApp +{ + struct InitialPosition + { + Int64 X; + Int64 Y; + }; + + delegate void SystemMenuItemHandler(); + + enum SystemMenuChangeAction + { + Add = 0, + Remove = 1 + }; + + [default_interface] runtimeclass SystemMenuChangeArgs { + String Name { get; }; + SystemMenuChangeAction Action { get; }; + SystemMenuItemHandler Handler { get; }; + }; + + [default_interface] runtimeclass SettingsLoadEventArgs + { + Boolean Reload { get; }; + UInt64 Result { get; }; + IVector Warnings { get; }; + String ExceptionText { get; }; + + Microsoft.Terminal.Settings.Model.CascadiaSettings NewSettings { get; }; + + }; + + // See IDialogPresenter and TerminalPage's DialogPresenter for more + // information. + [default_interface] runtimeclass TerminalWindow : IDirectKeyListener, IDialogPresenter, Windows.UI.Xaml.Data.INotifyPropertyChanged + { + TerminalWindow(SettingsLoadEventArgs result, ContentManager manager); + + // For your own sanity, it's better to do setup outside the ctor. + // If you do any setup in the ctor that ends up throwing an exception, + // then it might look like TermApp just failed to activate, which will + // cause you to chase down the rabbit hole of "why is TermApp not + // registered?" when it definitely is. + void Create(); + + Boolean HasCommandlineArguments(); + + Int32 SetStartupCommandline(String[] commands); + void SetStartupContent(String json, Windows.Foundation.IReference bounds); + Int32 ExecuteCommandline(String[] commands, String cwd); + String ParseCommandlineMessage { get; }; + Boolean ShouldExitEarly { get; }; + + Boolean ShouldImmediatelyHandoffToElevated(); + void HandoffToElevated(); + + void Quit(); + + Windows.UI.Xaml.UIElement GetRoot(); + + String Title { get; }; + Boolean FocusMode { get; }; + Boolean Fullscreen { get; }; + void Maximized(Boolean newMaximized); + Boolean AlwaysOnTop { get; }; + Boolean AutoHideWindow { get; }; + + void IdentifyWindow(); + void SetPersistedLayoutIdx(UInt32 idx); + void ClearPersistedWindowState(); + + void RenameFailed(); + void RequestExitFullscreen(); + + Windows.Foundation.Size GetLaunchDimensions(UInt32 dpi); + Boolean CenterOnLaunch { get; }; + + InitialPosition GetInitialPosition(Int64 defaultInitialX, Int64 defaultInitialY); + Windows.UI.Xaml.ElementTheme GetRequestedTheme(); + Microsoft.Terminal.Settings.Model.LaunchMode GetLaunchMode(); + Boolean GetShowTabsInTitlebar(); + Boolean GetInitialAlwaysOnTop(); + Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); + void TitlebarClicked(); + void CloseWindow(Microsoft.Terminal.Settings.Model.LaunchPosition position, Boolean isLastWindow); + void WindowVisibilityChanged(Boolean showOrHide); + + TaskbarState TaskbarState{ get; }; + Windows.UI.Xaml.Media.Brush TitlebarBrush { get; }; + void WindowActivated(Boolean activated); + + String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position); + + Boolean GetMinimizeToNotificationArea(); + Boolean GetAlwaysShowNotificationIcon(); + Boolean GetShowTitleInTitlebar(); + + // These already have accessors as a part of IWindowProperties, but we + // also want to be able to set them. + WindowProperties WindowProperties { get; }; + void WindowName(String name); + void WindowId(UInt64 id); + Boolean IsQuakeWindow(); + + // See IDialogPresenter and TerminalPage's DialogPresenter for more + // information. + void DismissDialog(); + + event Windows.Foundation.TypedEventHandler SetTitleBarContent; + event Windows.Foundation.TypedEventHandler TitleChanged; + event Windows.Foundation.TypedEventHandler LastTabClosed; + event Windows.Foundation.TypedEventHandler RequestedThemeChanged; + event Windows.Foundation.TypedEventHandler FocusModeChanged; + event Windows.Foundation.TypedEventHandler FullscreenChanged; + event Windows.Foundation.TypedEventHandler ChangeMaximizeRequested; + event Windows.Foundation.TypedEventHandler AlwaysOnTopChanged; + event Windows.Foundation.TypedEventHandler RaiseVisualBell; + event Windows.Foundation.TypedEventHandler SetTaskbarProgress; + event Windows.Foundation.TypedEventHandler IdentifyWindowsRequested; + 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; + event Windows.Foundation.TypedEventHandler QuitRequested; + event Windows.Foundation.TypedEventHandler SystemMenuChangeRequested; + event Windows.Foundation.TypedEventHandler ShowWindowChanged; + + event Windows.Foundation.TypedEventHandler SettingsChanged; + + event Windows.Foundation.TypedEventHandler RequestMoveContent; + event Windows.Foundation.TypedEventHandler RequestReceiveContent; + + void AttachContent(String content, UInt32 tabIndex); + void SendContentToOther(RequestReceiveContentArgs args); + } +} diff --git a/src/cascadia/TerminalApp/packages.config b/src/cascadia/TerminalApp/packages.config deleted file mode 100644 index e151e41548d..00000000000 --- a/src/cascadia/TerminalApp/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj b/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj index 078405cec9c..eada6967cb8 100644 --- a/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj +++ b/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj @@ -41,7 +41,7 @@ - {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} diff --git a/src/cascadia/TerminalAzBridge/pch.h b/src/cascadia/TerminalAzBridge/pch.h index 80a01be209d..e07f7c24ed7 100644 --- a/src/cascadia/TerminalAzBridge/pch.h +++ b/src/cascadia/TerminalAzBridge/pch.h @@ -24,8 +24,6 @@ Module Name: #define NOCOMM #include -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) - #include #include "../inc/LibraryIncludes.h" diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index 9ddc90cdf2d..fef88c33628 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -5,6 +5,7 @@ #include "ConptyConnection.h" #include +#include #include #include "CTerminalHandoff.h" @@ -95,7 +96,20 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation }); // Populate the environment map with the current environment. - RETURN_IF_FAILED(Utils::UpdateEnvironmentMapW(environment)); + if (_reloadEnvironmentVariables) + { + til::env refreshedEnvironment; + refreshedEnvironment.regenerate(); + + for (auto& [key, value] : refreshedEnvironment.as_map()) + { + environment.try_emplace(key, std::move(value)); + } + } + else + { + RETURN_IF_FAILED(Utils::UpdateEnvironmentMapW(environment)); + } { // Convert connection Guid to string and ignore the enclosing '{}'. @@ -272,6 +286,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { _passthroughMode = winrt::unbox_value_or(settings.TryLookup(L"passthroughMode").try_as(), _passthroughMode); } + _reloadEnvironmentVariables = winrt::unbox_value_or(settings.TryLookup(L"reloadEnvironmentVariables").try_as(), + _reloadEnvironmentVariables); } if (_guid == guid{}) diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index 3709ccef33a..60528dc0463 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -89,6 +89,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation std::wstring _u16Str{}; std::array _buffer{}; bool _passthroughMode{}; + bool _reloadEnvironmentVariables{}; struct StartupInfoFromDefTerm { diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 5bf00b07982..54cca0ca559 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -154,7 +154,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); } + _setupDispatcherAndCallbacks(); + UpdateSettings(settings, unfocusedAppearance); + } + + void ControlCore::_setupDispatcherAndCallbacks() + { // Get our dispatcher. If we're hosted in-proc with XAML, this will get // us the same dispatcher as TermControl::Dispatcher(). If we're out of // proc, this'll return null. We'll need to instead make a new @@ -211,8 +217,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation core->_ScrollPositionChangedHandlers(*core, update); } }); - - UpdateSettings(settings, unfocusedAppearance); } ControlCore::~ControlCore() @@ -225,6 +229,33 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } + void ControlCore::Detach() + { + // Disable the renderer, so that it doesn't try to start any new frames + // for our engines while we're not attached to anything. + _renderer->WaitForPaintCompletionAndDisable(INFINITE); + + // Clear out any throttled funcs that we had wired up to run on this UI + // thread. These will be recreated in _setupDispatcherAndCallbacks, when + // we're re-attached to a new control (on a possibly new UI thread). + _tsfTryRedrawCanvas.reset(); + _updatePatternLocations.reset(); + _updateScrollBar.reset(); + } + + void ControlCore::AttachToNewControl(const Microsoft::Terminal::Control::IKeyBindings& keyBindings) + { + _settings->KeyBindings(keyBindings); + _setupDispatcherAndCallbacks(); + const auto actualNewSize = _actualFont.GetSize(); + // Bubble this up, so our new control knows how big we want the font. + _FontSizeChangedHandlers(actualNewSize.width, actualNewSize.height, true); + + // Turn the rendering back on now that we're ready to go. + _renderer->EnablePainting(); + _AttachedHandlers(*this, nullptr); + } + bool ControlCore::Initialize(const double actualWidth, const double actualHeight, const double compositionScale) @@ -304,9 +335,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // the first paint will be ignored! _renderEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); - // Tell the DX Engine to notify us when the swap chain changes. - // We do this after we initially set the swapchain so as to avoid unnecessary callbacks (and locking problems) - _renderEngine->SetCallback([this](auto handle) { _renderEngineSwapChainChanged(handle); }); + // Tell the render engine to notify us when the swap chain changes. + // We do this after we initially set the swapchain so as to avoid + // unnecessary callbacks (and locking problems) + _renderEngine->SetCallback([this](HANDLE handle) { + _renderEngineSwapChainChanged(handle); + }); _renderEngine->SetRetroTerminalEffect(_settings->RetroTerminalEffect()); _renderEngine->SetPixelShaderPath(_settings->PixelShaderPath()); @@ -574,7 +608,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // itself - it was initiated by the mouse wheel, or the scrollbar. _terminal->UserScrollViewport(viewTop); - (*_updatePatternLocations)(); + if (_updatePatternLocations) + { + (*_updatePatternLocations)(); + } } void ControlCore::AdjustOpacity(const double adjustment) @@ -728,6 +765,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto lock = _terminal->LockForWriting(); + _cellWidth = CSSLengthPercentage::FromString(_settings->CellWidth().c_str()); + _cellHeight = CSSLengthPercentage::FromString(_settings->CellHeight().c_str()); _runtimeOpacity = std::nullopt; // Manually turn off acrylic if they turn off transparency. @@ -880,6 +919,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation _actualFont = { fontFace, 0, fontWeight.Weight, _desiredFont.GetEngineSize(), CP_UTF8, false }; _actualFontFaceName = { fontFace }; + _desiredFont.SetCellSize(_cellWidth, _cellHeight); + const auto before = _actualFont.GetSize(); _updateFont(); const auto after = _actualFont.GetSize(); @@ -967,18 +1008,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ControlCore::SizeChanged(const double width, const double height) { - // _refreshSizeUnderLock redraws the entire terminal. - // Don't call it if we don't have to. - if (_panelWidth == width && _panelHeight == height) - { - return; - } - - _panelWidth = width; - _panelHeight = height; - - auto lock = _terminal->LockForWriting(); - _refreshSizeUnderLock(); + SizeOrScaleChanged(width, height, _compositionScale); } void ControlCore::ScaleChanged(const double scale) @@ -987,19 +1017,31 @@ namespace winrt::Microsoft::Terminal::Control::implementation { return; } + SizeOrScaleChanged(_panelWidth, _panelHeight, scale); + } + void ControlCore::SizeOrScaleChanged(const double width, + const double height, + const double scale) + { // _refreshSizeUnderLock redraws the entire terminal. // Don't call it if we don't have to. - if (_compositionScale == scale) + if (_panelWidth == width && _panelHeight == height && _compositionScale == scale) { return; } + const auto oldScale = _compositionScale; + _panelWidth = width; + _panelHeight = height; _compositionScale = scale; auto lock = _terminal->LockForWriting(); - // _updateFont relies on the new _compositionScale set above - _updateFont(); + if (oldScale != scale) + { + // _updateFont relies on the new _compositionScale set above + _updateFont(); + } _refreshSizeUnderLock(); } @@ -1100,7 +1142,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // content, which is unexpected. const auto htmlData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::HTML) ? TextBuffer::GenHTML(bufferData, - _actualFont.GetUnscaledSize().width, + _actualFont.GetUnscaledSize().height, _actualFont.GetFaceName(), bgColor) : ""; @@ -1255,7 +1297,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::IReference ControlCore::TabColor() noexcept { auto coreColor = _terminal->GetTabColor(); - return coreColor.has_value() ? Windows::Foundation::IReference(til::color{ coreColor.value() }) : + return coreColor.has_value() ? Windows::Foundation::IReference{ static_cast(coreColor.value()) } : nullptr; } @@ -1360,7 +1402,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation auto update{ winrt::make(viewTop, viewHeight, bufferSize) }; - if (!_inUnitTests) + if (!_inUnitTests && _updateScrollBar) { _updateScrollBar->Run(update); } @@ -1370,14 +1412,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // Additionally, start the throttled update of where our links are. - (*_updatePatternLocations)(); + + if (_updatePatternLocations) + { + (*_updatePatternLocations)(); + } } void ControlCore::_terminalCursorPositionChanged() { // When the buffer's cursor moves, start the throttled func to // eventually dispatch a CursorPositionChanged event. - _tsfTryRedrawCanvas->Run(); + if (_tsfTryRedrawCanvas) + { + _tsfTryRedrawCanvas->Run(); + } } void ControlCore::_terminalTaskbarProgressChanged() @@ -1405,7 +1454,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // The UI thread might try to acquire the console lock from time to time. // --> Unlock it, so the UI doesn't hang while we're busy. const auto suspension = _terminal->SuspendLock(); - // This call will block for the duration, unless shutdown early. _midiAudio.PlayNote(reinterpret_cast(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast(duration)); } @@ -1507,9 +1555,32 @@ namespace winrt::Microsoft::Terminal::Control::implementation _RendererWarningHandlers(*this, winrt::make(hr)); } - void ControlCore::_renderEngineSwapChainChanged(const HANDLE handle) + winrt::fire_and_forget ControlCore::_renderEngineSwapChainChanged(const HANDLE sourceHandle) { - _SwapChainChangedHandlers(*this, winrt::box_value(reinterpret_cast(handle))); + // `sourceHandle` is a weak ref to a HANDLE that's ultimately owned by the + // render engine's own unique_handle. We'll add another ref to it here. + // This will make sure that we always have a valid HANDLE to give to + // callers of our own SwapChainHandle method, even if the renderer is + // currently in the process of discarding this value and creating a new + // one. Callers should have already set up the SwapChainChanged + // callback, so this all works out. + + winrt::handle duplicatedHandle; + const auto processHandle = GetCurrentProcess(); + THROW_IF_WIN32_BOOL_FALSE(DuplicateHandle(processHandle, sourceHandle, processHandle, duplicatedHandle.put(), 0, FALSE, DUPLICATE_SAME_ACCESS)); + + const auto weakThis{ get_weak() }; + + co_await wil::resume_foreground(_dispatcher); + + if (auto core{ weakThis.get() }) + { + // `this` is safe to use now + + _lastSwapChainHandle = std::move(duplicatedHandle); + // Now bubble the event up to the control. + _SwapChainChangedHandlers(*this, winrt::box_value(reinterpret_cast(_lastSwapChainHandle.get()))); + } } void ControlCore::_rendererBackgroundColorChanged() @@ -1656,6 +1727,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // _renderer will always exist since it's introduced in the ctor _renderer->AddRenderEngine(pEngine); } + void ControlCore::DetachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine) + { + _renderer->RemoveRenderEngine(pEngine); + } bool ControlCore::IsInReadOnlyMode() const { @@ -1667,6 +1742,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation _isReadOnly = !_isReadOnly; } + void ControlCore::SetReadOnlyMode(const bool readOnlyState) + { + _isReadOnly = readOnlyState; + } + void ControlCore::_raiseReadOnlyWarning() { auto noticeArgs = winrt::make(NoticeLevel::Info, RS_(L"TermControlReadOnly")); @@ -1679,7 +1759,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->Write(hstr); // Start the throttled update of where our hyperlinks are. - (*_updatePatternLocations)(); + if (_updatePatternLocations) + { + (*_updatePatternLocations)(); + } } catch (...) { @@ -1688,6 +1771,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } + uint64_t ControlCore::SwapChainHandle() const + { + // This is only ever called by TermControl::AttachContent, which occurs + // when we're taking an existing core and moving it to a new control. + // Otherwise, we only ever use the value from the SwapChainChanged + // event. + return reinterpret_cast(_lastSwapChainHandle.get()); + } + // Method Description: // - Clear the contents of the buffer. The region cleared is given by // clearType: diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 890e9ec3d51..8f7e40e6242 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -63,6 +63,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation const double compositionScale); void EnablePainting(); + void Detach(); + void UpdateSettings(const Control::IControlSettings& settings, const IControlAppearance& newAppearance); void ApplyAppearance(const bool& focused); Control::IControlSettings Settings() { return *_settings; }; @@ -73,8 +75,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Microsoft::Terminal::Core::Scheme ColorScheme() const noexcept; void ColorScheme(const winrt::Microsoft::Terminal::Core::Scheme& scheme); + uint64_t SwapChainHandle() const; + void AttachToNewControl(const Microsoft::Terminal::Control::IKeyBindings& keyBindings); + void SizeChanged(const double width, const double height); void ScaleChanged(const double scale); + void SizeOrScaleChanged(const double width, const double height, const double scale); void AdjustFontSize(float fontSizeDelta); void ResetFontSize(); @@ -190,9 +196,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool& selectionNeedsToBeCopied); void AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine); + void DetachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine); bool IsInReadOnlyMode() const; void ToggleReadOnlyMode(); + void SetReadOnlyMode(const bool readOnlyState); hstring ReadEntireBuffer() const; @@ -233,6 +241,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(UpdateSelectionMarkers, IInspectable, Control::UpdateSelectionMarkersEventArgs); TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); TYPED_EVENT(CloseTerminalRequested, IInspectable, IInspectable); + + TYPED_EVENT(Attached, IInspectable, IInspectable); // clang-format on private: @@ -255,9 +265,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::unique_ptr<::Microsoft::Console::Render::IRenderEngine> _renderEngine{ nullptr }; std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer{ nullptr }; + winrt::handle _lastSwapChainHandle{ nullptr }; + FontInfoDesired _desiredFont; FontInfo _actualFont; winrt::hstring _actualFontFaceName; + CSSLengthPercentage _cellWidth; + CSSLengthPercentage _cellHeight; // storage location for the leading surrogate of a utf-16 surrogate pair std::optional _leadingSurrogate{ std::nullopt }; @@ -283,6 +297,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::unique_ptr> _updatePatternLocations; std::shared_ptr> _updateScrollBar; + void _setupDispatcherAndCallbacks(); + bool _setFontSizeUnderLock(float fontSize); void _updateFont(const bool initialUpdate = false); void _refreshSizeUnderLock(); @@ -312,7 +328,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation #pragma region RendererCallbacks void _rendererWarning(const HRESULT hr); - void _renderEngineSwapChainChanged(const HANDLE handle); + winrt::fire_and_forget _renderEngineSwapChainChanged(const HANDLE handle); void _rendererBackgroundColorChanged(); void _rendererTabColorChanged(); #pragma endregion diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index a8df501469b..7110d9d342e 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -76,6 +76,8 @@ namespace Microsoft.Terminal.Control IControlAppearance UnfocusedAppearance { get; }; Boolean HasUnfocusedAppearance(); + UInt64 SwapChainHandle { get; }; + Windows.Foundation.Size FontSize { get; }; String FontFaceName { get; }; UInt16 FontWeight { get; }; @@ -108,9 +110,11 @@ namespace Microsoft.Terminal.Control void AdjustFontSize(Single fontSizeDelta); void SizeChanged(Double width, Double height); void ScaleChanged(Double scale); + void SizeOrScaleChanged(Double width, Double height, Double scale); void ToggleShaderEffects(); void ToggleReadOnlyMode(); + void SetReadOnlyMode(Boolean readOnlyState); Microsoft.Terminal.Core.Point CursorPosition { get; }; void ResumeRendering(); @@ -126,7 +130,6 @@ namespace Microsoft.Terminal.Control String HoveredUriText { get; }; Windows.Foundation.IReference HoveredCell { get; }; - void Close(); void BlinkCursor(); Boolean IsInReadOnlyMode { get; }; Boolean CursorOn; @@ -162,5 +165,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler UpdateSelectionMarkers; event Windows.Foundation.TypedEventHandler OpenHyperlink; event Windows.Foundation.TypedEventHandler CloseTerminalRequested; + + event Windows.Foundation.TypedEventHandler Attached; }; } diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 7eaf4275535..b626eb77ce1 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -27,6 +27,8 @@ static constexpr unsigned int MAX_CLICK_COUNT = 3; namespace winrt::Microsoft::Terminal::Control::implementation { + std::atomic ControlInteractivity::_nextId{ 1 }; + static constexpr TerminalInput::MouseButtonState toInternalMouseState(const Control::MouseButtonState& state) { return TerminalInput::MouseButtonState{ @@ -44,7 +46,48 @@ namespace winrt::Microsoft::Terminal::Control::implementation _lastMouseClickPos{}, _selectionNeedsToBeCopied{ false } { + _id = _nextId.fetch_add(1, std::memory_order_relaxed); + _core = winrt::make_self(settings, unfocusedAppearance, connection); + + _core->Attached([weakThis = get_weak()](auto&&, auto&&) { + if (auto self{ weakThis.get() }) + { + self->_AttachedHandlers(*self, nullptr); + } + }); + } + + uint64_t ControlInteractivity::Id() + { + return _id; + } + + void ControlInteractivity::Detach() + { + if (_uiaEngine) + { + // There's a potential race here where we've removed the TermControl + // from the UI tree, but the UIA engine is in the middle of a paint, + // and the UIA engine will try to dispatch to the + // TermControlAutomationPeer, which (is now)/(will very soon be) gone. + // + // To alleviate, make sure to disable the UIA engine and remove it, + // and ALSO disable the renderer. Core.Detach will take care of the + // WaitForPaintCompletionAndDisable (which will stop the renderer + // after all current engines are done painting). + // + // Simply disabling the UIA engine is not enough, because it's + // possible that it had already started presenting here. + LOG_IF_FAILED(_uiaEngine->Disable()); + _core->DetachUiaEngine(_uiaEngine.get()); + } + _core->Detach(); + } + + void ControlInteractivity::AttachToNewControl(const Microsoft::Terminal::Control::IKeyBindings& keyBindings) + { + _core->AttachToNewControl(keyBindings); } // Method Description: @@ -73,6 +116,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation return *_core; } + void ControlInteractivity::Close() + { + _ClosedHandlers(*this, nullptr); + if (_core) + { + _core->Close(); + } + } + // Method Description: // - Returns the number of clicks that occurred (double and triple click support). // Every call to this function registers a click. @@ -205,7 +257,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // GH#9396: we prioritize hyper-link over VT mouse events auto hyperlink = _core->GetHyperlink(terminalPosition.to_core_point()); if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown) && - ctrlEnabled && !hyperlink.empty()) + ctrlEnabled && + !hyperlink.empty()) { const auto clickCount = _numberOfClicks(pixelPosition, timestamp); // Handle hyper-link only on the first click to prevent multiple activations @@ -255,14 +308,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (WI_IsFlagSet(buttonState, MouseButtonState::IsRightButtonDown)) { - // Try to copy the text and clear the selection - const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, nullptr); - _core->ClearSelection(); - if (_core->CopyOnSelect() || !successfulCopy) + if (_core->Settings().RightClickContextMenu()) { - // CopyOnSelect: right click always pastes! - // Otherwise: no selection --> paste - RequestPasteTextFromClipboard(); + auto contextArgs = winrt::make(til::point{ pixelPosition }.to_winrt_point()); + _ContextMenuRequestedHandlers(*this, contextArgs); + } + else + { + // Try to copy the text and clear the selection + const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, nullptr); + _core->ClearSelection(); + if (_core->CopyOnSelect() || !successfulCopy) + { + // CopyOnSelect: right click always pastes! + // Otherwise: no selection --> paste + RequestPasteTextFromClipboard(); + } } } } @@ -649,7 +710,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation try { const auto autoPeer = winrt::make_self(this); - + if (_uiaEngine) + { + _core->DetachUiaEngine(_uiaEngine.get()); + } _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get()); _core->AttachUiaEngine(_uiaEngine.get()); return *autoPeer; diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index 4cc4df10bdd..dee2fe9beed 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -44,6 +44,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation void Initialize(); Control::ControlCore Core(); + void Close(); + void Detach(); + Control::InteractivityAutomationPeer OnCreateAutomationPeer(); ::Microsoft::Console::Render::IRenderData* GetRenderData() const; @@ -85,9 +88,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetEndSelectionPoint(const Core::Point pixelPosition); bool ManglePathsForWsl(); + uint64_t Id(); + void AttachToNewControl(const Microsoft::Terminal::Control::IKeyBindings& keyBindings); + TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); TYPED_EVENT(ScrollPositionChanged, IInspectable, Control::ScrollPositionChangedArgs); + TYPED_EVENT(ContextMenuRequested, IInspectable, Control::ContextMenuRequestedEventArgs); + + TYPED_EVENT(Attached, IInspectable, IInspectable); + TYPED_EVENT(Closed, IInspectable, IInspectable); private: // NOTE: _uiaEngine must be ordered before _core. @@ -129,6 +139,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::optional::interval> _lastHoveredInterval{ std::nullopt }; + uint64_t _id; + static std::atomic _nextId; + unsigned int _numberOfClicks(Core::Point clickPos, Timestamp clickTime); void _updateSystemParameterSettings() noexcept; diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl index aada01ee0ad..086b009ba09 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.idl +++ b/src/cascadia/TerminalControl/ControlInteractivity.idl @@ -23,6 +23,13 @@ namespace Microsoft.Terminal.Control void GotFocus(); void LostFocus(); + UInt64 Id { get; }; + + void AttachToNewControl(Microsoft.Terminal.Control.IKeyBindings keyBindings); + void Detach(); + + void Close(); + InteractivityAutomationPeer OnCreateAutomationPeer(); Boolean CopySelectionToClipboard(Boolean singleLine, Windows.Foundation.IReference formats); @@ -65,6 +72,12 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ScrollPositionChanged; event Windows.Foundation.TypedEventHandler PasteFromClipboard; + event Windows.Foundation.TypedEventHandler Closed; + + event Windows.Foundation.TypedEventHandler Attached; + + // Used to communicate to the TermControl, but not necessarily higher up in the stack + event Windows.Foundation.TypedEventHandler ContextMenuRequested; }; } diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 9f9b41ac1f7..c4089ebd76c 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -5,6 +5,7 @@ #include "EventArgs.h" #include "TitleChangedEventArgs.g.cpp" #include "CopyToClipboardEventArgs.g.cpp" +#include "ContextMenuRequestedEventArgs.g.cpp" #include "PasteFromClipboardEventArgs.g.cpp" #include "OpenHyperlinkEventArgs.g.cpp" #include "NoticeEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index a542e391f63..2551843df20 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -5,6 +5,7 @@ #include "TitleChangedEventArgs.g.h" #include "CopyToClipboardEventArgs.g.h" +#include "ContextMenuRequestedEventArgs.g.h" #include "PasteFromClipboardEventArgs.g.h" #include "OpenHyperlinkEventArgs.g.h" #include "NoticeEventArgs.g.h" @@ -53,6 +54,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::IReference _formats; }; + struct ContextMenuRequestedEventArgs : public ContextMenuRequestedEventArgsT + { + public: + ContextMenuRequestedEventArgs(winrt::Windows::Foundation::Point pos) : + _Position(pos) {} + + WINRT_PROPERTY(winrt::Windows::Foundation::Point, Position); + }; + struct PasteFromClipboardEventArgs : public PasteFromClipboardEventArgsT { public: diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 941384c5b05..bc67bd13624 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -22,6 +22,11 @@ namespace Microsoft.Terminal.Control Windows.Foundation.IReference Formats { get; }; } + runtimeclass ContextMenuRequestedEventArgs + { + Windows.Foundation.Point Position { get; }; + } + runtimeclass TitleChangedEventArgs { String Title; diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index d014b8ac9be..1e2f7d71f24 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -42,6 +42,8 @@ namespace Microsoft.Terminal.Control String Padding { get; }; Windows.Foundation.Collections.IMap FontFeatures { get; }; Windows.Foundation.Collections.IMap FontAxes { get; }; + String CellWidth { get; }; + String CellHeight { get; }; Microsoft.Terminal.Control.IKeyBindings KeyBindings { get; }; @@ -59,5 +61,6 @@ namespace Microsoft.Terminal.Control Boolean SoftwareRendering { get; }; Boolean ShowMarks { get; }; Boolean UseBackgroundImageForWindow { get; }; + Boolean RightClickContextMenu { get; }; }; } diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index cff7cb1c9bd..aa2473890f2 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -208,4 +208,36 @@ Please either install the missing font or choose another one. No results found Announced to a screen reader when the user searches for some text and there are no matches for that text in the terminal. - \ No newline at end of file + + Paste + The label of a button for pasting the contents of the clipboard. + + + Paste + The tooltip for a paste button + + + Copy + The label of a button for copying the selected text to the clipboard. + + + Copy + The tooltip for a copy button + + + Paste + The label of a button for pasting the contents of the clipboard. + + + Paste + The tooltip for a paste button + + + Find... + The label of a button for searching for the selected text + + + Find + The tooltip for a button for searching for the selected text + + diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 5f7dee504d7..4a680edcb68 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -14,8 +14,6 @@ Author(s): --*/ #pragma once -#include "winrt/Windows.UI.Xaml.h" -#include "winrt/Windows.UI.Xaml.Controls.h" #include "SearchBoxControl.g.h" diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 7cfe8cbdc4b..d772c6f1ecc 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -50,6 +50,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation TermControl::TermControl(IControlSettings settings, Control::IControlAppearance unfocusedAppearance, TerminalConnection::ITerminalConnection connection) : + TermControl{ winrt::make(settings, unfocusedAppearance, connection) } + { + } + + TermControl::TermControl(Control::ControlInteractivity content) : + _interactivity{ content }, _isInternalScrollBarUpdate{ false }, _autoScrollVelocity{ 0 }, _autoScrollingPointerPoint{ std::nullopt }, @@ -61,32 +67,45 @@ namespace winrt::Microsoft::Terminal::Control::implementation { InitializeComponent(); - _interactivity = winrt::make(settings, unfocusedAppearance, connection); _core = _interactivity.Core(); - // These events might all be triggered by the connection, but that - // should be drained and closed before we complete destruction. So these - // are safe. - _core.ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); - _core.WarningBell({ this, &TermControl::_coreWarningBell }); - _core.CursorPositionChanged({ this, &TermControl::_CursorPositionChanged }); - // This event is specifically triggered by the renderer thread, a BG thread. Use a weak ref here. - _core.RendererEnteredErrorState({ get_weak(), &TermControl::_RendererEnteredErrorState }); + _revokers.RendererEnteredErrorState = _core.RendererEnteredErrorState(winrt::auto_revoke, { get_weak(), &TermControl::_RendererEnteredErrorState }); + + // IMPORTANT! Set this callback up sooner rather than later. If we do it + // after Enable, then it'll be possible to paint the frame once + // _before_ the warning handler is set up, and then warnings from + // the first paint will be ignored! + _revokers.RendererWarning = _core.RendererWarning(winrt::auto_revoke, { get_weak(), &TermControl::_RendererWarning }); + // ALSO IMPORTANT: Make sure to set this callback up in the ctor, so + // that we won't miss any swap chain changes. + _revokers.SwapChainChanged = _core.SwapChainChanged(winrt::auto_revoke, { get_weak(), &TermControl::RenderEngineSwapChainChanged }); // These callbacks can only really be triggered by UI interactions. So // they don't need weak refs - they can't be triggered unless we're // alive. - _core.BackgroundColorChanged({ this, &TermControl::_coreBackgroundColorChanged }); - _core.FontSizeChanged({ this, &TermControl::_coreFontSizeChanged }); - _core.TransparencyChanged({ this, &TermControl::_coreTransparencyChanged }); - _core.RaiseNotice({ this, &TermControl::_coreRaisedNotice }); - _core.HoveredHyperlinkChanged({ this, &TermControl::_hoveredHyperlinkChanged }); - _core.FoundMatch({ this, &TermControl::_coreFoundMatch }); - _core.UpdateSelectionMarkers({ this, &TermControl::_updateSelectionMarkers }); - _core.OpenHyperlink({ this, &TermControl::_HyperlinkHandler }); - _interactivity.OpenHyperlink({ this, &TermControl::_HyperlinkHandler }); - _interactivity.ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); + _revokers.BackgroundColorChanged = _core.BackgroundColorChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreBackgroundColorChanged }); + _revokers.FontSizeChanged = _core.FontSizeChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreFontSizeChanged }); + _revokers.TransparencyChanged = _core.TransparencyChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreTransparencyChanged }); + _revokers.RaiseNotice = _core.RaiseNotice(winrt::auto_revoke, { get_weak(), &TermControl::_coreRaisedNotice }); + _revokers.HoveredHyperlinkChanged = _core.HoveredHyperlinkChanged(winrt::auto_revoke, { get_weak(), &TermControl::_hoveredHyperlinkChanged }); + _revokers.FoundMatch = _core.FoundMatch(winrt::auto_revoke, { get_weak(), &TermControl::_coreFoundMatch }); + _revokers.UpdateSelectionMarkers = _core.UpdateSelectionMarkers(winrt::auto_revoke, { get_weak(), &TermControl::_updateSelectionMarkers }); + _revokers.coreOpenHyperlink = _core.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler }); + _revokers.interactivityOpenHyperlink = _interactivity.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler }); + _revokers.interactivityScrollPositionChanged = _interactivity.ScrollPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_ScrollPositionChanged }); + _revokers.ContextMenuRequested = _interactivity.ContextMenuRequested(winrt::auto_revoke, { get_weak(), &TermControl::_contextMenuHandler }); + + // "Bubbled" events - ones we want to handle, by raising our own event. + _revokers.CopyToClipboard = _core.CopyToClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleCopyToClipboard }); + _revokers.TitleChanged = _core.TitleChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleTitleChanged }); + _revokers.TabColorChanged = _core.TabColorChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleTabColorChanged }); + _revokers.TaskbarProgressChanged = _core.TaskbarProgressChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleSetTaskbarProgress }); + _revokers.ConnectionStateChanged = _core.ConnectionStateChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleConnectionStateChanged }); + _revokers.ShowWindowChanged = _core.ShowWindowChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleShowWindowChanged }); + _revokers.CloseTerminalRequested = _core.CloseTerminalRequested(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleCloseTerminalRequested }); + + _revokers.PasteFromClipboard = _interactivity.PasteFromClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubblePasteFromClipboard }); // Initialize the terminal only once the swapchainpanel is loaded - that // way, we'll be able to query the real pixel size it got on layout @@ -95,8 +114,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // in any layout change chain. That gives us great flexibility in finding the right point // at which to initialize our renderer (and our terminal). // Any earlier than the last layout update and we may not know the terminal's starting size. - - if (_InitializeTerminal()) + if (_InitializeTerminal(InitializeReason::Create)) { // Only let this succeed once. _layoutUpdatedRevoker.revoke(); @@ -130,11 +148,118 @@ namespace winrt::Microsoft::Terminal::Control::implementation } }); + // These events might all be triggered by the connection, but that + // should be drained and closed before we complete destruction. So these + // are safe. + // + // NOTE: _ScrollPositionChanged has to be registered after we set up the + // _updateScrollBar func. Otherwise, we could get a callback from an + // attached content before we set up the throttled func, and that'll A/V + _revokers.coreScrollPositionChanged = _core.ScrollPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_ScrollPositionChanged }); + _revokers.WarningBell = _core.WarningBell(winrt::auto_revoke, { get_weak(), &TermControl::_coreWarningBell }); + _revokers.CursorPositionChanged = _core.CursorPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_CursorPositionChanged }); + static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast(1.0 / 30.0 * 1000000)); _autoScrollTimer.Interval(AutoScrollUpdateInterval); - _autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll }); + _autoScrollTimer.Tick({ get_weak(), &TermControl::_UpdateAutoScroll }); _ApplyUISettings(); + + _originalPrimaryElements = winrt::single_threaded_observable_vector(); + _originalSecondaryElements = winrt::single_threaded_observable_vector(); + _originalSelectedPrimaryElements = winrt::single_threaded_observable_vector(); + _originalSelectedSecondaryElements = winrt::single_threaded_observable_vector(); + for (const auto& e : ContextMenu().PrimaryCommands()) + { + _originalPrimaryElements.Append(e); + } + for (const auto& e : ContextMenu().SecondaryCommands()) + { + _originalSecondaryElements.Append(e); + } + for (const auto& e : SelectionContextMenu().PrimaryCommands()) + { + _originalSelectedPrimaryElements.Append(e); + } + for (const auto& e : SelectionContextMenu().SecondaryCommands()) + { + _originalSelectedSecondaryElements.Append(e); + } + ContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { + if (auto control{ weakThis.get() }; !control->_IsClosing()) + { + const auto& menu{ control->ContextMenu() }; + menu.PrimaryCommands().Clear(); + menu.SecondaryCommands().Clear(); + for (const auto& e : control->_originalPrimaryElements) + { + menu.PrimaryCommands().Append(e); + } + for (const auto& e : control->_originalSecondaryElements) + { + menu.SecondaryCommands().Append(e); + } + } + }); + SelectionContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { + if (auto control{ weakThis.get() }; !control->_IsClosing()) + { + const auto& menu{ control->SelectionContextMenu() }; + menu.PrimaryCommands().Clear(); + menu.SecondaryCommands().Clear(); + for (const auto& e : control->_originalSelectedPrimaryElements) + { + menu.PrimaryCommands().Append(e); + } + for (const auto& e : control->_originalSelectedSecondaryElements) + { + menu.SecondaryCommands().Append(e); + } + } + }); + } + + // Function Description: + // - Static helper for building a new TermControl from an already existing + // content. We'll attach the existing swapchain to this new control's + // SwapChainPanel. The IKeyBindings might belong to a non-agile object on + // a new thread, so we'll hook up the core to these new bindings. + // Arguments: + // - content: The preexisting ControlInteractivity to connect to. + // - keybindings: The new IKeyBindings instance to use for this control. + // Return Value: + // - The newly constructed TermControl. + Control::TermControl TermControl::NewControlByAttachingContent(Control::ControlInteractivity content, + const Microsoft::Terminal::Control::IKeyBindings& keyBindings) + { + const auto term{ winrt::make_self(content) }; + term->_initializeForAttach(keyBindings); + return *term; + } + + void TermControl::_initializeForAttach(const Microsoft::Terminal::Control::IKeyBindings& keyBindings) + { + _AttachDxgiSwapChainToXaml(reinterpret_cast(_core.SwapChainHandle())); + _interactivity.AttachToNewControl(keyBindings); + + // Initialize the terminal only once the swapchainpanel is loaded - that + // way, we'll be able to query the real pixel size it got on layout + auto r = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { + // Replace the normal initialize routine with one that will allow up + // to complete initialization even though the Core was already + // initialized. + if (_InitializeTerminal(InitializeReason::Reattach)) + { + // Only let this succeed once. + _layoutUpdatedRevoker.revoke(); + } + }); + _layoutUpdatedRevoker.swap(r); + } + + uint64_t TermControl::ContentId() const + { + return _interactivity.Id(); } void TermControl::_throttledUpdateScrollbar(const ScrollBarUpdate& update) @@ -292,7 +417,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - Given Settings having been updated, applies the settings to the current terminal. // Return Value: // - - winrt::fire_and_forget TermControl::UpdateControlSettings(IControlSettings settings, IControlAppearance unfocusedAppearance) + winrt::fire_and_forget TermControl::UpdateControlSettings(IControlSettings settings, + IControlAppearance unfocusedAppearance) { auto weakThis{ get_weak() }; @@ -739,25 +865,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _core.ConnectionState(); } - winrt::fire_and_forget TermControl::RenderEngineSwapChainChanged(IInspectable /*sender*/, IInspectable args) + void TermControl::RenderEngineSwapChainChanged(IInspectable /*sender*/, IInspectable args) { - // This event is only registered during terminal initialization, - // so we don't need to check _initializedTerminal. - const auto weakThis{ get_weak() }; - - // Create a copy of the swap chain HANDLE in args, since we don't own that parameter. - // By the time we return from the co_await below, it might be deleted already. - winrt::handle handle; - const auto processHandle = GetCurrentProcess(); - const auto sourceHandle = reinterpret_cast(winrt::unbox_value(args)); - THROW_IF_WIN32_BOOL_FALSE(DuplicateHandle(processHandle, sourceHandle, processHandle, handle.put(), 0, FALSE, DUPLICATE_SAME_ACCESS)); - - co_await wil::resume_foreground(Dispatcher()); - - if (auto control{ weakThis.get() }) - { - _AttachDxgiSwapChainToXaml(handle.get()); - } + // This event comes in on the UI thread + HANDLE h = reinterpret_cast(winrt::unbox_value(args)); + _AttachDxgiSwapChainToXaml(h); } // Method Description: @@ -808,7 +920,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation nativePanel->SetSwapChainHandle(swapChainHandle); } - bool TermControl::_InitializeTerminal() + bool TermControl::_InitializeTerminal(const InitializeReason reason) { if (_initializedTerminal) { @@ -828,22 +940,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation return false; } - // IMPORTANT! Set this callback up sooner rather than later. If we do it - // after Enable, then it'll be possible to paint the frame once - // _before_ the warning handler is set up, and then warnings from - // the first paint will be ignored! - _core.RendererWarning({ get_weak(), &TermControl::_RendererWarning }); - - const auto coreInitialized = _core.Initialize(panelWidth, - panelHeight, - panelScaleX); - if (!coreInitialized) + // If we're re-attaching an existing content, then we want to proceed even though the Terminal was already initialized. + if (reason == InitializeReason::Create) { - return false; + const auto coreInitialized = _core.Initialize(panelWidth, + panelHeight, + panelScaleX); + if (!coreInitialized) + { + return false; + } + _interactivity.Initialize(); + } + else + { + _core.SizeOrScaleChanged(panelWidth, panelHeight, panelScaleX); } - _interactivity.Initialize(); - _core.SwapChainChanged({ get_weak(), &TermControl::RenderEngineSwapChainChanged }); _core.EnablePainting(); auto bufferHeight = _core.BufferHeight(); @@ -1144,12 +1257,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation return true; } - // TODO: GH#5000 - // The Core owning the keybindings is weird. That's for sure. In the - // future, we may want to pass the keybindings into the control - // separately, so the control can have a pointer to an in-proc - // Keybindings object, rather than routing through the ControlCore. - // (see GH#5000) auto bindings = _core.Settings().KeyBindings(); if (!bindings) { @@ -1687,7 +1794,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation // GH#5421: Enable the UiaEngine before checking for the SearchBox // That way, new selections are notified to automation clients. // The _uiaEngine lives in _interactivity, so call into there to enable it. - _interactivity.GotFocus(); + + if (_interactivity) + { + _interactivity.GotFocus(); + } // If the searchbox is focused, we don't want TSFInputControl to think // it has focus so it doesn't intercept IME input. We also don't want the @@ -1742,7 +1853,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // This will disable the accessibility notifications, because the // UiaEngine lives in ControlInteractivity - _interactivity.LostFocus(); + if (_interactivity) + { + _interactivity.LostFocus(); + } if (TSFInputControl() != nullptr) { @@ -1998,13 +2112,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation _RestorePointerCursorHandlers(*this, nullptr); + _revokers = {}; + // Disconnect the TSF input control so it doesn't receive EditContext events. TSFInputControl().Close(); _autoScrollTimer.Stop(); - _core.Close(); + if (!_detached) + { + _interactivity.Close(); + } } } + void TermControl::Detach() + { + _revokers = {}; + + Control::ControlInteractivity old{ nullptr }; + std::swap(old, _interactivity); + old.Detach(); + + _detached = true; + } // Method Description: // - Scrolls the viewport of the terminal and updates the scroll bar accordingly @@ -2708,7 +2837,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Just in case someone was holding a lock when they called us and // the handlers decide to do something that take another lock // (like ShellExecute pumping our messaging thread...GH#7994) - co_await Dispatcher(); + co_await winrt::resume_foreground(Dispatcher()); _OpenHyperlinkHandlers(*strongThis, args); } @@ -2719,7 +2848,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation IInspectable /*args*/) { auto strongThis{ get_strong() }; - co_await Dispatcher(); // pop up onto the UI thread + co_await winrt::resume_foreground(Dispatcher()); // pop up onto the UI thread if (auto loadedUiElement{ FindName(L"RendererFailedNotice") }) { @@ -2741,18 +2870,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation IControlSettings TermControl::Settings() const { - // TODO: GH#5000 - // We still need this in a couple places: - // - Pane.cpp uses this for parsing out the StartingTitle, Commandline, - // etc for Pane::GetTerminalArgsForPane. - // - TerminalTab::_CreateToolTipTitle uses the ProfileName for the - // tooltip for the tab. - // - // These both happen on the UI thread right now. In the future, when we - // have to hop across the process boundary to get at the core settings, - // it may make sense to cache these values inside the TermControl - // itself, so it can do the hop once when it's first setup, rather than - // when it's needed by the UI thread. return _core.Settings(); } @@ -2869,6 +2986,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation _ReadOnlyChangedHandlers(*this, winrt::box_value(_core.IsInReadOnlyMode())); } + // Method Description: + // - Sets the read-only flag, raises event describing the value change + void TermControl::SetReadOnly(const bool readOnlyState) + { + _core.SetReadOnlyMode(readOnlyState); + _ReadOnlyChangedHandlers(*this, winrt::box_value(_core.IsInReadOnlyMode())); + } + // Method Description: // - Handle a mouse exited event, specifically clearing last hovered cell // and removing selection from hyper link if exists @@ -2939,9 +3064,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Ensure the marker is oriented properly // (i.e. if start is at the beginning of the buffer, it should be flipped) - auto transform{ marker.RenderTransform().as() }; - transform.ScaleX(std::abs(transform.ScaleX()) * (flipMarker ? -1.0 : 1.0)); - marker.RenderTransform(transform); + // + // Note - This RenderTransform might not be a + // ScaleTransform, if we haven't had a _coreFontSizeChanged + // handled yet, because that's the first place we set the + // RenderTransform + if (const auto& transform{ marker.RenderTransform().try_as() }) + { + transform.ScaleX(std::abs(transform.ScaleX()) * (flipMarker ? -1.0 : 1.0)); + marker.RenderTransform(transform); + } // Compute the location of the top left corner of the cell in DIPS auto terminalPos{ targetEnd ? markerData.EndPos : markerData.StartPos }; @@ -3174,4 +3306,52 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _core.ColorSelection(fg, bg, matchMode); } + + void TermControl::_contextMenuHandler(IInspectable /*sender*/, + Control::ContextMenuRequestedEventArgs args) + { + Controls::Primitives::FlyoutShowOptions myOption{}; + myOption.ShowMode(Controls::Primitives::FlyoutShowMode::Standard); + myOption.Placement(Controls::Primitives::FlyoutPlacementMode::TopEdgeAlignedLeft); + + // Position the menu where the pointer is. This was the best way I found how. + const til::point absolutePointerPos{ til::math::rounding, CoreWindow::GetForCurrentThread().PointerPosition() }; + const til::point absoluteWindowOrigin{ til::math::rounding, + CoreWindow::GetForCurrentThread().Bounds().X, + CoreWindow::GetForCurrentThread().Bounds().Y }; + // Get the offset (margin + tabs, etc..) of the control within the window + const til::point controlOrigin{ til::math::flooring, + this->TransformToVisual(nullptr).TransformPoint(Windows::Foundation::Point(0, 0)) }; + + const auto pos = (absolutePointerPos - absoluteWindowOrigin - controlOrigin).to_winrt_point(); + myOption.Position(pos); + + (_core.HasSelection() ? SelectionContextMenu() : + ContextMenu()) + .ShowAt(*this, myOption); + } + + void TermControl::_PasteCommandHandler(const IInspectable& /*sender*/, + const IInspectable& /*args*/) + { + _interactivity.RequestPasteTextFromClipboard(); + ContextMenu().Hide(); + SelectionContextMenu().Hide(); + } + void TermControl::_CopyCommandHandler(const IInspectable& /*sender*/, + const IInspectable& /*args*/) + { + // formats = nullptr -> copy all formats + _interactivity.CopySelectionToClipboard(false, nullptr); + ContextMenu().Hide(); + SelectionContextMenu().Hide(); + } + void TermControl::_SearchCommandHandler(const IInspectable& /*sender*/, + const IInspectable& /*args*/) + { + ContextMenu().Hide(); + SelectionContextMenu().Hide(); + SearchMatch(false); + } + } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 590780e7524..ef4bf00fdfa 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -25,14 +25,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation { struct TermControl : TermControlT { - TermControl(IControlSettings settings, - Control::IControlAppearance unfocusedAppearance, - TerminalConnection::ITerminalConnection connection); + TermControl(Control::ControlInteractivity content); + + TermControl(IControlSettings settings, Control::IControlAppearance unfocusedAppearance, TerminalConnection::ITerminalConnection connection); + + static Control::TermControl NewControlByAttachingContent(Control::ControlInteractivity content, const Microsoft::Terminal::Control::IKeyBindings& keyBindings); winrt::fire_and_forget UpdateControlSettings(Control::IControlSettings settings); winrt::fire_and_forget UpdateControlSettings(Control::IControlSettings settings, Control::IControlAppearance unfocusedAppearance); IControlSettings Settings() const; + uint64_t ContentId() const; + hstring GetProfileName() const; bool CopySelectionToClipboard(bool singleLine, const Windows::Foundation::IReference& formats); @@ -91,7 +95,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ToggleShaderEffects(); - winrt::fire_and_forget RenderEngineSwapChainChanged(IInspectable sender, IInspectable args); + void RenderEngineSwapChainChanged(IInspectable sender, IInspectable args); void _AttachDxgiSwapChainToXaml(HANDLE swapChainHandle); winrt::fire_and_forget _RendererEnteredErrorState(IInspectable sender, IInspectable args); @@ -124,6 +128,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool ReadOnly() const noexcept; void ToggleReadOnly(); + void SetReadOnly(const bool readOnlyState); static Control::MouseButtonState GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point); static unsigned int GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point); @@ -136,21 +141,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation void AdjustOpacity(const double opacity, const bool relative); - WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + void Detach(); + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); // -------------------------------- WinRT Events --------------------------------- // clang-format off WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); - PROJECTED_FORWARDED_TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs, _core, CopyToClipboard); - PROJECTED_FORWARDED_TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs, _core, TitleChanged); - PROJECTED_FORWARDED_TYPED_EVENT(TabColorChanged, IInspectable, IInspectable, _core, TabColorChanged); - PROJECTED_FORWARDED_TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable, _core, TaskbarProgressChanged); - PROJECTED_FORWARDED_TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable, _core, ConnectionStateChanged); - PROJECTED_FORWARDED_TYPED_EVENT(ShowWindowChanged, IInspectable, Control::ShowWindowArgs, _core, ShowWindowChanged); - PROJECTED_FORWARDED_TYPED_EVENT(CloseTerminalRequested, IInspectable, IInspectable, _core, CloseTerminalRequested); + // UNDER NO CIRCUMSTANCES SHOULD YOU ADD A (PROJECTED_)FORWARDED_TYPED_EVENT HERE + // Those attach the handler to the core directly, and will explode if + // the core ever gets detached & reattached to another window. + BUBBLED_FORWARDED_TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs); + BUBBLED_FORWARDED_TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs); + BUBBLED_FORWARDED_TYPED_EVENT(TabColorChanged, IInspectable, IInspectable); + BUBBLED_FORWARDED_TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable); + BUBBLED_FORWARDED_TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable); + BUBBLED_FORWARDED_TYPED_EVENT(ShowWindowChanged, IInspectable, Control::ShowWindowArgs); + BUBBLED_FORWARDED_TYPED_EVENT(CloseTerminalRequested, IInspectable, IInspectable); - PROJECTED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs, _interactivity, PasteFromClipboard); + BUBBLED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); TYPED_EVENT(RaiseNotice, IInspectable, Control::NoticeEventArgs); @@ -160,6 +169,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(FocusFollowMouseRequested, IInspectable, IInspectable); TYPED_EVENT(Initialized, Control::TermControl, Windows::UI::Xaml::RoutedEventArgs); TYPED_EVENT(WarningBell, IInspectable, IInspectable); + // clang-format on WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, BackgroundBrush, _PropertyChangedHandlers, nullptr); @@ -218,6 +228,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool _showMarksInScrollbar{ false }; bool _isBackgroundLight{ false }; + bool _detached{ false }; + + winrt::Windows::Foundation::Collections::IObservableVector _originalPrimaryElements{ nullptr }; + winrt::Windows::Foundation::Collections::IObservableVector _originalSecondaryElements{ nullptr }; + winrt::Windows::Foundation::Collections::IObservableVector _originalSelectedPrimaryElements{ nullptr }; + winrt::Windows::Foundation::Collections::IObservableVector _originalSelectedSecondaryElements{ nullptr }; inline bool _IsClosing() const noexcept { @@ -231,6 +247,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _closing; } + void _initializeForAttach(const Microsoft::Terminal::Control::IKeyBindings& keyBindings); + void _UpdateSettingsFromUIThread(); void _UpdateAppearanceFromUIThread(Control::IControlAppearance newAppearance); void _ApplyUISettings(); @@ -243,7 +261,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation static bool _isColorLight(til::color bg) noexcept; void _changeBackgroundOpacity(); - bool _InitializeTerminal(); + enum InitializeReason : bool + { + Create, + Reattach + }; + bool _InitializeTerminal(const InitializeReason reason); void _SetFontSize(int fontSize); void _TappedHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e); void _KeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); @@ -315,6 +338,43 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::point _toPosInDips(const Core::Point terminalCellPos); void _throttledUpdateScrollbar(const ScrollBarUpdate& update); + + void _contextMenuHandler(IInspectable sender, Control::ContextMenuRequestedEventArgs args); + + void _PasteCommandHandler(const IInspectable& sender, const IInspectable& args); + void _CopyCommandHandler(const IInspectable& sender, const IInspectable& args); + void _SearchCommandHandler(const IInspectable& sender, const IInspectable& args); + + struct Revokers + { + Control::ControlCore::ScrollPositionChanged_revoker coreScrollPositionChanged; + Control::ControlCore::WarningBell_revoker WarningBell; + Control::ControlCore::CursorPositionChanged_revoker CursorPositionChanged; + Control::ControlCore::RendererEnteredErrorState_revoker RendererEnteredErrorState; + Control::ControlCore::BackgroundColorChanged_revoker BackgroundColorChanged; + Control::ControlCore::FontSizeChanged_revoker FontSizeChanged; + Control::ControlCore::TransparencyChanged_revoker TransparencyChanged; + Control::ControlCore::RaiseNotice_revoker RaiseNotice; + Control::ControlCore::HoveredHyperlinkChanged_revoker HoveredHyperlinkChanged; + Control::ControlCore::FoundMatch_revoker FoundMatch; + Control::ControlCore::UpdateSelectionMarkers_revoker UpdateSelectionMarkers; + Control::ControlCore::OpenHyperlink_revoker coreOpenHyperlink; + Control::ControlCore::CopyToClipboard_revoker CopyToClipboard; + Control::ControlCore::TitleChanged_revoker TitleChanged; + Control::ControlCore::TabColorChanged_revoker TabColorChanged; + Control::ControlCore::TaskbarProgressChanged_revoker TaskbarProgressChanged; + Control::ControlCore::ConnectionStateChanged_revoker ConnectionStateChanged; + Control::ControlCore::ShowWindowChanged_revoker ShowWindowChanged; + Control::ControlCore::CloseTerminalRequested_revoker CloseTerminalRequested; + // These are set up in _InitializeTerminal + Control::ControlCore::RendererWarning_revoker RendererWarning; + Control::ControlCore::SwapChainChanged_revoker SwapChainChanged; + + Control::ControlInteractivity::OpenHyperlink_revoker interactivityOpenHyperlink; + Control::ControlInteractivity::ScrollPositionChanged_revoker interactivityScrollPositionChanged; + Control::ControlInteractivity::PasteFromClipboard_revoker PasteFromClipboard; + Control::ControlInteractivity::ContextMenuRequested_revoker ContextMenuRequested; + } _revokers{}; }; } diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 044bfb0a6b1..b622db628e9 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -3,6 +3,7 @@ import "IMouseWheelListener.idl"; import "IControlSettings.idl"; +import "ControlInteractivity.idl"; import "IDirectKeyListener.idl"; import "EventArgs.idl"; import "ICoreState.idl"; @@ -17,10 +18,14 @@ namespace Microsoft.Terminal.Control ICoreState, Windows.UI.Xaml.Data.INotifyPropertyChanged { + TermControl(ControlInteractivity content); + TermControl(IControlSettings settings, IControlAppearance unfocusedAppearance, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); + static TermControl NewControlByAttachingContent(ControlInteractivity content, Microsoft.Terminal.Control.IKeyBindings keyBindings); + static Windows.Foundation.Size GetProposedDimensions(IControlSettings settings, UInt32 dpi, Int32 commandlineCols, @@ -29,6 +34,8 @@ namespace Microsoft.Terminal.Control void UpdateControlSettings(IControlSettings settings); void UpdateControlSettings(IControlSettings settings, IControlAppearance unfocusedAppearance); + UInt64 ContentId{ get; }; + Microsoft.Terminal.Control.IControlSettings Settings { get; }; event FontSizeChangedEventArgs FontSizeChanged; @@ -45,6 +52,9 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusFollowMouseRequested; + Microsoft.UI.Xaml.Controls.CommandBarFlyout ContextMenu { get; }; + Microsoft.UI.Xaml.Controls.CommandBarFlyout SelectionContextMenu { get; }; + event Windows.Foundation.TypedEventHandler Initialized; // This is an event handler forwarder for the underlying connection. // We expose this and ConnectionState here so that it might eventually be data bound. @@ -86,6 +96,7 @@ namespace Microsoft.Terminal.Control Boolean ReadOnly { get; }; void ToggleReadOnly(); + void SetReadOnly(Boolean readOnlyState); String ReadEntireBuffer(); @@ -100,5 +111,7 @@ namespace Microsoft.Terminal.Control Windows.UI.Xaml.Media.Brush BackgroundBrush { get; }; void ColorSelection(SelectionColor fg, SelectionColor bg, Microsoft.Terminal.Core.MatchMode matchMode); + + void Detach(); } } diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index 1e2428eba3b..58b5c8b08d0 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -10,6 +10,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:Microsoft.Terminal.Control" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:mux="using:Microsoft.UI.Xaml.Controls" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" d:DesignHeight="768" @@ -31,6 +32,30 @@ mc:Ignorable="d"> + + + + + + + + + + + + 16 - 0 - 3