From 04d3ec8253a5fd8b2ecb9d4c49d02d0e118a3d9d Mon Sep 17 00:00:00 2001 From: Jamie D Date: Thu, 25 Jun 2020 20:54:06 +0100 Subject: [PATCH] Add project files. --- .github/workflows/dotnet-core.yml | 528 +++++ GitVersion.yml | 24 + README.md | 4 + SmartThingsTerminal.sln | 37 + SmartThingsTerminal/API/SmartThingsClient.cs | 251 +++ SmartThingsTerminal/Program.cs | 473 ++++ .../Properties/launchSettings.json | 7 + SmartThingsTerminal/Scenario.cs | 307 +++ SmartThingsTerminal/Scenarios/Devices.cs | 192 ++ SmartThingsTerminal/Scenarios/Locations.cs | 108 + SmartThingsTerminal/Scenarios/Rules.cs | 109 + SmartThingsTerminal/Scenarios/Scenes.cs | 109 + SmartThingsTerminal/Scenarios/Schedules.cs | 110 + .../Scenarios/Subscriptions.cs | 108 + .../SmartThingsTerminal.csproj | 28 + SmartThingsTerminal/smartthings.ico | Bin 0 -> 20846 bytes .../CursesDriver/CursesDriver.cs | 761 +++++++ .../ConsoleDrivers/CursesDriver/README.md | 5 + .../CursesDriver/UnixMainLoop.cs | 229 ++ .../CursesDriver/UnmanagedLibrary.cs | 268 +++ .../ConsoleDrivers/CursesDriver/binding.cs | 454 ++++ .../ConsoleDrivers/CursesDriver/constants.cs | 158 ++ .../ConsoleDrivers/CursesDriver/handles.cs | 174 ++ .../ConsoleDrivers/FakeDriver/FakeConsole.cs | 1979 +++++++++++++++++ .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 468 ++++ Terminal.Gui/ConsoleDrivers/NetDriver.cs | 476 ++++ Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 1341 +++++++++++ Terminal.Gui/Core/Application.cs | 711 ++++++ Terminal.Gui/Core/ConsoleDriver.cs | 1051 +++++++++ Terminal.Gui/Core/Event.cs | 593 +++++ Terminal.Gui/Core/MainLoop.cs | 246 ++ Terminal.Gui/Core/PosDim.cs | 604 +++++ Terminal.Gui/Core/Responder.cs | 185 ++ Terminal.Gui/Core/Toplevel.cs | 335 +++ Terminal.Gui/Core/View.cs | 1692 ++++++++++++++ Terminal.Gui/Core/Window.cs | 267 +++ Terminal.Gui/README.md | 25 + Terminal.Gui/Terminal.Gui.csproj | 195 ++ Terminal.Gui/Types/Point.cs | 258 +++ Terminal.Gui/Types/Rect.cs | 497 +++++ Terminal.Gui/Types/Size.cs | 251 +++ Terminal.Gui/Views/Button.cs | 263 +++ Terminal.Gui/Views/Checkbox.cs | 162 ++ Terminal.Gui/Views/Clipboard.cs | 15 + Terminal.Gui/Views/ComboBox.cs | 372 ++++ Terminal.Gui/Views/DateField.cs | 414 ++++ Terminal.Gui/Views/FrameView.cs | 162 ++ Terminal.Gui/Views/HexView.cs | 399 ++++ Terminal.Gui/Views/Label.cs | 364 +++ Terminal.Gui/Views/ListView.cs | 665 ++++++ Terminal.Gui/Views/Menu.cs | 1277 +++++++++++ Terminal.Gui/Views/ProgressBar.cs | 106 + Terminal.Gui/Views/RadioGroup.cs | 280 +++ Terminal.Gui/Views/ScrollView.cs | 649 ++++++ Terminal.Gui/Views/StatusBar.cs | 196 ++ Terminal.Gui/Views/TextField.cs | 804 +++++++ Terminal.Gui/Views/TextView.cs | 1234 ++++++++++ Terminal.Gui/Views/TimeField.cs | 299 +++ Terminal.Gui/Windows/Dialog.cs | 140 ++ Terminal.Gui/Windows/FileDialog.cs | 718 ++++++ Terminal.Gui/Windows/MessageBox.cs | 156 ++ Terminal.Gui/packages.config | 4 + 62 files changed, 24297 insertions(+) create mode 100644 .github/workflows/dotnet-core.yml create mode 100644 GitVersion.yml create mode 100644 README.md create mode 100644 SmartThingsTerminal.sln create mode 100644 SmartThingsTerminal/API/SmartThingsClient.cs create mode 100644 SmartThingsTerminal/Program.cs create mode 100644 SmartThingsTerminal/Properties/launchSettings.json create mode 100644 SmartThingsTerminal/Scenario.cs create mode 100644 SmartThingsTerminal/Scenarios/Devices.cs create mode 100644 SmartThingsTerminal/Scenarios/Locations.cs create mode 100644 SmartThingsTerminal/Scenarios/Rules.cs create mode 100644 SmartThingsTerminal/Scenarios/Scenes.cs create mode 100644 SmartThingsTerminal/Scenarios/Schedules.cs create mode 100644 SmartThingsTerminal/Scenarios/Subscriptions.cs create mode 100644 SmartThingsTerminal/SmartThingsTerminal.csproj create mode 100644 SmartThingsTerminal/smartthings.ico create mode 100644 Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs create mode 100644 Terminal.Gui/ConsoleDrivers/CursesDriver/README.md create mode 100644 Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs create mode 100644 Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs create mode 100644 Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs create mode 100644 Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs create mode 100644 Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs create mode 100644 Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs create mode 100644 Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs create mode 100644 Terminal.Gui/ConsoleDrivers/NetDriver.cs create mode 100644 Terminal.Gui/ConsoleDrivers/WindowsDriver.cs create mode 100644 Terminal.Gui/Core/Application.cs create mode 100644 Terminal.Gui/Core/ConsoleDriver.cs create mode 100644 Terminal.Gui/Core/Event.cs create mode 100644 Terminal.Gui/Core/MainLoop.cs create mode 100644 Terminal.Gui/Core/PosDim.cs create mode 100644 Terminal.Gui/Core/Responder.cs create mode 100644 Terminal.Gui/Core/Toplevel.cs create mode 100644 Terminal.Gui/Core/View.cs create mode 100644 Terminal.Gui/Core/Window.cs create mode 100644 Terminal.Gui/README.md create mode 100644 Terminal.Gui/Terminal.Gui.csproj create mode 100644 Terminal.Gui/Types/Point.cs create mode 100644 Terminal.Gui/Types/Rect.cs create mode 100644 Terminal.Gui/Types/Size.cs create mode 100644 Terminal.Gui/Views/Button.cs create mode 100644 Terminal.Gui/Views/Checkbox.cs create mode 100644 Terminal.Gui/Views/Clipboard.cs create mode 100644 Terminal.Gui/Views/ComboBox.cs create mode 100644 Terminal.Gui/Views/DateField.cs create mode 100644 Terminal.Gui/Views/FrameView.cs create mode 100644 Terminal.Gui/Views/HexView.cs create mode 100644 Terminal.Gui/Views/Label.cs create mode 100644 Terminal.Gui/Views/ListView.cs create mode 100644 Terminal.Gui/Views/Menu.cs create mode 100644 Terminal.Gui/Views/ProgressBar.cs create mode 100644 Terminal.Gui/Views/RadioGroup.cs create mode 100644 Terminal.Gui/Views/ScrollView.cs create mode 100644 Terminal.Gui/Views/StatusBar.cs create mode 100644 Terminal.Gui/Views/TextField.cs create mode 100644 Terminal.Gui/Views/TextView.cs create mode 100644 Terminal.Gui/Views/TimeField.cs create mode 100644 Terminal.Gui/Windows/Dialog.cs create mode 100644 Terminal.Gui/Windows/FileDialog.cs create mode 100644 Terminal.Gui/Windows/MessageBox.cs create mode 100644 Terminal.Gui/packages.config diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml new file mode 100644 index 0000000..c34f384 --- /dev/null +++ b/.github/workflows/dotnet-core.yml @@ -0,0 +1,528 @@ +name: .NET Core + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Fetch all history for all tags and branches + run: git fetch --prune --unshallow + + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v0.9.2 + with: + versionSpec: '5.2.x' + + - name: GitVersion + uses: docker://gittools/gitversion:5.2.5-linux-ubuntu-16.04-netcoreapp2.1 + with: + args: /github/workspace /nofetch /output buildserver + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.101 + + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + - name: Test + run: dotnet test --no-restore --verbosity normal + + - name: Publish win-arm + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win-arm -c Release -r win-arm -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win-arm release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win-arm/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win-arm.zip + + - name: Publish Win-arm64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win-arm64 -c Release -r win-arm64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win-arm64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win-arm64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win-arm64.zip + + - name: Publish win7-x86 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win7-x86 -c Release -r win7-x86 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win7-x86 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win7-x86/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win7-x86.zip + + - name: Publish win7-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win7-x64 -c Release -r win7-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win7-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win7-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win7-x64.zip + + - name: Publish win81-x86 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win81-x86 -c Release -r win81-x86 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win81-x86 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win81-x86/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win81-x86.zip + + - name: Publish win81-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win81-x64 -c Release -r win81-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win81-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win81-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win81-x64.zip + + - name: Publish win10-x86 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win10-x86 -c Release -r win10-x86 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win10-x86 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win10-x86/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win10-x86.zip + + - name: Publish win10-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win10-x86 -c Release -r win10-x86 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win10-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win10-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win10-x64.zip + + - name: Publish win10-arm + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win10-arm -c Release -r win10-arm -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win10-arm release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win10-arm/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win10-arm.zip + + - name: Publish win10-arm64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win10-arm64 -c Release -r win10-arm64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win10-arm64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win10-arm64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win10-arm64.zip + + - name: Publish win-x86 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win-x86 -c Release -r win-x86 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win-x86 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win-x86/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win-x86.zip + + - name: Publish win10x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/win-x64 -c Release -r win-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip win10x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/win10x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-win10x64.zip + + - name: Publish linux-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/linux-x64 -c Release -r linux-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip linux-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/linux-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-linux-x64.zip + + - name: Publish linux-musl-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/linux-musl-x64 -c Release -r linux-musl-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip linux-musl-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/linux-musl-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-linux-musl-x64.zip + + - name: Publish linux-arm + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/linux-arm -c Release -r linux-arm -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip linux-arm release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/linux-arm/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-linux-arm.zip + + - name: Publish linux-arm64 + run: dotnet publish ./SmartThingsTerminal/SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/linux-arm64 -c Release -r linux-arm64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip linux-arm64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/linux-arm64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-linux-arm64.zip + + - name: Publish rhel-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/rhel-x64 -c Release -r rhel-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip rhel-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/rhel-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-rhel-x64.zip + + - name: Publish rhel.6-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/rhel.6-x64 -c Release -r rhel.6-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip rhel.6-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/rhel.6-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-rhel.6-x64.zip + + - name: Publish osx-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/osx-x64 -c Release -r osx-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip osx-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/osx-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-osx-x64.zip + + - name: Publish osx.10.10-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/osx.10.10-x64 -c Release -r osx.10.10-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip osx.10.10-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/osx.10.10-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-osx.10.10-x64.zip + + - name: Publish osx.10.11-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/osx.10.11-x64 -c Release -r osx.10.11-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip osx.10.11-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/osx.10.11-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-osx.10.11-x64.zip + + - name: Publish osx.10.12-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/osx.10.12-x64 -c Release -r osx.10.12-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip osx.10.12-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/osx.10.12-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-osx.10.12-x64.zip + + - name: Publish osx.10.13-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/osx.10.13-x64 -c Release -r osx.10.13-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip osx.10.13-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/osx.10.13-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-osx.10.13-x64.zip + + - name: Publish osx.10.14-x64 + run: dotnet publish .//SmartThingsTerminal//SmartThingsTerminal.csproj -o publish/v${{ env.GitVersion_SemVer }}/osx.10.14-x64 -c Release -r osx.10.14-x64 -f netcoreapp3.1 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:AssemblyVersion=${{ env.GitVersion_SemVer }} /p:FileVersion=${{ env.GitVersion_SemVer }} /p:InformationalVersion=${{ env.GitVersion_SemVer }} + + - name: Zip osx.10.14-x64 release + uses: papeloto/action-zip@v1 + with: + files: publish/v${{ env.GitVersion_SemVer }}/osx.10.14-x64/ + recursive: true + dest: v${{ env.GitVersion_SemVer }}-osx.10.14-x64.zip + + #- name: Upload releases + # uses: actions/upload-artifact@v2 + # with: + # name: Assets + # path: .//publish/**/* + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.GitVersion_SemVer }} + release_name: Release ${{ env.GitVersion_SemVer }} + draft: false + prerelease: false + + + - name: Upload win-arm Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win-arm.zip + asset_name: v${{ env.GitVersion_SemVer }}-win-arm.zip + asset_content_type: application/zip + + - name: Upload win-arm64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win-arm64.zip + asset_name: v${{ env.GitVersion_SemVer }}-win-arm64.zip + asset_content_type: application/zip + + - name: Upload win7-x86 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win7-x86.zip + asset_name: v${{ env.GitVersion_SemVer }}-win7-x86.zip + asset_content_type: application/zip + + - name: Upload win7-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win7-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-win7-x64.zip + asset_content_type: application/zip + + - name: Upload win81-x86 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win81-x86.zip + asset_name: v${{ env.GitVersion_SemVer }}-win81-x86.zip + asset_content_type: application/zip + + - name: Upload win81-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win81-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-win81-x64.zip + asset_content_type: application/zip + + - name: Upload win10-x86 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win10-x86.zip + asset_name: v${{ env.GitVersion_SemVer }}-win10-x86.zip + asset_content_type: application/zip + + - name: Upload win10-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win10-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-win10-x64.zip + asset_content_type: application/zip + + - name: Upload win10-arm Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win10-arm.zip + asset_name: v${{ env.GitVersion_SemVer }}-win10-arm.zip + asset_content_type: application/zip + + - name: Upload win10-arm64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win10-arm64.zip + asset_name: v${{ env.GitVersion_SemVer }}-win10-arm64.zip + asset_content_type: application/zip + + - name: Upload win-x86 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-win-x86.zip + asset_name: v${{ env.GitVersion_SemVer }}-win-x86.zip + asset_content_type: application/zip + + - name: Upload linux-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-linux-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-linux-x64.zip + asset_content_type: application/zip + + - name: Upload linux-musl-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-linux-musl-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-linux-musl-x64.zip + asset_content_type: application/zip + + - name: Upload linux-arm Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-linux-arm.zip + asset_name: v${{ env.GitVersion_SemVer }}-linux-arm.zip + asset_content_type: application/zip + + - name: Upload linux-arm64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-linux-arm64.zip + asset_name: v${{ env.GitVersion_SemVer }}-linux-arm64.zip + asset_content_type: application/zip + + - name: Upload rhel-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-rhel-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-rhel-x64.zip + asset_content_type: application/zip + + - name: Upload rhel.6-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-rhel.6-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-rhel.6-x64.zip + asset_content_type: application/zip + + - name: Upload osx-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-osx-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-osx-x64.zip + asset_content_type: application/zip + + - name: Upload osx.10.10-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-osx.10.10-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-osx.10.10-x64.zip + asset_content_type: application/zip + + - name: Upload osx.10.11-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-osx.10.11-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-osx.10.11-x64.zip + asset_content_type: application/zip + + - name: Upload osx.10.12-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-osx.10.12-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-osx.10.12-x64.zip + asset_content_type: application/zip + + - name: Upload osx.10.13-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-osx.10.13-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-osx.10.13-x64.zip + asset_content_type: application/zip + + - name: Upload osx.10.14-x64 Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./v${{ env.GitVersion_SemVer }}-osx.10.14-x64.zip + asset_name: v${{ env.GitVersion_SemVer }}-osx.10.14-x64.zip + asset_content_type: application/zip + diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..29c38d3 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,24 @@ +mode: Mainline +next-version: 0.9.0 +assembly-versioning-scheme: MajorMinorPatch +assembly-file-versioning-format: '{Major}.{Minor}.{Patch}' +assembly-informational-format: '{FullBuildMetaData}' +major-version-bump-message: '\+semver:\s?(breaking|major)' +minor-version-bump-message: '\+semver:\s?(feature|minor)' +patch-version-bump-message: '\+semver:\s?(fix|patch)' +no-bump-message: '\+semver:\s?(none|skip)' +commit-message-incrementing: Enabled +merge-message-formats: {} +branches: + master: + track-merge-target: false + tracks-release-branches: false + is-release-branch: false + is-mainline: true + increment: Patch + prevent-increment-of-merged-branch-version: true + + # mode: ContinuousDelivery + tag: '' +ignore: + sha: [] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..38b75d5 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# SmartThings Terminal + +![.NET Core](https://github.com/daltskin/SmartThingsTerminal/workflows/.NET%20Core/badge.svg) +[![Release](https://img.shields.io/github/release/daltskin/SmartThingsTerminal.svg?style=flat-square)](https://github.com/daltskin/SmartThingsTerminal/releases/latest) \ No newline at end of file diff --git a/SmartThingsTerminal.sln b/SmartThingsTerminal.sln new file mode 100644 index 0000000..a7f04ea --- /dev/null +++ b/SmartThingsTerminal.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30204.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartThingsTerminal", "SmartThingsTerminal\SmartThingsTerminal.csproj", "{9DF95721-8429-48A4-BFA7-8F1C006E8322}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8AB9BD79-B877-4441-B6A4-88F1F71EE4E2}" + ProjectSection(SolutionItems) = preProject + GitVersion.yml = GitVersion.yml + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui", "Terminal.Gui\Terminal.Gui.csproj", "{007B0279-3E5F-440D-A5BC-522415D5ABF1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9DF95721-8429-48A4-BFA7-8F1C006E8322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DF95721-8429-48A4-BFA7-8F1C006E8322}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DF95721-8429-48A4-BFA7-8F1C006E8322}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DF95721-8429-48A4-BFA7-8F1C006E8322}.Release|Any CPU.Build.0 = Release|Any CPU + {007B0279-3E5F-440D-A5BC-522415D5ABF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {007B0279-3E5F-440D-A5BC-522415D5ABF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {007B0279-3E5F-440D-A5BC-522415D5ABF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {007B0279-3E5F-440D-A5BC-522415D5ABF1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67A7EDCD-7026-4DEE-A90D-4758DF431428} + EndGlobalSection +EndGlobal diff --git a/SmartThingsTerminal/API/SmartThingsClient.cs b/SmartThingsTerminal/API/SmartThingsClient.cs new file mode 100644 index 0000000..f0b2972 --- /dev/null +++ b/SmartThingsTerminal/API/SmartThingsClient.cs @@ -0,0 +1,251 @@ +using SmartThingsNet.Api; +using SmartThingsNet.Client; +using SmartThingsNet.Model; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SmartThingsTerminal +{ + public class SmartThingsClient + { + private PagedDevices _allDevices; + private PagedLocations _allLocations; + private PagedRooms _allRooms; + private PagedScene _allScenes; + private PagedRules _allRules; + private PagedSchedules _allSchedules; + private PagedApps _allApps; + private PagedSubscriptions _allSubscriptions; + private PagedInstalledApps _allInstalledApps; + private PagedDeviceProfiles _allDeviceProfiles; + + private DevicesApi _devicesApi; + private LocationsApi _locationsApi; + private RoomsApi _roomsApi; + private ScenesApi _scenesApi; + private RulesApi _rulesApi; + private SchedulesApi _schedulesApi; + private AppsApi _appsApi; + private SubscriptionsApi _subscriptionsApi; + private InstalledAppsApi _installedAppsApi; + private DeviceProfilesApi _deviceProfilesApi; + + public SmartThingsClient(string accessToken) + { + if (accessToken == null) + { + throw new ArgumentNullException(accessToken); + } + + var configuration = new Configuration(); + configuration.AccessToken = accessToken; + + _devicesApi = new DevicesApi(configuration); + _locationsApi = new LocationsApi(configuration); + _roomsApi = new RoomsApi(configuration); + _scenesApi = new ScenesApi(configuration); + _rulesApi = new RulesApi(configuration); + _schedulesApi = new SchedulesApi(configuration); + _appsApi = new AppsApi(configuration); + _subscriptionsApi = new SubscriptionsApi(configuration); + _installedAppsApi = new InstalledAppsApi(configuration); + _deviceProfilesApi = new DeviceProfilesApi(configuration); + } + + public void ResetData() + { + _allDevices = null; + _allLocations = null; + _allRooms = null; + _allScenes = null; + _allRules = null; + _allSchedules = null; + _allApps = null; + _allSubscriptions = null; + _allInstalledApps = null; + _allDeviceProfiles = null; + } + + public PagedApps GetAllApps() + { + if (_allApps == null) + { + _allApps = _appsApi.ListApps(); + } + return _allApps; + } + + public PagedInstalledApps GetAllInstalledApps(string locationId = null) + { + if (_allInstalledApps == null) + { + if (locationId != null) + { + _allInstalledApps = _installedAppsApi.ListInstallations(locationId); + } + else + { + _allInstalledApps = new PagedInstalledApps(); + foreach (var location in GetAllLocations().Items) + { + var locationApps = _installedAppsApi.ListInstallations(location.LocationId.ToString()); + if (locationApps.Items?.Count > 0) + { + _allInstalledApps.Items ??= new List(); + _allInstalledApps.Items.AddRange(locationApps.Items); + } + } + } + } + return _allInstalledApps; + } + + public PagedSubscriptions GetAllSubscriptions(string appId = null) + { + if (_allSubscriptions == null) + { + if (appId != null) + { + _allSubscriptions = _subscriptionsApi.ListSubscriptions(appId); + } + else + { + _allSubscriptions = new PagedSubscriptions(); + foreach (var app in GetAllInstalledApps().Items) + { + var appSubscriptions = _subscriptionsApi.ListSubscriptions(app.InstalledAppId.ToString()); + if (appSubscriptions.Items?.Count > 0) + { + _allSubscriptions.Items ??= new List(); + _allSubscriptions.Items.AddRange(appSubscriptions.Items); + } + } + } + } + return _allSubscriptions; + } + + public async Task GetAllDevicesAsync() + { + if (_allDevices == null) + { + _allDevices = await _devicesApi.GetDevicesAsync(); + } + return _allDevices; + } + + public PagedDevices GetAllDevices() + { + if (_allDevices == null) + { + _allDevices = _devicesApi.GetDevices(); + } + return _allDevices; + } + + public PagedDeviceProfiles GetAllDeviceProfiles() + { + if (_allDeviceProfiles == null) + { + _allDeviceProfiles = _deviceProfilesApi.ListDeviceProfiles(); + } + return _allDeviceProfiles; + } + + public async Task GetAllLocationsAsync() + { + if (_allLocations == null) + { + _allLocations = await _locationsApi.ListLocationsAsync(); + } + return _allLocations; + } + + public PagedLocations GetAllLocations() + { + if (_allLocations == null) + { + _allLocations = _locationsApi.ListLocations(); + } + return _allLocations; + } + + public async Task GetAllRoomsAsync(string locationId) + { + if (_allRooms == null) + { + _allRooms = await _roomsApi.ListRoomsAsync(locationId); + } + return _allRooms; + } + + public PagedRooms GetAllRooms(string locationId) + { + if (_allRooms == null) + { + _allRooms = _roomsApi.ListRooms(locationId); + } + return _allRooms; + } + + public PagedScene GetAllScenes(string locationId = null) + { + if (_allScenes == null) + { + _allScenes = _scenesApi.ListScenes(locationId); + } + return _allScenes; + } + + public PagedRules GetAllRules(string locationId = null) + { + if (_allRules == null) + { + if (locationId != null) + { + _allRules = _rulesApi.ListRules(locationId); + } + else + { + _allRules = new PagedRules(); + foreach (var location in GetAllLocations().Items) + { + var locationRules = _rulesApi.ListRules(location.LocationId.ToString()); + if (locationRules.Items?.Count > 0) + { + _allRules.Items ??= new List(); + _allRules.Items.AddRange(locationRules.Items); + } + } + } + } + return _allRules; + } + + public PagedSchedules GetAllSchedules(string appId = null) + { + if (_allSchedules == null) + { + if (appId != null) + { + _allSchedules = _schedulesApi.GetSchedules(appId); + } + else + { + _allSchedules = new PagedSchedules(); + foreach (var app in GetAllInstalledApps().Items) + { + var appSchedules = _schedulesApi.GetSchedules(app.InstalledAppId.ToString()); + if (appSchedules.Items?.Count > 0) + { + _allSchedules.Items ??= new List(); + _allSchedules.Items.AddRange(appSchedules.Items); + } + } + } + } + return _allSchedules; + } + } +} diff --git a/SmartThingsTerminal/Program.cs b/SmartThingsTerminal/Program.cs new file mode 100644 index 0000000..ce4b2b1 --- /dev/null +++ b/SmartThingsTerminal/Program.cs @@ -0,0 +1,473 @@ +using CommandLine; +using CommandLine.Text; +using NStack; +using SmartThingsTerminal; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using Terminal.Gui; +using Rune = System.Rune; + +namespace SmartThingsTerminal +{ + class Program + { + private static Toplevel _top; + private static MenuBar _menu; + private static int _nameColumnWidth; + private static FrameView _leftPane; + private static List _categories; + private static ListView _categoryListView; + private static FrameView _rightPane; + private static FrameView _appTitlePane; + private static List _scenarios; + private static ListView _scenarioListView; + private static StatusBar _statusBar; + private static StatusItem _capslock; + private static StatusItem _numlock; + private static StatusItem _scrolllock; + + private static Scenario _runningScenario = null; + private static bool _useSystemConsole = false; + + private static SmartThingsClient _stClient; + + public class Options + { + [Option('t', "accesstoken", Required = true, HelpText = "OAuth Personal access token - generate from: https://account.smartthings.com/tokens")] + public string AccessToken { get; set; } + + [Option('n', "apiname", Required = false, HelpText = "Jump into specific api: {device/locations/rules/scenes/schedules/subscriptions}")] + public string ApiName { get; set; } + } + + static void Main(string[] args) + { + CommandLine.Parser.Default.ParseArguments(args).WithParsed(Init); + } + + private static void Init(Options opts) + { + Console.Title = "SmartThings Terminal"; + _stClient = new SmartThingsClient(opts.AccessToken); + + if (Debugger.IsAttached) + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo("en-US"); + + _scenarios = Scenario.GetDerivedClasses().OrderBy(t => Scenario.ScenarioMetadata.GetName(t)).ToList(); + + if (opts.ApiName != null) + { + var item = _scenarios.FindIndex(t => Scenario.ScenarioMetadata.GetName(t).Equals(opts.ApiName, StringComparison.OrdinalIgnoreCase)); + + try + { + var selectedScenario = _scenarios[item]; + if (selectedScenario != null) + { + _runningScenario = (Scenario)Activator.CreateInstance(selectedScenario); + Application.Init(); + _runningScenario.Init(Application.Top, _baseColorScheme, _stClient); + _runningScenario.Setup(); + _runningScenario.Run(); + _runningScenario = null; + return; + } + } + catch (Exception) + { + // invalid option + } + + } + + Scenario scenario = GetScenarioToRun(); + while (scenario != null) + { + Application.UseSystemConsole = _useSystemConsole; + Application.Init(); + scenario.Init(Application.Top, _baseColorScheme, _stClient); + scenario.Setup(); + scenario.Run(); + scenario = GetScenarioToRun(); + } + if (!_top.Running) + Application.Shutdown(true); + } + + /// + /// This shows the selection UI. Each time it is run, it calls Application.Init to reset everything. + /// + /// + private static Scenario GetScenarioToRun() + { + Application.UseSystemConsole = false; + Application.Init(); + + if (_menu == null) + { + Setup(); + } + + _top = Application.Top; + _top.KeyDown += KeyDownHandler; + + _top.Add(_menu); + _top.Add(_leftPane); + _top.Add(_appTitlePane); + _top.Add(_rightPane); + _top.Add(_statusBar); + + _top.Ready += () => { + if (_runningScenario != null) + { + _top.SetFocus(_rightPane); + _runningScenario = null; + } + }; + + Application.Run(_top, false); + Application.Shutdown(false); + return _runningScenario; + } + + static MenuItem[] CreateDiagnosticMenuItems() + { + MenuItem CheckedMenuMenuItem(ustring menuItem, Action action, Func checkFunction) + { + var mi = new MenuItem(); + mi.Title = menuItem; + mi.CheckType |= MenuItemCheckStyle.Checked; + mi.Checked = checkFunction(); + mi.Action = () => { + action?.Invoke(); + mi.Title = menuItem; + mi.Checked = checkFunction(); + }; + return mi; + } + + return new MenuItem[] { + CheckedMenuMenuItem ("Use _System Console", + () => { + _useSystemConsole = !_useSystemConsole; + }, + () => _useSystemConsole), + CheckedMenuMenuItem ("Diagnostics: _Frame Padding", + () => { + ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding; + _top.SetNeedsDisplay (); + }, + () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding), + CheckedMenuMenuItem ("Diagnostics: Frame _Ruler", + () => { + ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler; + _top.SetNeedsDisplay (); + }, + () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler), + }; + } + + static void SetColorScheme() + { + _leftPane.ColorScheme = _baseColorScheme; + _rightPane.ColorScheme = _baseColorScheme; + _appTitlePane.ColorScheme = _baseColorScheme; + _top?.SetNeedsDisplay(); + } + + static ColorScheme _baseColorScheme; + static MenuItem[] CreateColorSchemeMenuItems() + { + List menuItems = new List(); + foreach (var sc in Colors.ColorSchemes) + { + var item = new MenuItem(); + item.Title = sc.Key; + item.CheckType |= MenuItemCheckStyle.Radio; + item.Checked = sc.Value == _baseColorScheme; + item.Action += () => { + _baseColorScheme = sc.Value; + SetColorScheme(); + foreach (var menuItem in menuItems) + { + menuItem.Checked = menuItem.Title.Equals(sc.Key) && sc.Value == _baseColorScheme; + } + }; + menuItems.Add(item); + } + return menuItems.ToArray(); + } + + /// + /// Create all controls. This gets called once and the controls remain with their state between Sceanrio runs. + /// + private static void Setup() + { + // Set this here because not initilzied until driver is loaded + _baseColorScheme = Colors.Base; + + StringBuilder aboutMessage = new StringBuilder(); + aboutMessage.AppendLine(); + aboutMessage.AppendLine(GetAppTitle()); + aboutMessage.AppendLine(); + aboutMessage.AppendLine("SmartThings Terminal - a terminal for the SmartThings REST API"); + aboutMessage.AppendLine(); + aboutMessage.AppendLine("SmartThings REST API: https://smartthings.developer.samsung.com/docs/api-ref/st-api.html"); + aboutMessage.AppendLine(); + aboutMessage.AppendLine($"Version: {typeof(Program).Assembly.GetName().Version}"); + aboutMessage.AppendLine($"Using Terminal.Gui Version: {typeof(Terminal.Gui.Application).Assembly.GetName().Version}"); + aboutMessage.AppendLine(); + + _menu = new MenuBar(new MenuBarItem[] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Quit", "", () => Application.RequestStop() ) + }), + new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()), + //new MenuBarItem ("_Diagostics", CreateDiagnosticMenuItems()), + new MenuBarItem ("_About...", "About this app", () => MessageBox.Query ("About SmartThings Terminal", aboutMessage.ToString(), "Ok")), + }); + + _leftPane = new FrameView("API") + { + X = 0, + Y = 1, // for menu + Width = 40, + Height = Dim.Fill(1), + CanFocus = false, + }; + + _categories = Scenario.GetAllCategories().OrderBy(c => c).ToList(); + _categoryListView = new ListView(_categories) + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(0), + AllowsMarking = false, + CanFocus = true, + }; + _categoryListView.OpenSelectedItem += (a) => { + _top.SetFocus(_rightPane); + }; + _categoryListView.SelectedItemChanged += CategoryListView_SelectedChanged; + _leftPane.Add(_categoryListView); + + Label appNameView = new Label() { X = 0, Y = 0, Height = Dim.Fill(), Width = Dim.Fill() }; + + StringBuilder sbTitle = new StringBuilder(); + appNameView.Text = GetAppTitle(); + + _appTitlePane = new FrameView() + { + X = 25, + Y = 1, // for menu + Width = Dim.Fill(), + Height = 9, + CanFocus = false + }; + _appTitlePane.Add(appNameView); + + _rightPane = new FrameView("API Description") + { + X = 25, + //Y = 1, // for menu + Y = Pos.Bottom(_appTitlePane), + Width = Dim.Fill(), + Height = Dim.Fill(1), + CanFocus = true, + + }; + + _nameColumnWidth = Scenario.ScenarioMetadata.GetName(_scenarios.OrderByDescending(t => Scenario.ScenarioMetadata.GetName(t).Length).FirstOrDefault()).Length; + _scenarioListView = new ListView() + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(0), + AllowsMarking = false, + CanFocus = true, + }; + + _scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem; + _rightPane.Add(_scenarioListView); + + _categoryListView.SelectedItem = 0; + _categoryListView.OnSelectedChanged(); + + _capslock = new StatusItem(Key.CharMask, "Caps", null); + _numlock = new StatusItem(Key.CharMask, "Num", null); + _scrolllock = new StatusItem(Key.CharMask, "Scroll", null); + + _statusBar = new StatusBar(new StatusItem[] { + _capslock, + _numlock, + _scrolllock, + new StatusItem(Key.ControlR, "~CTRL-R~ Refresh Data", () => { + if (_runningScenario is null) + { + _stClient.ResetData(); + } + }), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Back/Quit", () => { + if (_runningScenario is null){ + // This causes GetScenarioToRun to return null + _runningScenario = null; + Application.RequestStop(); + } else { + _runningScenario.RequestStop(); + } + }), + }); + + SetColorScheme(); + } + + private static void _scenarioListView_OpenSelectedItem(EventArgs e) + { + if (_runningScenario is null) + { + var source = _scenarioListView.Source as ScenarioListDataSource; + _runningScenario = (Scenario)Activator.CreateInstance(source.Scenarios[_scenarioListView.SelectedItem]); + Application.RequestStop(); + } + } + + internal class ScenarioListDataSource : IListDataSource + { + public List Scenarios { get; set; } + + public bool IsMarked(int item) => false; + + public int Count => Scenarios.Count; + + public ScenarioListDataSource(List itemList) => Scenarios = itemList; + + public void Render(ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width) + { + container.Move(col, line); + // Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible + var s = String.Format(String.Format("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName(Scenarios[item])); + RenderUstr(driver, $"{s} {Scenario.ScenarioMetadata.GetDescription(Scenarios[item])}", col, line, width); + } + + public void SetMark(int item, bool value) + { + } + + // A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 + private void RenderUstr(ConsoleDriver driver, ustring ustr, int col, int line, int width) + { + int used = 0; + int index = 0; + while (index < ustr.Length) + { + (var rune, var size) = Utf8.DecodeRune(ustr, index, index - ustr.Length); + var count = Rune.ColumnWidth(rune); + if (used + count >= width) break; + driver.AddRune(rune); + used += count; + index += size; + } + + while (used < width) + { + driver.AddRune(' '); + used++; + } + } + + public IList ToList() + { + return Scenarios; + } + } + + /// + /// When Scenarios are running we need to override the behavior of the Menu + /// and Statusbar to enable Scenarios that use those (or related key input) + /// to not be impacted. Same as for tabs. + /// + /// + private static void KeyDownHandler(View.KeyEventEventArgs a) + { + //if (a.KeyEvent.Key == Key.Tab || a.KeyEvent.Key == Key.BackTab) { + // // BUGBUG: Work around Issue #434 by implementing our own TAB navigation + // if (_top.MostFocused == _categoryListView) + // _top.SetFocus (_rightPane); + // else + // _top.SetFocus (_leftPane); + //} + + if (a.KeyEvent.IsCapslock) + { + _capslock.Title = "Caps: On"; + _statusBar.SetNeedsDisplay(); + } + else + { + _capslock.Title = "Caps: Off"; + _statusBar.SetNeedsDisplay(); + } + + if (a.KeyEvent.IsNumlock) + { + _numlock.Title = "Num: On"; + _statusBar.SetNeedsDisplay(); + } + else + { + _numlock.Title = "Num: Off"; + _statusBar.SetNeedsDisplay(); + } + + if (a.KeyEvent.IsScrolllock) + { + _scrolllock.Title = "Scroll: On"; + _statusBar.SetNeedsDisplay(); + } + else + { + _scrolllock.Title = "Scroll: Off"; + _statusBar.SetNeedsDisplay(); + } + } + + private static void CategoryListView_SelectedChanged(ListViewItemEventArgs e) + { + var item = _categories[_categoryListView.SelectedItem]; + List newlist; + if (item.Equals("All")) + { + newlist = _scenarios; + + } + else + { + newlist = _scenarios.Where(t => Scenario.ScenarioCategory.GetCategories(t).Contains(item)).ToList(); + } + _scenarioListView.Source = new ScenarioListDataSource(newlist); + _scenarioListView.SelectedItem = 0; + } + + private static string GetAppTitle() + { + StringBuilder appName = new StringBuilder(); + appName.AppendLine(@" _______.___________.___________. "); + appName.AppendLine(@" / | | | "); + appName.AppendLine(@" | (----`---| |----`---| |----` "); + appName.AppendLine(@" \ \ | | | | "); + appName.AppendLine(@" .----) |mart | |hings | |erminal"); + appName.AppendLine(@" |_______/ |__| |__| "); + appName.AppendLine(@" Interactive CLI for browsing SmartThings "); + return appName.ToString(); + } + } +} diff --git a/SmartThingsTerminal/Properties/launchSettings.json b/SmartThingsTerminal/Properties/launchSettings.json new file mode 100644 index 0000000..dd889b5 --- /dev/null +++ b/SmartThingsTerminal/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "SmartThingsTerminal": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/SmartThingsTerminal/Scenario.cs b/SmartThingsTerminal/Scenario.cs new file mode 100644 index 0000000..5145284 --- /dev/null +++ b/SmartThingsTerminal/Scenario.cs @@ -0,0 +1,307 @@ +using NStack; +using SmartThingsTerminal; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace SmartThingsTerminal +{ + public class Scenario : IDisposable + { + private bool _disposedValue; + + public Toplevel Top { get; set; } + + public Window Win { get; set; } + + //public string AccessToken { get; set; } + + public SmartThingsClient STClient { get; set; } + + public Window LeftPane { get; set; } + public ListView ClassListView { get; set; } + public FrameView HostPane { get; set; } + + public FrameView SettingsPane { get; set; } + + public Label ErrorView { get; set; } + + public View CurrentView { get; set; } + + public virtual View CreateJsonView(string json) + { + var view = new TextView() + { + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + view.Text = ustring.Make(json); + view.ReadOnly = true; + + // Set the colorscheme to make it stand out + view.ColorScheme = Colors.Dialog; + + // Add + HostPane.Add(view); + HostPane.LayoutSubviews(); + HostPane.Clear(); + HostPane.SetNeedsDisplay(); + HostPane.Title = "Raw Data"; + return view; + } + + public virtual void SetErrorView(string message) + { + ErrorView = new Label() + { + X = Pos.Center(), + Y = Pos.Center(), + Width = 50, + Height = 5 + }; + + ErrorView.Text = ustring.Make(message); + } + + public virtual void DisplayErrorView() + { + if (ErrorView != null) + { + HostPane.Add(ErrorView); + } + } + + public virtual void ClearClass(View view) + { + // Remove existing class, if any + if (view != null) + { + HostPane.Remove(view); + HostPane.Clear(); + } + } + + public virtual void RefreshScreen() + { + ErrorView = null; + STClient.ResetData(); + Top.Clear(); + Setup(); + } + + /// + /// Helper that provides the default implementation with a frame and + /// label showing the name of the and logic to exit back to + /// the Scenario picker UI. + /// Override to provide any behavior needed. + /// + /// The Toplevel created by the UI Catalog host. + /// The colorscheme to use. + /// + /// + /// Thg base implementation calls , sets to the passed in , creates a for and adds it to . + /// + /// + /// Overrides that do not call the base., must call before creating any views or calling other Terminal.Gui APIs. + /// + /// + public virtual void Init(Toplevel top, ColorScheme colorScheme, SmartThingsClient SmartThingsTerminalent) + { + Application.Init(); + + Top = top; + if (Top == null) + { + Top = Application.Top; + } + + Win = new Window($"CTRL-Q to Close - Scenario: {GetName()}") + { + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill(), + ColorScheme = colorScheme, + }; + Top.Add(Win); + + STClient = SmartThingsTerminalent; + } + + /// + /// Defines the metadata (Name and Description) for a + /// + [System.AttributeUsage(System.AttributeTargets.Class)] + public class ScenarioMetadata : System.Attribute + { + /// + /// Name + /// + public string Name { get; set; } + + /// + /// Description + /// + public string Description { get; set; } + + public ScenarioMetadata(string Name, string Description) + { + this.Name = Name; + this.Description = Description; + } + + /// + /// Static helper function to get the Name given a Type + /// + /// + /// + public static string GetName(Type t) => ((ScenarioMetadata)System.Attribute.GetCustomAttributes(t)[0]).Name; + + /// + /// Static helper function to get the Description given a Type + /// + /// + /// + public static string GetDescription(Type t) => ((ScenarioMetadata)System.Attribute.GetCustomAttributes(t)[0]).Description; + } + + /// + /// Helper to get the Name (defined in ) + /// + /// + public string GetName() => ScenarioMetadata.GetName(this.GetType()); + + /// + /// Helper to get the Description (defined in ) + /// + /// + public string GetDescription() => ScenarioMetadata.GetDescription(this.GetType()); + + /// + /// Defines the category names used to catagorize a + /// + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] + public class ScenarioCategory : System.Attribute + { + /// + /// Category Name + /// + public string Name { get; set; } + + public ScenarioCategory(string Name) => this.Name = Name; + + /// + /// Static helper function to get the Name given a Type + /// + /// + /// Name of the catagory + public static string GetName(Type t) => ((ScenarioCategory)System.Attribute.GetCustomAttributes(t)[0]).Name; + + /// + /// Static helper function to get the Categories given a Type + /// + /// + /// list of catagory names + public static List GetCategories(Type t) => System.Attribute.GetCustomAttributes(t) + .ToList() + .Where(a => a is ScenarioCategory) + .Select(a => ((ScenarioCategory)a).Name) + .ToList(); + } + + /// + /// Helper function to get the list of categories a belongs to (defined in ) + /// + /// list of catagory names + public List GetCategories() => ScenarioCategory.GetCategories(this.GetType()); + + /// + public override string ToString() => $"{GetName(),-30}{GetDescription()}"; + + /// + /// Override this to implement the setup logic (create controls, etc...). + /// + /// This is typically the best place to put scenario logic code. + public virtual void Setup() + { + } + + /// + /// Runs the . Override to start the using a different than `Top`. + /// + /// + /// + /// Overrides that do not call the base., must call before returning. + /// + public virtual void Run() + { + // This method already performs a later automatic shutdown. + Application.Run(Top); + } + + /// + /// Stops the scenario. Override to change shutdown behavior for the . + /// + public virtual void RequestStop() + { + Application.RequestStop(); + } + + /// + /// Returns a list of all Categories set by all of the s defined in the project. + /// + internal static List GetAllCategories() + { + List categories = new List() { "All" }; + foreach (Type type in typeof(Scenario).Assembly.GetTypes() + .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(Scenario)))) + { + List attrs = System.Attribute.GetCustomAttributes(type).ToList(); + categories = categories.Union(attrs.Where(a => a is ScenarioCategory).Select(a => ((ScenarioCategory)a).Name)).ToList(); + } + return categories; + } + + /// + /// Returns an instance of each defined in the project. + /// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class + /// + public static List GetDerivedClasses() + { + List objects = new List(); + foreach (Type type in typeof(T).Assembly.GetTypes() + .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(T)))) + { + objects.Add(type); + } + return objects; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} + diff --git a/SmartThingsTerminal/Scenarios/Devices.cs b/SmartThingsTerminal/Scenarios/Devices.cs new file mode 100644 index 0000000..4fc47d5 --- /dev/null +++ b/SmartThingsTerminal/Scenarios/Devices.cs @@ -0,0 +1,192 @@ +using SmartThingsNet.Model; +using SmartThingsTerminal; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace SmartThingsTerminal.Scenarios +{ + [ScenarioMetadata(Name: "Devices", Description: "SmartThings devices")] + [ScenarioCategory("Devices")] + class Devices : Scenario + { + Dictionary _viewDevices; + FrameView _deviceDetailsFrame; + FrameView _deviceLocationFrame; + + public override void Init(Toplevel top, ColorScheme colorScheme, SmartThingsClient SmartThingsTerminalent) + { + Application.Init(); + + Top = top; + if (Top == null) + { + Top = Application.Top; + } + STClient = SmartThingsTerminalent; + } + + public override void Setup() + { + var statusBar = new StatusBar(new StatusItem[] { + new StatusItem(Key.ControlR, "~CTRL-R~ Refresh Data", () => RefreshScreen()), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Back/Quit", () => Quit()) + }); + + LeftPane = new Window("Devices") + { + X = 0, + Y = 0, // for menu + Width = 40, + Height = Dim.Fill(), + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + try + { + if (STClient.GetAllDevices().Items?.Count > 0) + { + _viewDevices = STClient.GetAllDevices().Items + .OrderBy(t => t.Name) + .Select(t => new KeyValuePair(t.Label, t)) + .ToDictionary(t => t.Key, t => t.Value); + } + } + catch (System.Exception exp) + { + SetErrorView($"No data returned from API:{Environment.NewLine}{exp.Message}"); + } + + ClassListView = new ListView(_viewDevices.Keys.ToList()) + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(), // for status bar + AllowsMarking = false, + ColorScheme = Colors.TopLevel, + }; + + if (_viewDevices.Keys.Count > 0) + { + ClassListView.OpenSelectedItem += (a) => + { + Top.SetFocus(SettingsPane); + }; + ClassListView.SelectedItemChanged += (args) => + { + ClearClass(CurrentView); + + var selectedDevice = _viewDevices.Values.ToArray()[ClassListView.SelectedItem]; + string json = selectedDevice.ToJson().Replace("\r", ""); + CurrentView = CreateJsonView(json); + UpdateSettings(selectedDevice); + }; + } + LeftPane.Add(ClassListView); + + SettingsPane = new FrameView("Settings") + { + X = Pos.Right(LeftPane), + Y = 0, // for menu + Width = Dim.Fill(), + Height = 8, + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + _deviceDetailsFrame = new FrameView("Device Details") + { + X = 0, + Y = 0, + Height = 6, + Width = 50, + }; + SettingsPane.Add(_deviceDetailsFrame); + + _deviceLocationFrame = new FrameView("Device Location") + { + X = Pos.Right(_deviceDetailsFrame), + Y = Pos.Y(_deviceDetailsFrame), + Height = 6, + Width = 40, + }; + + SettingsPane.Add(_deviceLocationFrame); + + HostPane = new FrameView("") + { + X = Pos.Right(LeftPane), + Y = Pos.Bottom(SettingsPane), + Width = Dim.Fill(), + Height = Dim.Fill(1), // + 1 for status bar + ColorScheme = Colors.Dialog, + }; + + Top.Add(LeftPane, SettingsPane, HostPane); + Top.Add(statusBar); + + if (_viewDevices.Count > 0) + { + CurrentView = CreateJsonView(_viewDevices?.FirstOrDefault().Value?.ToJson()); + } + } + + void UpdateSettings(Device device) + { + _deviceDetailsFrame.Clear(); + + var labelId = new Label("Id:") { X = 0, Y = 0 }; + _deviceDetailsFrame.Add(labelId); + var deviceId = new TextField($"{device.DeviceId}") { X = Pos.Right(labelId) + 1, Y = 0, Width = 40 }; + _deviceDetailsFrame.Add(deviceId); + + var labelDeviceLabel = new Label("Label:") { X = 0, Y = 1 }; + _deviceDetailsFrame.Add(labelDeviceLabel); + var deviceLabel = new TextField($"{device?.Label}") { X = Pos.Right(labelDeviceLabel) + 1, Y = 1, Width = 40 }; + _deviceDetailsFrame.Add(deviceLabel); + + var labelType = new Label("Type:") { X = 0, Y = 2 }; + _deviceDetailsFrame.Add(labelType); + var deviceType = new TextField($"{device.DeviceTypeName?.Trim()}") { X = Pos.Right(labelType) + 1, Y = 2, Width = 40 }; + _deviceDetailsFrame.Add(deviceType); + + var labelComponents = new Label("Components:") { X = 0, Y = 3 }; + _deviceDetailsFrame.Add(labelComponents); + var deviceComponents = new TextField($"{device.Components.Count}") { X = Pos.Right(labelComponents) + 1, Y = 3, Width = 40 }; + _deviceDetailsFrame.Add(deviceComponents); + + // Device Location pane + _deviceLocationFrame.Clear(); + + string locationName = ""; + if (device.LocationId != null) + { + locationName = STClient.GetAllLocations().Items.Where(l => l.LocationId.ToString().Equals(device.LocationId))?.FirstOrDefault().Name; + } + + string roomName = ""; + if (device.RoomId != null) + { + roomName = STClient.GetAllRooms(device.LocationId).Items.Where(r => r.RoomId.ToString().Equals(device.RoomId))?.FirstOrDefault().Name; + } + + var labelLocation = new Label("Location:") { X = 0, Y = 0 }; + _deviceLocationFrame.Add(labelLocation); + var deviceLocation = new TextField($"{locationName}") { X = Pos.Right(labelLocation) + 1, Y = 0, Width = 40 }; + _deviceLocationFrame.Add(deviceLocation); + + var labelRoom = new Label("Room:") { X = 0, Y = 1 }; + _deviceLocationFrame.Add(labelRoom); + var deviceRoom = new TextField($"{roomName}") { X = Pos.Right(labelRoom) + 1, Y = 1, Width = 40 }; + _deviceLocationFrame.Add(deviceRoom); + } + + private void Quit() + { + Application.RequestStop(); + } + } +} diff --git a/SmartThingsTerminal/Scenarios/Locations.cs b/SmartThingsTerminal/Scenarios/Locations.cs new file mode 100644 index 0000000..097ae74 --- /dev/null +++ b/SmartThingsTerminal/Scenarios/Locations.cs @@ -0,0 +1,108 @@ +using SmartThingsNet.Model; +using SmartThingsTerminal; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace SmartThingsTerminal.Scenarios +{ + [ScenarioMetadata(Name: "Locations", Description: "SmartThings locations")] + [ScenarioCategory("Locations")] + class Locations : Scenario + { + Dictionary _viewLocations = new Dictionary(); + + public override void Init(Toplevel top, ColorScheme colorScheme, SmartThingsClient SmartThingsTerminalent) + { + Application.Init(); + + Top = top; + if (Top == null) + { + Top = Application.Top; + } + + STClient = SmartThingsTerminalent; + } + + public override void Setup() + { + var statusBar = new StatusBar(new StatusItem[] { + new StatusItem(Key.ControlR, "~CTRL-R~ Refresh Data", () => RefreshScreen()), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Back/Quit", () => Quit()) + }); + + LeftPane = new Window("Locations") + { + X = 0, + Y = 0, // for menu + Width = 40, + Height = Dim.Fill(), + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + try + { + if (STClient.GetAllLocations().Items?.Count > 0) + { + _viewLocations = STClient.GetAllLocations().Items + .OrderBy(t => t.Name) + .Select(t => new KeyValuePair(t.Name, t)) + .ToDictionary(t => t.Key, t => t.Value); + } + } + catch (System.Exception exp) + { + SetErrorView($"No data returned from API:{Environment.NewLine}{exp.Message}"); + } + + ClassListView = new ListView(_viewLocations.Keys?.ToList()) + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(), // for status bar + AllowsMarking = false, + ColorScheme = Colors.TopLevel, + }; + + if (_viewLocations.Keys.Count > 0) + { + ClassListView.SelectedItemChanged += (args) => + { + ClearClass(CurrentView); + var selectedItem = _viewLocations.Values.ToArray()[ClassListView.SelectedItem]; + string json = selectedItem.ToJson().Replace("\r", ""); + CurrentView = CreateJsonView(json); + }; + } + LeftPane.Add(ClassListView); + + HostPane = new FrameView("") + { + X = Pos.Right(LeftPane), + //Y = Pos.Bottom(_settingsPane), + Width = Dim.Fill(), + Height = Dim.Fill(1), // + 1 for status bar + ColorScheme = Colors.Dialog, + }; + + Top.Add(LeftPane, HostPane); + Top.Add(statusBar); + + if (_viewLocations.Count > 0) + { + CurrentView = CreateJsonView(_viewLocations?.FirstOrDefault().Value?.ToJson()); + } + + DisplayErrorView(); + } + + private void Quit() + { + Application.RequestStop(); + } + } +} diff --git a/SmartThingsTerminal/Scenarios/Rules.cs b/SmartThingsTerminal/Scenarios/Rules.cs new file mode 100644 index 0000000..898cc92 --- /dev/null +++ b/SmartThingsTerminal/Scenarios/Rules.cs @@ -0,0 +1,109 @@ +using NStack; +using SmartThingsNet.Model; +using SmartThingsTerminal; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace SmartThingsTerminal.Scenarios +{ + [ScenarioMetadata(Name: "Rules", Description: "SmartThings rules")] + [ScenarioCategory("Rules")] + class Rules : Scenario + { + Dictionary _viewRules = new Dictionary(); + + public override void Init(Toplevel top, ColorScheme colorScheme, SmartThingsClient SmartThingsTerminalent) + { + Application.Init(); + + Top = top; + if (Top == null) + { + Top = Application.Top; + } + + STClient = SmartThingsTerminalent; + } + + public override void Setup() + { + var statusBar = new StatusBar(new StatusItem[] { + new StatusItem(Key.ControlR, "~CTRL-R~ Refresh Data", () => RefreshScreen()), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Back/Quit", () => Quit()) + }); + + LeftPane = new Window("Automations") + { + X = 0, + Y = 0, // for menu + Width = 40, + Height = Dim.Fill(), + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + try + { + if (STClient.GetAllRules().Items?.Count > 0) + { + _viewRules = STClient.GetAllRules().Items + .OrderBy(t => t.Name) + .Select(t => new KeyValuePair(t.Name, t)) + .ToDictionary(t => t.Key, t => t.Value); + } + } + catch (System.Exception exp) + { + SetErrorView($"No data returned from API:{Environment.NewLine}{exp.Message}"); + } + + ClassListView = new ListView(_viewRules?.Keys.ToList()) + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(), // for status bar + AllowsMarking = false, + ColorScheme = Colors.TopLevel, + }; + + if (_viewRules.Keys.Count > 0) + { + ClassListView.SelectedItemChanged += (args) => + { + ClearClass(CurrentView); + var selectedItem = _viewRules.Values.ToArray()[ClassListView.SelectedItem]; + string json = selectedItem.ToJson().Replace("\r", ""); + CurrentView = CreateJsonView(json); + }; + } + LeftPane.Add(ClassListView); + + HostPane = new FrameView("") + { + X = Pos.Right(LeftPane), + //Y = Pos.Bottom(_settingsPane), + Width = Dim.Fill(), + Height = Dim.Fill(1), // + 1 for status bar + ColorScheme = Colors.Dialog, + }; + + Top.Add(LeftPane, HostPane); + Top.Add(statusBar); + + if (_viewRules.Count > 0) + { + CurrentView = CreateJsonView(_viewRules?.FirstOrDefault().Value?.ToJson()); + } + + DisplayErrorView(); + } + + private void Quit() + { + Application.RequestStop(); + } + } +} diff --git a/SmartThingsTerminal/Scenarios/Scenes.cs b/SmartThingsTerminal/Scenarios/Scenes.cs new file mode 100644 index 0000000..f8ef7bc --- /dev/null +++ b/SmartThingsTerminal/Scenarios/Scenes.cs @@ -0,0 +1,109 @@ +using NStack; +using SmartThingsNet.Model; +using SmartThingsTerminal; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace SmartThingsTerminal.Scenarios +{ + [ScenarioMetadata(Name: "Scenes", Description: "SmartThings scenes")] + [ScenarioCategory("Scenes")] + class Scenes : Scenario + { + Dictionary _viewScenes; + + public override void Init(Toplevel top, ColorScheme colorScheme, SmartThingsClient SmartThingsTerminalent) + { + Application.Init(); + + Top = top; + if (Top == null) + { + Top = Application.Top; + } + + STClient = SmartThingsTerminalent; + } + + public override void Setup() + { + var statusBar = new StatusBar(new StatusItem[] { + new StatusItem(Key.ControlR, "~CTRL-R~ Refresh Data", () => RefreshScreen()), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Back/Quit", () => Quit()) + }); + + LeftPane = new Window("Scenes") + { + X = 0, + Y = 0, // for menu + Width = 40, + Height = Dim.Fill(), + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + try + { + if (STClient.GetAllScenes().Items?.Count > 0) + { + _viewScenes = STClient.GetAllScenes().Items + .OrderBy(t => t.SceneName) + .Select(t => new KeyValuePair(t.SceneName, t)) + .ToDictionary(t => t.Key, t => t.Value); + } + } + catch (System.Exception exp) + { + SetErrorView($"No data returned from API:{Environment.NewLine}{exp.Message}"); + } + + ClassListView = new ListView(_viewScenes.Keys.ToList()) + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(), // for status bar + AllowsMarking = false, + ColorScheme = Colors.TopLevel, + }; + + if (_viewScenes.Keys.Count > 0) + { + ClassListView.SelectedItemChanged += (args) => + { + ClearClass(CurrentView); + var selectedItem = _viewScenes.Values.ToArray()[ClassListView.SelectedItem]; + string json = selectedItem.ToJson().Replace("\r", ""); + CurrentView = CreateJsonView(json); + }; + } + LeftPane.Add(ClassListView); + + HostPane = new FrameView("") + { + X = Pos.Right(LeftPane), + //Y = Pos.Bottom(_settingsPane), + Width = Dim.Fill(), + Height = Dim.Fill(1), // + 1 for status bar + ColorScheme = Colors.Dialog, + }; + + Top.Add(LeftPane, HostPane); + Top.Add(statusBar); + + if (_viewScenes.Count > 0) + { + CurrentView = CreateJsonView(_viewScenes?.FirstOrDefault().Value?.ToJson()); + } + + DisplayErrorView(); + } + + private void Quit() + { + Application.RequestStop(); + } + } +} diff --git a/SmartThingsTerminal/Scenarios/Schedules.cs b/SmartThingsTerminal/Scenarios/Schedules.cs new file mode 100644 index 0000000..dc5b4cb --- /dev/null +++ b/SmartThingsTerminal/Scenarios/Schedules.cs @@ -0,0 +1,110 @@ +using NStack; +using SmartThingsNet.Model; +using SmartThingsTerminal; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace SmartThingsTerminal.Scenarios +{ + [ScenarioMetadata(Name: "Schedules", Description: "SmartThings application schedules")] + [ScenarioCategory("Schedules")] + class Schedules : Scenario + { + Dictionary _viewSchedules = new Dictionary(); + + public override void Init(Toplevel top, ColorScheme colorScheme, SmartThingsClient SmartThingsTerminalent) + { + Application.Init(); + + Top = top; + if (Top == null) + { + Top = Application.Top; + } + + STClient = SmartThingsTerminalent; + } + + public override void Setup() + { + var statusBar = new StatusBar(new StatusItem[] { + new StatusItem(Key.ControlR, "~CTRL-R~ Refresh Data", () => RefreshScreen()), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Back/Quit", () => Quit()) + }); + + LeftPane = new Window("Schedules") + { + X = 0, + Y = 0, // for menu + Width = 40, + Height = Dim.Fill(), + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + try + { + if (STClient.GetAllSchedules().Items?.Count > 0) + { + _viewSchedules = STClient.GetAllSchedules().Items + .OrderBy(t => t.Name) + .Select(t => new KeyValuePair(t.Name, t)) + .ToDictionary(t => t.Key, t => t.Value); + } + } + catch (System.Exception exp) + { + SetErrorView($"No data returned from API:{Environment.NewLine}{exp.Message}"); + } + + ClassListView = new ListView(_viewSchedules?.Keys.ToList()) + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(), // for status bar + AllowsMarking = false, + ColorScheme = Colors.TopLevel, + }; + + if (_viewSchedules.Keys.Count > 0) + { + ClassListView.SelectedItemChanged += (args) => + { + ClearClass(CurrentView); + var selectedItem = _viewSchedules.Values.ToArray()[ClassListView.SelectedItem]; + string json = selectedItem.ToJson().Replace("\r", ""); + CurrentView = CreateJsonView(json); + }; + } + + LeftPane.Add(ClassListView); + + HostPane = new FrameView("") + { + X = Pos.Right(LeftPane), + //Y = Pos.Bottom(_settingsPane), + Width = Dim.Fill(), + Height = Dim.Fill(1), // + 1 for status bar + ColorScheme = Colors.Dialog, + }; + + Top.Add(LeftPane, HostPane); + Top.Add(statusBar); + + if (_viewSchedules.Count > 0) + { + CurrentView = CreateJsonView(_viewSchedules?.FirstOrDefault().Value?.ToJson()); + } + + DisplayErrorView(); + } + + private void Quit() + { + Application.RequestStop(); + } + } +} diff --git a/SmartThingsTerminal/Scenarios/Subscriptions.cs b/SmartThingsTerminal/Scenarios/Subscriptions.cs new file mode 100644 index 0000000..0ec7d71 --- /dev/null +++ b/SmartThingsTerminal/Scenarios/Subscriptions.cs @@ -0,0 +1,108 @@ +using SmartThingsNet.Model; +using SmartThingsTerminal; +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace SmartThingsTerminal.Scenarios +{ + [ScenarioMetadata(Name: "Subscriptions", Description: "SmartThings application subscriptions *untested*")] + [ScenarioCategory("Subscriptions")] + class Subscriptions : Scenario + { + Dictionary _viewSubscriptions = new Dictionary(); + + public override void Init(Toplevel top, ColorScheme colorScheme, SmartThingsClient SmartThingsTerminalent) + { + Application.Init(); + + Top = top; + if (Top == null) + { + Top = Application.Top; + } + + STClient = SmartThingsTerminalent; + } + + public override void Setup() + { + var statusBar = new StatusBar(new StatusItem[] { + new StatusItem(Key.ControlR, "~CTRL-R~ Refresh Data", () => RefreshScreen()), + new StatusItem(Key.ControlQ, "~CTRL-Q~ Back/Quit", () => Quit()) + }); + + LeftPane = new Window("Subscriptions") + { + X = 0, + Y = 0, // for menu + Width = 40, + Height = Dim.Fill(), + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + try + { + if (STClient.GetAllSubscriptions().Items?.Count > 0) + { + _viewSubscriptions = STClient.GetAllSubscriptions().Items + .OrderBy(t => t.Id) + .Select(t => new KeyValuePair(t.Id, t)) + .ToDictionary(t => t.Key, t => t.Value); + } + } + catch (System.Exception exp) + { + SetErrorView($"No data returned from API:{Environment.NewLine}{exp.Message}"); + } + + ClassListView = new ListView(_viewSubscriptions?.Keys.ToList()) + { + X = 0, + Y = 0, + Width = Dim.Fill(0), + Height = Dim.Fill(), // for status bar + AllowsMarking = false, + ColorScheme = Colors.TopLevel, + }; + + if (_viewSubscriptions.Keys.Count > 0) + { + ClassListView.SelectedItemChanged += (args) => + { + ClearClass(CurrentView); + var selectedItem = _viewSubscriptions.Values.ToArray()[ClassListView.SelectedItem]; + string json = selectedItem.ToJson().Replace("\r", ""); + CurrentView = CreateJsonView(json); + }; + } + LeftPane.Add(ClassListView); + + HostPane = new FrameView("") + { + X = Pos.Right(LeftPane), + //Y = Pos.Bottom(_settingsPane), + Width = Dim.Fill(), + Height = Dim.Fill(1), // + 1 for status bar + ColorScheme = Colors.Dialog, + }; + + Top.Add(LeftPane, HostPane); + Top.Add(statusBar); + + if (_viewSubscriptions.Count > 0) + { + CurrentView = CreateJsonView(_viewSubscriptions?.FirstOrDefault().Value?.ToJson()); + } + + DisplayErrorView(); + } + + private void Quit() + { + Application.RequestStop(); + } + } +} diff --git a/SmartThingsTerminal/SmartThingsTerminal.csproj b/SmartThingsTerminal/SmartThingsTerminal.csproj new file mode 100644 index 0000000..d195dbe --- /dev/null +++ b/SmartThingsTerminal/SmartThingsTerminal.csproj @@ -0,0 +1,28 @@ + + + + Exe + netcoreapp3.1 + STT + SmartThingsTerminal + 0.9.0 + JamieD + + SmartThings Terminal + + JamieD + + smartthings.ico + SmartThingsTerminal.Program + + + + + + + + + + + + diff --git a/SmartThingsTerminal/smartthings.ico b/SmartThingsTerminal/smartthings.ico new file mode 100644 index 0000000000000000000000000000000000000000..a6da11c3c6317adf07571ef48f09279a3293f592 GIT binary patch literal 20846 zcmd^nX>eOtmL4ZFQivV$z-b2)ju+sPO6f4W~x%Dnp90!r`w(BIBh%ay}ik{ zHcPT3%eEG4cWbdNYqzL<7bQ|FC5ocNeFXuK00Ckl2oMB75DQ6=;3`qF`^$Ik!vg_u zX}82SnB#O^YRA2mzVe5?`qc_|12->(2w)-e!xE1ls#(qdC1FS zKe>OClatp8;{-f>_(*}AXF|^X7@w^se2b7r_>zDJ_wOSa4Pm&i7n2Vjq~Cw=;68@? zdk7)OxwIt=5atnnFLjNc)>^a`?t}f>LBu>Qn0)w9wT*fj;JCX7&hpO?^)=}4%h5#m ztI5fSm>3&@{p#n4d#s3cn9x~yj<_D+{=^vERr!efTM+MVhNs~W?%yBN`;alO5d1=y zul5RDrAN?ya}WH6t7$&?3}@i1+67PJL4+JPvc|@DZ^`>Y!}#zZBCaL`TW%xVUID+k z2;Hqk2)oMo-Yw#9BArPcw|?xBcNKpheSt?`e2I}n5A3De;B7h$ui+#dmD?~n)Qd-t ze$94Yr0LS|P7Y0s4I}DlChl6&Fo2M~gk#>P`hW0X0`5lgt*Z*-qy2~lE72cmB40)n z&Fo_&&~g**@+0uppMm|(cC?kOhwaXK3=ec8IpBq(ZZpE2#qhLzif-pgc$#hL z-(7JC5qB*Ty$-bB-Ht@K9o=mgVXIh+w#xSqa1>y8(2tJR?HEdQAktHUfcLmQ{=s;c z39ib$a8>U{q_ZYH??LB9bhRBz$;?B?5`Ar|tvnkY4q!$>PYD? z)fshG?jP|jAeae{2s$la=QzaoPZAy@{1bvg#iNW%=^`G}Yw97_)x*@>%Ka?hef9j& zm+ARWJ?CMvgz&cn(QyJ(@AAFzWCHzNP9%aJrGIq!Nw;GgS?`Ik3yB`5w)V5>c*wTL z3ICdq?q6t4#(Lnmegwv&8(=!MnfmM^#*zb5<6xW7!5BQXC(wFj9a^uhL1*I$j3#5G zF>AhsC%+{80YN#NY?DCP4h-LCaHS;*sG$hpvde{2-C%lqKGb)0*`1jjuLXK6mXH3cc!9`gP;X+H+% zz1>QmN;^j-*LL%X)LP5n_2gtX`+i*E68@1kU(IFmR@& zY`XJhPuQsX7akGE$Up?{yC+~evlh)KSHOH>4d>CL z_LH&UKHMK4!9ds!^W`OIxwZ_}lK0?mJ&UoCe(5h4L?&gAow`pPvS$rN{TO7weO;~G zGs@wr*aut5X1J^Op|_(PeE~BDV;xEt%U+f?^bMEjXnAUafzTEzUpZKwWbjW63z$Zf%DB_D7ly=G+bs z1Yj@Ug!YP!m>5m4e{D|E?`6xV^xr2kHyH7vtEmv3b!X7$ZzaFSG1%*d@zUFz&+D9D z*#l{#$ox94qUH7?3@5sk%=Cqu;CCEHsPiKC8=pqs6wmvIB0gBJY$a?#d+9F9_WSVE zAA`MY8|qFx&HX)}_(nPZk1#QwgvYcGZ51mq5Vf%16R_33!#(U{IE`!JYTbyDWOr&@ zYF_)h&BbUfT2I*tE4g>qe2&I5ub`vu5L`98VY)USKJ#%z{Z;T;j=)s%DuS*;^oN_# zQvRyiTg5gR9`eE2@*X0=;?%q-{>KBY#jsrYkoHwT=@NI%KDcZ4E1Jhg1`x1af~9mB znyx&JmOBf$@1Do_Sb}32g2(zXy4}ZlKFB@T1xM>jL_%5phlXN4*lus*{=X59@}02W zT1}h9km92@@BLvDEcccm+Fhyb%c*&dhigdl2k_Xp!fAaE9qk)1GVJHON}tI5w#wWO zMLo2M3ejagkN%*{Z`u}P+xy&e>(P4eE%bJkkcav+>XiGV7>FAY^qoRaH|>cb58J2p zkgTuEx;)BWE&4^1XQ7+==D|4oyaQX!N?0lv!csjS;jZf$x<=ZG9XPIyD_cG%_-vi( zU&)i!r+zK+m`rfanl>Qhxk4QfLyxZr?aeE=kM}5f(%Vkal%WeICusvt{&=cQ)-xqz zgK;|?O>5BC%k}#3*XWNKU^gyNvZAf6?00hdy2NXxpL6c3F^jwl&7Iay;cDR+d>7!h zuA`mxDJCWcwKh}hDEGRLeP--0v(qs&;6|70AUfJMqT7?t^)qda=JqXi!pRivvOmhc zFS3-|E_F>gnMl&Ui&9?F_U$Zf*~|3({e9kZ6Vf&W&vV{S`A4!3O?#GZo8+_8guf=F z?UTH`->01-@VgjXxmK0*rS&Uz*!KxPB#0lW{=@;y`6Eq#PdGy8A^myhPYwk0d zpHV_N;U&VK5oGStRPelp5FtDy+4T0piz>ns1o0JQF4FkLUK<}CLT_6m+Ai*a>BJVa zocRdOl4G<FsFPvjXkc^6AGPMC0cx;JkeVL%m(8xsd)iZt8ro5v^0 z3+$#Hwt{=tCi1~YKFGe1O*3(cEIdg_*})oa8DD2*5$gA@fWNVX`eR(pMQ2q3%xBgk zoM98oT*$r>w3fhdb}^5)RNIO1VFcQ)q50B$^f*hky(fof#4CF8GlIy0;=kza@sT9l zWoJ3wRfxJw>G|$ytDsH3p0R{Gsk!2K*r)KmtN9|#g)0&DHm1iJ_0__BeHpr~McN!p z$4|6sP5zCJUmMSTbT^d{|8h8P<&)n&`YImSZhVT?OPd*oF;4Mcjei1>jv6#ySb&cD z!x)UZk%%~HI~|~Xvj8!FgC@7r`7h(>AiSQ&ujc;|h9Z8r%1)sEz(N>~uOPgGicg+| z{pK#(3tiySQZ&eVlf5d*IW}Kjiu#kkfa&r=+Odn!a$~Wwt;Rp_BD?AMc;WZwo zk8uYhgFz*qS^a0MHQtwfAr&u5)0Ccb@smaddKC;seYAgRcN#Cjc54gFMJv#9WhLV> zTPSN+=ogrf?C&Pd9{PL{$|3hV@@59P&*Xu`yDYQP$M+vn_lf=+A033xa301BZ*#5} z(Z1hEyKn=HMGMLE`3SaOXPX4u%G`+mpzOR%Tr-_ZjKZ(umv_Y9)O?f-+PQv3#ti2d zpyl$r2s`i5uL#qp2$IitVY<2yEyZsm9-wWSUhAnCnu?E2i;?NH$bOUZV1iALQ~!KLw)G6SHe}E4@cQv_)S+S>pr!R2@$pev3mI~z;g3y>CgH8SfacT7 zVJ=t$>(x#44VS}Oyixhg9d-GrKlL0M&b~l@|CZv5+5)trb=zP%{~nxo4{}}Exz`2B=VBPn zy@iI;zob7+8~x^L;(rF#($&-{^-9*pM&gJC>d;!o^V<%@^|E;-MkF3 z-tt`h;kdUM#`DY2S$~Q$9a6qr*mV!)tM9;PJga0tb$-fG(GL-e}>aN9P(X<3E-xcCd{@yq)2jt)i`XV}k} z(Q3FDCl+01yR!+_+w0IDvMPC!bs+jg_Pc?I6|Tl@^eNv&f3KPJW$T*0NF&^>8|V|S zMtA37`VBkLW}JtB?->1`5b=vGrRHDarOl$B6JZBq90$-!ez#m(L4R{Q;~f?3J9FJU zRQA=#PzYYjLB^7o(y#AU<4)^2(kMKT*s$BW9_^;Z3$Wxj4N|JhrPvUt9}QjJ1-#EQ9#}YG#;h*I9Zny9BUYf z{#N?e4kh>6nA5n02V(Q?)b0yEv^`z%I(wZeJv25Fh2MSxmWqY2RxXCMdJ)X!FQUtF zi2fRLEHX#A@3Bvbjbx7_tpi+a|MK*A($A;*BVQx*m)q;!hp+8(wXTu_9q>Ezxwhw_ zKiZ_mtnE27yg!XD6`#IK+>d=F{CwZvevE7DLqwUgAUO)3_`Q~fR}c-AQdWjDWowG| z>A2;Y#6dqLr0uV{>0o@xbBXJEB?6xF)O({!pZ53}V`+RD@kou@3sUio--KJ9Pq#m3 zrc>*S10L*aXT0fGT-$G>v;9+aIzFM#zlg`}7*2M56`Xn-vA+)!{(_Kgf6qpP>^0Q& zeZ6()wC&`&TF&^+D&}8yQpQ_S``b+NH%%7A|EMPXuM}?aKg2JZ!;jbd(Du6#=4=My z%_{ygHcGjl-M-Vus~-hVJ7GEDpA&TdOY$!L1bMEvo&CtU(DH7x+7-F)>i(zLTJIA6 z4I$J26hB1bRgweEefM|07QTv}6`xPm`oAOy&(rvM@~4D9B7D=qh)f)r=X#JraQ>;p zDI{0)Iw2jW__w^s`6JDuD`X582ojf(IGphIcjeL7d0+S{c>*t?h%le<1H!imx%^cl z;eC<2y@W=B=zjV83UdE{Wqg8H>>tsC+=G=rK99%$NccA4_X$}uz-x(J1PD6c|FLM- z=S%R*{L4HDKPw3<2oh&Y&jZhO{tMmmHzVC*(@w>F7{~dNpkhDtMP}SvXPFo2hrWTl ztKvcFm=Nur+4~XQ6C;!oBu4e`2!BLSF&Y^F&-HQ7coe%|;ujN)A&e&nRsDhFl*FH% z%CpSfhv1Ny%6-~-;(v-CBp~M!3z27Y&>`_z(W5sAzaWU7Q*y-P?DVI2GBz}Tf$k3U zSZY=5CD2@kC}UsAnApT(W2Wq*+4!LPd7x+=ihAiY)~lF|#AW1sDB4LoY?$lT<7mHLssKuap8JLfzkNox~9_L}bz z{*W*u|HbDX9f%`rsfX>-e$;)s1Pyza!;rs*u$pn8l`tHB51#UK7zntSyAvBL$G0aB zgm#gUnAb@Em2txYzIU4TY2kZ}Rji^fH($l%TvdCRLo#adiL|et%?Hvdb0BhblJFzK zzaY#=zsP*QpZTlnM@jEGG#&gv)%|#@ufkhZ1l!epFdTg!)=N9lZJ<9G@8R0areD^o z#L*&LY5ksMFdpB8psAeksu&msQhszY7C=|SRah==C2T>wlXYBb-lZ?i zUKAZUmh7khU8>~WSnv+ITdtD6BxC7X>|NqdeoG-tSLn}`Z)4o2jO(di>DNrUa?Zt` zl0D%$!oSLVHbs9w0!GrmkN(_Aq5qzWwQF%GiA7Q-8cBcQN3as&-Z|*El``kOoie|I zIn67iZ$OPzWOQsePWlMPJM{<+vWvL8H_Nx!bY?o%)J+8fY(jPjHJ zE*!<5BkDG)c#_nEgxhOjyZ$L!3h4ir9z-JK;`+_D((7_a(lyi||xdyA%z=8__GS+s9P> zM)UCx7`y4^$VES083#ASx%5^IF ziN^e8s6V&>4Tlz@A%7ui_PmJ7Po9FMXcOam71Zx>ZOvuOfzY62gta;W>s1&o%tPJr zr_gZbIpz=;U%LDT427>K`hBKj80fWWG5xvsVx3m8dCCZTW~N{KI>ssn<7y1vnv3NB z0obm6Lb>^ba=)3fvlgD}Q;4%BMd+8cm}JetV5EcbevgV-Nezt1bJSah_Ipw@_#x{k zHgl~qcT}~5xq-b1lJ5gyYpNzDTLx#OP2;z$0kKb|rbr)C&Qa_FiQf)09?#gk-^Mjy z;CKw^ZLd;s`(9@a_f;$7i+slXn8b0rnlDhdUqH}yGZo`=sJ$s@FIF|0QC~glOKX+v z4@Mj+&ac%GOzE!KX-d;7HnxjUHD}wDJSe-1@!0Xr}M; zwCU{Yj7iT!a{=R>H&|m)e~@&Ss{G3c>qABVC}3RvKJ6Z52hJV~%He66e%Z$Y1nZpi ztGSVRNX73{wy)@#@sR}UL0Jz%8>adEA~c+MS;g{AmzJt}ERpx73$IYOFNC{pFXBP! zFUpeW7FFLPdw`1Le`T4;9E)yO%U+gwf6jARlcHN>|LhC2vfh|_=;X^P_t9R)T#)q! z<*1JJskO|pU8YXo0mH>#s3^WrSIb(OV4g}q^4*$zrtDgd zL*kbGiuWHYd6ArMud7n&AF&~H-imCpj5o@WpaUI(f-5M_?j`T>imG#*dV%>B7PBa!i1!vuM#@X8xt?aL;`m(5HqXTuT~p1<2>l}M}&UK^$hlQB4jOtyW%M9w?2jK<_=}Ei|rfp8dQ!` zKBDb-Mdb^{2D6oJ=3e?V^H*LMLrCo%pc{6iM3t+EX35P*)Jj>BmPkXfa zUDikyG7rGILnUun{zaO8Wsfn(*IKw9#&fG+z4j~CHJ*jPxrp}q5!OzuSNhR%eJ$F{ zxQE_mO#}Un#?$p!NEv(^dE8^PqkzVb)0pW?r@nGD^8p30+++6}=@r$aSFcGD! zbeA(;e+ag^6|hzl-y>l=d}DtZmzTL z92$?42b7y=M-ycvqGU?G7$_;w#A7OAn^g}v7CiDsa6NH|K zyAB;y^m9)y<~*EM>r}U4AJBj8?WiQbx3Erg9qr&_%14#`SmZ?7pM>GYv#O3$zONXJJJdVUx70BPJI=#W^Cmo&9T@DlskNf#IR^U7 z%s=deqh&cf?Hc_#IUu25eEIHykORKP^JqFXpD_*Q_gqz~enRXmI%%zfx1 z>hjz4ndslK)bnjZ@h?x%TFk}J6jGg_gkv+&$5=~9jW^n z9~+>aEMv}uewXn*^mJWfKF-Pew5ymG4F`%~H**cQvR0<^Fy+Tf{)|3o)YCS?v1aSL-Uj3t@ggWJNKJij54THR3BIds@J z!`l2RJdQ2QE#FnOjyd#;{nkY=O5YL}lQ>e_&5cUVn+xXCS6WCv<1N#|T;0JjYc&p|!#ygWE;>eP zn%&klaJSJXb?-uNh_xDn&e`@a@n0GU;zOq9bb|hQjQ*30wG2&)-vi9EOO3p;(?tfR z{9!`yU_YNO#v%ddu$YeDkjvPa39?Tgl^ zT7V?!9v|yda;DRlbCen(`R?Uo?Y`KR?3*-7jghQf@%?08L{=5u@;ptyMu*s4Bgv4e z$>=bB%>4N>^8O_lOP@hY#cQk+*`VsLlL?O+gUt059n#0tdXO5D@iF?L;{)Wm)b$Eq zGjZy3u8v|moF*)p>QDPFMQ|vanaWt5E4s7ZP1!k&bBPSI7Tw*vP4Qjo#vP68)c0hT zYWiQ*ufyH4nY!k-(k)Z=QBHrPQ}bm6r-pZmSGjFfo|+|K&0Y9sVZjginR_E&rT`|4XfktNY{s_vEF5C%J`PwQW2 zz4|(}r%K(6rbmUwxpB<#T=uPU!n1@wo1@*VI28Tde|x;w(B8NPF5^~pF8h+$0Me(_ zr$>AEErMYooULnAEx*XNS_4_@;I|rw#BV)>!-T&hOx3ym7W~lp-|J^hS2Js#n%<|) zepJ=G2!Ca6Cw!K5yGd+IFzikU_#s%=X_OO;hYKAkv7x}IDE_3>o^A~J8tL&&}YW|rQ z;{Fs1R={OmPkVhH?9J~eS#ep`!PZQB$~d2P825qjUFvzpozwdFH}pRE|NzVsCl~2M9kU{0Tv5|C;ot`=(7iO8SCapUtdgyuxpJPo{0LM4w50NBrP{e4{jH zZf-7qiLE9&r% z(IvG6cLvrl!tD_s3(y}S-}`7iQ2jSo^a8M)~Q&+4ED$fas5LUKK3!1 zh@*PPeCDx_(!XRa6YC?|E;E14`UfpPpz%p~D8ENgoBsl1Ev$*)p4%U4Lmj^X>T%vt zJZ9fndHh7s|K1$Q*j3R`11{uYjopO-GihxTa3G;PccTSKrKQF*m6h z8C5Y^pXu~$_(Zq;`!pRKpZrEd!+~WS&lc7-F^9_UtBhyfhrM{0suj@Nifl+cvi9iz zR^u5>Mi~<;M*Z0*)i+g2cBYR{^yzDQAL>Y)QV&qO_f1sqdJ)y1y@0ZS`Uh0yKhC`LR@%`~HCEMD+1Ar!Bv)pn{v!8Y`dVa2>L#iV{EXi{@_X!V{ysyr zgR$20s5$;P9M#+Bimjyk$-p7|GVjZH81s;E5l7lSU>wI*vIPyNUS`fdqK!p8e*`aU z1M1HHlyOpbYHiB9!VkTV%$Q1gOe!xV`JQf5G2{B5P|i>Cn>zlM0cAq+a?J$`)VGuw zG^&34f(F!_`w{ad^;!;#>!G)c-^`h}F~@L#{*_DN&U}yOQR&lO{0VCIzlgT$zk=b+ zVtzll03llmb4{sy&isJZ=;arHvf75s>?42${=h7R*i#=o9aeyO#V zH6m5d(x0=Z#-1KuRO%M0_dUz{D1&+?H4Mg!OHg_6C+gdx_Iuk<~%D4md+10;e?(Lq6D~SJYGc1C~wl#MSd*yqg`XjG0#>ZL@SzDYR z`<;)N&)GnSZhS6VZ0K8u;55mwwEc=Oq;f zRWTAi9~+IsQu`Xb?YmT5yg$|mbN$cJ?LEx#Wyy`iV!4KIGM`Kxagwk}I6i)djCmyM043ss9%pqmco5HhO$EVg^YX0PTFY&$L#texdXQ^_^Q&$>aR)>JkW_?PJ_= zfr_C@J}2lo2UEpUyzbWGT+`ms-_1IT|MgeX`_F2lHRk6S;jayzBbm(R=>tPa3|xZ11gu-QvZzhdl{6^+22j$pKedg z#uGJv0kkzNgt77owA4KdQ_atqQ@Qa~@aSzWlBOB$rI~4!HO72psD!`cby4}<(`6++ z79EFv^svuNyEis7j@oAejOHxR$?ZwZ;W#GObI XRuJT~knf~`hi~Lh?@#((K}h}|I%o6y literal 0 HcmV?d00001 diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs new file mode 100644 index 0000000..4ba2734 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -0,0 +1,761 @@ +// +// Driver.cs: Curses-based Driver +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using NStack; +using Unix.Terminal; + +namespace Terminal.Gui { + + /// + /// This is the Curses driver for the gui.cs/Terminal framework. + /// + internal class CursesDriver : ConsoleDriver { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public override int Cols => Curses.Cols; + public override int Rows => Curses.Lines; + + // Current row, and current col, tracked by Move/AddRune only + int ccol, crow; + bool needMove; + public override void Move (int col, int row) + { + ccol = col; + crow = row; + + if (Clip.Contains (col, row)) { + Curses.move (row, col); + needMove = false; + } else { + Curses.move (Clip.Y, Clip.X); + needMove = true; + } + } + + static bool sync = false; + public override void AddRune (Rune rune) + { + if (Clip.Contains (ccol, crow)) { + if (needMove) { + Curses.move (crow, ccol); + needMove = false; + } + Curses.addch ((int)(uint)MakePrintable(rune)); + } else + needMove = true; + if (sync) + Application.Driver.Refresh (); + ccol++; + var runeWidth = Rune.ColumnWidth (rune); + if (runeWidth > 1) { + for (int i = 1; i < runeWidth; i++) { + ccol++; + } + } + } + + public override void AddStr (ustring str) + { + // TODO; optimize this to determine if the str fits in the clip region, and if so, use Curses.addstr directly + foreach (var rune in str) + AddRune (rune); + } + + public override void Refresh () { + Curses.refresh (); + if (Curses.CheckWinChange ()) { + TerminalResized?.Invoke (); + } + } + public override void UpdateCursor () => Refresh (); + public override void End () => Curses.endwin (); + public override void UpdateScreen () => window.redrawwin (); + public override void SetAttribute (Attribute c) => Curses.attrset (c.value); + public Curses.Window window; + + static short last_color_pair = 16; + + /// + /// Creates a curses color from the provided foreground and background colors + /// + /// Contains the curses attributes for the foreground (color, plus any attributes) + /// Contains the curses attributes for the background (color, plus any attributes) + /// + public static Attribute MakeColor (short foreground, short background) + { + Curses.InitColorPair (++last_color_pair, foreground, background); + return new Attribute () { value = Curses.ColorPair (last_color_pair) }; + } + + int [,] colorPairs = new int [16, 16]; + + public override void SetColors (ConsoleColor foreground, ConsoleColor background) + { + int f = (short)foreground; + int b = (short)background; + var v = colorPairs [f, b]; + if ((v & 0x10000) == 0) { + b = b & 0x7; + bool bold = (f & 0x8) != 0; + f = f & 0x7; + + v = MakeColor ((short)f, (short)b) | (bold ? Curses.A_BOLD : 0); + colorPairs [(int)foreground, (int)background] = v | 0x1000; + } + SetAttribute (v & 0xffff); + } + + Dictionary rawPairs = new Dictionary (); + public override void SetColors (short foreColorId, short backgroundColorId) + { + int key = (((ushort)foreColorId << 16)) | (ushort)backgroundColorId; + if (!rawPairs.TryGetValue (key, out var v)) { + v = MakeColor (foreColorId, backgroundColorId); + rawPairs [key] = v; + } + SetAttribute (v); + } + + static Key MapCursesKey (int cursesKey) + { + switch (cursesKey) { + case Curses.KeyF1: return Key.F1; + case Curses.KeyF2: return Key.F2; + case Curses.KeyF3: return Key.F3; + case Curses.KeyF4: return Key.F4; + case Curses.KeyF5: return Key.F5; + case Curses.KeyF6: return Key.F6; + case Curses.KeyF7: return Key.F7; + case Curses.KeyF8: return Key.F8; + case Curses.KeyF9: return Key.F9; + case Curses.KeyF10: return Key.F10; + case Curses.KeyF11: return Key.F11; + case Curses.KeyF12: return Key.F12; + case Curses.KeyUp: return Key.CursorUp; + case Curses.KeyDown: return Key.CursorDown; + case Curses.KeyLeft: return Key.CursorLeft; + case Curses.KeyRight: return Key.CursorRight; + case Curses.KeyHome: return Key.Home; + case Curses.KeyEnd: return Key.End; + case Curses.KeyNPage: return Key.PageDown; + case Curses.KeyPPage: return Key.PageUp; + case Curses.KeyDeleteChar: return Key.DeleteChar; + case Curses.KeyInsertChar: return Key.InsertChar; + case Curses.KeyTab: return Key.Tab; + case Curses.KeyBackTab: return Key.BackTab; + case Curses.KeyBackspace: return Key.Backspace; + case Curses.ShiftKeyUp: return Key.CursorUp | Key.ShiftMask; + case Curses.ShiftKeyDown: return Key.CursorDown | Key.ShiftMask; + case Curses.ShiftKeyLeft: return Key.CursorLeft | Key.ShiftMask; + case Curses.ShiftKeyRight: return Key.CursorRight | Key.ShiftMask; + case Curses.ShiftKeyHome: return Key.Home | Key.ShiftMask; + case Curses.ShiftKeyEnd: return Key.End | Key.ShiftMask; + case Curses.ShiftKeyNPage: return Key.PageDown | Key.ShiftMask; + case Curses.ShiftKeyPPage: return Key.PageUp | Key.ShiftMask; + case Curses.AltKeyUp: return Key.CursorUp | Key.AltMask; + case Curses.AltKeyDown: return Key.CursorDown | Key.AltMask; + case Curses.AltKeyLeft: return Key.CursorLeft | Key.AltMask; + case Curses.AltKeyRight: return Key.CursorRight | Key.AltMask; + case Curses.AltKeyHome: return Key.Home | Key.AltMask; + case Curses.AltKeyEnd: return Key.End | Key.AltMask; + case Curses.AltKeyNPage: return Key.PageDown | Key.AltMask; + case Curses.AltKeyPPage: return Key.PageUp | Key.AltMask; + case Curses.CtrlKeyUp: return Key.CursorUp | Key.CtrlMask; + case Curses.CtrlKeyDown: return Key.CursorDown | Key.CtrlMask; + case Curses.CtrlKeyLeft: return Key.CursorLeft | Key.CtrlMask; + case Curses.CtrlKeyRight: return Key.CursorRight | Key.CtrlMask; + case Curses.CtrlKeyHome: return Key.Home | Key.CtrlMask; + case Curses.CtrlKeyEnd: return Key.End | Key.CtrlMask; + case Curses.CtrlKeyNPage: return Key.PageDown | Key.CtrlMask; + case Curses.CtrlKeyPPage: return Key.PageUp | Key.CtrlMask; + case Curses.ShiftCtrlKeyUp: return Key.CursorUp | Key.ShiftMask | Key.CtrlMask; + case Curses.ShiftCtrlKeyDown: return Key.CursorDown | Key.ShiftMask | Key.CtrlMask; + case Curses.ShiftCtrlKeyLeft: return Key.CursorLeft | Key.ShiftMask | Key.CtrlMask; + case Curses.ShiftCtrlKeyRight: return Key.CursorRight | Key.ShiftMask | Key.CtrlMask; + case Curses.ShiftCtrlKeyHome: return Key.Home | Key.ShiftMask | Key.CtrlMask; + case Curses.ShiftCtrlKeyEnd: return Key.End | Key.ShiftMask | Key.CtrlMask; + case Curses.ShiftCtrlKeyNPage: return Key.PageDown | Key.ShiftMask | Key.CtrlMask; + case Curses.ShiftCtrlKeyPPage: return Key.PageUp | Key.ShiftMask | Key.CtrlMask; + default: return Key.Unknown; + } + } + + Curses.Event? LastMouseButtonPressed; + bool IsButtonPressed; + bool cancelButtonClicked; + bool canWheeledDown; + bool isReportMousePosition; + Point point; + + MouseEvent ToDriverMouse (Curses.MouseEvent cev) + { + MouseFlags mouseFlag = MouseFlags.AllEvents; + + if (LastMouseButtonPressed != null && cev.ButtonState != Curses.Event.ReportMousePosition) { + LastMouseButtonPressed = null; + IsButtonPressed = false; + } + + + if ((cev.ButtonState == Curses.Event.Button1Clicked || cev.ButtonState == Curses.Event.Button2Clicked || + cev.ButtonState == Curses.Event.Button3Clicked) && + LastMouseButtonPressed == null) { + + IsButtonPressed = false; + mouseFlag = ProcessButtonClickedEvent (cev); + + } else if (((cev.ButtonState == Curses.Event.Button1Pressed || cev.ButtonState == Curses.Event.Button2Pressed || + cev.ButtonState == Curses.Event.Button3Pressed) && LastMouseButtonPressed == null) || + IsButtonPressed && cev.ButtonState == Curses.Event.ReportMousePosition) { + + mouseFlag = (MouseFlags)cev.ButtonState; + if (cev.ButtonState != Curses.Event.ReportMousePosition) + LastMouseButtonPressed = cev.ButtonState; + IsButtonPressed = true; + isReportMousePosition = false; + + if (cev.ButtonState == Curses.Event.ReportMousePosition) { + mouseFlag = (MouseFlags)LastMouseButtonPressed | MouseFlags.ReportMousePosition; + point = new Point (); + //cancelButtonClicked = true; + } else { + point = new Point () { + X = cev.X, + Y = cev.Y + }; + } + + if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) { + Task.Run (async () => { + while (IsButtonPressed && LastMouseButtonPressed != null) { + await Task.Delay (100); + var me = new MouseEvent () { + X = cev.X, + Y = cev.Y, + Flags = mouseFlag + }; + + var view = Application.wantContinuousButtonPressedView; + if (view == null) + break; + if (IsButtonPressed && LastMouseButtonPressed != null && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { + mouseHandler (me); + //mainLoop.Driver.Wakeup (); + } + } + }); + } + + + } else if ((cev.ButtonState == Curses.Event.Button1Released || cev.ButtonState == Curses.Event.Button2Released || + cev.ButtonState == Curses.Event.Button3Released)) { + + mouseFlag = ProcessButtonReleasedEvent (cev); + IsButtonPressed = false; + canWheeledDown = false; + + } else if (cev.ButtonState == Curses.Event.Button4Pressed) { + + mouseFlag = MouseFlags.WheeledUp; + + } else if (cev.ButtonState == Curses.Event.ReportMousePosition && cev.X == point.X && cev.Y == point.Y && + canWheeledDown) { + + mouseFlag = MouseFlags.WheeledDown; + canWheeledDown = true; + + } + else if (cev.ButtonState == Curses.Event.ReportMousePosition && !canWheeledDown) { + + mouseFlag = MouseFlags.ReportMousePosition; + canWheeledDown = true; + isReportMousePosition = true; + + } else { + mouseFlag = (MouseFlags)cev.ButtonState; + canWheeledDown = false; + if (cev.ButtonState == Curses.Event.ReportMousePosition) + isReportMousePosition = true; + } + + point = new Point () { + X = cev.X, + Y = cev.Y + }; + + return new MouseEvent () { + X = cev.X, + Y = cev.Y, + //Flags = (MouseFlags)cev.ButtonState + Flags = mouseFlag + }; + } + + private MouseFlags ProcessButtonClickedEvent (Curses.MouseEvent cev) + { + LastMouseButtonPressed = cev.ButtonState; + var mf = GetButtonState (cev, true); + mouseHandler (ProcessButtonState (cev, mf)); + if (LastMouseButtonPressed != null && LastMouseButtonPressed == cev.ButtonState) { + mf = GetButtonState (cev, false); + mouseHandler (ProcessButtonState (cev, mf)); + if (LastMouseButtonPressed != null && LastMouseButtonPressed == cev.ButtonState) { + mf = (MouseFlags)cev.ButtonState; + } + } + LastMouseButtonPressed = null; + canWheeledDown = false; + return mf; + } + + private MouseFlags ProcessButtonReleasedEvent (Curses.MouseEvent cev) + { + var mf = (MouseFlags)cev.ButtonState; + if (!cancelButtonClicked && LastMouseButtonPressed == null && !isReportMousePosition) { + mouseHandler (ProcessButtonState (cev, mf)); + mf = GetButtonState (cev); + } else if (isReportMousePosition) { + mf = MouseFlags.ReportMousePosition; + } + cancelButtonClicked = false; + canWheeledDown = false; + return mf; + } + + MouseFlags GetButtonState (Curses.MouseEvent cev, bool pressed = false) + { + MouseFlags mf = default; + switch (cev.ButtonState) { + case Curses.Event.Button1Clicked: + if (pressed) + mf = MouseFlags.Button1Pressed; + else + mf = MouseFlags.Button1Released; + break; + + case Curses.Event.Button2Clicked: + if (pressed) + mf = MouseFlags.Button2Pressed; + else + mf = MouseFlags.Button2Released; + break; + + case Curses.Event.Button3Clicked: + if (pressed) + mf = MouseFlags.Button3Pressed; + else + mf = MouseFlags.Button3Released; + break; + + case Curses.Event.Button1Released: + mf = MouseFlags.Button1Clicked; + break; + + case Curses.Event.Button2Released: + mf = MouseFlags.Button2Clicked; + break; + + case Curses.Event.Button3Released: + mf = MouseFlags.Button3Clicked; + break; + + + } + return mf; + } + + MouseEvent ProcessButtonState (Curses.MouseEvent cev, MouseFlags mf) + { + return new MouseEvent () { + X = cev.X, + Y = cev.Y, + Flags = mf + }; + } + + KeyModifiers keyModifiers; + + KeyModifiers MapKeyModifiers (Key key) + { + if (keyModifiers == null) + keyModifiers = new KeyModifiers (); + + if (!keyModifiers.Shift && key.HasFlag (Key.ShiftMask)) + keyModifiers.Shift = true; + if (!keyModifiers.Alt && key.HasFlag (Key.AltMask)) + keyModifiers.Alt = true; + if (!keyModifiers.Ctrl && key.HasFlag (Key.CtrlMask)) + keyModifiers.Ctrl = true; + + return keyModifiers; + } + + void ProcessInput (Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) + { + int wch; + var code = Curses.get_wch (out wch); + if (code == Curses.ERR) + return; + + keyModifiers = new KeyModifiers (); + Key k; + + if (code == Curses.KEY_CODE_YES) { + if (wch == Curses.KeyResize) { + if (Curses.CheckWinChange ()) { + TerminalResized?.Invoke (); + return; + } + } + if (wch == Curses.KeyMouse) { + Curses.MouseEvent ev; + Curses.getmouse (out ev); + mouseHandler (ToDriverMouse (ev)); + return; + } + keyHandler (new KeyEvent (MapCursesKey (wch), keyModifiers)); + keyUpHandler (new KeyEvent (MapCursesKey (wch), keyModifiers)); + return; + } + + // Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey + if (wch == 27) { + Curses.timeout (200); + + code = Curses.get_wch (out int wch2); + + if (code == Curses.KEY_CODE_YES) { + k = Key.AltMask | MapCursesKey (wch); + keyHandler (new KeyEvent (k, MapKeyModifiers (k))); + } + if (code == 0) { + KeyEvent key; + + // The ESC-number handling, debatable. + // Simulates the AltMask itself by pressing Alt + Space. + if (wch2 == (int)Key.Space) { + k = Key.AltMask; + key = new KeyEvent (k, MapKeyModifiers (k)); + } else if (wch2 - (int)Key.Space >= 'A' && wch2 - (int)Key.Space <= 'Z') { + k = (Key)((uint)Key.AltMask + (wch2 - (int)Key.Space)); + key = new KeyEvent (k, MapKeyModifiers (k)); + } else if (wch2 >= '1' && wch <= '9') { + k = (Key)((int)Key.F1 + (wch2 - '0' - 1)); + key = new KeyEvent (k, MapKeyModifiers (k)); + } else if (wch2 == '0') { + k = Key.F10; + key = new KeyEvent (k, MapKeyModifiers (k)); + } else if (wch2 == 27) { + k = (Key)wch2; + key = new KeyEvent (k, MapKeyModifiers (k)); + } else { + k = Key.AltMask | (Key)wch2; + key = new KeyEvent (k, MapKeyModifiers (k)); + } + keyHandler (key); + } else { + k = Key.Esc; + keyHandler (new KeyEvent (k, MapKeyModifiers (k))); + } + } else if (wch == Curses.KeyTab) { + k = MapCursesKey (wch); + keyDownHandler (new KeyEvent (k, MapKeyModifiers (k))); + keyHandler (new KeyEvent (k, MapKeyModifiers (k))); + } else { + k = (Key)wch; + keyDownHandler (new KeyEvent (k, MapKeyModifiers (k))); + keyHandler (new KeyEvent (k, MapKeyModifiers (k))); + } + // Cause OnKeyUp and OnKeyPressed. Note that the special handling for ESC above + // will not impact KeyUp. + // This is causing ESC firing even if another keystroke was handled. + //if (wch == Curses.KeyTab) { + // keyUpHandler (new KeyEvent (MapCursesKey (wch), keyModifiers)); + //} else { + // keyUpHandler (new KeyEvent ((Key)wch, keyModifiers)); + //} + } + + Action mouseHandler; + MainLoop mainLoop; + + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) + { + // Note: Curses doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called + Curses.timeout (0); + this.mouseHandler = mouseHandler; + this.mainLoop = mainLoop; + + (mainLoop.Driver as UnixMainLoop).AddWatch (0, UnixMainLoop.Condition.PollIn, x => { + ProcessInput (keyHandler, keyDownHandler, keyUpHandler, mouseHandler); + return true; + }); + + } + + Curses.Event oldMouseEvents, reportableMouseEvents; + public override void Init (Action terminalResized) + { + if (window != null) + return; + + try { + window = Curses.initscr (); + } catch (Exception e) { + Console.WriteLine ("Curses failed to initialize, the exception is: " + e); + } + Curses.raw (); + Curses.noecho (); + + Curses.Window.Standard.keypad (true); + reportableMouseEvents = Curses.mousemask (Curses.Event.AllEvents | Curses.Event.ReportMousePosition, out oldMouseEvents); + TerminalResized = terminalResized; + if (reportableMouseEvents.HasFlag (Curses.Event.ReportMousePosition)) + StartReportingMouseMoves (); + + HLine = Curses.ACS_HLINE; + VLine = Curses.ACS_VLINE; + Stipple = Curses.ACS_CKBOARD; + Diamond = Curses.ACS_DIAMOND; + ULCorner = Curses.ACS_ULCORNER; + LLCorner = Curses.ACS_LLCORNER; + URCorner = Curses.ACS_URCORNER; + LRCorner = Curses.ACS_LRCORNER; + LeftTee = Curses.ACS_LTEE; + RightTee = Curses.ACS_RTEE; + TopTee = Curses.ACS_TTEE; + BottomTee = Curses.ACS_BTEE; + Checked = '\u221a'; + UnChecked = ' '; + Selected = '\u25cf'; + UnSelected = '\u25cc'; + RightArrow = Curses.ACS_RARROW; + LeftArrow = Curses.ACS_LARROW; + UpArrow = Curses.ACS_UARROW; + DownArrow = Curses.ACS_DARROW; + LeftDefaultIndicator = '\u25e6'; + RightDefaultIndicator = '\u25e6'; + LeftBracket = '['; + RightBracket = ']'; + OnMeterSegment = '\u258c'; + OffMeterSegement = ' '; + + Colors.TopLevel = new ColorScheme (); + Colors.Base = new ColorScheme (); + Colors.Dialog = new ColorScheme (); + Colors.Menu = new ColorScheme (); + Colors.Error = new ColorScheme (); + Clip = new Rect (0, 0, Cols, Rows); + if (Curses.HasColors) { + Curses.StartColor (); + Curses.UseDefaultColors (); + + Colors.TopLevel.Normal = MakeColor (Curses.COLOR_GREEN, Curses.COLOR_BLACK); + Colors.TopLevel.Focus = MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN); + Colors.TopLevel.HotNormal = MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLACK); + Colors.TopLevel.HotFocus = MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN); + + Colors.Base.Normal = MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLUE); + Colors.Base.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN); + Colors.Base.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLUE); + Colors.Base.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN); + + // Focused, + // Selected, Hot: Yellow on Black + // Selected, text: white on black + // Unselected, hot: yellow on cyan + // unselected, text: same as unfocused + Colors.Menu.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLACK); + Colors.Menu.Focus = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLACK); + Colors.Menu.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN); + Colors.Menu.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN); + Colors.Menu.Disabled = MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN); + + Colors.Dialog.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE); + Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN); + Colors.Dialog.HotNormal = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_WHITE); + Colors.Dialog.HotFocus = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_CYAN); + + Colors.Error.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_RED); + Colors.Error.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE); + Colors.Error.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_RED); + Colors.Error.HotFocus = Colors.Error.HotNormal; + } else { + Colors.TopLevel.Normal = Curses.COLOR_GREEN; + Colors.TopLevel.Focus = Curses.COLOR_WHITE; + Colors.TopLevel.HotNormal = Curses.COLOR_YELLOW; + Colors.TopLevel.HotFocus = Curses.COLOR_YELLOW; + Colors.Base.Normal = Curses.A_NORMAL; + Colors.Base.Focus = Curses.A_REVERSE; + Colors.Base.HotNormal = Curses.A_BOLD; + Colors.Base.HotFocus = Curses.A_BOLD | Curses.A_REVERSE; + Colors.Menu.Normal = Curses.A_REVERSE; + Colors.Menu.Focus = Curses.A_NORMAL; + Colors.Menu.HotNormal = Curses.A_BOLD; + Colors.Menu.HotFocus = Curses.A_NORMAL; + Colors.Dialog.Normal = Curses.A_REVERSE; + Colors.Dialog.Focus = Curses.A_NORMAL; + Colors.Dialog.HotNormal = Curses.A_BOLD; + Colors.Dialog.HotFocus = Curses.A_NORMAL; + Colors.Error.Normal = Curses.A_BOLD; + Colors.Error.Focus = Curses.A_BOLD | Curses.A_REVERSE; + Colors.Error.HotNormal = Curses.A_BOLD | Curses.A_REVERSE; + Colors.Error.HotFocus = Curses.A_REVERSE; + } + } + + static int MapColor (Color color) + { + switch (color) { + case Color.Black: + return Curses.COLOR_BLACK; + case Color.Blue: + return Curses.COLOR_BLUE; + case Color.Green: + return Curses.COLOR_GREEN; + case Color.Cyan: + return Curses.COLOR_CYAN; + case Color.Red: + return Curses.COLOR_RED; + case Color.Magenta: + return Curses.COLOR_MAGENTA; + case Color.Brown: + return Curses.COLOR_YELLOW; + case Color.Gray: + return Curses.COLOR_WHITE; + case Color.DarkGray: + return Curses.COLOR_BLACK | Curses.A_BOLD; + case Color.BrightBlue: + return Curses.COLOR_BLUE | Curses.A_BOLD; + case Color.BrightGreen: + return Curses.COLOR_GREEN | Curses.A_BOLD; + case Color.BrighCyan: + return Curses.COLOR_CYAN | Curses.A_BOLD; + case Color.BrightRed: + return Curses.COLOR_RED | Curses.A_BOLD; + case Color.BrightMagenta: + return Curses.COLOR_MAGENTA | Curses.A_BOLD; + case Color.BrightYellow: + return Curses.COLOR_YELLOW | Curses.A_BOLD; + case Color.White: + return Curses.COLOR_WHITE | Curses.A_BOLD; + } + throw new ArgumentException ("Invalid color code"); + } + + public override Attribute MakeAttribute (Color fore, Color back) + { + var f = MapColor (fore); + return MakeColor ((short)(f & 0xffff), (short)MapColor (back)) | ((f & Curses.A_BOLD) != 0 ? Curses.A_BOLD : 0); + } + + public override void Suspend () + { + if (reportableMouseEvents.HasFlag (Curses.Event.ReportMousePosition)) + StopReportingMouseMoves (); + Platform.Suspend (); + Curses.Window.Standard.redrawwin (); + Curses.refresh (); + if (reportableMouseEvents.HasFlag (Curses.Event.ReportMousePosition)) + StartReportingMouseMoves (); + } + + public override void StartReportingMouseMoves () + { + Console.Out.Write ("\x1b[?1003h"); + Console.Out.Flush (); + } + + public override void StopReportingMouseMoves () + { + Console.Out.Write ("\x1b[?1003l"); + Console.Out.Flush (); + } + + //int lastMouseInterval; + //bool mouseGrabbed; + + public override void UncookMouse () + { + //if (mouseGrabbed) + // return; + //lastMouseInterval = Curses.mouseinterval (0); + //mouseGrabbed = true; + } + + public override void CookMouse () + { + //mouseGrabbed = false; + //Curses.mouseinterval (lastMouseInterval); + } + } + + internal static class Platform { + [DllImport ("libc")] + static extern int uname (IntPtr buf); + + [DllImport ("libc")] + static extern int killpg (int pgrp, int pid); + + static int suspendSignal; + + static int GetSuspendSignal () + { + if (suspendSignal != 0) + return suspendSignal; + + IntPtr buf = Marshal.AllocHGlobal (8192); + if (uname (buf) != 0) { + Marshal.FreeHGlobal (buf); + suspendSignal = -1; + return suspendSignal; + } + try { + switch (Marshal.PtrToStringAnsi (buf)) { + case "Darwin": + case "DragonFly": + case "FreeBSD": + case "NetBSD": + case "OpenBSD": + suspendSignal = 18; + break; + case "Linux": + // TODO: should fetch the machine name and + // if it is MIPS return 24 + suspendSignal = 20; + break; + case "Solaris": + suspendSignal = 24; + break; + default: + suspendSignal = -1; + break; + } + return suspendSignal; + } finally { + Marshal.FreeHGlobal (buf); + } + } + + /// + /// Suspends the process by sending SIGTSTP to itself + /// + /// The suspend. + static public bool Suspend () + { + int signal = GetSuspendSignal (); + if (signal == -1) + return false; + killpg (0, signal); + return true; + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } + +} diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/README.md b/Terminal.Gui/ConsoleDrivers/CursesDriver/README.md new file mode 100644 index 0000000..c725455 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/README.md @@ -0,0 +1,5 @@ +This directory contains a copy of the MonoCurses binding from: + +http://github.com/mono/mono-curses + +The code has diverged from `mono-curses` a it's been leveraged for `Terminal.Gui`'s Curses driver. \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs new file mode 100644 index 0000000..83ccf98 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs @@ -0,0 +1,229 @@ +// +// mainloop.cs: Simple managed mainloop implementation. +// +// Authors: +// Miguel de Icaza (miguel.de.icaza@gmail.com) +// +// Copyright (C) 2011 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Terminal.Gui { + /// + /// Unix main loop, suitable for using on Posix systems + /// + /// + /// In addition to the general functions of the mainloop, the Unix version + /// can watch file descriptors using the AddWatch methods. + /// + internal class UnixMainLoop : IMainLoopDriver { + [StructLayout (LayoutKind.Sequential)] + struct Pollfd { + public int fd; + public short events, revents; + } + + /// + /// Condition on which to wake up from file descriptor activity. These match the Linux/BSD poll definitions. + /// + [Flags] + public enum Condition : short { + /// + /// There is data to read + /// + PollIn = 1, + /// + /// Writing to the specified descriptor will not block + /// + PollOut = 4, + /// + /// There is urgent data to read + /// + PollPri = 2, + /// + /// Error condition on output + /// + PollErr = 8, + /// + /// Hang-up on output + /// + PollHup = 16, + /// + /// File descriptor is not open. + /// + PollNval = 32 + } + + class Watch { + public int File; + public Condition Condition; + public Func Callback; + } + + Dictionary descriptorWatchers = new Dictionary (); + + [DllImport ("libc")] + extern static int poll ([In, Out]Pollfd [] ufds, uint nfds, int timeout); + + [DllImport ("libc")] + extern static int pipe ([In, Out]int [] pipes); + + [DllImport ("libc")] + extern static int read (int fd, IntPtr buf, IntPtr n); + + [DllImport ("libc")] + extern static int write (int fd, IntPtr buf, IntPtr n); + + Pollfd [] pollmap; + bool poll_dirty = true; + int [] wakeupPipes = new int [2]; + static IntPtr ignore = Marshal.AllocHGlobal (1); + MainLoop mainLoop; + + void IMainLoopDriver.Wakeup () + { + write (wakeupPipes [1], ignore, (IntPtr) 1); + } + + void IMainLoopDriver.Setup (MainLoop mainLoop) { + this.mainLoop = mainLoop; + pipe (wakeupPipes); + AddWatch (wakeupPipes [0], Condition.PollIn, ml => { + read (wakeupPipes [0], ignore, (IntPtr)1); + return true; + }); + } + + /// + /// Removes an active watch from the mainloop. + /// + /// + /// The token parameter is the value returned from AddWatch + /// + public void RemoveWatch (object token) + { + var watch = token as Watch; + if (watch == null) + return; + descriptorWatchers.Remove (watch.File); + } + + /// + /// Watches a file descriptor for activity. + /// + /// + /// When the condition is met, the provided callback + /// is invoked. If the callback returns false, the + /// watch is automatically removed. + /// + /// The return value is a token that represents this watch, you can + /// use this token to remove the watch by calling RemoveWatch. + /// + public object AddWatch (int fileDescriptor, Condition condition, Func callback) + { + if (callback == null) + throw new ArgumentNullException (nameof(callback)); + + var watch = new Watch () { Condition = condition, Callback = callback, File = fileDescriptor }; + descriptorWatchers [fileDescriptor] = watch; + poll_dirty = true; + return watch; + } + + void UpdatePollMap () + { + if (!poll_dirty) + return; + poll_dirty = false; + + pollmap = new Pollfd [descriptorWatchers.Count]; + int i = 0; + foreach (var fd in descriptorWatchers.Keys) { + pollmap [i].fd = fd; + pollmap [i].events = (short)descriptorWatchers [fd].Condition; + i++; + } + } + + bool IMainLoopDriver.EventsPending (bool wait) + { + int pollTimeout = 0; + int n; + + if (CkeckTimeout (wait, ref pollTimeout)) + return true; + + UpdatePollMap (); + + while (true) { + n = poll (pollmap, (uint)pollmap.Length, 0); + if (pollmap != null) { + break; + } + if (mainLoop.idleHandlers.Count > 0 || CkeckTimeout (wait, ref pollTimeout)) { + return true; + } + } + int ic; + lock (mainLoop.idleHandlers) + ic = mainLoop.idleHandlers.Count; + return n > 0 || mainLoop.timeouts.Count > 0 && ((mainLoop.timeouts.Keys [0] - DateTime.UtcNow.Ticks) < 0) || ic > 0; + } + + bool CkeckTimeout (bool wait, ref int pollTimeout) + { + long now = DateTime.UtcNow.Ticks; + + if (mainLoop.timeouts.Count > 0) { + pollTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); + if (pollTimeout < 0) + return true; + + } else + pollTimeout = -1; + + if (!wait) + pollTimeout = 0; + + return false; + } + + void IMainLoopDriver.MainIteration () + { + if (pollmap != null) { + foreach (var p in pollmap) { + Watch watch; + + if (p.revents == 0) + continue; + + if (!descriptorWatchers.TryGetValue (p.fd, out watch)) + continue; + if (!watch.Callback (this.mainLoop)) + descriptorWatchers.Remove (p.fd); + } + } + } + } +} diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs new file mode 100644 index 0000000..46360d8 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs @@ -0,0 +1,268 @@ + + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#define GUICS + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; + + + +namespace Unix.Terminal { + /// + /// Represents a dynamically loaded unmanaged library in a (partially) platform independent manner. + /// First, the native library is loaded using dlopen (on Unix systems) or using LoadLibrary (on Windows). + /// dlsym or GetProcAddress are then used to obtain symbol addresses. Marshal.GetDelegateForFunctionPointer + /// transforms the addresses into delegates to native methods. + /// See http://stackoverflow.com/questions/13461989/p-invoke-to-dynamically-loaded-library-on-mono. + /// + internal class UnmanagedLibrary { + const string UnityEngineApplicationClassName = "UnityEngine.Application, UnityEngine"; + const string XamarinAndroidObjectClassName = "Java.Lang.Object, Mono.Android"; + const string XamarinIOSObjectClassName = "Foundation.NSObject, Xamarin.iOS"; + static bool IsWindows, IsLinux, IsMacOS; + static bool Is64Bit; +#if GUICS + static bool IsMono; +#else + static bool IsMono, IsUnity, IsXamarinIOS, IsXamarinAndroid, IsXamarin; +#endif + static bool IsNetCore; + + public static bool IsMacOSPlatform => IsMacOS; + + [DllImport ("libc")] + static extern int uname (IntPtr buf); + + static string GetUname () + { + var buffer = Marshal.AllocHGlobal (8192); + try { + if (uname (buffer) == 0) { + return Marshal.PtrToStringAnsi (buffer); + } + return string.Empty; + } catch { + return string.Empty; + } finally { + if (buffer != IntPtr.Zero) { + Marshal.FreeHGlobal (buffer); + } + } + } + + static UnmanagedLibrary () + { + var platform = Environment.OSVersion.Platform; + + IsMacOS = (platform == PlatformID.Unix && GetUname () == "Darwin"); + IsLinux = (platform == PlatformID.Unix && !IsMacOS); + IsWindows = (platform == PlatformID.Win32NT || platform == PlatformID.Win32S || platform == PlatformID.Win32Windows); + Is64Bit = Marshal.SizeOf (typeof (IntPtr)) == 8; + IsMono = Type.GetType ("Mono.Runtime") != null; + if (!IsMono) { + IsNetCore = Type.GetType ("System.MathF") != null; + } +#if GUICS + //IsUnity = IsXamarinIOS = IsXamarinAndroid = IsXamarin = false; +#else + IsUnity = Type.GetType (UnityEngineApplicationClassName) != null; + IsXamarinIOS = Type.GetType (XamarinIOSObjectClassName) != null; + IsXamarinAndroid = Type.GetType (XamarinAndroidObjectClassName) != null; + IsXamarin = IsXamarinIOS || IsXamarinAndroid; +#endif + + } + + // flags for dlopen + const int RTLD_LAZY = 1; + const int RTLD_GLOBAL = 8; + + readonly string libraryPath; + readonly IntPtr handle; + + public IntPtr NativeLibraryHandle => handle; + + // + // if isFullPath is set to true, the provided array of libraries are full paths + // and are tested for the file existing, otherwise the file is merely the name + // of the shared library that we pass to dlopen + // + public UnmanagedLibrary (string [] libraryPathAlternatives, bool isFullPath) + { + if (isFullPath){ + this.libraryPath = FirstValidLibraryPath (libraryPathAlternatives); + this.handle = PlatformSpecificLoadLibrary (this.libraryPath); + } else { + foreach (var lib in libraryPathAlternatives){ + this.handle = PlatformSpecificLoadLibrary (lib); + if (this.handle != IntPtr.Zero) + break; + } + } + + if (this.handle == IntPtr.Zero) { + throw new IOException (string.Format ("Error loading native library \"{0}\"", this.libraryPath)); + } + } + + /// + /// Loads symbol in a platform specific way. + /// + /// + /// + public IntPtr LoadSymbol (string symbolName) + { + if (IsWindows) { + // See http://stackoverflow.com/questions/10473310 for background on this. + if (Is64Bit) { + return Windows.GetProcAddress (this.handle, symbolName); + } else { + // Yes, we could potentially predict the size... but it's a lot simpler to just try + // all the candidates. Most functions have a suffix of @0, @4 or @8 so we won't be trying + // many options - and if it takes a little bit longer to fail if we've really got the wrong + // library, that's not a big problem. This is only called once per function in the native library. + symbolName = "_" + symbolName + "@"; + for (int stackSize = 0; stackSize < 128; stackSize += 4) { + IntPtr candidate = Windows.GetProcAddress (this.handle, symbolName + stackSize); + if (candidate != IntPtr.Zero) { + return candidate; + } + } + // Fail. + return IntPtr.Zero; + } + } + if (IsLinux) { + if (IsMono) { + return Mono.dlsym (this.handle, symbolName); + } + if (IsNetCore) { + return CoreCLR.dlsym (this.handle, symbolName); + } + return Linux.dlsym (this.handle, symbolName); + } + if (IsMacOS) { + return MacOSX.dlsym (this.handle, symbolName); + } + throw new InvalidOperationException ("Unsupported platform."); + } + + public T GetNativeMethodDelegate (string methodName) + where T : class + { + var ptr = LoadSymbol (methodName); + if (ptr == IntPtr.Zero) { + throw new MissingMethodException (string.Format ("The native method \"{0}\" does not exist", methodName)); + } + return Marshal.GetDelegateForFunctionPointer(ptr); // non-generic version is obsolete + } + + /// + /// Loads library in a platform specific way. + /// + static IntPtr PlatformSpecificLoadLibrary (string libraryPath) + { + if (IsWindows) { + return Windows.LoadLibrary (libraryPath); + } + if (IsLinux) { + if (IsMono) { + return Mono.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY); + } + if (IsNetCore) { + return CoreCLR.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY); + } + return Linux.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY); + } + if (IsMacOS) { + return MacOSX.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY); + } + throw new InvalidOperationException ("Unsupported platform."); + } + + static string FirstValidLibraryPath (string [] libraryPathAlternatives) + { + foreach (var path in libraryPathAlternatives) { + if (File.Exists (path)) { + return path; + } + } + throw new FileNotFoundException ( + String.Format ("Error loading native library. Not found in any of the possible locations: {0}", + string.Join (",", libraryPathAlternatives))); + } + + static class Windows + { + [DllImport ("kernel32.dll")] + internal static extern IntPtr LoadLibrary (string filename); + + [DllImport ("kernel32.dll")] + internal static extern IntPtr GetProcAddress (IntPtr hModule, string procName); + } + + static class Linux + { + [DllImport ("libdl.so")] + internal static extern IntPtr dlopen (string filename, int flags); + + [DllImport ("libdl.so")] + internal static extern IntPtr dlsym (IntPtr handle, string symbol); + } + + static class MacOSX + { + [DllImport ("libSystem.dylib")] + internal static extern IntPtr dlopen (string filename, int flags); + + [DllImport ("libSystem.dylib")] + internal static extern IntPtr dlsym (IntPtr handle, string symbol); + } + + /// + /// On Linux systems, using using dlopen and dlsym results in + /// DllNotFoundException("libdl.so not found") if libc6-dev + /// is not installed. As a workaround, we load symbols for + /// dlopen and dlsym from the current process as on Linux + /// Mono sure is linked against these symbols. + /// + static class Mono + { + [DllImport ("__Internal")] + internal static extern IntPtr dlopen (string filename, int flags); + + [DllImport ("__Internal")] + internal static extern IntPtr dlsym (IntPtr handle, string symbol); + } + + /// + /// Similarly as for Mono on Linux, we load symbols for + /// dlopen and dlsym from the "libcoreclr.so", + /// to avoid the dependency on libc-dev Linux. + /// + static class CoreCLR + { + [DllImport ("libcoreclr.so")] + internal static extern IntPtr dlopen (string filename, int flags); + + [DllImport ("libcoreclr.so")] + internal static extern IntPtr dlsym (IntPtr handle, string symbol); + } + } +} diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs new file mode 100644 index 0000000..ab963b8 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs @@ -0,0 +1,454 @@ +// +// TODO: +// * FindNCurses needs to remove the old probing code +// * Removal of that proxy code +// * Need to implement reading pointers with the new API +// * Can remove the manual Dlopen features +// * initscr() diagnostics based on DLL can be fixed +// +// binding.cs.in: Core binding for curses. +// +// This file attempts to call into ncurses without relying on Mono's +// dllmap, so it will work with .NET Core. This means that it needs +// two sets of bindings, one for "ncurses" which works on OSX, and one +// that works against "libncursesw.so.5" which is what you find on +// assorted Linux systems. +// +// Additionally, I do not want to rely on an external native library +// which is why all this pain to bind two separate ncurses is here. +// +// Authors: +// Miguel de Icaza (miguel.de.icaza@gmail.com) +// +// Copyright (C) 2007 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using System; +using System.Runtime.InteropServices; + +namespace Unix.Terminal { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + public partial class Curses { + [StructLayout (LayoutKind.Sequential)] + public struct MouseEvent { + public short ID; + public int X, Y, Z; + public Event ButtonState; + } + + static int lines, cols; + static Window main_window; + static IntPtr curses_handle, curscr_ptr, lines_ptr, cols_ptr; + + // If true, uses the DllImport into "ncurses", otherwise "libncursesw.so.5" + //static bool use_naked_driver; + + static UnmanagedLibrary curses_library; + static NativeMethods methods; + + + [DllImport("libc")] + public extern static int setlocale(int cate, [MarshalAs(UnmanagedType.LPStr)] string locale); + + static void LoadMethods () + { + var libs = UnmanagedLibrary.IsMacOSPlatform ? new string [] { "libncurses.dylib" } : new string [] { "libncursesw.so.6", "libncursesw.so.5" }; + curses_library = new UnmanagedLibrary (libs, false); + methods = new NativeMethods (curses_library); + } + + static void FindNCurses () + { + LoadMethods (); + curses_handle = methods.UnmanagedLibrary.NativeLibraryHandle; + + stdscr = read_static_ptr ("stdscr"); + curscr_ptr = get_ptr ("curscr"); + lines_ptr = get_ptr ("LINES"); + cols_ptr = get_ptr ("COLS"); + } + + static public Window initscr () + { + setlocale(LC_ALL, ""); + FindNCurses (); + + main_window = new Window (methods.initscr ()); + try { + console_sharp_get_dims (out lines, out cols); + } catch (DllNotFoundException){ + endwin (); + Console.Error.WriteLine ("Unable to find the @MONO_CURSES@ native library\n" + + "this is different than the managed mono-curses.dll\n\n" + + "Typically you need to install to a LD_LIBRARY_PATH directory\n" + + "or DYLD_LIBRARY_PATH directory or run /sbin/ldconfig"); + Environment.Exit (1); + } + return main_window; + } + + public static int Lines { + get { + return lines; + } + } + + public static int Cols { + get { + return cols; + } + } + + // + // Returns true if the window changed since the last invocation, as a + // side effect, the Lines and Cols properties are updated + // + public static bool CheckWinChange () + { + int l, c; + + console_sharp_get_dims (out l, out c); + if (l != lines || c != cols){ + lines = l; + cols = c; + return true; + } + return false; + } + + public static int addstr (string format, params object [] args) + { + var s = string.Format (format, args); + return addwstr (s); + } + + static char [] r = new char [1]; + + // + // Have to wrap the native addch, as it can not + // display unicode characters, we have to use addstr + // for that. but we need addch to render special ACS + // characters + // + public static int addch (int ch) + { + if (ch < 127 || ch > 0xffff ) + return methods.addch (ch); + char c = (char) ch; + return addwstr (new String (c, 1)); + } + + static IntPtr stdscr; + + static IntPtr get_ptr (string key) + { + var ptr = curses_library.LoadSymbol (key); + + if (ptr == IntPtr.Zero) + throw new Exception ("Could not load the key " + key); + return ptr; + } + + internal static IntPtr read_static_ptr (string key) + { + var ptr = get_ptr (key); + return Marshal.ReadIntPtr (ptr); + } + + internal static IntPtr console_sharp_get_stdscr () => stdscr; + + + internal static IntPtr console_sharp_get_curscr () + { + return Marshal.ReadIntPtr (curscr_ptr); + } + + internal static void console_sharp_get_dims (out int lines, out int cols) + { + lines = Marshal.ReadInt32 (lines_ptr); + cols = Marshal.ReadInt32 (cols_ptr); + } + + public static Event mousemask (Event newmask, out Event oldmask) + { + IntPtr e; + var ret = (Event) (methods.mousemask ((IntPtr) newmask, out e)); + oldmask = (Event) e; + return ret; + } + + + // We encode ESC + char (what Alt-char generates) as 0x2000 + char + public const int KeyAlt = 0x2000; + + static public int IsAlt (int key) + { + if ((key & KeyAlt) != 0) + return key & ~KeyAlt; + return 0; + } + public static int StartColor () => methods.start_color (); + public static bool HasColors => methods.has_colors (); + public static int InitColorPair (short pair, short foreground, short background) => methods.init_pair (pair, foreground, background); + public static int UseDefaultColors () => methods.use_default_colors (); + public static int ColorPairs => methods.COLOR_PAIRS(); + + // + // The proxy methods to call into each version + // + static public int endwin () => methods.endwin (); + static public bool isendwin () => methods.isendwin (); + static public int cbreak () => methods.cbreak (); + static public int nocbreak () => methods.nocbreak (); + static public int echo () => methods.echo (); + static public int noecho () => methods.noecho (); + static public int halfdelay (int t) => methods.halfdelay (t); + static public int raw () => methods.raw (); + static public int noraw () => methods.noraw (); + static public void noqiflush () => methods.noqiflush (); + static public void qiflush () => methods.qiflush (); + static public int typeahead (IntPtr fd) => methods.typeahead (fd); + static public int timeout (int delay) => methods.timeout (delay); + static public int wtimeout (IntPtr win, int delay) => methods.wtimeout (win, delay); + static public int notimeout (IntPtr win, bool bf) => methods.notimeout (win, bf); + static public int keypad (IntPtr win, bool bf) => methods.keypad (win, bf); + static public int meta (IntPtr win, bool bf) => methods.meta (win, bf); + static public int intrflush (IntPtr win, bool bf) => methods.intrflush (win, bf); + static public int clearok (IntPtr win, bool bf) => methods.clearok (win, bf); + static public int idlok (IntPtr win, bool bf) => methods.idlok (win, bf); + static public void idcok (IntPtr win, bool bf) => methods.idcok (win, bf); + static public void immedok (IntPtr win, bool bf) => methods.immedok (win, bf); + static public int leaveok (IntPtr win, bool bf) => methods.leaveok (win, bf); + static public int wsetscrreg (IntPtr win, int top, int bot) => methods.wsetscrreg (win, top, bot); + static public int scrollok (IntPtr win, bool bf) => methods.scrollok (win, bf); + static public int nl() => methods.nl(); + static public int nonl() => methods.nonl(); + static public int setscrreg (int top, int bot) => methods.setscrreg (top, bot); + static public int refresh () => methods.refresh (); + static public int doupdate() => methods.doupdate(); + static public int wrefresh (IntPtr win) => methods.wrefresh (win); + static public int redrawwin (IntPtr win) => methods.redrawwin (win); + //static public int wredrawwin (IntPtr win, int beg_line, int num_lines) => methods.wredrawwin (win, beg_line, num_lines); + static public int wnoutrefresh (IntPtr win) => methods.wnoutrefresh (win); + static public int move (int line, int col) => methods.move (line, col); + //static public int addch (int ch) => methods.addch (ch); + static public int addwstr (string s) => methods.addwstr (s); + static public int wmove (IntPtr win, int line, int col) => methods.wmove (win, line, col); + static public int waddch (IntPtr win, int ch) => methods.waddch (win, ch); + static public int attron (int attrs) => methods.attron (attrs); + static public int attroff (int attrs) => methods.attroff (attrs); + static public int attrset (int attrs) => methods.attrset (attrs); + static public int getch () => methods.getch (); + static public int get_wch (out int sequence) => methods.get_wch (out sequence); + static public int ungetch (int ch) => methods.ungetch (ch); + static public int mvgetch (int y, int x) => methods.mvgetch (y, x); + static public bool has_colors () => methods.has_colors (); + static public int start_color () => methods.start_color (); + static public int init_pair (short pair, short f, short b) => methods.init_pair (pair, f, b); + static public int use_default_colors () => methods.use_default_colors (); + static public int COLOR_PAIRS() => methods.COLOR_PAIRS(); + static public uint getmouse (out MouseEvent ev) => methods.getmouse (out ev); + static public uint ungetmouse (ref MouseEvent ev) => methods.ungetmouse (ref ev); + static public int mouseinterval (int interval) => methods.mouseinterval (interval); + } + + internal class Delegates { + public delegate IntPtr initscr (); + public delegate int endwin (); + public delegate bool isendwin (); + public delegate int cbreak (); + public delegate int nocbreak (); + public delegate int echo (); + public delegate int noecho (); + public delegate int halfdelay (int t); + public delegate int raw (); + public delegate int noraw (); + public delegate void noqiflush (); + public delegate void qiflush (); + public delegate int typeahead (IntPtr fd); + public delegate int timeout (int delay); + public delegate int wtimeout (IntPtr win, int delay); + public delegate int notimeout (IntPtr win, bool bf); + public delegate int keypad (IntPtr win, bool bf); + public delegate int meta (IntPtr win, bool bf); + public delegate int intrflush (IntPtr win, bool bf); + public delegate int clearok (IntPtr win, bool bf); + public delegate int idlok (IntPtr win, bool bf); + public delegate void idcok (IntPtr win, bool bf); + public delegate void immedok (IntPtr win, bool bf); + public delegate int leaveok (IntPtr win, bool bf); + public delegate int wsetscrreg (IntPtr win, int top, int bot); + public delegate int scrollok (IntPtr win, bool bf); + public delegate int nl (); + public delegate int nonl (); + public delegate int setscrreg (int top, int bot); + public delegate int refresh (); + public delegate int doupdate (); + public delegate int wrefresh (IntPtr win); + public delegate int redrawwin (IntPtr win); + //public delegate int wredrawwin (IntPtr win, int beg_line, int num_lines); + public delegate int wnoutrefresh (IntPtr win); + public delegate int move (int line, int col); + public delegate int addch (int ch); + public delegate int addwstr([MarshalAs(UnmanagedType.LPWStr)]string s); + public delegate int wmove (IntPtr win, int line, int col); + public delegate int waddch (IntPtr win, int ch); + public delegate int attron (int attrs); + public delegate int attroff (int attrs); + public delegate int attrset (int attrs); + public delegate int getch (); + public delegate int get_wch (out int sequence); + public delegate int ungetch (int ch); + public delegate int mvgetch (int y, int x); + public delegate bool has_colors (); + public delegate int start_color (); + public delegate int init_pair (short pair, short f, short b); + public delegate int use_default_colors (); + public delegate int COLOR_PAIRS (); + public delegate uint getmouse (out Curses.MouseEvent ev); + public delegate uint ungetmouse (ref Curses.MouseEvent ev); + public delegate int mouseinterval (int interval); + public delegate IntPtr mousemask (IntPtr newmask, out IntPtr oldMask); + } + + internal class NativeMethods { + public readonly Delegates.initscr initscr; + public readonly Delegates.endwin endwin; + public readonly Delegates.isendwin isendwin; + public readonly Delegates.cbreak cbreak; + public readonly Delegates.nocbreak nocbreak; + public readonly Delegates.echo echo; + public readonly Delegates.noecho noecho; + public readonly Delegates.halfdelay halfdelay; + public readonly Delegates.raw raw; + public readonly Delegates.noraw noraw; + public readonly Delegates.noqiflush noqiflush; + public readonly Delegates.qiflush qiflush; + public readonly Delegates.typeahead typeahead; + public readonly Delegates.timeout timeout; + public readonly Delegates.wtimeout wtimeout; + public readonly Delegates.notimeout notimeout; + public readonly Delegates.keypad keypad; + public readonly Delegates.meta meta; + public readonly Delegates.intrflush intrflush; + public readonly Delegates.clearok clearok; + public readonly Delegates.idlok idlok; + public readonly Delegates.idcok idcok; + public readonly Delegates.immedok immedok; + public readonly Delegates.leaveok leaveok; + public readonly Delegates.wsetscrreg wsetscrreg; + public readonly Delegates.scrollok scrollok; + public readonly Delegates.nl nl; + public readonly Delegates.nonl nonl; + public readonly Delegates.setscrreg setscrreg; + public readonly Delegates.refresh refresh; + public readonly Delegates.doupdate doupdate; + public readonly Delegates.wrefresh wrefresh; + public readonly Delegates.redrawwin redrawwin; + //public readonly Delegates.wredrawwin wredrawwin; + public readonly Delegates.wnoutrefresh wnoutrefresh; + public readonly Delegates.move move; + public readonly Delegates.addch addch; + public readonly Delegates.addwstr addwstr; + public readonly Delegates.wmove wmove; + public readonly Delegates.waddch waddch; + public readonly Delegates.attron attron; + public readonly Delegates.attroff attroff; + public readonly Delegates.attrset attrset; + public readonly Delegates.getch getch; + public readonly Delegates.get_wch get_wch; + public readonly Delegates.ungetch ungetch; + public readonly Delegates.mvgetch mvgetch; + public readonly Delegates.has_colors has_colors; + public readonly Delegates.start_color start_color; + public readonly Delegates.init_pair init_pair; + public readonly Delegates.use_default_colors use_default_colors; + public readonly Delegates.COLOR_PAIRS COLOR_PAIRS; + public readonly Delegates.getmouse getmouse; + public readonly Delegates.ungetmouse ungetmouse; + public readonly Delegates.mouseinterval mouseinterval; + public readonly Delegates.mousemask mousemask; + public UnmanagedLibrary UnmanagedLibrary; + + public NativeMethods (UnmanagedLibrary lib) + { + this.UnmanagedLibrary = lib; + initscr = lib.GetNativeMethodDelegate ("initscr"); + endwin = lib.GetNativeMethodDelegate ("endwin"); + isendwin = lib.GetNativeMethodDelegate ("isendwin"); + cbreak = lib.GetNativeMethodDelegate ("cbreak"); + nocbreak = lib.GetNativeMethodDelegate ("nocbreak"); + echo = lib.GetNativeMethodDelegate ("echo"); + noecho = lib.GetNativeMethodDelegate ("noecho"); + halfdelay = lib.GetNativeMethodDelegate ("halfdelay"); + raw = lib.GetNativeMethodDelegate ("raw"); + noraw = lib.GetNativeMethodDelegate ("noraw"); + noqiflush = lib.GetNativeMethodDelegate ("noqiflush"); + qiflush = lib.GetNativeMethodDelegate ("qiflush"); + typeahead = lib.GetNativeMethodDelegate ("typeahead"); + timeout = lib.GetNativeMethodDelegate ("timeout"); + wtimeout = lib.GetNativeMethodDelegate ("wtimeout"); + notimeout = lib.GetNativeMethodDelegate ("notimeout"); + keypad = lib.GetNativeMethodDelegate ("keypad"); + meta = lib.GetNativeMethodDelegate ("meta"); + intrflush = lib.GetNativeMethodDelegate ("intrflush"); + clearok = lib.GetNativeMethodDelegate ("clearok"); + idlok = lib.GetNativeMethodDelegate ("idlok"); + idcok = lib.GetNativeMethodDelegate ("idcok"); + immedok = lib.GetNativeMethodDelegate ("immedok"); + leaveok = lib.GetNativeMethodDelegate ("leaveok"); + wsetscrreg = lib.GetNativeMethodDelegate ("wsetscrreg"); + scrollok = lib.GetNativeMethodDelegate ("scrollok"); + nl = lib.GetNativeMethodDelegate ("nl"); + nonl = lib.GetNativeMethodDelegate ("nonl"); + setscrreg = lib.GetNativeMethodDelegate ("setscrreg"); + refresh = lib.GetNativeMethodDelegate ("refresh"); + doupdate = lib.GetNativeMethodDelegate ("doupdate"); + wrefresh = lib.GetNativeMethodDelegate ("wrefresh"); + redrawwin = lib.GetNativeMethodDelegate ("redrawwin"); + //wredrawwin = lib.GetNativeMethodDelegate ("wredrawwin"); + wnoutrefresh = lib.GetNativeMethodDelegate ("wnoutrefresh"); + move = lib.GetNativeMethodDelegate ("move"); + addch = lib.GetNativeMethodDelegate("addch"); + addwstr = lib.GetNativeMethodDelegate ("addwstr"); + wmove = lib.GetNativeMethodDelegate ("wmove"); + waddch = lib.GetNativeMethodDelegate ("waddch"); + attron = lib.GetNativeMethodDelegate ("attron"); + attroff = lib.GetNativeMethodDelegate ("attroff"); + attrset = lib.GetNativeMethodDelegate ("attrset"); + getch = lib.GetNativeMethodDelegate ("getch"); + get_wch = lib.GetNativeMethodDelegate ("get_wch"); + ungetch = lib.GetNativeMethodDelegate ("ungetch"); + mvgetch = lib.GetNativeMethodDelegate ("mvgetch"); + has_colors = lib.GetNativeMethodDelegate ("has_colors"); + start_color = lib.GetNativeMethodDelegate ("start_color"); + init_pair = lib.GetNativeMethodDelegate ("init_pair"); + use_default_colors = lib.GetNativeMethodDelegate ("use_default_colors"); + COLOR_PAIRS = lib.GetNativeMethodDelegate ("COLOR_PAIRS"); + getmouse = lib.GetNativeMethodDelegate ("getmouse"); + ungetmouse = lib.GetNativeMethodDelegate ("ungetmouse"); + mouseinterval = lib.GetNativeMethodDelegate ("mouseinterval"); + mousemask = lib.GetNativeMethodDelegate ("mousemask"); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs new file mode 100644 index 0000000..082db0d --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs @@ -0,0 +1,158 @@ +/* + * This file is autogenerated by the attrib.c program, do not edit + */ + +#define XTERM1006 + +using System; + +namespace Unix.Terminal { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public partial class Curses { + public const int A_NORMAL = unchecked((int)0x0); + public const int A_STANDOUT = unchecked((int)0x10000); + public const int A_UNDERLINE = unchecked((int)0x20000); + public const int A_REVERSE = unchecked((int)0x40000); + public const int A_BLINK = unchecked((int)0x80000); + public const int A_DIM = unchecked((int)0x100000); + public const int A_BOLD = unchecked((int)0x200000); + public const int A_PROTECT = unchecked((int)0x1000000); + public const int A_INVIS = unchecked((int)0x800000); + public const int ACS_LLCORNER = unchecked((int)0x40006d); + public const int ACS_LRCORNER = unchecked((int)0x40006a); + public const int ACS_HLINE = unchecked((int)0x400071); + public const int ACS_ULCORNER = unchecked((int)0x40006c); + public const int ACS_URCORNER = unchecked((int)0x40006b); + public const int ACS_VLINE = unchecked((int)0x400078); + public const int ACS_LTEE = unchecked((int)0x400074); + public const int ACS_RTEE = unchecked((int)0x400075); + public const int ACS_BTEE = unchecked((int)0x400076); + public const int ACS_TTEE = unchecked((int)0x400077); + public const int ACS_PLUS = unchecked((int)0x40006e); + public const int ACS_S1 = unchecked((int)0x40006f); + public const int ACS_S9 = unchecked((int)0x400073); + public const int ACS_DIAMOND = unchecked((int)0x400060); + public const int ACS_CKBOARD = unchecked((int)0x400061); + public const int ACS_DEGREE = unchecked((int)0x400066); + public const int ACS_PLMINUS = unchecked((int)0x400067); + public const int ACS_BULLET = unchecked((int)0x40007e); + public const int ACS_LARROW = unchecked((int)0x40002c); + public const int ACS_RARROW = unchecked((int)0x40002b); + public const int ACS_DARROW = unchecked((int)0x40002e); + public const int ACS_UARROW = unchecked((int)0x40002d); + public const int ACS_BOARD = unchecked((int)0x400068); + public const int ACS_LANTERN = unchecked((int)0x400069); + public const int ACS_BLOCK = unchecked((int)0x400030); + public const int COLOR_BLACK = unchecked((int)0x0); + public const int COLOR_RED = unchecked((int)0x1); + public const int COLOR_GREEN = unchecked((int)0x2); + public const int COLOR_YELLOW = unchecked((int)0x3); + public const int COLOR_BLUE = unchecked((int)0x4); + public const int COLOR_MAGENTA = unchecked((int)0x5); + public const int COLOR_CYAN = unchecked((int)0x6); + public const int COLOR_WHITE = unchecked((int)0x7); + public const int KEY_CODE_YES = unchecked((int)0x100); + public enum Event : long { + Button1Pressed = unchecked((int)0x2), + Button1Released = unchecked((int)0x1), + Button1Clicked = unchecked((int)0x4), + Button1DoubleClicked = unchecked((int)0x8), + Button1TripleClicked = unchecked((int)0x10), + Button2Pressed = unchecked((int)0x80), + Button2Released = unchecked((int)0x40), + Button2Clicked = unchecked((int)0x100), + Button2DoubleClicked = unchecked((int)0x200), + Button2TrippleClicked = unchecked((int)0x400), + Button3Pressed = unchecked((int)0x2000), + Button3Released = unchecked((int)0x1000), + Button3Clicked = unchecked((int)0x4000), + Button3DoubleClicked = unchecked((int)0x8000), + Button3TripleClicked = unchecked((int)0x10000), + Button4Pressed = unchecked((int)0x80000), + Button4Released = unchecked((int)0x40000), + Button4Clicked = unchecked((int)0x100000), + Button4DoubleClicked = unchecked((int)0x200000), + Button4TripleClicked = unchecked((int)0x400000), + ButtonShift = unchecked((int)0x2000000), + ButtonCtrl = unchecked((int)0x1000000), + ButtonAlt = unchecked((int)0x4000000), + ReportMousePosition = unchecked((int)0x8000000), + AllEvents = unchecked((int)0x7ffffff), + } +#if XTERM1006 + public const int LeftRightUpNPagePPage= unchecked((int)0x8); + public const int DownEnd = unchecked((int)0x6); + public const int Home = unchecked((int)0x7); +#else + public const int LeftRightUpNPagePPage= unchecked((int)0x0); + public const int DownEnd = unchecked((int)0x0); + public const int Home = unchecked((int)0x0); +#endif + public const int ERR = unchecked((int)0xffffffff); + public const int KeyBackspace = unchecked((int)0x107); + public const int KeyUp = unchecked((int)0x103); + public const int KeyDown = unchecked((int)0x102); + public const int KeyLeft = unchecked((int)0x104); + public const int KeyRight = unchecked((int)0x105); + public const int KeyNPage = unchecked((int)0x152); + public const int KeyPPage = unchecked((int)0x153); + public const int KeyHome = unchecked((int)0x106); + public const int KeyMouse = unchecked((int)0x199); + public const int KeyEnd = unchecked((int)0x168); + public const int KeyDeleteChar = unchecked((int)0x14a); + public const int KeyInsertChar = unchecked((int)0x14b); + public const int KeyTab = unchecked((int)0x009); + public const int KeyBackTab = unchecked((int)0x161); + public const int KeyF1 = unchecked((int)0x109); + public const int KeyF2 = unchecked((int)0x10a); + public const int KeyF3 = unchecked((int)0x10b); + public const int KeyF4 = unchecked((int)0x10c); + public const int KeyF5 = unchecked((int)0x10d); + public const int KeyF6 = unchecked((int)0x10e); + public const int KeyF7 = unchecked((int)0x10f); + public const int KeyF8 = unchecked((int)0x110); + public const int KeyF9 = unchecked((int)0x111); + public const int KeyF10 = unchecked((int)0x112); + public const int KeyF11 = unchecked((int)0x113); + public const int KeyF12 = unchecked((int)0x114); + public const int KeyResize = unchecked((int)0x19a); + public const int ShiftKeyUp = unchecked((int)0x151); + public const int ShiftKeyDown = unchecked((int)0x150); + public const int ShiftKeyLeft = unchecked((int)0x189); + public const int ShiftKeyRight = unchecked((int)0x192); + public const int ShiftKeyNPage = unchecked((int)0x18c); + public const int ShiftKeyPPage = unchecked((int)0x18e); + public const int ShiftKeyHome = unchecked((int)0x187); + public const int ShiftKeyEnd = unchecked((int)0x182); + public const int AltKeyUp = unchecked((int)0x234 + LeftRightUpNPagePPage); + public const int AltKeyDown = unchecked((int)0x20b + DownEnd); + public const int AltKeyLeft = unchecked((int)0x21f + LeftRightUpNPagePPage); + public const int AltKeyRight = unchecked((int)0x22e + LeftRightUpNPagePPage); + public const int AltKeyNPage = unchecked((int)0x224 + LeftRightUpNPagePPage); + public const int AltKeyPPage = unchecked((int)0x229 + LeftRightUpNPagePPage); + public const int AltKeyHome = unchecked((int)0x215 + Home); + public const int AltKeyEnd = unchecked((int)0x210 + DownEnd); + public const int CtrlKeyUp = unchecked((int)0x236 + LeftRightUpNPagePPage); + public const int CtrlKeyDown = unchecked((int)0x20d + DownEnd); + public const int CtrlKeyLeft = unchecked((int)0x221 + LeftRightUpNPagePPage); + public const int CtrlKeyRight = unchecked((int)0x230 + LeftRightUpNPagePPage); + public const int CtrlKeyNPage = unchecked((int)0x226 + LeftRightUpNPagePPage); + public const int CtrlKeyPPage = unchecked((int)0x22b + LeftRightUpNPagePPage); + public const int CtrlKeyHome = unchecked((int)0x217 + Home); + public const int CtrlKeyEnd = unchecked((int)0x212 + DownEnd); + public const int ShiftCtrlKeyUp = unchecked((int)0x237 + LeftRightUpNPagePPage); + public const int ShiftCtrlKeyDown = unchecked((int)0x20e + DownEnd); + public const int ShiftCtrlKeyLeft = unchecked((int)0x222 + LeftRightUpNPagePPage); + public const int ShiftCtrlKeyRight = unchecked((int)0x231 + LeftRightUpNPagePPage); + public const int ShiftCtrlKeyNPage = unchecked((int)0x227 + LeftRightUpNPagePPage); + public const int ShiftCtrlKeyPPage = unchecked((int)0x22c + LeftRightUpNPagePPage); + public const int ShiftCtrlKeyHome = unchecked((int)0x218 + Home); + public const int ShiftCtrlKeyEnd = unchecked((int)0x213 + DownEnd); + + public const int LC_ALL = 6; + static public int ColorPair(int n){ + return 0 + n * 256; + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs new file mode 100644 index 0000000..606f7f8 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs @@ -0,0 +1,174 @@ +// +// handles.cs: OO wrappers for some curses objects +// +// Authors: +// Miguel de Icaza (miguel.de.icaza@gmail.com) +// +// Copyright (C) 2007 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using System; + +namespace Unix.Terminal { + + public partial class Curses { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public class Window { + public readonly IntPtr Handle; + static Window curscr; + static Window stdscr; + + static Window () + { + Curses.initscr (); + stdscr = new Window (Curses.console_sharp_get_stdscr ()); + curscr = new Window (Curses.console_sharp_get_curscr ()); + } + + internal Window (IntPtr handle) + { + Handle = handle; + } + + static public Window Standard { + get { + return stdscr; + } + } + + static public Window Current { + get { + return curscr; + } + } + + + public int wtimeout (int delay) + { + return Curses.wtimeout (Handle, delay); + } + + public int notimeout (bool bf) + { + return Curses.notimeout (Handle, bf); + } + + public int keypad (bool bf) + { + return Curses.keypad (Handle, bf); + } + + public int meta (bool bf) + { + return Curses.meta (Handle, bf); + } + + public int intrflush (bool bf) + { + return Curses.intrflush (Handle, bf); + } + + public int clearok (bool bf) + { + return Curses.clearok (Handle, bf); + } + + public int idlok (bool bf) + { + return Curses.idlok (Handle, bf); + } + + public void idcok (bool bf) + { + Curses.idcok (Handle, bf); + } + + public void immedok (bool bf) + { + Curses.immedok (Handle, bf); + } + + public int leaveok (bool bf) + { + return Curses.leaveok (Handle, bf); + } + + public int setscrreg (int top, int bot) + { + return Curses.wsetscrreg (Handle, top, bot); + } + + public int scrollok (bool bf) + { + return Curses.scrollok (Handle, bf); + } + + public int wrefresh () + { + return Curses.wrefresh (Handle); + } + + public int redrawwin () + { + return Curses.redrawwin (Handle); + } + +#if false + public int wredrawwin (int beg_line, int num_lines) + { + return Curses.wredrawwin (Handle, beg_line, num_lines); + } +#endif + public int wnoutrefresh () + { + return Curses.wnoutrefresh (Handle); + } + + public int move (int line, int col) + { + return Curses.wmove (Handle, line, col); + } + + public int addch (char ch) + { + return Curses.waddch (Handle, ch); + } + + public int refresh () + { + return Curses.wrefresh (Handle); + } + } + + // Currently unused, to do later + internal class Screen { + public readonly IntPtr Handle; + + internal Screen (IntPtr handle) + { + Handle = handle; + } + } + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } + +} diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs new file mode 100644 index 0000000..a336997 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs @@ -0,0 +1,1979 @@ +// +// FakeConsole.cs: A fake .NET Windows Console API implementaiton for unit tests. +// +// Authors: +// Charlie Kindel (github.com/tig) +// +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui { + /// + /// + /// + public static class FakeConsole { + + // + // Summary: + // Gets or sets the width of the console window. + // + // Returns: + // The width of the console window measured in columns. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight + // property is less than or equal to 0.-or-The value of the System.Console.WindowHeight + // property plus the value of the System.Console.WindowTop property is greater than + // or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth + // property or the value of the System.Console.WindowHeight property is greater + // than the largest possible window width or height for the current screen resolution + // and console font. + // + // T:System.IO.IOException: + // Error reading or writing information. + /// + /// + /// + public static int WindowWidth { get; set; } = 80; + // + // Summary: + // Gets a value that indicates whether output has been redirected from the standard + // output stream. + // + // Returns: + // true if output is redirected; otherwise, false. + /// + /// + /// + public static bool IsOutputRedirected { get; } + // + // Summary: + // Gets a value that indicates whether the error output stream has been redirected + // from the standard error stream. + // + // Returns: + // true if error output is redirected; otherwise, false. + /// + /// + /// + public static bool IsErrorRedirected { get; } + // + // Summary: + // Gets the standard input stream. + // + // Returns: + // A System.IO.TextReader that represents the standard input stream. + /// + /// + /// + public static TextReader In { get; } + // + // Summary: + // Gets the standard output stream. + // + // Returns: + // A System.IO.TextWriter that represents the standard output stream. + /// + /// + /// + public static TextWriter Out { get; } + // + // Summary: + // Gets the standard error output stream. + // + // Returns: + // A System.IO.TextWriter that represents the standard error output stream. + /// + /// + /// + public static TextWriter Error { get; } + // + // Summary: + // Gets or sets the encoding the console uses to read input. + // + // Returns: + // The encoding used to read console input. + // + // Exceptions: + // T:System.ArgumentNullException: + // The property value in a set operation is null. + // + // T:System.IO.IOException: + // An error occurred during the execution of this operation. + // + // T:System.Security.SecurityException: + // Your application does not have permission to perform this operation. + /// + /// + /// + public static Encoding InputEncoding { get; set; } + // + // Summary: + // Gets or sets the encoding the console uses to write output. + // + // Returns: + // The encoding used to write console output. + // + // Exceptions: + // T:System.ArgumentNullException: + // The property value in a set operation is null. + // + // T:System.IO.IOException: + // An error occurred during the execution of this operation. + // + // T:System.Security.SecurityException: + // Your application does not have permission to perform this operation. + /// + /// + /// + public static Encoding OutputEncoding { get; set; } + // + // Summary: + // Gets or sets the background color of the console. + // + // Returns: + // A value that specifies the background color of the console; that is, the color + // that appears behind each character. The default is black. + // + // Exceptions: + // T:System.ArgumentException: + // The color specified in a set operation is not a valid member of System.ConsoleColor. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static ConsoleColor BackgroundColor { get; set; } = _defaultBackgroundColor; + static ConsoleColor _defaultBackgroundColor = ConsoleColor.Black; + + // + // Summary: + // Gets or sets the foreground color of the console. + // + // Returns: + // A System.ConsoleColor that specifies the foreground color of the console; that + // is, the color of each character that is displayed. The default is gray. + // + // Exceptions: + // T:System.ArgumentException: + // The color specified in a set operation is not a valid member of System.ConsoleColor. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static ConsoleColor ForegroundColor { get; set; } = _defaultForegroundColor; + static ConsoleColor _defaultForegroundColor = ConsoleColor.Gray; + // + // Summary: + // Gets or sets the height of the buffer area. + // + // Returns: + // The current height, in rows, of the buffer area. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // The value in a set operation is less than or equal to zero.-or- The value in + // a set operation is greater than or equal to System.Int16.MaxValue.-or- The value + // in a set operation is less than System.Console.WindowTop + System.Console.WindowHeight. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static int BufferHeight { get; set; } = 25; + // + // Summary: + // Gets or sets the width of the buffer area. + // + // Returns: + // The current width, in columns, of the buffer area. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // The value in a set operation is less than or equal to zero.-or- The value in + // a set operation is greater than or equal to System.Int16.MaxValue.-or- The value + // in a set operation is less than System.Console.WindowLeft + System.Console.WindowWidth. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static int BufferWidth { get; set; } = 80; + // + // Summary: + // Gets or sets the height of the console window area. + // + // Returns: + // The height of the console window measured in rows. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight + // property is less than or equal to 0.-or-The value of the System.Console.WindowHeight + // property plus the value of the System.Console.WindowTop property is greater than + // or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth + // property or the value of the System.Console.WindowHeight property is greater + // than the largest possible window width or height for the current screen resolution + // and console font. + // + // T:System.IO.IOException: + // Error reading or writing information. + /// + /// + /// + public static int WindowHeight { get; set; } = 25; + // + // Summary: + // Gets or sets a value indicating whether the combination of the System.ConsoleModifiers.Control + // modifier key and System.ConsoleKey.C console key (Ctrl+C) is treated as ordinary + // input or as an interruption that is handled by the operating system. + // + // Returns: + // true if Ctrl+C is treated as ordinary input; otherwise, false. + // + // Exceptions: + // T:System.IO.IOException: + // Unable to get or set the input mode of the console input buffer. + /// + /// + /// + public static bool TreatControlCAsInput { get; set; } + // + // Summary: + // Gets the largest possible number of console window columns, based on the current + // font and screen resolution. + // + // Returns: + // The width of the largest possible console window measured in columns. + /// + /// + /// + public static int LargestWindowWidth { get; } + // + // Summary: + // Gets the largest possible number of console window rows, based on the current + // font and screen resolution. + // + // Returns: + // The height of the largest possible console window measured in rows. + /// + /// + /// + public static int LargestWindowHeight { get; } + // + // Summary: + // Gets or sets the leftmost position of the console window area relative to the + // screen buffer. + // + // Returns: + // The leftmost console window position measured in columns. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // In a set operation, the value to be assigned is less than zero.-or-As a result + // of the assignment, System.Console.WindowLeft plus System.Console.WindowWidth + // would exceed System.Console.BufferWidth. + // + // T:System.IO.IOException: + // Error reading or writing information. + /// + /// + /// + public static int WindowLeft { get; set; } + // + // Summary: + // Gets or sets the top position of the console window area relative to the screen + // buffer. + // + // Returns: + // The uppermost console window position measured in rows. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // In a set operation, the value to be assigned is less than zero.-or-As a result + // of the assignment, System.Console.WindowTop plus System.Console.WindowHeight + // would exceed System.Console.BufferHeight. + // + // T:System.IO.IOException: + // Error reading or writing information. + /// + /// + /// + public static int WindowTop { get; set; } + // + // Summary: + // Gets or sets the column position of the cursor within the buffer area. + // + // Returns: + // The current position, in columns, of the cursor. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // The value in a set operation is less than zero.-or- The value in a set operation + // is greater than or equal to System.Console.BufferWidth. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static int CursorLeft { get; set; } + // + // Summary: + // Gets or sets the row position of the cursor within the buffer area. + // + // Returns: + // The current position, in rows, of the cursor. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // The value in a set operation is less than zero.-or- The value in a set operation + // is greater than or equal to System.Console.BufferHeight. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static int CursorTop { get; set; } + // + // Summary: + // Gets or sets the height of the cursor within a character cell. + // + // Returns: + // The size of the cursor expressed as a percentage of the height of a character + // cell. The property value ranges from 1 to 100. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // The value specified in a set operation is less than 1 or greater than 100. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static int CursorSize { get; set; } + // + // Summary: + // Gets or sets a value indicating whether the cursor is visible. + // + // Returns: + // true if the cursor is visible; otherwise, false. + // + // Exceptions: + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static bool CursorVisible { get; set; } + // + // Summary: + // Gets or sets the title to display in the console title bar. + // + // Returns: + // The string to be displayed in the title bar of the console. The maximum length + // of the title string is 24500 characters. + // + // Exceptions: + // T:System.InvalidOperationException: + // In a get operation, the retrieved title is longer than 24500 characters. + // + // T:System.ArgumentOutOfRangeException: + // In a set operation, the specified title is longer than 24500 characters. + // + // T:System.ArgumentNullException: + // In a set operation, the specified title is null. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static string Title { get; set; } + // + // Summary: + // Gets a value indicating whether a key press is available in the input stream. + // + // Returns: + // true if a key press is available; otherwise, false. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.InvalidOperationException: + // Standard input is redirected to a file instead of the keyboard. + /// + /// + /// + public static bool KeyAvailable { get; } + // + // Summary: + // Gets a value indicating whether the NUM LOCK keyboard toggle is turned on or + // turned off. + // + // Returns: + // true if NUM LOCK is turned on; false if NUM LOCK is turned off. + /// + /// + /// + public static bool NumberLock { get; } + // + // Summary: + // Gets a value indicating whether the CAPS LOCK keyboard toggle is turned on or + // turned off. + // + // Returns: + // true if CAPS LOCK is turned on; false if CAPS LOCK is turned off. + /// + /// + /// + public static bool CapsLock { get; } + // + // Summary: + // Gets a value that indicates whether input has been redirected from the standard + // input stream. + // + // Returns: + // true if input is redirected; otherwise, false. + /// + /// + /// + public static bool IsInputRedirected { get; } + + // + // Summary: + // Plays the sound of a beep through the console speaker. + // + // Exceptions: + // T:System.Security.HostProtectionException: + // This method was executed on a server, such as SQL Server, that does not permit + // access to a user interface. + /// + /// + /// + public static void Beep () + { + throw new NotImplementedException (); + } + // + // Summary: + // Plays the sound of a beep of a specified frequency and duration through the console + // speaker. + // + // Parameters: + // frequency: + // The frequency of the beep, ranging from 37 to 32767 hertz. + // + // duration: + // The duration of the beep measured in milliseconds. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // frequency is less than 37 or more than 32767 hertz.-or- duration is less than + // or equal to zero. + // + // T:System.Security.HostProtectionException: + // This method was executed on a server, such as SQL Server, that does not permit + // access to the console. + /// + /// + /// + public static void Beep (int frequency, int duration) + { + throw new NotImplementedException (); + } + // + // Summary: + // Clears the console buffer and corresponding console window of display information. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static void Clear () + { + _buffer = new char [WindowWidth, WindowHeight]; + SetCursorPosition (0, 0); + } + + static char [,] _buffer = new char [WindowWidth, WindowHeight]; + + // + // Summary: + // Copies a specified source area of the screen buffer to a specified destination + // area. + // + // Parameters: + // sourceLeft: + // The leftmost column of the source area. + // + // sourceTop: + // The topmost row of the source area. + // + // sourceWidth: + // The number of columns in the source area. + // + // sourceHeight: + // The number of rows in the source area. + // + // targetLeft: + // The leftmost column of the destination area. + // + // targetTop: + // The topmost row of the destination area. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // One or more of the parameters is less than zero.-or- sourceLeft or targetLeft + // is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop + // is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight + // is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth + // is greater than or equal to System.Console.BufferWidth. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static void MoveBufferArea (int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Copies a specified source area of the screen buffer to a specified destination + // area. + // + // Parameters: + // sourceLeft: + // The leftmost column of the source area. + // + // sourceTop: + // The topmost row of the source area. + // + // sourceWidth: + // The number of columns in the source area. + // + // sourceHeight: + // The number of rows in the source area. + // + // targetLeft: + // The leftmost column of the destination area. + // + // targetTop: + // The topmost row of the destination area. + // + // sourceChar: + // The character used to fill the source area. + // + // sourceForeColor: + // The foreground color used to fill the source area. + // + // sourceBackColor: + // The background color used to fill the source area. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // One or more of the parameters is less than zero.-or- sourceLeft or targetLeft + // is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop + // is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight + // is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth + // is greater than or equal to System.Console.BufferWidth. + // + // T:System.ArgumentException: + // One or both of the color parameters is not a member of the System.ConsoleColor + // enumeration. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + //[SecuritySafeCritical] + /// + /// + /// + public static void MoveBufferArea (int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop, char sourceChar, ConsoleColor sourceForeColor, ConsoleColor sourceBackColor) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Acquires the standard error stream. + // + // Returns: + // The standard error stream. + /// + /// + /// + public static Stream OpenStandardError () + { + throw new NotImplementedException (); + } + + // + // Summary: + // Acquires the standard error stream, which is set to a specified buffer size. + // + // Parameters: + // bufferSize: + // The internal stream buffer size. + // + // Returns: + // The standard error stream. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // bufferSize is less than or equal to zero. + /// + /// + /// + public static Stream OpenStandardError (int bufferSize) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Acquires the standard input stream, which is set to a specified buffer size. + // + // Parameters: + // bufferSize: + // The internal stream buffer size. + // + // Returns: + // The standard input stream. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // bufferSize is less than or equal to zero. + /// + /// + /// + public static Stream OpenStandardInput (int bufferSize) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Acquires the standard input stream. + // + // Returns: + // The standard input stream. + /// + /// + /// + public static Stream OpenStandardInput () + { + throw new NotImplementedException (); + } + + // + // Summary: + // Acquires the standard output stream, which is set to a specified buffer size. + // + // Parameters: + // bufferSize: + // The internal stream buffer size. + // + // Returns: + // The standard output stream. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // bufferSize is less than or equal to zero. + /// + /// + /// + public static Stream OpenStandardOutput (int bufferSize) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Acquires the standard output stream. + // + // Returns: + // The standard output stream. + /// + /// + /// + public static Stream OpenStandardOutput () + { + throw new NotImplementedException (); + } + + // + // Summary: + // Reads the next character from the standard input stream. + // + // Returns: + // The next character from the input stream, or negative one (-1) if there are currently + // no more characters to be read. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static int Read () + { + throw new NotImplementedException (); + } + + // + // Summary: + // Obtains the next character or function key pressed by the user. The pressed key + // is optionally displayed in the console window. + // + // Parameters: + // intercept: + // Determines whether to display the pressed key in the console window. true to + // not display the pressed key; otherwise, false. + // + // Returns: + // An object that describes the System.ConsoleKey constant and Unicode character, + // if any, that correspond to the pressed console key. The System.ConsoleKeyInfo + // object also describes, in a bitwise combination of System.ConsoleModifiers values, + // whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously + // with the console key. + // + // Exceptions: + // T:System.InvalidOperationException: + // The System.Console.In property is redirected from some stream other than the + // console. + //[SecuritySafeCritical] + /// + /// + /// + public static ConsoleKeyInfo ReadKey (bool intercept) + { + if (MockKeyPresses.Count > 0) { + return MockKeyPresses.Pop(); + } else { + return new ConsoleKeyInfo ('~', ConsoleKey.Oem3, false,false,false); + } + } + + /// + /// + /// + public static Stack MockKeyPresses = new Stack (); + + // + // Summary: + // Obtains the next character or function key pressed by the user. The pressed key + // is displayed in the console window. + // + // Returns: + // An object that describes the System.ConsoleKey constant and Unicode character, + // if any, that correspond to the pressed console key. The System.ConsoleKeyInfo + // object also describes, in a bitwise combination of System.ConsoleModifiers values, + // whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously + // with the console key. + // + // Exceptions: + // T:System.InvalidOperationException: + // The System.Console.In property is redirected from some stream other than the + // console. + /// + /// + /// + public static ConsoleKeyInfo ReadKey () + { + throw new NotImplementedException (); + } + + // + // Summary: + // Reads the next line of characters from the standard input stream. + // + // Returns: + // The next line of characters from the input stream, or null if no more lines are + // available. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.OutOfMemoryException: + // There is insufficient memory to allocate a buffer for the returned string. + // + // T:System.ArgumentOutOfRangeException: + // The number of characters in the next line of characters is greater than System.Int32.MaxValue. + /// + /// + /// + public static string ReadLine () + { + throw new NotImplementedException (); + } + + // + // Summary: + // Sets the foreground and background console colors to their defaults. + // + // Exceptions: + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + //[SecuritySafeCritical] + /// + /// + /// + public static void ResetColor () + { + BackgroundColor = _defaultBackgroundColor; + ForegroundColor = _defaultForegroundColor; + } + + // + // Summary: + // Sets the height and width of the screen buffer area to the specified values. + // + // Parameters: + // width: + // The width of the buffer area measured in columns. + // + // height: + // The height of the buffer area measured in rows. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // height or width is less than or equal to zero.-or- height or width is greater + // than or equal to System.Int16.MaxValue.-or- width is less than System.Console.WindowLeft + // + System.Console.WindowWidth.-or- height is less than System.Console.WindowTop + // + System.Console.WindowHeight. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + //[SecuritySafeCritical] + /// + /// + /// + public static void SetBufferSize (int width, int height) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Sets the position of the cursor. + // + // Parameters: + // left: + // The column position of the cursor. Columns are numbered from left to right starting + // at 0. + // + // top: + // The row position of the cursor. Rows are numbered from top to bottom starting + // at 0. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // left or top is less than zero.-or- left is greater than or equal to System.Console.BufferWidth.-or- + // top is greater than or equal to System.Console.BufferHeight. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + //[SecuritySafeCritical] + /// + /// + /// + public static void SetCursorPosition (int left, int top) + { + CursorLeft = left; + CursorTop = top; + } + + // + // Summary: + // Sets the System.Console.Error property to the specified System.IO.TextWriter + // object. + // + // Parameters: + // newError: + // A stream that is the new standard error output. + // + // Exceptions: + // T:System.ArgumentNullException: + // newError is null. + // + // T:System.Security.SecurityException: + // The caller does not have the required permission. + //[SecuritySafeCritical] + /// + /// + /// + public static void SetError (TextWriter newError) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Sets the System.Console.In property to the specified System.IO.TextReader object. + // + // Parameters: + // newIn: + // A stream that is the new standard input. + // + // Exceptions: + // T:System.ArgumentNullException: + // newIn is null. + // + // T:System.Security.SecurityException: + // The caller does not have the required permission. + //[SecuritySafeCritical] + /// + /// + /// + public static void SetIn (TextReader newIn) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Sets the System.Console.Out property to the specified System.IO.TextWriter object. + // + // Parameters: + // newOut: + // A stream that is the new standard output. + // + // Exceptions: + // T:System.ArgumentNullException: + // newOut is null. + // + // T:System.Security.SecurityException: + // The caller does not have the required permission. + //[SecuritySafeCritical] + /// + /// + /// + /// + public static void SetOut (TextWriter newOut) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Sets the position of the console window relative to the screen buffer. + // + // Parameters: + // left: + // The column position of the upper left corner of the console window. + // + // top: + // The row position of the upper left corner of the console window. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // left or top is less than zero.-or- left + System.Console.WindowWidth is greater + // than System.Console.BufferWidth.-or- top + System.Console.WindowHeight is greater + // than System.Console.BufferHeight. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + //[SecuritySafeCritical] + /// + /// + /// + /// + /// + public static void SetWindowPosition (int left, int top) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Sets the height and width of the console window to the specified values. + // + // Parameters: + // width: + // The width of the console window measured in columns. + // + // height: + // The height of the console window measured in rows. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // width or height is less than or equal to zero.-or- width plus System.Console.WindowLeft + // or height plus System.Console.WindowTop is greater than or equal to System.Int16.MaxValue. + // -or- width or height is greater than the largest possible window width or height + // for the current screen resolution and console font. + // + // T:System.Security.SecurityException: + // The user does not have permission to perform this action. + // + // T:System.IO.IOException: + // An I/O error occurred. + //[SecuritySafeCritical] + /// + /// + /// + /// + /// + public static void SetWindowSize (int width, int height) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the specified string value to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (string value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified object to the standard output + // stream. + // + // Parameters: + // value: + // The value to write, or null. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (object value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 64-bit unsigned integer value + // to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + //[CLSCompliant (false)] + /// + /// + /// + /// + public static void Write (ulong value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 64-bit signed integer value to + // the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (long value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified objects to the standard output + // stream using the specified format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg0: + // The first object to write using format. + // + // arg1: + // The second object to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + /// + public static void Write (string format, object arg0, object arg1) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 32-bit signed integer value to + // the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (int value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified object to the standard output + // stream using the specified format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg0: + // An object to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + public static void Write (string format, object arg0) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 32-bit unsigned integer value + // to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + //[CLSCompliant (false)] + /// + /// + /// + /// + public static void Write (uint value) + { + throw new NotImplementedException (); + } + + //[CLSCompliant (false)] + /// + /// + /// + /// + /// + /// + /// + /// + public static void Write (string format, object arg0, object arg1, object arg2, object arg3) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified array of objects to the standard + // output stream using the specified format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg: + // An array of objects to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format or arg is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + public static void Write (string format, params object [] arg) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified Boolean value to the standard + // output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (bool value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the specified Unicode character value to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (char value) + { + _buffer [CursorLeft, CursorTop] = value; + } + + // + // Summary: + // Writes the specified array of Unicode characters to the standard output stream. + // + // Parameters: + // buffer: + // A Unicode character array. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (char [] buffer) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the specified subarray of Unicode characters to the standard output stream. + // + // Parameters: + // buffer: + // An array of Unicode characters. + // + // index: + // The starting position in buffer. + // + // count: + // The number of characters to write. + // + // Exceptions: + // T:System.ArgumentNullException: + // buffer is null. + // + // T:System.ArgumentOutOfRangeException: + // index or count is less than zero. + // + // T:System.ArgumentException: + // index plus count specify a position that is not within buffer. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + /// + /// + public static void Write (char [] buffer, int index, int count) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified objects to the standard output + // stream using the specified format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg0: + // The first object to write using format. + // + // arg1: + // The second object to write using format. + // + // arg2: + // The third object to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + /// + /// + public static void Write (string format, object arg0, object arg1, object arg2) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified System.Decimal value to the standard + // output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (decimal value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified single-precision floating-point + // value to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (float value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified double-precision floating-point + // value to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void Write (double value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the current line terminator to the standard output stream. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + public static void WriteLine () + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified single-precision floating-point + // value, followed by the current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (float value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 32-bit signed integer value, + // followed by the current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (int value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 32-bit unsigned integer value, + // followed by the current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + //[CLSCompliant (false)] + /// + /// + /// + /// + public static void WriteLine (uint value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 64-bit signed integer value, + // followed by the current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (long value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified 64-bit unsigned integer value, + // followed by the current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + //[CLSCompliant (false)] + /// + /// + /// + /// + public static void WriteLine (ulong value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified object, followed by the current + // line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (object value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the specified string value, followed by the current line terminator, to + // the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (string value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified object, followed by the current + // line terminator, to the standard output stream using the specified format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg0: + // An object to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + public static void WriteLine (string format, object arg0) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified objects, followed by the current + // line terminator, to the standard output stream using the specified format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg0: + // The first object to write using format. + // + // arg1: + // The second object to write using format. + // + // arg2: + // The third object to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + /// + /// + public static void WriteLine (string format, object arg0, object arg1, object arg2) + { + throw new NotImplementedException (); + } + + //[CLSCompliant (false)] + /// + /// + /// + /// + /// + /// + /// + /// + public static void WriteLine (string format, object arg0, object arg1, object arg2, object arg3) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified array of objects, followed by + // the current line terminator, to the standard output stream using the specified + // format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg: + // An array of objects to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format or arg is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + public static void WriteLine (string format, params object [] arg) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the specified subarray of Unicode characters, followed by the current + // line terminator, to the standard output stream. + // + // Parameters: + // buffer: + // An array of Unicode characters. + // + // index: + // The starting position in buffer. + // + // count: + // The number of characters to write. + // + // Exceptions: + // T:System.ArgumentNullException: + // buffer is null. + // + // T:System.ArgumentOutOfRangeException: + // index or count is less than zero. + // + // T:System.ArgumentException: + // index plus count specify a position that is not within buffer. + // + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + /// + /// + public static void WriteLine (char [] buffer, int index, int count) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified System.Decimal value, followed + // by the current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (decimal value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the specified array of Unicode characters, followed by the current line + // terminator, to the standard output stream. + // + // Parameters: + // buffer: + // A Unicode character array. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (char [] buffer) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the specified Unicode character, followed by the current line terminator, + // value to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (char value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified Boolean value, followed by the + // current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (bool value) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified objects, followed by the current + // line terminator, to the standard output stream using the specified format information. + // + // Parameters: + // format: + // A composite format string (see Remarks). + // + // arg0: + // The first object to write using format. + // + // arg1: + // The second object to write using format. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + // + // T:System.ArgumentNullException: + // format is null. + // + // T:System.FormatException: + // The format specification in format is invalid. + /// + /// + /// + /// + /// + /// + public static void WriteLine (string format, object arg0, object arg1) + { + throw new NotImplementedException (); + } + + // + // Summary: + // Writes the text representation of the specified double-precision floating-point + // value, followed by the current line terminator, to the standard output stream. + // + // Parameters: + // value: + // The value to write. + // + // Exceptions: + // T:System.IO.IOException: + // An I/O error occurred. + /// + /// + /// + /// + public static void WriteLine (double value) + { + throw new NotImplementedException (); + } + + } +} diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs new file mode 100644 index 0000000..11cd1c9 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -0,0 +1,468 @@ +// +// FakeDriver.cs: A fake ConsoleDriver for unit tests. +// +// Authors: +// Charlie Kindel (github.com/tig) +// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using NStack; + +namespace Terminal.Gui { + /// + /// Implements a mock ConsoleDriver for unit testing + /// + public class FakeDriver : ConsoleDriver { + int cols, rows; + /// + /// + /// + public override int Cols => cols; + /// + /// + /// + public override int Rows => rows; + + // The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag + int [,,] contents; + bool [] dirtyLine; + + void UpdateOffscreen () + { + int cols = Cols; + int rows = Rows; + + contents = new int [rows, cols, 3]; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + contents [r, c, 0] = ' '; + contents [r, c, 1] = MakeColor (ConsoleColor.Gray, ConsoleColor.Black); + contents [r, c, 2] = 0; + } + } + dirtyLine = new bool [rows]; + for (int row = 0; row < rows; row++) + dirtyLine [row] = true; + } + + static bool sync = false; + + /// + /// + /// + public FakeDriver () + { + cols = FakeConsole.WindowWidth; + rows = FakeConsole.WindowHeight; // - 1; + UpdateOffscreen (); + } + + bool needMove; + // Current row, and current col, tracked by Move/AddCh only + int ccol, crow; + /// + /// + /// + /// + /// + public override void Move (int col, int row) + { + ccol = col; + crow = row; + + if (Clip.Contains (col, row)) { + FakeConsole.CursorTop = row; + FakeConsole.CursorLeft = col; + needMove = false; + } else { + FakeConsole.CursorTop = Clip.Y; + FakeConsole.CursorLeft = Clip.X; + needMove = true; + } + + } + + /// + /// + /// + /// + public override void AddRune (Rune rune) + { + rune = MakePrintable (rune); + if (Clip.Contains (ccol, crow)) { + if (needMove) { + //MockConsole.CursorLeft = ccol; + //MockConsole.CursorTop = crow; + needMove = false; + } + contents [crow, ccol, 0] = (int)(uint)rune; + contents [crow, ccol, 1] = currentAttribute; + contents [crow, ccol, 2] = 1; + dirtyLine [crow] = true; + } else + needMove = true; + ccol++; + //if (ccol == Cols) { + // ccol = 0; + // if (crow + 1 < Rows) + // crow++; + //} + if (sync) + UpdateScreen (); + } + + /// + /// + /// + /// + public override void AddStr (ustring str) + { + foreach (var rune in str) + AddRune (rune); + } + + /// + /// + /// + public override void End () + { + FakeConsole.ResetColor (); + FakeConsole.Clear (); + } + + static Attribute MakeColor (ConsoleColor f, ConsoleColor b) + { + // Encode the colors into the int value. + return new Attribute () { value = ((((int)f) & 0xffff) << 16) | (((int)b) & 0xffff) }; + } + + /// + /// + /// + /// + public override void Init (Action terminalResized) + { + Colors.TopLevel = new ColorScheme (); + Colors.Base = new ColorScheme (); + Colors.Dialog = new ColorScheme (); + Colors.Menu = new ColorScheme (); + Colors.Error = new ColorScheme (); + Clip = new Rect (0, 0, Cols, Rows); + + Colors.TopLevel.Normal = MakeColor (ConsoleColor.Green, ConsoleColor.Black); + Colors.TopLevel.Focus = MakeColor (ConsoleColor.White, ConsoleColor.DarkCyan); + Colors.TopLevel.HotNormal = MakeColor (ConsoleColor.DarkYellow, ConsoleColor.Black); + Colors.TopLevel.HotFocus = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.DarkCyan); + + Colors.Base.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Blue); + Colors.Base.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); + Colors.Base.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Blue); + Colors.Base.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); + + // Focused, + // Selected, Hot: Yellow on Black + // Selected, text: white on black + // Unselected, hot: yellow on cyan + // unselected, text: same as unfocused + Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); + Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); + Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); + Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); + Colors.Menu.Disabled = MakeColor (ConsoleColor.DarkGray, ConsoleColor.Cyan); + + Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); + Colors.Dialog.HotNormal = MakeColor (ConsoleColor.Blue, ConsoleColor.Gray); + Colors.Dialog.HotFocus = MakeColor (ConsoleColor.Blue, ConsoleColor.Cyan); + + Colors.Error.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Red); + Colors.Error.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Error.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Red); + Colors.Error.HotFocus = Colors.Error.HotNormal; + + HLine = '\u2500'; + VLine = '\u2502'; + Stipple = '\u2592'; + Diamond = '\u25c6'; + ULCorner = '\u250C'; + LLCorner = '\u2514'; + URCorner = '\u2510'; + LRCorner = '\u2518'; + LeftTee = '\u251c'; + RightTee = '\u2524'; + TopTee = '\u22a4'; + BottomTee = '\u22a5'; + Checked = '\u221a'; + UnChecked = ' '; + Selected = '\u25cf'; + UnSelected = '\u25cc'; + RightArrow = '\u25ba'; + LeftArrow = '\u25c4'; + UpArrow = '\u25b2'; + DownArrow = '\u25bc'; + LeftDefaultIndicator = '\u25e6'; + RightDefaultIndicator = '\u25e6'; + LeftBracket = '['; + RightBracket = ']'; + OnMeterSegment = '\u258c'; + OffMeterSegement = ' '; + + //MockConsole.Clear (); + } + + /// + /// + /// + /// + /// + /// + public override Attribute MakeAttribute (Color fore, Color back) + { + return MakeColor ((ConsoleColor)fore, (ConsoleColor)back); + } + + int redrawColor = -1; + void SetColor (int color) + { + redrawColor = color; + IEnumerable values = Enum.GetValues (typeof (ConsoleColor)) + .OfType () + .Select (s => (int)s); + if (values.Contains (color & 0xffff)) { + FakeConsole.BackgroundColor = (ConsoleColor)(color & 0xffff); + } + if (values.Contains ((color >> 16) & 0xffff)) { + FakeConsole.ForegroundColor = (ConsoleColor)((color >> 16) & 0xffff); + } + } + + /// + /// + /// + public override void UpdateScreen () + { + int rows = Rows; + int cols = Cols; + + FakeConsole.CursorTop = 0; + FakeConsole.CursorLeft = 0; + for (int row = 0; row < rows; row++) { + dirtyLine [row] = false; + for (int col = 0; col < cols; col++) { + contents [row, col, 2] = 0; + var color = contents [row, col, 1]; + if (color != redrawColor) + SetColor (color); + FakeConsole.Write ((char)contents [row, col, 0]); + } + } + } + + /// + /// + /// + public override void Refresh () + { + int rows = Rows; + int cols = Cols; + + var savedRow = FakeConsole.CursorTop; + var savedCol = FakeConsole.CursorLeft; + for (int row = 0; row < rows; row++) { + if (!dirtyLine [row]) + continue; + dirtyLine [row] = false; + for (int col = 0; col < cols; col++) { + if (contents [row, col, 2] != 1) + continue; + + FakeConsole.CursorTop = row; + FakeConsole.CursorLeft = col; + for (; col < cols && contents [row, col, 2] == 1; col++) { + var color = contents [row, col, 1]; + if (color != redrawColor) + SetColor (color); + + FakeConsole.Write ((char)contents [row, col, 0]); + contents [row, col, 2] = 0; + } + } + } + FakeConsole.CursorTop = savedRow; + FakeConsole.CursorLeft = savedCol; + } + + /// + /// + /// + public override void UpdateCursor () + { + // + } + + /// + /// + /// + public override void StartReportingMouseMoves () + { + } + + /// + /// + /// + public override void StopReportingMouseMoves () + { + } + + /// + /// + /// + public override void Suspend () + { + } + + int currentAttribute; + /// + /// + /// + /// + public override void SetAttribute (Attribute c) + { + currentAttribute = c.value; + } + + Key MapKey (ConsoleKeyInfo keyInfo) + { + switch (keyInfo.Key) { + case ConsoleKey.Escape: + return Key.Esc; + case ConsoleKey.Tab: + return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; + case ConsoleKey.Home: + return Key.Home; + case ConsoleKey.End: + return Key.End; + case ConsoleKey.LeftArrow: + return Key.CursorLeft; + case ConsoleKey.RightArrow: + return Key.CursorRight; + case ConsoleKey.UpArrow: + return Key.CursorUp; + case ConsoleKey.DownArrow: + return Key.CursorDown; + case ConsoleKey.PageUp: + return Key.PageUp; + case ConsoleKey.PageDown: + return Key.PageDown; + case ConsoleKey.Enter: + return Key.Enter; + case ConsoleKey.Spacebar: + return Key.Space; + case ConsoleKey.Backspace: + return Key.Backspace; + case ConsoleKey.Delete: + return Key.Delete; + + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + return (Key)((uint)keyInfo.KeyChar); + } + + var key = keyInfo.Key; + if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { + var delta = key - ConsoleKey.A; + if (keyInfo.Modifiers == ConsoleModifiers.Control) + return (Key)((uint)Key.ControlA + delta); + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'A' + delta)); + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + return (Key)((uint)'A' + delta); + else + return (Key)((uint)'a' + delta); + } + if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { + var delta = key - ConsoleKey.D0; + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'0' + delta)); + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + return (Key)((uint)keyInfo.KeyChar); + return (Key)((uint)'0' + delta); + } + if (key >= ConsoleKey.F1 && key <= ConsoleKey.F10) { + var delta = key - ConsoleKey.F1; + + return (Key)((int)Key.F1 + delta); + } + return (Key)(0xffffffff); + } + + KeyModifiers keyModifiers = new KeyModifiers (); + + /// + /// + /// + /// + /// + /// + /// + /// + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) + { + // Note: Net doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called + (mainLoop.Driver as NetMainLoop).KeyPressed = delegate (ConsoleKeyInfo consoleKey) { + var map = MapKey (consoleKey); + if (map == (Key)0xffffffff) + return; + keyHandler (new KeyEvent (map, keyModifiers)); + keyUpHandler (new KeyEvent (map, keyModifiers)); + }; + } + + /// + /// + /// + /// + /// + public override void SetColors (ConsoleColor foreground, ConsoleColor background) + { + throw new NotImplementedException (); + } + + /// + /// + /// + /// + /// + public override void SetColors (short foregroundColorId, short backgroundColorId) + { + throw new NotImplementedException (); + } + + /// + /// + /// + public override void CookMouse () + { + } + + /// + /// + /// + public override void UncookMouse () + { + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs new file mode 100644 index 0000000..1fe8f01 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -0,0 +1,476 @@ +// +// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient. +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using NStack; + +namespace Terminal.Gui { + + internal class NetDriver : ConsoleDriver { + int cols, rows; + public override int Cols => cols; + public override int Rows => rows; + + // The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag + int [,,] contents; + bool [] dirtyLine; + + void UpdateOffscreen () + { + int cols = Cols; + int rows = Rows; + + contents = new int [rows, cols, 3]; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + contents [r, c, 0] = ' '; + contents [r, c, 1] = MakeColor (ConsoleColor.Gray, ConsoleColor.Black); + contents [r, c, 2] = 0; + } + } + dirtyLine = new bool [rows]; + for (int row = 0; row < rows; row++) + dirtyLine [row] = true; + } + + static bool sync = false; + + public NetDriver () + { + cols = Console.WindowWidth; + rows = Console.WindowHeight - 1; + UpdateOffscreen (); + } + + bool needMove; + // Current row, and current col, tracked by Move/AddCh only + int ccol, crow; + public override void Move (int col, int row) + { + ccol = col; + crow = row; + + if (Clip.Contains (col, row)) { + Console.CursorTop = row; + Console.CursorLeft = col; + needMove = false; + } else { + Console.CursorTop = Clip.Y; + Console.CursorLeft = Clip.X; + needMove = true; + } + + } + + public override void AddRune (Rune rune) + { + rune = MakePrintable (rune); + if (Clip.Contains (ccol, crow)) { + if (needMove) { + //Console.CursorLeft = ccol; + //Console.CursorTop = crow; + needMove = false; + } + contents [crow, ccol, 0] = (int)(uint)rune; + contents [crow, ccol, 1] = currentAttribute; + contents [crow, ccol, 2] = 1; + dirtyLine [crow] = true; + } else + needMove = true; + ccol++; + //if (ccol == Cols) { + // ccol = 0; + // if (crow + 1 < Rows) + // crow++; + //} + if (sync) + UpdateScreen (); + } + + public override void AddStr (ustring str) + { + foreach (var rune in str) + AddRune (rune); + } + + public override void End () + { + Console.ResetColor (); + Console.Clear (); + } + + static Attribute MakeColor (ConsoleColor f, ConsoleColor b) + { + // Encode the colors into the int value. + return new Attribute () { value = ((((int)f) & 0xffff) << 16) | (((int)b) & 0xffff) }; + } + + + public override void Init (Action terminalResized) + { + Colors.TopLevel = new ColorScheme (); + Colors.Base = new ColorScheme (); + Colors.Dialog = new ColorScheme (); + Colors.Menu = new ColorScheme (); + Colors.Error = new ColorScheme (); + Clip = new Rect (0, 0, Cols, Rows); + + Colors.TopLevel.Normal = MakeColor (ConsoleColor.Green, ConsoleColor.Black); + Colors.TopLevel.Focus = MakeColor (ConsoleColor.White, ConsoleColor.DarkCyan); + Colors.TopLevel.HotNormal = MakeColor (ConsoleColor.DarkYellow, ConsoleColor.Black); + Colors.TopLevel.HotFocus = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.DarkCyan); + + Colors.Base.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Blue); + Colors.Base.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); + Colors.Base.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Blue); + Colors.Base.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); + + // Focused, + // Selected, Hot: Yellow on Black + // Selected, text: white on black + // Unselected, hot: yellow on cyan + // unselected, text: same as unfocused + Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); + Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); + Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); + Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); + Colors.Menu.Disabled = MakeColor (ConsoleColor.DarkGray, ConsoleColor.Cyan); + + Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); + Colors.Dialog.HotNormal = MakeColor (ConsoleColor.Blue, ConsoleColor.Gray); + Colors.Dialog.HotFocus = MakeColor (ConsoleColor.Blue, ConsoleColor.Cyan); + + Colors.Error.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Red); + Colors.Error.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Error.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Red); + Colors.Error.HotFocus = Colors.Error.HotNormal; + Console.Clear (); + + HLine = '\u2500'; + VLine = '\u2502'; + Stipple = '\u2592'; + Diamond = '\u25c6'; + ULCorner = '\u250C'; + LLCorner = '\u2514'; + URCorner = '\u2510'; + LRCorner = '\u2518'; + LeftTee = '\u251c'; + RightTee = '\u2524'; + TopTee = '\u22a4'; + BottomTee = '\u22a5'; + Checked = '\u221a'; + UnChecked = ' '; + Selected = '\u25cf'; + UnSelected = '\u25cc'; + RightArrow = '\u25ba'; + LeftArrow = '\u25c4'; + UpArrow = '\u25b2'; + DownArrow = '\u25bc'; + LeftDefaultIndicator = '\u25e6'; + RightDefaultIndicator = '\u25e6'; + LeftBracket = '['; + RightBracket = ']'; + OnMeterSegment = '\u258c'; + OffMeterSegement = ' '; + } + + public override Attribute MakeAttribute (Color fore, Color back) + { + return MakeColor ((ConsoleColor)fore, (ConsoleColor)back); + } + + int redrawColor = -1; + void SetColor (int color) + { + redrawColor = color; + IEnumerable values = Enum.GetValues (typeof (ConsoleColor)) + .OfType () + .Select (s => (int)s); + if (values.Contains (color & 0xffff)) { + Console.BackgroundColor = (ConsoleColor)(color & 0xffff); + } + if (values.Contains ((color >> 16) & 0xffff)) { + Console.ForegroundColor = (ConsoleColor)((color >> 16) & 0xffff); + } + } + + public override void UpdateScreen () + { + int rows = Rows; + int cols = Cols; + + Console.CursorTop = 0; + Console.CursorLeft = 0; + for (int row = 0; row < rows; row++) { + dirtyLine [row] = false; + for (int col = 0; col < cols; col++) { + contents [row, col, 2] = 0; + var color = contents [row, col, 1]; + if (color != redrawColor) + SetColor (color); + Console.Write ((char)contents [row, col, 0]); + } + } + } + + public override void Refresh () + { + int rows = Rows; + int cols = Cols; + + var savedRow = Console.CursorTop; + var savedCol = Console.CursorLeft; + for (int row = 0; row < rows; row++) { + if (!dirtyLine [row]) + continue; + dirtyLine [row] = false; + for (int col = 0; col < cols; col++) { + if (contents [row, col, 2] != 1) + continue; + + Console.CursorTop = row; + Console.CursorLeft = col; + for (; col < cols && contents [row, col, 2] == 1; col++) { + var color = contents [row, col, 1]; + if (color != redrawColor) + SetColor (color); + + Console.Write ((char)contents [row, col, 0]); + contents [row, col, 2] = 0; + } + } + } + Console.CursorTop = savedRow; + Console.CursorLeft = savedCol; + } + + public override void UpdateCursor () + { + // + } + + public override void StartReportingMouseMoves () + { + } + + public override void StopReportingMouseMoves () + { + } + + public override void Suspend () + { + } + + int currentAttribute; + public override void SetAttribute (Attribute c) + { + currentAttribute = c.value; + } + + Key MapKey (ConsoleKeyInfo keyInfo) + { + switch (keyInfo.Key) { + case ConsoleKey.Escape: + return Key.Esc; + case ConsoleKey.Tab: + return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; + case ConsoleKey.Home: + return Key.Home; + case ConsoleKey.End: + return Key.End; + case ConsoleKey.LeftArrow: + return Key.CursorLeft; + case ConsoleKey.RightArrow: + return Key.CursorRight; + case ConsoleKey.UpArrow: + return Key.CursorUp; + case ConsoleKey.DownArrow: + return Key.CursorDown; + case ConsoleKey.PageUp: + return Key.PageUp; + case ConsoleKey.PageDown: + return Key.PageDown; + case ConsoleKey.Enter: + return Key.Enter; + case ConsoleKey.Spacebar: + return Key.Space; + case ConsoleKey.Backspace: + return Key.Backspace; + case ConsoleKey.Delete: + return Key.Delete; + + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + return (Key)((uint)keyInfo.KeyChar); + } + + var key = keyInfo.Key; + if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { + var delta = key - ConsoleKey.A; + if (keyInfo.Modifiers == ConsoleModifiers.Control) + return (Key)((uint)Key.ControlA + delta); + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'A' + delta)); + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + return (Key)((uint)'A' + delta); + else + return (Key)((uint)'a' + delta); + } + if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { + var delta = key - ConsoleKey.D0; + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'0' + delta)); + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + return (Key)((uint)keyInfo.KeyChar); + return (Key)((uint)'0' + delta); + } + if (key >= ConsoleKey.F1 && key <= ConsoleKey.F10) { + var delta = key - ConsoleKey.F1; + + return (Key)((int)Key.F1 + delta); + } + return (Key)(0xffffffff); + } + + KeyModifiers keyModifiers = new KeyModifiers (); + + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) + { + // Note: Net doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called + (mainLoop.Driver as NetMainLoop).KeyPressed = delegate (ConsoleKeyInfo consoleKey) { + var map = MapKey (consoleKey); + if (map == (Key)0xffffffff) + return; + keyHandler (new KeyEvent (map, keyModifiers)); + keyUpHandler (new KeyEvent (map, keyModifiers)); + }; + } + + public override void SetColors (ConsoleColor foreground, ConsoleColor background) + { + throw new NotImplementedException (); + } + + public override void SetColors (short foregroundColorId, short backgroundColorId) + { + throw new NotImplementedException (); + } + + public override void CookMouse () + { + } + + public override void UncookMouse () + { + } + + // + // These are for the .NET driver, but running natively on Windows, wont run + // on the Mono emulation + // + + } + + /// + /// Mainloop intended to be used with the .NET System.Console API, and can + /// be used on Windows and Unix, it is cross platform but lacks things like + /// file descriptor monitoring. + /// + /// + /// This implementation is used for both NetDriver and FakeDriver. + /// + public class NetMainLoop : IMainLoopDriver { + AutoResetEvent keyReady = new AutoResetEvent (false); + AutoResetEvent waitForProbe = new AutoResetEvent (false); + ConsoleKeyInfo? keyResult = null; + MainLoop mainLoop; + Func consoleKeyReaderFn = null; + + /// + /// Invoked when a Key is pressed. + /// + public Action KeyPressed; + + /// + /// Initializes the class. + /// + /// + /// Passing a consoleKeyReaderfn is provided to support unit test sceanrios. + /// + /// The method to be called to get a key from the console. + public NetMainLoop (Func consoleKeyReaderFn = null) + { + if (consoleKeyReaderFn == null) { + throw new ArgumentNullException ("key reader function must be provided."); + } + this.consoleKeyReaderFn = consoleKeyReaderFn; + } + + void WindowsKeyReader () + { + while (true) { + waitForProbe.WaitOne (); + keyResult = consoleKeyReaderFn(); + keyReady.Set (); + } + } + + void IMainLoopDriver.Setup (MainLoop mainLoop) + { + this.mainLoop = mainLoop; + Thread readThread = new Thread (WindowsKeyReader); + readThread.Start (); + } + + void IMainLoopDriver.Wakeup () + { + } + + bool IMainLoopDriver.EventsPending (bool wait) + { + long now = DateTime.UtcNow.Ticks; + + int waitTimeout; + if (mainLoop.timeouts.Count > 0) { + waitTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); + if (waitTimeout < 0) + return true; + } else + waitTimeout = -1; + + if (!wait) + waitTimeout = 0; + + keyResult = null; + waitForProbe.Set (); + keyReady.WaitOne (waitTimeout); + return keyResult.HasValue; + } + + void IMainLoopDriver.MainIteration () + { + if (keyResult.HasValue) { + KeyPressed?.Invoke (keyResult.Value); + keyResult = null; + } + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs new file mode 100644 index 0000000..9c85fec --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -0,0 +1,1341 @@ +// +// WindowsDriver.cs: Windows specific driver +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// Nick Van Dyck (vandyck.nick@outlook.com) +// +// Copyright (c) 2018 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +using NStack; +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Terminal.Gui { + + internal class WindowsConsole { + public const int STD_OUTPUT_HANDLE = -11; + public const int STD_INPUT_HANDLE = -10; + public const int STD_ERROR_HANDLE = -12; + + internal IntPtr InputHandle, OutputHandle; + IntPtr ScreenBuffer; + uint originalConsoleMode; + + public WindowsConsole () + { + InputHandle = GetStdHandle (STD_INPUT_HANDLE); + OutputHandle = GetStdHandle (STD_OUTPUT_HANDLE); + originalConsoleMode = ConsoleMode; + var newConsoleMode = originalConsoleMode; + newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); + newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; + newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; + ConsoleMode = newConsoleMode; + } + + public CharInfo [] OriginalStdOutChars; + + public bool WriteToConsole (CharInfo [] charInfoBuffer, Coord coords, SmallRect window) + { + if (ScreenBuffer == IntPtr.Zero) { + ScreenBuffer = CreateConsoleScreenBuffer ( + DesiredAccess.GenericRead | DesiredAccess.GenericWrite, + ShareMode.FileShareRead | ShareMode.FileShareWrite, + IntPtr.Zero, + 1, + IntPtr.Zero + ); + if (ScreenBuffer == INVALID_HANDLE_VALUE) { + var err = Marshal.GetLastWin32Error (); + + if (err != 0) + throw new System.ComponentModel.Win32Exception (err); + } + + if (!SetConsoleActiveScreenBuffer (ScreenBuffer)) { + var err = Marshal.GetLastWin32Error (); + throw new System.ComponentModel.Win32Exception (err); + } + + OriginalStdOutChars = new CharInfo [Console.WindowHeight * Console.WindowWidth]; + + ReadConsoleOutput (OutputHandle, OriginalStdOutChars, coords, new Coord () { X = 0, Y = 0 }, ref window); + } + + return WriteConsoleOutput (ScreenBuffer, charInfoBuffer, coords, new Coord () { X = window.Left, Y = window.Top }, ref window); + } + + public bool SetCursorPosition (Coord position) + { + return SetConsoleCursorPosition (ScreenBuffer, position); + } + + public void Cleanup () + { + ConsoleMode = originalConsoleMode; + //ContinueListeningForConsoleEvents = false; + if (!SetConsoleActiveScreenBuffer (OutputHandle)) { + var err = Marshal.GetLastWin32Error (); + Console.WriteLine ("Error: {0}", err); + } + + if (ScreenBuffer != IntPtr.Zero) + CloseHandle (ScreenBuffer); + + ScreenBuffer = IntPtr.Zero; + } + + //bool ContinueListeningForConsoleEvents = true; + + public uint ConsoleMode { + get { + uint v; + GetConsoleMode (InputHandle, out v); + return v; + } + + set { + SetConsoleMode (InputHandle, value); + } + } + + [Flags] + public enum ConsoleModes : uint { + EnableProcessedInput = 1, + EnableMouseInput = 16, + EnableQuickEditMode = 64, + EnableExtendedFlags = 128, + } + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct KeyEventRecord { + [FieldOffset (0), MarshalAs (UnmanagedType.Bool)] + public bool bKeyDown; + [FieldOffset (4), MarshalAs (UnmanagedType.U2)] + public ushort wRepeatCount; + [FieldOffset (6), MarshalAs (UnmanagedType.U2)] + public ushort wVirtualKeyCode; + [FieldOffset (8), MarshalAs (UnmanagedType.U2)] + public ushort wVirtualScanCode; + [FieldOffset (10)] + public char UnicodeChar; + [FieldOffset (12), MarshalAs (UnmanagedType.U4)] + public ControlKeyState dwControlKeyState; + } + + [Flags] + public enum ButtonState { + Button1Pressed = 1, + Button2Pressed = 4, + Button3Pressed = 8, + Button4Pressed = 16, + RightmostButtonPressed = 2, + WheeledUp = unchecked((int)0x780000), + WheeledDown = unchecked((int)0xFF880000), + } + + [Flags] + public enum ControlKeyState { + RightAltPressed = 1, + LeftAltPressed = 2, + RightControlPressed = 4, + LeftControlPressed = 8, + ShiftPressed = 16, + NumlockOn = 32, + ScrolllockOn = 64, + CapslockOn = 128, + EnhancedKey = 256 + } + + [Flags] + public enum EventFlags { + MouseMoved = 1, + DoubleClick = 2, + MouseWheeled = 4, + MouseHorizontalWheeled = 8 + } + + [StructLayout (LayoutKind.Explicit)] + public struct MouseEventRecord { + [FieldOffset (0)] + public Coordinate MousePosition; + [FieldOffset (4)] + public ButtonState ButtonState; + [FieldOffset (8)] + public ControlKeyState ControlKeyState; + [FieldOffset (12)] + public EventFlags EventFlags; + + public override string ToString () + { + return $"[Mouse({MousePosition},{ButtonState},{ControlKeyState},{EventFlags}"; + } + } + + [StructLayout (LayoutKind.Sequential)] + public struct Coordinate { + public short X; + public short Y; + + public Coordinate (short X, short Y) + { + this.X = X; + this.Y = Y; + } + + public override string ToString () => $"({X},{Y})"; + }; + + internal struct WindowBufferSizeRecord { + public Coordinate size; + + public WindowBufferSizeRecord (short x, short y) + { + this.size = new Coordinate (x, y); + } + + public override string ToString () => $"[WindowBufferSize{size}"; + } + + [StructLayout (LayoutKind.Sequential)] + public struct MenuEventRecord { + public uint dwCommandId; + } + + [StructLayout (LayoutKind.Sequential)] + public struct FocusEventRecord { + public uint bSetFocus; + } + + public enum EventType : ushort { + Focus = 0x10, + Key = 0x1, + Menu = 0x8, + Mouse = 2, + WindowBufferSize = 4 + } + + [StructLayout (LayoutKind.Explicit)] + public struct InputRecord { + [FieldOffset (0)] + public EventType EventType; + [FieldOffset (4)] + public KeyEventRecord KeyEvent; + [FieldOffset (4)] + public MouseEventRecord MouseEvent; + [FieldOffset (4)] + public WindowBufferSizeRecord WindowBufferSizeEvent; + [FieldOffset (4)] + public MenuEventRecord MenuEvent; + [FieldOffset (4)] + public FocusEventRecord FocusEvent; + + public override string ToString () + { + switch (EventType) { + case EventType.Focus: + return FocusEvent.ToString (); + case EventType.Key: + return KeyEvent.ToString (); + case EventType.Menu: + return MenuEvent.ToString (); + case EventType.Mouse: + return MouseEvent.ToString (); + case EventType.WindowBufferSize: + return WindowBufferSizeEvent.ToString (); + default: + return "Unknown event type: " + EventType; + } + } + }; + + [Flags] + enum ShareMode : uint { + FileShareRead = 1, + FileShareWrite = 2, + } + + [Flags] + enum DesiredAccess : uint { + GenericRead = 2147483648, + GenericWrite = 1073741824, + } + + [StructLayout (LayoutKind.Sequential)] + public struct ConsoleScreenBufferInfo { + public Coord dwSize; + public Coord dwCursorPosition; + public ushort wAttributes; + public SmallRect srWindow; + public Coord dwMaximumWindowSize; + } + + [StructLayout (LayoutKind.Sequential)] + public struct Coord { + public short X; + public short Y; + + public Coord (short X, short Y) + { + this.X = X; + this.Y = Y; + } + public override string ToString () => $"({X},{Y})"; + }; + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct CharUnion { + [FieldOffset (0)] public char UnicodeChar; + [FieldOffset (0)] public byte AsciiChar; + } + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct CharInfo { + [FieldOffset (0)] public CharUnion Char; + [FieldOffset (2)] public ushort Attributes; + } + + [StructLayout (LayoutKind.Sequential)] + public struct SmallRect { + public short Left; + public short Top; + public short Right; + public short Bottom; + + public static void MakeEmpty (ref SmallRect rect) + { + rect.Left = -1; + } + + public static void Update (ref SmallRect rect, short col, short row) + { + if (rect.Left == -1) { + //System.Diagnostics.Debugger.Log (0, "debug", $"damager From Empty {col},{row}\n"); + rect.Left = rect.Right = col; + rect.Bottom = rect.Top = row; + return; + } + if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom) + return; + if (col < rect.Left) + rect.Left = col; + if (col > rect.Right) + rect.Right = col; + if (row < rect.Top) + rect.Top = row; + if (row > rect.Bottom) + rect.Bottom = row; + //System.Diagnostics.Debugger.Log (0, "debug", $"Expanding {rect.ToString ()}\n"); + } + + public override string ToString () + { + return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; + } + } + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern IntPtr GetStdHandle (int nStdHandle); + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern bool CloseHandle (IntPtr handle); + + [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] + public static extern bool ReadConsoleInput ( + IntPtr hConsoleInput, + IntPtr lpBuffer, + uint nLength, + out uint lpNumberOfEventsRead); + + [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern bool ReadConsoleOutput ( + IntPtr hConsoleOutput, + [Out] CharInfo [] lpBuffer, + Coord dwBufferSize, + Coord dwBufferCoord, + ref SmallRect lpReadRegion + ); + + [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutput", SetLastError = true, CharSet = CharSet.Unicode)] + static extern bool WriteConsoleOutput ( + IntPtr hConsoleOutput, + CharInfo [] lpBuffer, + Coord dwBufferSize, + Coord dwBufferCoord, + ref SmallRect lpWriteRegion + ); + + [DllImport ("kernel32.dll")] + static extern bool SetConsoleCursorPosition (IntPtr hConsoleOutput, Coord dwCursorPosition); + + [DllImport ("kernel32.dll")] + static extern bool GetConsoleMode (IntPtr hConsoleHandle, out uint lpMode); + + + [DllImport ("kernel32.dll")] + static extern bool SetConsoleMode (IntPtr hConsoleHandle, uint dwMode); + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern IntPtr CreateConsoleScreenBuffer ( + DesiredAccess dwDesiredAccess, + ShareMode dwShareMode, + IntPtr secutiryAttributes, + UInt32 flags, + IntPtr screenBufferData + ); + + internal static IntPtr INVALID_HANDLE_VALUE = new IntPtr (-1); + + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern bool SetConsoleActiveScreenBuffer (IntPtr Handle); + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern bool GetNumberOfConsoleInputEvents (IntPtr handle, out uint lpcNumberOfEvents); + public uint InputEventCount { + get { + uint v; + GetNumberOfConsoleInputEvents (InputHandle, out v); + return v; + } + } + + public InputRecord [] ReadConsoleInput () + { + const int bufferSize = 1; + var pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * bufferSize); + try { + ReadConsoleInput (InputHandle, pRecord, bufferSize, + out var numberEventsRead); + + return numberEventsRead == 0 + ? null + : new [] {Marshal.PtrToStructure (pRecord)}; + } catch (Exception) { + return null; + } finally { + Marshal.FreeHGlobal (pRecord); + } + } + +#if false // See: https://github.com/migueldeicaza/gui.cs/issues/357 + [StructLayout (LayoutKind.Sequential)] + public struct SMALL_RECT { + public short Left; + public short Top; + public short Right; + public short Bottom; + + public SMALL_RECT (short Left, short Top, short Right, short Bottom) + { + this.Left = Left; + this.Top = Top; + this.Right = Right; + this.Bottom = Bottom; + } + } + + [StructLayout (LayoutKind.Sequential)] + public struct CONSOLE_SCREEN_BUFFER_INFO { + public int dwSize; + public int dwCursorPosition; + public short wAttributes; + public SMALL_RECT srWindow; + public int dwMaximumWindowSize; + } + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern bool GetConsoleScreenBufferInfo (IntPtr hConsoleOutput, out CONSOLE_SCREEN_BUFFER_INFO ConsoleScreenBufferInfo); + + // Theoretically GetConsoleScreenBuffer height should give the console Windoww size + // It does not work, however, and always returns the size the window was initially created at + internal Size GetWindowSize () + { + var consoleScreenBufferInfo = new CONSOLE_SCREEN_BUFFER_INFO (); + //consoleScreenBufferInfo.dwSize = Marshal.SizeOf (typeof (CONSOLE_SCREEN_BUFFER_INFO)); + GetConsoleScreenBufferInfo (OutputHandle, out consoleScreenBufferInfo); + return new Size (consoleScreenBufferInfo.srWindow.Right - consoleScreenBufferInfo.srWindow.Left, + consoleScreenBufferInfo.srWindow.Bottom - consoleScreenBufferInfo.srWindow.Top); + } +#endif + } + + internal class WindowsDriver : ConsoleDriver, IMainLoopDriver { + static bool sync = false; + ManualResetEventSlim eventReady = new ManualResetEventSlim (false); + ManualResetEventSlim waitForProbe = new ManualResetEventSlim (false); + MainLoop mainLoop; + WindowsConsole.CharInfo [] OutputBuffer; + int cols, rows; + WindowsConsole winConsole; + WindowsConsole.SmallRect damageRegion; + + public override int Cols => cols; + public override int Rows => rows; + + public WindowsDriver () + { + winConsole = new WindowsConsole (); + + SetupColorsAndBorders (); + + cols = Console.WindowWidth; + rows = Console.WindowHeight; + WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); + + ResizeScreen (); + UpdateOffScreen (); + + Task.Run ((Action)WindowsInputHandler); + } + + private void SetupColorsAndBorders () + { + Colors.TopLevel = new ColorScheme (); + Colors.Base = new ColorScheme (); + Colors.Dialog = new ColorScheme (); + Colors.Menu = new ColorScheme (); + Colors.Error = new ColorScheme (); + + Colors.TopLevel.Normal = MakeColor (ConsoleColor.Green, ConsoleColor.Black); + Colors.TopLevel.Focus = MakeColor (ConsoleColor.White, ConsoleColor.DarkCyan); + Colors.TopLevel.HotNormal = MakeColor (ConsoleColor.DarkYellow, ConsoleColor.Black); + Colors.TopLevel.HotFocus = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.DarkCyan); + + Colors.Base.Normal = MakeColor (ConsoleColor.White, ConsoleColor.DarkBlue); + Colors.Base.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Base.HotNormal = MakeColor (ConsoleColor.DarkCyan, ConsoleColor.DarkBlue); + Colors.Base.HotFocus = MakeColor (ConsoleColor.Blue, ConsoleColor.Gray); + + Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.DarkGray); + Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); + Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.DarkGray); + Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); + Colors.Menu.Disabled = MakeColor (ConsoleColor.Gray, ConsoleColor.DarkGray); + + Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.DarkGray); + Colors.Dialog.HotNormal = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.Gray); + Colors.Dialog.HotFocus = MakeColor (ConsoleColor.DarkBlue, ConsoleColor.DarkGray); + + Colors.Error.Normal = MakeColor (ConsoleColor.DarkRed, ConsoleColor.White); + Colors.Error.Focus = MakeColor (ConsoleColor.White, ConsoleColor.DarkRed); + Colors.Error.HotNormal = MakeColor (ConsoleColor.Black, ConsoleColor.White); + Colors.Error.HotFocus = MakeColor (ConsoleColor.Black, ConsoleColor.DarkRed); + + HLine = '\u2500'; + VLine = '\u2502'; + Stipple = '\u2591'; + Diamond = '\u25ca'; + ULCorner = '\u250C'; + LLCorner = '\u2514'; + URCorner = '\u2510'; + LRCorner = '\u2518'; + LeftTee = '\u251c'; + RightTee = '\u2524'; + TopTee = '\u252c'; + BottomTee = '\u2534'; + Checked = '\u221a'; + UnChecked = ' '; + Selected = '\u25cf'; + UnSelected = '\u25cc'; + RightArrow = '\u25ba'; + LeftArrow = '\u25c4'; + UpArrow = '\u25b2'; + DownArrow = '\u25bc'; + LeftDefaultIndicator = '\u25e6'; + RightDefaultIndicator = '\u25e6'; + LeftBracket = '['; + RightBracket = ']'; + OnMeterSegment = '\u258c'; + OffMeterSegement = ' '; + } + + [StructLayout (LayoutKind.Sequential)] + public struct ConsoleKeyInfoEx { + public ConsoleKeyInfo consoleKeyInfo; + public bool CapsLock; + public bool NumLock; + + public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock) + { + this.consoleKeyInfo = consoleKeyInfo; + CapsLock = capslock; + NumLock = numlock; + } + } + + // The records that we keep fetching + WindowsConsole.InputRecord [] result, records = new WindowsConsole.InputRecord [1]; + + void WindowsInputHandler () + { + while (true) { + waitForProbe.Wait (); + waitForProbe.Reset (); + + result = winConsole.ReadConsoleInput (); + + eventReady.Set (); + } + } + + void IMainLoopDriver.Setup (MainLoop mainLoop) + { + this.mainLoop = mainLoop; + } + + void IMainLoopDriver.Wakeup () + { + tokenSource.Cancel (); + //eventReady.Reset (); + //eventReady.Set (); + } + + bool IMainLoopDriver.EventsPending (bool wait) + { + int waitTimeout = 0; + + if (CkeckTimeout (wait, ref waitTimeout)) + return true; + + result = null; + waitForProbe.Set (); + + try { + while (result == null) { + if (!tokenSource.IsCancellationRequested) + eventReady.Wait (0, tokenSource.Token); + if (result != null) { + break; + } + if (mainLoop.idleHandlers.Count > 0 || CkeckTimeout (wait, ref waitTimeout)) { + return true; + } + } + } catch (OperationCanceledException) { + return true; + } finally { + eventReady.Reset (); + } + + if (!tokenSource.IsCancellationRequested) + return result != null; + + tokenSource.Dispose (); + tokenSource = new CancellationTokenSource (); + return true; + } + + bool CkeckTimeout (bool wait, ref int waitTimeout) + { + long now = DateTime.UtcNow.Ticks; + + if (mainLoop.timeouts.Count > 0) { + waitTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); + if (waitTimeout < 0) + return true; + } else { + waitTimeout = -1; + } + + if (!wait) + waitTimeout = 0; + + return false; + } + + Action keyHandler; + Action keyDownHandler; + Action keyUpHandler; + Action mouseHandler; + + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) + { + this.keyHandler = keyHandler; + this.keyDownHandler = keyDownHandler; + this.keyUpHandler = keyUpHandler; + this.mouseHandler = mouseHandler; + } + + void IMainLoopDriver.MainIteration () + { + if (result == null) + return; + + var inputEvent = result [0]; + switch (inputEvent.EventType) { + case WindowsConsole.EventType.Key: + var map = MapKey (ToConsoleKeyInfoEx (inputEvent.KeyEvent)); + if (map == (Key)0xffffffff) { + KeyEvent key = new KeyEvent (); + + // Shift = VK_SHIFT = 0x10 + // Ctrl = VK_CONTROL = 0x11 + // Alt = VK_MENU = 0x12 + + if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.CapslockOn)) { + inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.CapslockOn; + } + + if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ScrolllockOn)) { + inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.ScrolllockOn; + } + + if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.NumlockOn)) { + inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.NumlockOn; + } + + switch (inputEvent.KeyEvent.dwControlKeyState) { + case WindowsConsole.ControlKeyState.RightAltPressed: + case WindowsConsole.ControlKeyState.RightAltPressed | + WindowsConsole.ControlKeyState.LeftControlPressed | + WindowsConsole.ControlKeyState.EnhancedKey: + case WindowsConsole.ControlKeyState.EnhancedKey: + key = new KeyEvent (Key.CtrlMask | Key.AltMask, keyModifiers); + break; + case WindowsConsole.ControlKeyState.LeftAltPressed: + key = new KeyEvent (Key.AltMask, keyModifiers); + break; + case WindowsConsole.ControlKeyState.RightControlPressed: + case WindowsConsole.ControlKeyState.LeftControlPressed: + key = new KeyEvent (Key.CtrlMask, keyModifiers); + break; + case WindowsConsole.ControlKeyState.ShiftPressed: + key = new KeyEvent (Key.ShiftMask, keyModifiers); + break; + case WindowsConsole.ControlKeyState.NumlockOn: + break; + case WindowsConsole.ControlKeyState.ScrolllockOn: + break; + case WindowsConsole.ControlKeyState.CapslockOn: + break; + default: + switch (inputEvent.KeyEvent.wVirtualKeyCode) { + case 0x10: + key = new KeyEvent (Key.ShiftMask, keyModifiers); + break; + case 0x11: + key = new KeyEvent (Key.CtrlMask, keyModifiers); + break; + case 0x12: + key = new KeyEvent (Key.AltMask, keyModifiers); + break; + default: + key = new KeyEvent (Key.Unknown, keyModifiers); + break; + } + break; + } + + if (inputEvent.KeyEvent.bKeyDown) + keyDownHandler (key); + else + keyUpHandler (key); + } else { + if (inputEvent.KeyEvent.bKeyDown) { + // Key Down - Fire KeyDown Event and KeyStroke (ProcessKey) Event + keyDownHandler (new KeyEvent (map, keyModifiers)); + keyHandler (new KeyEvent (map, keyModifiers)); + } else { + keyUpHandler (new KeyEvent (map, keyModifiers)); + } + } + if (!inputEvent.KeyEvent.bKeyDown) { + keyModifiers = null; + } + break; + + case WindowsConsole.EventType.Mouse: + mouseHandler (ToDriverMouse (inputEvent.MouseEvent)); + if (IsButtonReleased) + mouseHandler (ToDriverMouse (inputEvent.MouseEvent)); + break; + + case WindowsConsole.EventType.WindowBufferSize: + cols = inputEvent.WindowBufferSizeEvent.size.X; + rows = inputEvent.WindowBufferSizeEvent.size.Y; + ResizeScreen (); + UpdateOffScreen (); + TerminalResized?.Invoke (); + break; + + case WindowsConsole.EventType.Focus: + break; + + default: + break; + } + result = null; + } + + WindowsConsole.ButtonState? LastMouseButtonPressed = null; + bool IsButtonPressed = false; + bool IsButtonReleased = false; + bool IsButtonDoubleClicked = false; + Point point; + + MouseEvent ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) + { + MouseFlags mouseFlag = MouseFlags.AllEvents; + + if (IsButtonDoubleClicked) { + Application.MainLoop.AddIdle (() => { + ProcessButtonDoubleClickedAsync ().ConfigureAwait (false); + return false; + }); + } + + // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button. + // This will tell when a mouse button is pressed. When the button is released this event will + // be fired with it's bit set to 0. So when the button is up ButtonState will be 0. + // To map to the correct driver events we save the last pressed mouse button so we can + // map to the correct clicked event. + if ((LastMouseButtonPressed != null || IsButtonReleased) && mouseEvent.ButtonState != 0) { + LastMouseButtonPressed = null; + IsButtonPressed = false; + IsButtonReleased = false; + } + + if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && LastMouseButtonPressed == null && !IsButtonDoubleClicked) || + (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved && + mouseEvent.ButtonState != 0 && !IsButtonReleased && !IsButtonDoubleClicked)) { + switch (mouseEvent.ButtonState) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Pressed; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Pressed; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3Pressed; + break; + } + + if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) { + mouseFlag |= MouseFlags.ReportMousePosition; + point = new Point (); + IsButtonReleased = false; + } else { + point = new Point () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y + }; + } + LastMouseButtonPressed = mouseEvent.ButtonState; + IsButtonPressed = true; + + if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) { + Application.MainLoop.AddIdle (() => { + ProcessContinuousButtonPressedAsync (mouseEvent, mouseFlag).ConfigureAwait (false); + return false; + }); + } + + } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && + LastMouseButtonPressed != null && !IsButtonReleased && !IsButtonDoubleClicked) { + switch (LastMouseButtonPressed) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Released; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Released; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3Released; + break; + } + IsButtonPressed = false; + IsButtonReleased = true; + } else if ((mouseEvent.EventFlags == 0 || mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) && + IsButtonReleased) { + var p = new Point () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y + }; + //if (p == point) { + switch (LastMouseButtonPressed) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Clicked; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Clicked; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3Clicked; + break; + } + point = new Point () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y + }; + //} else { + // mouseFlag = 0; + //} + LastMouseButtonPressed = null; + IsButtonReleased = false; + } else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick)) { + switch (mouseEvent.ButtonState) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1DoubleClicked; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2DoubleClicked; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3DoubleClicked; + break; + } + IsButtonDoubleClicked = true; + } else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && IsButtonDoubleClicked) { + switch (mouseEvent.ButtonState) { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1TripleClicked; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2TripleClicked; + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3TripleClicked; + break; + } + IsButtonDoubleClicked = false; + } else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled) { + switch (mouseEvent.ButtonState) { + case WindowsConsole.ButtonState.WheeledUp: + mouseFlag = MouseFlags.WheeledUp; + break; + + case WindowsConsole.ButtonState.WheeledDown: + mouseFlag = MouseFlags.WheeledDown; + break; + } + + } else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) { + if (mouseEvent.MousePosition.X != point.X || mouseEvent.MousePosition.Y != point.Y) { + mouseFlag = MouseFlags.ReportMousePosition; + point = new Point (); + } else { + mouseFlag = 0; + } + } else if (mouseEvent.ButtonState == 0 && mouseEvent.EventFlags == 0) { + mouseFlag = 0; + } + + mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag); + + return new MouseEvent () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y, + Flags = mouseFlag + }; + } + + async Task ProcessButtonDoubleClickedAsync () + { + await Task.Delay (200); + IsButtonDoubleClicked = false; + } + + async Task ProcessContinuousButtonPressedAsync (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag) + { + while (IsButtonPressed) { + await Task.Delay (200); + var me = new MouseEvent () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y, + Flags = mouseFlag + }; + + var view = Application.wantContinuousButtonPressedView; + if (view == null) { + break; + } + if (IsButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { + mouseHandler (me); + } + } + } + + static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag) + { + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed) || + mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)) + mouseFlag |= MouseFlags.ButtonCtrl; + + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) + mouseFlag |= MouseFlags.ButtonShift; + + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) || + mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) + mouseFlag |= MouseFlags.ButtonAlt; + return mouseFlag; + } + + KeyModifiers keyModifiers; + + public ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) + { + var state = keyEvent.dwControlKeyState; + + bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0; + bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0; + bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0; + bool capslock = (state & (WindowsConsole.ControlKeyState.CapslockOn)) != 0; + bool numlock = (state & (WindowsConsole.ControlKeyState.NumlockOn)) != 0; + bool scrolllock = (state & (WindowsConsole.ControlKeyState.ScrolllockOn)) != 0; + + if (keyModifiers == null) + keyModifiers = new KeyModifiers (); + if (shift) + keyModifiers.Shift = shift; + if (alt) + keyModifiers.Alt = alt; + if (control) + keyModifiers.Ctrl = control; + if (capslock) + keyModifiers.Capslock = capslock; + if (numlock) + keyModifiers.Numlock = numlock; + if (scrolllock) + keyModifiers.Scrolllock = scrolllock; + + var ConsoleKeyInfo = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); + return new ConsoleKeyInfoEx (ConsoleKeyInfo, capslock, numlock); + } + + public Key MapKey (ConsoleKeyInfoEx keyInfoEx) + { + var keyInfo = keyInfoEx.consoleKeyInfo; + switch (keyInfo.Key) { + case ConsoleKey.Escape: + return MapKeyModifiers (keyInfo, Key.Esc); + case ConsoleKey.Tab: + return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; + case ConsoleKey.Home: + return MapKeyModifiers (keyInfo, Key.Home); + case ConsoleKey.End: + return MapKeyModifiers (keyInfo, Key.End); + case ConsoleKey.LeftArrow: + return MapKeyModifiers (keyInfo, Key.CursorLeft); + case ConsoleKey.RightArrow: + return MapKeyModifiers (keyInfo, Key.CursorRight); + case ConsoleKey.UpArrow: + return MapKeyModifiers (keyInfo, Key.CursorUp); + case ConsoleKey.DownArrow: + return MapKeyModifiers (keyInfo, Key.CursorDown); + case ConsoleKey.PageUp: + return MapKeyModifiers (keyInfo, Key.PageUp); + case ConsoleKey.PageDown: + return MapKeyModifiers (keyInfo, Key.PageDown); + case ConsoleKey.Enter: + return MapKeyModifiers (keyInfo, Key.Enter); + case ConsoleKey.Spacebar: + return MapKeyModifiers (keyInfo, Key.Space); + case ConsoleKey.Backspace: + return MapKeyModifiers (keyInfo, Key.Backspace); + case ConsoleKey.Delete: + return MapKeyModifiers (keyInfo, Key.DeleteChar); + case ConsoleKey.Insert: + return MapKeyModifiers (keyInfo, Key.InsertChar); + + case ConsoleKey.NumPad0: + return keyInfoEx.NumLock ? (Key)(uint)'0' : Key.InsertChar; + case ConsoleKey.NumPad1: + return keyInfoEx.NumLock ? (Key)(uint)'1' : Key.End; + case ConsoleKey.NumPad2: + return keyInfoEx.NumLock ? (Key)(uint)'2' : Key.CursorDown; + case ConsoleKey.NumPad3: + return keyInfoEx.NumLock ? (Key)(uint)'3' : Key.PageDown; + case ConsoleKey.NumPad4: + return keyInfoEx.NumLock ? (Key)(uint)'4' : Key.CursorLeft; + case ConsoleKey.NumPad5: + return keyInfoEx.NumLock ? (Key)(uint)'5' : (Key)((uint)keyInfo.KeyChar); + case ConsoleKey.NumPad6: + return keyInfoEx.NumLock ? (Key)(uint)'6' : Key.CursorRight; + case ConsoleKey.NumPad7: + return keyInfoEx.NumLock ? (Key)(uint)'7' : Key.Home; + case ConsoleKey.NumPad8: + return keyInfoEx.NumLock ? (Key)(uint)'8' : Key.CursorUp; + case ConsoleKey.NumPad9: + return keyInfoEx.NumLock ? (Key)(uint)'9' : Key.PageUp; + + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + if (keyInfo.KeyChar == 0) + return Key.Unknown; + + return (Key)((uint)keyInfo.KeyChar); + } + + var key = keyInfo.Key; + //var alphaBase = ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ (keyInfoEx.CapsLock)) ? 'A' : 'a'; + + if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { + var delta = key - ConsoleKey.A; + if (keyInfo.Modifiers == ConsoleModifiers.Control) + return (Key)((uint)Key.ControlA + delta); + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'A' + delta)); + if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { + if (keyInfo.KeyChar == 0) + return (Key)(((uint)Key.AltMask) + ((uint)Key.ControlA + delta)); + else + return (Key)((uint)keyInfo.KeyChar); + } + //return (Key)((uint)alphaBase + delta); + return (Key)((uint)keyInfo.KeyChar); + } + if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { + var delta = key - ConsoleKey.D0; + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'0' + delta)); + + return (Key)((uint)keyInfo.KeyChar); + } + if (key >= ConsoleKey.F1 && key <= ConsoleKey.F12) { + var delta = key - ConsoleKey.F1; + + return (Key)((int)Key.F1 + delta); + } + + return (Key)(0xffffffff); + } + + private Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key) + { + Key keyMod = new Key (); + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift)) + keyMod = Key.ShiftMask; + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) + keyMod |= Key.CtrlMask; + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)) + keyMod |= Key.AltMask; + + return keyMod != Key.ControlSpace ? keyMod | key : key; + } + + public override void Init (Action terminalResized) + { + TerminalResized = terminalResized; + SetupColorsAndBorders (); + } + + void ResizeScreen () + { + OutputBuffer = new WindowsConsole.CharInfo [Rows * Cols]; + Clip = new Rect (0, 0, Cols, Rows); + damageRegion = new WindowsConsole.SmallRect () { + Top = 0, + Left = 0, + Bottom = (short)Rows, + Right = (short)Cols + }; + } + + void UpdateOffScreen () + { + for (int row = 0; row < rows; row++) + for (int col = 0; col < cols; col++) { + int position = row * cols + col; + OutputBuffer [position].Attributes = (ushort)Colors.TopLevel.Normal; + OutputBuffer [position].Char.UnicodeChar = ' '; + } + } + + int ccol, crow; + public override void Move (int col, int row) + { + ccol = col; + crow = row; + } + + public override void AddRune (Rune rune) + { + rune = MakePrintable (rune); + var position = crow * Cols + ccol; + + if (Clip.Contains (ccol, crow)) { + OutputBuffer [position].Attributes = (ushort)currentAttribute; + OutputBuffer [position].Char.UnicodeChar = (char)rune; + WindowsConsole.SmallRect.Update (ref damageRegion, (short)ccol, (short)crow); + } + + ccol++; + var runeWidth = Rune.ColumnWidth (rune); + if (runeWidth > 1) { + for (int i = 1; i < runeWidth; i++) { + AddStr (" "); + } + } + //if (ccol == Cols) { + // ccol = 0; + // if (crow + 1 < Rows) + // crow++; + //} + if (sync) + UpdateScreen (); + } + + public override void AddStr (ustring str) + { + foreach (var rune in str) + AddRune (rune); + } + + int currentAttribute; + CancellationTokenSource tokenSource = new CancellationTokenSource (); + + public override void SetAttribute (Attribute c) + { + currentAttribute = c.value; + } + + Attribute MakeColor (ConsoleColor f, ConsoleColor b) + { + // Encode the colors into the int value. + return new Attribute () { + value = ((int)f | (int)b << 4), + foreground = (Color)f, + background = (Color)b + }; + } + + public override Attribute MakeAttribute (Color fore, Color back) + { + return MakeColor ((ConsoleColor)fore, (ConsoleColor)back); + } + + public override void Refresh () + { + UpdateScreen (); +#if false + var bufferCoords = new WindowsConsole.Coord (){ + X = (short)Clip.Width, + Y = (short)Clip.Height + }; + + var window = new WindowsConsole.SmallRect (){ + Top = 0, + Left = 0, + Right = (short)Clip.Right, + Bottom = (short)Clip.Bottom + }; + + UpdateCursor(); + winConsole.WriteToConsole (OutputBuffer, bufferCoords, window); +#endif + } + + public override void UpdateScreen () + { + if (damageRegion.Left == -1) + return; + + var bufferCoords = new WindowsConsole.Coord () { + X = (short)Clip.Width, + Y = (short)Clip.Height + }; + + var window = new WindowsConsole.SmallRect () { + Top = 0, + Left = 0, + Right = (short)Clip.Right, + Bottom = (short)Clip.Bottom + }; + + UpdateCursor (); + winConsole.WriteToConsole (OutputBuffer, bufferCoords, damageRegion); + // System.Diagnostics.Debugger.Log(0, "debug", $"Region={damageRegion.Right - damageRegion.Left},{damageRegion.Bottom - damageRegion.Top}\n"); + WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); + } + + public override void UpdateCursor () + { + var position = new WindowsConsole.Coord () { + X = (short)ccol, + Y = (short)crow + }; + winConsole.SetCursorPosition (position); + } + + public override void End () + { + winConsole.Cleanup (); + } + + #region Unused + public override void SetColors (ConsoleColor foreground, ConsoleColor background) + { + } + + public override void SetColors (short foregroundColorId, short backgroundColorId) + { + } + + public override void Suspend () + { + } + + public override void StartReportingMouseMoves () + { + } + + public override void StopReportingMouseMoves () + { + } + + public override void UncookMouse () + { + } + + public override void CookMouse () + { + } + #endregion + + } + +} diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs new file mode 100644 index 0000000..4ca0d99 --- /dev/null +++ b/Terminal.Gui/Core/Application.cs @@ -0,0 +1,711 @@ +// +// Core.cs: The core engine for gui.cs +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// Pending: +// - Check for NeedDisplay on the hierarchy and repaint +// - Layout support +// - "Colors" type or "Attributes" type? +// - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors? +// +// Optimziations +// - Add rendering limitation to the exposed area +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Linq; +using NStack; +using System.ComponentModel; + +namespace Terminal.Gui { + + /// + /// A static, singelton class provding the main application driver for Terminal.Gui apps. + /// + /// + /// + /// // A simple Terminal.Gui app that creates a window with a frame and title with + /// // 5 rows/columns of padding. + /// Application.Init(); + /// var win = new Window ("Hello World - CTRL-Q to quit") { + /// X = 5, + /// Y = 5, + /// Width = Dim.Fill (5), + /// Height = Dim.Fill (5) + /// }; + /// Application.Top.Add(win); + /// Application.Run(); + /// + /// + /// + /// + /// Creates a instance of to process input events, handle timers and + /// other sources of data. It is accessible via the property. + /// + /// + /// You can hook up to the event to have your method + /// invoked on each iteration of the . + /// + /// + /// When invoked sets the SynchronizationContext to one that is tied + /// to the mainloop, allowing user code to use async/await. + /// + /// + public static class Application { + /// + /// The current in use. + /// + public static ConsoleDriver Driver; + + /// + /// The object used for the application on startup () + /// + /// The top. + public static Toplevel Top { get; private set; } + + /// + /// The current object. This is updated when enters and leaves to point to the current . + /// + /// The current. + public static Toplevel Current { get; private set; } + + /// + /// TThe current object being redrawn. + /// + /// /// The current. + public static View CurrentView { get; set; } + + /// + /// The driver for the applicaiton + /// + /// The main loop. + public static MainLoop MainLoop { get; private set; } + + static Stack toplevels = new Stack (); + + /// + /// This event is raised on each iteration of the + /// + /// + /// See also + /// + public static Action Iteration; + + /// + /// Returns a rectangle that is centered in the screen for the provided size. + /// + /// The centered rect. + /// Size for the rectangle. + public static Rect MakeCenteredRect (Size size) + { + return new Rect (new Point ((Driver.Cols - size.Width) / 2, (Driver.Rows - size.Height) / 2), size); + } + + // + // provides the sync context set while executing code in Terminal.Gui, to let + // users use async/await on their code + // + class MainLoopSyncContext : SynchronizationContext { + MainLoop mainLoop; + + public MainLoopSyncContext (MainLoop mainLoop) + { + this.mainLoop = mainLoop; + } + + public override SynchronizationContext CreateCopy () + { + return new MainLoopSyncContext (MainLoop); + } + + public override void Post (SendOrPostCallback d, object state) + { + mainLoop.AddIdle (() => { + d (state); + return false; + }); + //mainLoop.Driver.Wakeup (); + } + + public override void Send (SendOrPostCallback d, object state) + { + mainLoop.Invoke (() => { + d (state); + }); + } + } + + /// + /// If set, it forces the use of the System.Console-based driver. + /// + public static bool UseSystemConsole; + + /// + /// Initializes a new instance of Application. + /// + /// + /// + /// Call this method once per instance (or after has been called). + /// + /// + /// Loads the right for the platform. + /// + /// + /// Creates a and assigns it to and + /// + /// + public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => Init (() => Toplevel.Create (), driver, mainLoopDriver); + + internal static bool _initialized = false; + + /// + /// Initializes the Terminal.Gui application + /// + static void Init (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) + { + if (_initialized) return; + + // This supports Unit Tests and the passing of a mock driver/loopdriver + if (driver != null) { + if (mainLoopDriver == null) { + throw new ArgumentNullException ("mainLoopDriver cannot be null if driver is provided."); + } + Driver = driver; + Driver.Init (TerminalResized); + MainLoop = new MainLoop (mainLoopDriver); + SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); + } + + if (Driver == null) { + var p = Environment.OSVersion.Platform; + if (UseSystemConsole) { + mainLoopDriver = new NetMainLoop (() => Console.ReadKey (true)); + Driver = new NetDriver (); + } else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { + var windowsDriver = new WindowsDriver (); + mainLoopDriver = windowsDriver; + Driver = windowsDriver; + } else { + mainLoopDriver = new UnixMainLoop (); + Driver = new CursesDriver (); + } + Driver.Init (TerminalResized); + MainLoop = new MainLoop (mainLoopDriver); + SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); + } + Top = topLevelFactory (); + Current = Top; + CurrentView = Top; + _initialized = true; + } + + /// + /// Captures the execution state for the provided view. + /// + public class RunState : IDisposable { + internal bool closeDriver = true; + + /// + /// Initializes a new class. + /// + /// + public RunState (Toplevel view) + { + Toplevel = view; + } + internal Toplevel Toplevel; + + /// + /// Releases alTop = l resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the + /// so the garbage collector can reclaim the memory that the + /// was occupying. + public void Dispose () + { + Dispose (closeDriver); + GC.SuppressFinalize (this); + } + + /// + /// Dispose the specified disposing. + /// + /// The dispose. + /// If set to true disposing. + protected virtual void Dispose (bool disposing) + { + if (Toplevel != null) { + End (Toplevel, disposing); + Toplevel = null; + } + } + } + + static void ProcessKeyEvent (KeyEvent ke) + { + + var chain = toplevels.ToList (); + foreach (var topLevel in chain) { + if (topLevel.ProcessHotKey (ke)) + return; + if (topLevel.Modal) + break; + } + + foreach (var topLevel in chain) { + if (topLevel.ProcessKey (ke)) + return; + if (topLevel.Modal) + break; + } + + foreach (var topLevel in chain) { + // Process the key normally + if (topLevel.ProcessColdKey (ke)) + return; + if (topLevel.Modal) + break; + } + } + + static void ProcessKeyDownEvent (KeyEvent ke) + { + var chain = toplevels.ToList (); + foreach (var topLevel in chain) { + if (topLevel.OnKeyDown (ke)) + return; + if (topLevel.Modal) + break; + } + } + + + static void ProcessKeyUpEvent (KeyEvent ke) + { + var chain = toplevels.ToList (); + foreach (var topLevel in chain) { + if (topLevel.OnKeyUp (ke)) + return; + if (topLevel.Modal) + break; + } + } + + static View FindDeepestView (View start, int x, int y, out int resx, out int resy) + { + var startFrame = start.Frame; + + if (!startFrame.Contains (x, y)) { + resx = 0; + resy = 0; + return null; + } + + if (start.InternalSubviews != null) { + int count = start.InternalSubviews.Count; + if (count > 0) { + var rx = x - startFrame.X; + var ry = y - startFrame.Y; + for (int i = count - 1; i >= 0; i--) { + View v = start.InternalSubviews [i]; + if (v.Frame.Contains (rx, ry)) { + var deep = FindDeepestView (v, rx, ry, out resx, out resy); + if (deep == null) + return v; + return deep; + } + } + } + } + resx = x - startFrame.X; + resy = y - startFrame.Y; + return start; + } + + internal static View mouseGrabView; + + /// + /// Grabs the mouse, forcing all mouse events to be routed to the specified view until UngrabMouse is called. + /// + /// The grab. + /// View that will receive all mouse events until UngrabMouse is invoked. + public static void GrabMouse (View view) + { + if (view == null) + return; + mouseGrabView = view; + Driver.UncookMouse (); + } + + /// + /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is. + /// + public static void UngrabMouse () + { + mouseGrabView = null; + Driver.CookMouse (); + } + + /// + /// Merely a debugging aid to see the raw mouse events + /// + public static Action RootMouseEvent; + + internal static View wantContinuousButtonPressedView; + static View lastMouseOwnerView; + + static void ProcessMouseEvent (MouseEvent me) + { + var view = FindDeepestView (Current, me.X, me.Y, out int rx, out int ry); + + if (view != null && view.WantContinuousButtonPressed) + wantContinuousButtonPressedView = view; + else + wantContinuousButtonPressedView = null; + + RootMouseEvent?.Invoke (me); + if (mouseGrabView != null) { + var newxy = mouseGrabView.ScreenToView (me.X, me.Y); + var nme = new MouseEvent () { + X = newxy.X, + Y = newxy.Y, + Flags = me.Flags, + OfX = me.X - newxy.X, + OfY = me.Y - newxy.Y, + View = view + }; + if (OutsideFrame (new Point (nme.X, nme.Y), mouseGrabView.Frame)) { + lastMouseOwnerView?.OnMouseLeave (me); + } + if (mouseGrabView != null) { + mouseGrabView.OnMouseEvent (nme); + return; + } + } + + if (view != null) { + var nme = new MouseEvent () { + X = rx, + Y = ry, + Flags = me.Flags, + OfX = rx, + OfY = ry, + View = view + }; + + if (lastMouseOwnerView == null) { + lastMouseOwnerView = view; + view.OnMouseEnter (nme); + } else if (lastMouseOwnerView != view) { + lastMouseOwnerView.OnMouseLeave (nme); + view.OnMouseEnter (nme); + lastMouseOwnerView = view; + } + + if (!view.WantMousePositionReports && me.Flags == MouseFlags.ReportMousePosition) + return; + + if (view.WantContinuousButtonPressed) + wantContinuousButtonPressedView = view; + else + wantContinuousButtonPressedView = null; + + // Should we bubbled up the event, if it is not handled? + view.OnMouseEvent (nme); + } + } + + static bool OutsideFrame (Point p, Rect r) + { + return p.X < 0 || p.X > r.Width - 1 || p.Y < 0 || p.Y > r.Height - 1; + } + + /// + /// This event is fired once when the application is first loaded. The dimensions of the + /// terminal are provided. + /// + public static Action Loaded; + + /// + /// Building block API: Prepares the provided for execution. + /// + /// The runstate handle that needs to be passed to the method upon completion. + /// Toplevel to prepare execution for. + /// + /// This method prepares the provided toplevel for running with the focus, + /// it adds this to the list of toplevels, sets up the mainloop to process the + /// event, lays out the subviews, focuses the first element, and draws the + /// toplevel in the screen. This is usually followed by executing + /// the method, and then the method upon termination which will + /// undo these changes. + /// + public static RunState Begin (Toplevel toplevel) + { + if (toplevel == null) + throw new ArgumentNullException (nameof (toplevel)); + var rs = new RunState (toplevel); + + Init (); + if (toplevel is ISupportInitializeNotification initializableNotification && + !initializableNotification.IsInitialized) { + initializableNotification.BeginInit (); + initializableNotification.EndInit (); + } else if (toplevel is ISupportInitialize initializable) { + initializable.BeginInit (); + initializable.EndInit (); + } + toplevels.Push (toplevel); + Current = toplevel; + Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent); + if (toplevel.LayoutStyle == LayoutStyle.Computed) + toplevel.SetRelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); + toplevel.LayoutSubviews (); + Loaded?.Invoke (new ResizedEventArgs () { Rows = Driver.Rows, Cols = Driver.Cols }); + toplevel.WillPresent (); + Redraw (toplevel); + toplevel.PositionCursor (); + Driver.Refresh (); + + return rs; + } + + /// + /// Building block API: completes the execution of a that was started with . + /// + /// The runstate returned by the method. + /// If true, closes the application. If false closes the toplevels only. + public static void End (RunState runState, bool closeDriver = true) + { + if (runState == null) + throw new ArgumentNullException (nameof (runState)); + + runState.closeDriver = closeDriver; + runState.Dispose (); + } + + /// + /// Shutdown an application initialized with + /// + /// trueCloses the application.falseCloses toplevels only. + public static void Shutdown (bool closeDriver = true) + { + // Shutdown is the bookend for Init. As such it needs to clean up all resources + // Init created. Apps that do any threading will need to code defensively for this. + // e.g. see Issue #537 + // TODO: Some of this state is actually related to Begin/End (not Init/Shutdown) and should be moved to `RunState` (#520) + foreach (var t in toplevels) { + t.Running = false; + } + toplevels.Clear (); + Current = null; + CurrentView = null; + Top = null; + + // Closes the application if it's true. + if (closeDriver) { + MainLoop = null; + Driver?.End (); + Driver = null; + } + + _initialized = false; + } + + static void Redraw (View view) + { + Application.CurrentView = view; + + view.Redraw (view.Bounds); + Driver.Refresh (); + } + + static void Refresh (View view) + { + view.Redraw (view.Bounds); + Driver.Refresh (); + } + + /// + /// Triggers a refresh of the entire display. + /// + public static void Refresh () + { + Driver.UpdateScreen (); + View last = null; + foreach (var v in toplevels.Reverse ()) { + v.SetNeedsDisplay (); + v.Redraw (v.Bounds); + last = v; + } + last?.PositionCursor (); + Driver.Refresh (); + } + + internal static void End (View view, bool closeDriver = true) + { + if (toplevels.Peek () != view) + throw new ArgumentException ("The view that you end with must be balanced"); + toplevels.Pop (); + if (toplevels.Count == 0) + Shutdown (closeDriver); + else { + Current = toplevels.Peek (); + Refresh (); + } + } + + /// + /// Building block API: Runs the main loop for the created dialog + /// + /// + /// Use the wait parameter to control whether this is a + /// blocking or non-blocking call. + /// + /// The state returned by the Begin method. + /// By default this is true which will execute the runloop waiting for events, if you pass false, you can use this method to run a single iteration of the events. + public static void RunLoop (RunState state, bool wait = true) + { + if (state == null) + throw new ArgumentNullException (nameof (state)); + if (state.Toplevel == null) + throw new ObjectDisposedException ("state"); + + bool firstIteration = true; + for (state.Toplevel.Running = true; state.Toplevel.Running;) { + if (MainLoop.EventsPending (wait)) { + // Notify Toplevel it's ready + if (firstIteration) { + state.Toplevel.OnReady (); + } + firstIteration = false; + + MainLoop.MainIteration (); + Iteration?.Invoke (); + } else if (wait == false) + return; + if (state.Toplevel.NeedDisplay != null && (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.childNeedsDisplay)) { + state.Toplevel.Redraw (state.Toplevel.Bounds); + if (DebugDrawBounds) + DrawBounds (state.Toplevel); + state.Toplevel.PositionCursor (); + Driver.Refresh (); + } else + Driver.UpdateCursor (); + } + } + + internal static bool DebugDrawBounds = false; + + // Need to look into why this does not work properly. + static void DrawBounds (View v) + { + v.DrawFrame (v.Frame, padding: 0, fill: false); + if (v.InternalSubviews != null && v.InternalSubviews.Count > 0) + foreach (var sub in v.InternalSubviews) + DrawBounds (sub); + } + + /// + /// Runs the application by calling with the value of + /// + public static void Run () + { + Run (Top); + } + + /// + /// Runs the application by calling with a new instance of the specified -derived class + /// + public static void Run () where T : Toplevel, new() + { + Init (() => new T ()); + Run (Top); + } + + /// + /// Runs the main loop on the given container. + /// + /// + /// + /// This method is used to start processing events + /// for the main application, but it is also used to + /// run other modal s such as boxes. + /// + /// + /// To make a stop execution, call . + /// + /// + /// Calling is equivalent to calling , followed by , + /// and then calling . + /// + /// + /// Alternatively, to have a program control the main loop and + /// process events manually, call to set things up manually and then + /// repeatedly call with the wait parameter set to false. By doing this + /// the method will only process any pending events, timers, idle handlers and + /// then return control immediately. + /// + /// + /// The tu run modally. + /// Set to to cause the MainLoop to end when is called, clsing the toplevels only. + public static void Run (Toplevel view, bool closeDriver = true) + { + var runToken = Begin (view); + RunLoop (runToken); + End (runToken, closeDriver); + } + + /// + /// Stops running the most recent . + /// + /// + /// + /// This will cause to return. + /// + /// + /// Calling is equivalent to setting the property on the curently running to false. + /// + /// + public static void RequestStop () + { + Current.Running = false; + } + + /// + /// Event arguments for the event. + /// + public class ResizedEventArgs : EventArgs { + /// + /// The number of rows in the resized terminal. + /// + public int Rows { get; set; } + /// + /// The number of columns in the resized terminal. + /// + public int Cols { get; set; } + } + + /// + /// Invoked when the terminal was resized. The new size of the terminal is provided. + /// + public static Action Resized; + + static void TerminalResized () + { + var full = new Rect (0, 0, Driver.Cols, Driver.Rows); + Resized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height }); + Driver.Clip = full; + foreach (var t in toplevels) { + t.PositionToplevels (); + t.SetRelativeLayout (full); + t.LayoutSubviews (); + } + Refresh (); + } + } +} diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs new file mode 100644 index 0000000..dc709cd --- /dev/null +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -0,0 +1,1051 @@ +// +// ConsoleDriver.cs: Definition for the Console Driver API +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// Define this to enable diagnostics drawing for Window Frames +using NStack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Terminal.Gui { + /// + /// Basic colors that can be used to set the foreground and background colors in console applications. + /// + public enum Color { + /// + /// The black color. + /// + Black, + /// + /// The blue color. + /// + Blue, + /// + /// The green color. + /// + Green, + /// + /// The cyan color. + /// + Cyan, + /// + /// The red color. + /// + Red, + /// + /// The magenta color. + /// + Magenta, + /// + /// The brown color. + /// + Brown, + /// + /// The gray color. + /// + Gray, + /// + /// The dark gray color. + /// + DarkGray, + /// + /// The bright bBlue color. + /// + BrightBlue, + /// + /// The bright green color. + /// + BrightGreen, + /// + /// The brigh cyan color. + /// + BrighCyan, + /// + /// The bright red color. + /// + BrightRed, + /// + /// The bright magenta color. + /// + BrightMagenta, + /// + /// The bright yellow color. + /// + BrightYellow, + /// + /// The White color. + /// + White + } + + /// + /// Attributes are used as elements that contain both a foreground and a background or platform specific features + /// + /// + /// s are needed to map colors to terminal capabilities that might lack colors, on color + /// scenarios, they encode both the foreground and the background color and are used in the + /// class to define color schemes that can be used in your application. + /// + public struct Attribute { + internal int value; + internal Color foreground; + internal Color background; + + /// + /// Initializes a new instance of the struct. + /// + /// Value. + /// Foreground + /// Background + public Attribute (int value, Color foreground = new Color (), Color background = new Color ()) + { + this.value = value; + this.foreground = foreground; + this.background = background; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Foreground + /// Background + public Attribute (Color foreground = new Color (), Color background = new Color ()) + { + this.value = value = ((int)foreground | (int)background << 4); + this.foreground = foreground; + this.background = background; + } + + /// + /// Implicit conversion from an to the underlying Int32 representation + /// + /// The integer value stored in the attribute. + /// The attribute to convert + public static implicit operator int (Attribute c) => c.value; + + /// + /// Implicitly convert an integer value into an + /// + /// An attribute with the specified integer value. + /// value + public static implicit operator Attribute (int v) => new Attribute (v); + + /// + /// Creates an from the specified foreground and background. + /// + /// The make. + /// Foreground color to use. + /// Background color to use. + public static Attribute Make (Color foreground, Color background) + { + if (Application.Driver == null) + throw new InvalidOperationException ("The Application has not been initialized"); + return Application.Driver.MakeAttribute (foreground, background); + } + } + + /// + /// Color scheme definitions, they cover some common scenarios and are used + /// typically in containers such as and to set the scheme that is used by all the + /// views contained inside. + /// + public class ColorScheme : IEquatable { + Attribute _normal; + Attribute _focus; + Attribute _hotNormal; + Attribute _hotFocus; + Attribute _disabled; + internal string caller = ""; + + /// + /// The default color for text, when the view is not focused. + /// + public Attribute Normal { get { return _normal; } set { _normal = SetAttribute (value); } } + + /// + /// The color for text when the view has the focus. + /// + public Attribute Focus { get { return _focus; } set { _focus = SetAttribute (value); } } + + /// + /// The color for the hotkey when a view is not focused + /// + public Attribute HotNormal { get { return _hotNormal; } set { _hotNormal = SetAttribute (value); } } + + /// + /// The color for the hotkey when the view is focused. + /// + public Attribute HotFocus { get { return _hotFocus; } set { _hotFocus = SetAttribute (value); } } + + /// + /// The default color for text, when the view is disabled. + /// + public Attribute Disabled { get { return _disabled; } set { _disabled = SetAttribute (value); } } + + bool preparingScheme = false; + + Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMemberName = null) + { + if (!Application._initialized && !preparingScheme) + return attribute; + + if (preparingScheme) + return attribute; + + preparingScheme = true; + switch (caller) { + case "TopLevel": + switch (callerMemberName) { + case "Normal": + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + case "Focus": + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + } + break; + + case "Base": + switch (callerMemberName) { + case "Normal": + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + case "Focus": + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + } + break; + + case "Menu": + switch (callerMemberName) { + case "Normal": + if (Focus.background != attribute.background) + Focus = Application.Driver.MakeAttribute (attribute.foreground, Focus.background); + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + Disabled = Application.Driver.MakeAttribute (Disabled.foreground, attribute.background); + break; + case "Focus": + Normal = Application.Driver.MakeAttribute (attribute.foreground, Normal.background); + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + if (Focus.background != attribute.background) + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + Disabled = Application.Driver.MakeAttribute (Disabled.foreground, attribute.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + case "Disabled": + if (Focus.background != attribute.background) + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + + } + break; + + case "Dialog": + switch (callerMemberName) { + case "Normal": + if (Focus.background != attribute.background) + Focus = Application.Driver.MakeAttribute (attribute.foreground, Focus.background); + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + break; + case "Focus": + Normal = Application.Driver.MakeAttribute (attribute.foreground, Normal.background); + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + if (Focus.background != attribute.background) + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, HotFocus.background); + if (Normal.foreground != attribute.background) + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + break; + case "HotFocus": + HotNormal = Application.Driver.MakeAttribute (attribute.foreground, HotNormal.background); + if (Focus.foreground != attribute.background) + Focus = Application.Driver.MakeAttribute (Focus.foreground, attribute.background); + break; + } + break; + + case "Error": + switch (callerMemberName) { + case "Normal": + HotNormal = Application.Driver.MakeAttribute (HotNormal.foreground, attribute.background); + HotFocus = Application.Driver.MakeAttribute (HotFocus.foreground, attribute.background); + break; + case "HotNormal": + case "HotFocus": + HotFocus = Application.Driver.MakeAttribute (attribute.foreground, attribute.background); + Normal = Application.Driver.MakeAttribute (Normal.foreground, attribute.background); + break; + } + break; + + } + preparingScheme = false; + return attribute; + } + + /// + /// Compares two objects for equality. + /// + /// + /// true if the two objects are equal + public override bool Equals (object obj) + { + return Equals (obj as ColorScheme); + } + + /// + /// Compares two objects for equality. + /// + /// + /// true if the two objects are equal + public bool Equals (ColorScheme other) + { + return other != null && + EqualityComparer.Default.Equals (_normal, other._normal) && + EqualityComparer.Default.Equals (_focus, other._focus) && + EqualityComparer.Default.Equals (_hotNormal, other._hotNormal) && + EqualityComparer.Default.Equals (_hotFocus, other._hotFocus) && + EqualityComparer.Default.Equals (_disabled, other._disabled); + } + + /// + /// Returns a hashcode for this instance. + /// + /// hashcode for this instance + public override int GetHashCode () + { + int hashCode = -1242460230; + hashCode = hashCode * -1521134295 + _normal.GetHashCode (); + hashCode = hashCode * -1521134295 + _focus.GetHashCode (); + hashCode = hashCode * -1521134295 + _hotNormal.GetHashCode (); + hashCode = hashCode * -1521134295 + _hotFocus.GetHashCode (); + hashCode = hashCode * -1521134295 + _disabled.GetHashCode (); + return hashCode; + } + + /// + /// Compares two objects for equality. + /// + /// + /// + /// true if the two objects are equivalent + public static bool operator == (ColorScheme left, ColorScheme right) + { + return EqualityComparer.Default.Equals (left, right); + } + + /// + /// Compares two objects for inequality. + /// + /// + /// + /// true if the two objects are not equivalent + public static bool operator != (ColorScheme left, ColorScheme right) + { + return !(left == right); + } + } + + /// + /// The default s for the application. + /// + public static class Colors { + static Colors () + { + // Use reflection to dynamically create the default set of ColorSchemes from the list defiined + // by the class. + ColorSchemes = typeof (Colors).GetProperties () + .Where (p => p.PropertyType == typeof (ColorScheme)) + .Select (p => new KeyValuePair (p.Name, new ColorScheme ())) // (ColorScheme)p.GetValue (p))) + .ToDictionary (t => t.Key, t => t.Value); + } + + /// + /// The application toplevel color scheme, for the default toplevel views. + /// + /// + /// + /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["TopLevel"]; + /// + /// + public static ColorScheme TopLevel { get => GetColorScheme (); set => SetColorScheme (value); } + + /// + /// The base color scheme, for the default toplevel views. + /// + /// + /// + /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Base"]; + /// + /// + public static ColorScheme Base { get => GetColorScheme (); set => SetColorScheme (value); } + + /// + /// The dialog color scheme, for standard popup dialog boxes + /// + /// + /// + /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Dialog"]; + /// + /// + public static ColorScheme Dialog { get => GetColorScheme (); set => SetColorScheme (value); } + + /// + /// The menu bar color + /// + /// + /// + /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Menu"]; + /// + /// + public static ColorScheme Menu { get => GetColorScheme (); set => SetColorScheme (value); } + + /// + /// The color scheme for showing errors. + /// + /// + /// + /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Error"]; + /// + /// + public static ColorScheme Error { get => GetColorScheme (); set => SetColorScheme (value); } + + static ColorScheme GetColorScheme ([CallerMemberName] string callerMemberName = null) + { + return ColorSchemes [callerMemberName]; + } + + static void SetColorScheme (ColorScheme colorScheme, [CallerMemberName] string callerMemberName = null) + { + ColorSchemes [callerMemberName] = colorScheme; + colorScheme.caller = callerMemberName; + } + + /// + /// Provides the defined s. + /// + public static Dictionary ColorSchemes { get; } + } + + ///// + ///// Special characters that can be drawn with + ///// + //public enum SpecialChar { + // /// + // /// Horizontal line character. + // /// + // HLine, + + // /// + // /// Vertical line character. + // /// + // VLine, + + // /// + // /// Stipple pattern + // /// + // Stipple, + + // /// + // /// Diamond character + // /// + // Diamond, + + // /// + // /// Upper left corner + // /// + // ULCorner, + + // /// + // /// Lower left corner + // /// + // LLCorner, + + // /// + // /// Upper right corner + // /// + // URCorner, + + // /// + // /// Lower right corner + // /// + // LRCorner, + + // /// + // /// Left tee + // /// + // LeftTee, + + // /// + // /// Right tee + // /// + // RightTee, + + // /// + // /// Top tee + // /// + // TopTee, + + // /// + // /// The bottom tee. + // /// + // BottomTee, + //} + + /// + /// ConsoleDriver is an abstract class that defines the requirements for a console driver. + /// There are currently three implementations: (for Unix and Mac), , and that uses the .NET Console API. + /// + public abstract class ConsoleDriver { + /// + /// The handler fired when the terminal is resized. + /// + protected Action TerminalResized; + + /// + /// The current number of columns in the terminal. + /// + public abstract int Cols { get; } + /// + /// The current number of rows in the terminal. + /// + public abstract int Rows { get; } + /// + /// Initializes the driver + /// + /// Method to invoke when the terminal is resized. + public abstract void Init (Action terminalResized); + /// + /// Moves the cursor to the specified column and row. + /// + /// Column to move the cursor to. + /// Row to move the cursor to. + public abstract void Move (int col, int row); + /// + /// Adds the specified rune to the display at the current cursor position + /// + /// Rune to add. + public abstract void AddRune (Rune rune); + /// + /// Ensures a Rune is not a control character and can be displayed by translating characters below 0x20 + /// to equivalent, printable, Unicode chars. + /// + /// Rune to translate + /// + public static Rune MakePrintable (Rune c) + { + if (c <= 0x1F) { + // ASCII (C0) control characters. + return new Rune (c + 0x2400); + } else if (c >= 0x80 && c <= 0x9F) { + // C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1) + return new Rune (0x25a1); // U+25A1, WHITE SQUARE, □: + } else if (Rune.ColumnWidth (c) > 1) { + // BUGBUG: Until we figure out how to fix #41. Note this still doesn't help when + // an Emoji or other char doesn't represent it's width correctly + return new Rune (0x25a1); // U+25A1, WHITE SQUARE, □: + } else { + return c; + } + } + /// + /// Adds the specified + /// + /// String. + public abstract void AddStr (ustring str); + /// + /// Prepare the driver and set the key and mouse events handlers. + /// + /// The main loop. + /// The handler for ProcessKey + /// The handler for key down events + /// The handler for key up events + /// The handler for mouse events + public abstract void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler); + + /// + /// Updates the screen to reflect all the changes that have been done to the display buffer + /// + public abstract void Refresh (); + + /// + /// Updates the location of the cursor position + /// + public abstract void UpdateCursor (); + + /// + /// Ends the execution of the console driver. + /// + public abstract void End (); + + /// + /// Redraws the physical screen with the contents that have been queued up via any of the printing commands. + /// + public abstract void UpdateScreen (); + + /// + /// Selects the specified attribute as the attribute to use for future calls to AddRune, AddString. + /// + /// C. + public abstract void SetAttribute (Attribute c); + + /// + /// Set Colors from limit sets of colors. + /// + /// Foreground. + /// Background. + public abstract void SetColors (ConsoleColor foreground, ConsoleColor background); + + // Advanced uses - set colors to any pre-set pairs, you would need to init_color + // that independently with the R, G, B values. + /// + /// Advanced uses - set colors to any pre-set pairs, you would need to init_color + /// that independently with the R, G, B values. + /// + /// Foreground color identifier. + /// Background color identifier. + public abstract void SetColors (short foregroundColorId, short backgroundColorId); + + /// + /// Set the handler when the terminal is resized. + /// + /// + public void SetTerminalResized (Action terminalResized) + { + TerminalResized = terminalResized; + } + + /// + /// Draws the title for a Window-style view incorporating padding. + /// + /// Screen relative region where the frame will be drawn. + /// The title for the window. The title will only be drawn if title is not null or empty and paddingTop is greater than 0. + /// Number of columns to pad on the left (if 0 the border will not appear on the left). + /// Number of rows to pad on the top (if 0 the border and title will not appear on the top). + /// Number of columns to pad on the right (if 0 the border will not appear on the right). + /// Number of rows to pad on the bottom (if 0 the border will not appear on the bottom). + /// Not yet immplemented. + /// + public virtual void DrawWindowTitle (Rect region, ustring title, int paddingLeft, int paddingTop, int paddingRight, int paddingBottom, TextAlignment textAlignment = TextAlignment.Left) + { + var width = region.Width - (paddingLeft + 2) * 2; + if (!ustring.IsNullOrEmpty (title) && width > 4 && region.Y + paddingTop <= region.Y + paddingBottom) { + Move (region.X + 1 + paddingLeft, region.Y + paddingTop); + AddRune (' '); + var str = title.Length >= width ? title [0, width - 2] : title; + AddStr (str); + AddRune (' '); + } + } + + /// + /// Enables diagnostic funcions + /// + [Flags] + public enum DiagnosticFlags : uint { + /// + /// All diagnostics off + /// + Off = 0b_0000_0000, + /// + /// When enabled, will draw a + /// ruler in the frame for any side with a padding value greater than 0. + /// + FrameRuler = 0b_0000_0001, + /// + /// When Enabled, will use + /// 'L', 'R', 'T', and 'B' for padding instead of ' '. + /// + FramePadding = 0b_0000_0010, + } + + /// + /// Set flags to enable/disable diagnostics. + /// + public static DiagnosticFlags Diagnostics { get; set; } + + /// + /// Draws a frame for a window with padding and an optional visible border inside the padding. + /// + /// Screen relative region where the frame will be drawn. + /// Number of columns to pad on the left (if 0 the border will not appear on the left). + /// Number of rows to pad on the top (if 0 the border and title will not appear on the top). + /// Number of columns to pad on the right (if 0 the border will not appear on the right). + /// Number of rows to pad on the bottom (if 0 the border will not appear on the bottom). + /// If set to true and any padding dimension is > 0 the border will be drawn. + /// If set to true it will clear the content area (the area inside the padding) with the current color, otherwise the content area will be left untouched. + public virtual void DrawWindowFrame (Rect region, int paddingLeft = 0, int paddingTop = 0, int paddingRight = 0, int paddingBottom = 0, bool border = true, bool fill = false) + { + char clearChar = ' '; + char leftChar = clearChar; + char rightChar = clearChar; + char topChar = clearChar; + char bottomChar = clearChar; + + if ((Diagnostics & DiagnosticFlags.FramePadding) == DiagnosticFlags.FramePadding) { + leftChar = 'L'; + rightChar = 'R'; + topChar = 'T'; + bottomChar = 'B'; + clearChar = 'C'; + } + + void AddRuneAt (int col, int row, Rune ch) + { + Move (col, row); + AddRune (ch); + } + + // fwidth is count of hLine chars + int fwidth = (int)(region.Width - (paddingRight + paddingLeft)); + + // fheight is count of vLine chars + int fheight = (int)(region.Height - (paddingBottom + paddingTop)); + + // fleft is location of left frame line + int fleft = region.X + paddingLeft - 1; + + // fright is location of right frame line + int fright = fleft + fwidth + 1; + + // ftop is location of top frame line + int ftop = region.Y + paddingTop - 1; + + // fbottom is locaiton of bottom frame line + int fbottom = ftop + fheight + 1; + + Rune hLine = border ? HLine : clearChar; + Rune vLine = border ? VLine : clearChar; + Rune uRCorner = border ? URCorner : clearChar; + Rune uLCorner = border ? ULCorner : clearChar; + Rune lLCorner = border ? LLCorner : clearChar; + Rune lRCorner = border ? LRCorner : clearChar; + + // Outside top + if (paddingTop > 1) { + for (int r = region.Y; r < ftop; r++) { + for (int c = region.X; c < region.X + region.Width; c++) { + AddRuneAt (c, r, topChar); + } + } + } + + // Outside top-left + for (int c = region.X; c < fleft; c++) { + AddRuneAt (c, ftop, leftChar); + } + + // Frame top-left corner + AddRuneAt (fleft, ftop, paddingTop >= 0 ? (paddingLeft >= 0 ? uLCorner : hLine) : leftChar); + + // Frame top + for (int c = fleft + 1; c < fleft + 1 + fwidth; c++) { + AddRuneAt (c, ftop, paddingTop > 0 ? hLine : topChar); + } + + // Frame top-right corner + if (fright > fleft) { + AddRuneAt (fright, ftop, paddingTop >= 0 ? (paddingRight >= 0 ? uRCorner : hLine) : rightChar); + } + + // Outside top-right corner + for (int c = fright + 1; c < fright + paddingRight; c++) { + AddRuneAt (c, ftop, rightChar); + } + + // Left, Fill, Right + if (fbottom > ftop) { + for (int r = ftop + 1; r < fbottom; r++) { + // Outside left + for (int c = region.X; c < fleft; c++) { + AddRuneAt (c, r, leftChar); + } + + // Frame left + AddRuneAt (fleft, r, paddingLeft > 0 ? vLine : leftChar); + + // Fill + if (fill) { + for (int x = fleft + 1; x < fright; x++) { + AddRuneAt (x, r, clearChar); + } + } + + // Frame right + if (fright > fleft) { + var v = vLine; + if ((Diagnostics & DiagnosticFlags.FrameRuler) == DiagnosticFlags.FrameRuler) { + v = (char)(((int)'0') + ((r - ftop) % 10)); // vLine; + } + AddRuneAt (fright, r, paddingRight > 0 ? v : rightChar); + } + + // Outside right + for (int c = fright + 1; c < fright + paddingRight; c++) { + AddRuneAt (c, r, rightChar); + } + } + + // Outside Bottom + for (int c = region.X; c < region.X + region.Width; c++) { + AddRuneAt (c, fbottom, leftChar); + } + + // Frame bottom-left + AddRuneAt (fleft, fbottom, paddingLeft > 0 ? lLCorner : leftChar); + + if (fright > fleft) { + // Frame bottom + for (int c = fleft + 1; c < fright; c++) { + var h = hLine; + if ((Diagnostics & DiagnosticFlags.FrameRuler) == DiagnosticFlags.FrameRuler) { + h = (char)(((int)'0') + ((c - fleft) % 10)); // hLine; + } + AddRuneAt (c, fbottom, paddingBottom > 0 ? h : bottomChar); + } + + // Frame bottom-right + AddRuneAt (fright, fbottom, paddingRight > 0 ? (paddingBottom > 0 ? lRCorner : hLine) : rightChar); + } + + // Outside right + for (int c = fright + 1; c < fright + paddingRight; c++) { + AddRuneAt (c, fbottom, rightChar); + } + } + + // Out bottom - ensure top is always drawn if we overlap + if (paddingBottom > 0) { + for (int r = fbottom + 1; r < fbottom + paddingBottom; r++) { + for (int c = region.X; c < region.X + region.Width; c++) { + AddRuneAt (c, r, bottomChar); + } + } + } + } + + /// + /// Draws a frame on the specified region with the specified padding around the frame. + /// + /// Screen relative region where the frame will be drawn. + /// Padding to add on the sides. + /// If set to true it will clear the contents with the current color, otherwise the contents will be left untouched. + /// This API has been superceded by . + /// This API is equivlalent to calling DrawWindowFrame(Rect, p - 1, p - 1, p - 1, p - 1). In other words, + /// A padding value of 0 means there is actually a one cell border. + /// + public virtual void DrawFrame (Rect region, int padding, bool fill) + { + // DrawFrame assumes the border is always at least one row/col thick + // DrawWindowFrame assumes a padding of 0 means NO padding and no frame + DrawWindowFrame (new Rect (region.X, region.Y, region.Width, region.Height), + padding + 1, padding + 1, padding + 1, padding + 1, border: false, fill: fill); + } + + + /// + /// Suspend the application, typically needs to save the state, suspend the app and upon return, reset the console driver. + /// + public abstract void Suspend (); + + Rect clip; + + /// + /// Controls the current clipping region that AddRune/AddStr is subject to. + /// + /// The clip. + public Rect Clip { + get => clip; + set => this.clip = value; + } + + /// + /// Start of mouse moves. + /// + public abstract void StartReportingMouseMoves (); + + /// + /// Stop reporting mouses moves. + /// + public abstract void StopReportingMouseMoves (); + + /// + /// Disables the cooked event processing from the mouse driver. At startup, it is assumed mouse events are cooked. + /// + public abstract void UncookMouse (); + + /// + /// Enables the cooked event processing from the mouse driver + /// + public abstract void CookMouse (); + + /// + /// Horizontal line character. + /// + public Rune HLine; + + /// + /// Vertical line character. + /// + public Rune VLine; + + /// + /// Stipple pattern + /// + public Rune Stipple; + + /// + /// Diamond character + /// + public Rune Diamond; + + /// + /// Upper left corner + /// + public Rune ULCorner; + + /// + /// Lower left corner + /// + public Rune LLCorner; + + /// + /// Upper right corner + /// + public Rune URCorner; + + /// + /// Lower right corner + /// + public Rune LRCorner; + + /// + /// Left tee + /// + public Rune LeftTee; + + /// + /// Right tee + /// + public Rune RightTee; + + /// + /// Top tee + /// + public Rune TopTee; + + /// + /// The bottom tee. + /// + public Rune BottomTee; + + /// + /// Checkmark. + /// + public Rune Checked; + + /// + /// Un-checked checkmark. + /// + public Rune UnChecked; + + /// + /// Selected mark. + /// + public Rune Selected; + + /// + /// Un-selected selected mark. + /// + public Rune UnSelected; + + /// + /// Right Arrow. + /// + public Rune RightArrow; + + /// + /// Left Arrow. + /// + public Rune LeftArrow; + + /// + /// Down Arrow. + /// + public Rune DownArrow; + + /// + /// Up Arrow. + /// + public Rune UpArrow; + + /// + /// Left indicator for default action (e.g. for ). + /// + public Rune LeftDefaultIndicator; + + /// + /// Right indicator for default action (e.g. for ). + /// + public Rune RightDefaultIndicator; + + /// + /// Left frame/bracket (e.g. '[' for ). + /// + public Rune LeftBracket; + + /// + /// Right frame/bracket (e.g. ']' for ). + /// + public Rune RightBracket; + + /// + /// On Segment indicator for meter views (e.g. . + /// + public Rune OnMeterSegment; + + /// + /// Off Segment indicator for meter views (e.g. . + /// + public Rune OffMeterSegement; + + /// + /// Make the attribute for the foreground and background colors. + /// + /// Foreground. + /// Background. + /// + public abstract Attribute MakeAttribute (Color fore, Color back); + } +} diff --git a/Terminal.Gui/Core/Event.cs b/Terminal.Gui/Core/Event.cs new file mode 100644 index 0000000..8d58c3f --- /dev/null +++ b/Terminal.Gui/Core/Event.cs @@ -0,0 +1,593 @@ +// +// Evemts.cs: Events, Key mappings +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; + +namespace Terminal.Gui { + + /// + /// Identifies the state of the "shift"-keys within a event. + /// + public class KeyModifiers { + /// + /// Check if the Shift key was pressed or not. + /// + public bool Shift; + /// + /// Check if the Alt key was pressed or not. + /// + public bool Alt; + /// + /// Check if the Ctrl key was pressed or not. + /// + public bool Ctrl; + /// + /// Check if the Caps lock key was pressed or not. + /// + public bool Capslock; + /// + /// Check if the Num lock key was pressed or not. + /// + public bool Numlock; + /// + /// Check if the Scroll lock key was pressed or not. + /// + public bool Scrolllock; + } + + /// + /// The enumeration contains special encoding for some keys, but can also + /// encode all the unicode values that can be passed. + /// + /// + /// + /// If the is set, then the value is that of the special mask, + /// otherwise, the value is the one of the lower bits (as extracted by ) + /// + /// + /// Control keys are the values between 1 and 26 corresponding to Control-A to Control-Z + /// + /// + /// Unicode runes are also stored here, the letter 'A" for example is encoded as a value 65 (not surfaced in the enum). + /// + /// + [Flags] + public enum Key : uint { + /// + /// Mask that indicates that this is a character value, values outside this range + /// indicate special characters like Alt-key combinations or special keys on the + /// keyboard like function keys, arrows keys and so on. + /// + CharMask = 0xfffff, + + /// + /// If the is set, then the value is that of the special mask, + /// otherwise, the value is the one of the lower bits (as extracted by ). + /// + SpecialMask = 0xfff00000, + + /// + /// The key code for the user pressing Control-spacebar + /// + ControlSpace = 0, + + /// + /// The key code for the user pressing Control-A + /// + ControlA = 1, + /// + /// The key code for the user pressing Control-B + /// + ControlB, + /// + /// The key code for the user pressing Control-C + /// + ControlC, + /// + /// The key code for the user pressing Control-D + /// + ControlD, + /// + /// The key code for the user pressing Control-E + /// + ControlE, + /// + /// The key code for the user pressing Control-F + /// + ControlF, + /// + /// The key code for the user pressing Control-G + /// + ControlG, + /// + /// The key code for the user pressing Control-H + /// + ControlH, + /// + /// The key code for the user pressing Control-I (same as the tab key). + /// + ControlI, + /// + /// The key code for the user pressing Control-J + /// + ControlJ, + /// + /// The key code for the user pressing Control-K + /// + ControlK, + /// + /// The key code for the user pressing Control-L + /// + ControlL, + /// + /// The key code for the user pressing Control-M + /// + ControlM, + /// + /// The key code for the user pressing Control-N (same as the return key). + /// + ControlN, + /// + /// The key code for the user pressing Control-O + /// + ControlO, + /// + /// The key code for the user pressing Control-P + /// + ControlP, + /// + /// The key code for the user pressing Control-Q + /// + ControlQ, + /// + /// The key code for the user pressing Control-R + /// + ControlR, + /// + /// The key code for the user pressing Control-S + /// + ControlS, + /// + /// The key code for the user pressing Control-T + /// + ControlT, + /// + /// The key code for the user pressing Control-U + /// + ControlU, + /// + /// The key code for the user pressing Control-V + /// + ControlV, + /// + /// The key code for the user pressing Control-W + /// + ControlW, + /// + /// The key code for the user pressing Control-X + /// + ControlX, + /// + /// The key code for the user pressing Control-Y + /// + ControlY, + /// + /// The key code for the user pressing Control-Z + /// + ControlZ, + + /// + /// The key code for the user pressing the escape key + /// + Esc = 27, + + /// + /// The key code for the user pressing the return key. + /// + Enter = '\n', + + /// + /// The key code for the user pressing the space bar + /// + Space = 32, + + /// + /// The key code for the user pressing the delete key. + /// + Delete = 127, + + /// + /// When this value is set, the Key encodes the sequence Shift-KeyValue. + /// + ShiftMask = 0x10000000, + + /// + /// When this value is set, the Key encodes the sequence Alt-KeyValue. + /// And the actual value must be extracted by removing the AltMask. + /// + AltMask = 0x80000000, + + /// + /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. + /// And the actual value must be extracted by removing the CtrlMask. + /// + CtrlMask = 0x40000000, + + /// + /// Backspace key. + /// + Backspace = 0x100000, + + /// + /// Cursor up key + /// + CursorUp, + /// + /// Cursor down key. + /// + CursorDown, + /// + /// Cursor left key. + /// + CursorLeft, + /// + /// Cursor right key. + /// + CursorRight, + /// + /// Page Up key. + /// + PageUp, + /// + /// Page Down key. + /// + PageDown, + /// + /// Home key + /// + Home, + /// + /// End key + /// + End, + /// + /// Delete character key + /// + DeleteChar, + /// + /// Insert character key + /// + InsertChar, + /// + /// F1 key. + /// + F1, + /// + /// F2 key. + /// + F2, + /// + /// F3 key. + /// + F3, + /// + /// F4 key. + /// + F4, + /// + /// F5 key. + /// + F5, + /// + /// F6 key. + /// + F6, + /// + /// F7 key. + /// + F7, + /// + /// F8 key. + /// + F8, + /// + /// F9 key. + /// + F9, + /// + /// F10 key. + /// + F10, + /// + /// F11 key. + /// + F11, + /// + /// F12 key. + /// + F12, + /// + /// The key code for the user pressing the tab key (forwards tab key). + /// + Tab, + /// + /// Shift-tab key (backwards tab key). + /// + BackTab, + /// + /// A key with an unknown mapping was raised. + /// + Unknown + } + + /// + /// Describes a keyboard event. + /// + public class KeyEvent { + KeyModifiers keyModifiers; + + /// + /// Symb olid definition for the key. + /// + public Key Key; + + /// + /// The key value cast to an integer, you will typical use this for + /// extracting the Unicode rune value out of a key, when none of the + /// symbolic options are in use. + /// + public int KeyValue => (int)Key; + + /// + /// Gets a value indicating whether the Shift key was pressed. + /// + /// true if is shift; otherwise, false. + public bool IsShift => keyModifiers.Shift; + + /// + /// Gets a value indicating whether the Alt key was pressed (real or synthesized) + /// + /// true if is alternate; otherwise, false. + public bool IsAlt => keyModifiers.Alt; + + /// + /// Determines whether the value is a control key (and NOT just the ctrl key) + /// + /// true if is ctrl; otherwise, false. + //public bool IsCtrl => ((uint)Key >= 1) && ((uint)Key <= 26); + public bool IsCtrl => keyModifiers.Ctrl; + + /// + /// Gets a value indicating whether the Caps lock key was pressed (real or synthesized) + /// + /// true if is alternate; otherwise, false. + public bool IsCapslock => keyModifiers.Capslock; + + /// + /// Gets a value indicating whether the Num lock key was pressed (real or synthesized) + /// + /// true if is alternate; otherwise, false. + public bool IsNumlock => keyModifiers.Numlock; + + /// + /// Gets a value indicating whether the Scroll lock key was pressed (real or synthesized) + /// + /// true if is alternate; otherwise, false. + public bool IsScrolllock => keyModifiers.Scrolllock; + + /// + /// Constructs a new + /// + public KeyEvent () + { + Key = Key.Unknown; + keyModifiers = new KeyModifiers (); + } + + /// + /// Constructs a new from the provided Key value - can be a rune cast into a Key value + /// + public KeyEvent (Key k, KeyModifiers km) + { + Key = k; + keyModifiers = km; + } + + /// + /// Pretty prints the KeyEvent + /// + /// + public override string ToString () + { + string msg = ""; + var key = this.Key; + if (keyModifiers.Shift) { + msg += "Shift-"; + } + if (keyModifiers.Alt) { + msg += "Alt-"; + } + if (keyModifiers.Ctrl) { + msg += "Ctrl-"; + } + if (keyModifiers.Capslock) { + msg += "Capslock-"; + } + if (keyModifiers.Numlock) { + msg += "Numlock-"; + } + if (keyModifiers.Scrolllock) { + msg += "Scrolllock-"; + } + + msg += $"{(((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"{key}")}"; + + return msg; + } + } + + /// + /// Mouse flags reported in . + /// + /// + /// They just happen to map to the ncurses ones. + /// + [Flags] + public enum MouseFlags { + /// + /// The first mouse button was pressed. + /// + Button1Pressed = unchecked((int)0x2), + /// + /// The first mouse button was released. + /// + Button1Released = unchecked((int)0x1), + /// + /// The first mouse button was clicked (press+release). + /// + Button1Clicked = unchecked((int)0x4), + /// + /// The first mouse button was double-clicked. + /// + Button1DoubleClicked = unchecked((int)0x8), + /// + /// The first mouse button was triple-clicked. + /// + Button1TripleClicked = unchecked((int)0x10), + /// + /// The second mouse button was pressed. + /// + Button2Pressed = unchecked((int)0x80), + /// + /// The second mouse button was released. + /// + Button2Released = unchecked((int)0x40), + /// + /// The second mouse button was clicked (press+release). + /// + Button2Clicked = unchecked((int)0x100), + /// + /// The second mouse button was double-clicked. + /// + Button2DoubleClicked = unchecked((int)0x200), + /// + /// The second mouse button was triple-clicked. + /// + Button2TripleClicked = unchecked((int)0x400), + /// + /// The third mouse button was pressed. + /// + Button3Pressed = unchecked((int)0x2000), + /// + /// The third mouse button was released. + /// + Button3Released = unchecked((int)0x1000), + /// + /// The third mouse button was clicked (press+release). + /// + Button3Clicked = unchecked((int)0x4000), + /// + /// The third mouse button was double-clicked. + /// + Button3DoubleClicked = unchecked((int)0x8000), + /// + /// The third mouse button was triple-clicked. + /// + Button3TripleClicked = unchecked((int)0x10000), + /// + /// The fourth mouse button was pressed. + /// + Button4Pressed = unchecked((int)0x80000), + /// + /// The fourth mouse button was released. + /// + Button4Released = unchecked((int)0x40000), + /// + /// The fourth button was clicked (press+release). + /// + Button4Clicked = unchecked((int)0x100000), + /// + /// The fourth button was double-clicked. + /// + Button4DoubleClicked = unchecked((int)0x200000), + /// + /// The fourth button was triple-clicked. + /// + Button4TripleClicked = unchecked((int)0x400000), + /// + /// Flag: the shift key was pressed when the mouse button took place. + /// + ButtonShift = unchecked((int)0x2000000), + /// + /// Flag: the ctrl key was pressed when the mouse button took place. + /// + ButtonCtrl = unchecked((int)0x1000000), + /// + /// Flag: the alt key was pressed when the mouse button took place. + /// + ButtonAlt = unchecked((int)0x4000000), + /// + /// The mouse position is being reported in this event. + /// + ReportMousePosition = unchecked((int)0x8000000), + /// + /// Vertical button wheeled up. + /// + WheeledUp = unchecked((int)0x10000000), + /// + /// Vertical button wheeled up. + /// + WheeledDown = unchecked((int)0x20000000), + /// + /// Mask that captures all the events. + /// + AllEvents = unchecked((int)0x7ffffff), + } + + /// + /// Describes a mouse event + /// + public struct MouseEvent { + /// + /// The X (column) location for the mouse event. + /// + public int X; + + /// + /// The Y (column) location for the mouse event. + /// + public int Y; + + /// + /// Flags indicating the kind of mouse event that is being posted. + /// + public MouseFlags Flags; + + /// + /// The offset X (column) location for the mouse event. + /// + public int OfX; + + /// + /// The offset Y (column) location for the mouse event. + /// + public int OfY; + + /// + /// The current view at the location for the mouse event. + /// + public View View; + + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return $"({X},{Y}:{Flags}"; + } + } +} diff --git a/Terminal.Gui/Core/MainLoop.cs b/Terminal.Gui/Core/MainLoop.cs new file mode 100644 index 0000000..8e1217a --- /dev/null +++ b/Terminal.Gui/Core/MainLoop.cs @@ -0,0 +1,246 @@ +// +// MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; +using System.Collections.Generic; + +namespace Terminal.Gui { + /// + /// Public interface to create your own platform specific main loop driver. + /// + public interface IMainLoopDriver { + /// + /// Initializes the main loop driver, gets the calling main loop for the initialization. + /// + /// Main loop. + void Setup (MainLoop mainLoop); + + /// + /// Wakes up the mainloop that might be waiting on input, must be thread safe. + /// + void Wakeup (); + + /// + /// Must report whether there are any events pending, or even block waiting for events. + /// + /// true, if there were pending events, false otherwise. + /// If set to true wait until an event is available, otherwise return immediately. + bool EventsPending (bool wait); + + /// + /// The interation function. + /// + void MainIteration (); + } + + /// + /// Simple main loop implementation that can be used to monitor + /// file descriptor, run timers and idle handlers. + /// + /// + /// Monitoring of file descriptors is only available on Unix, there + /// does not seem to be a way of supporting this on Windows. + /// + public class MainLoop { + internal class Timeout { + public TimeSpan Span; + public Func Callback; + } + + internal SortedList timeouts = new SortedList (); + internal List> idleHandlers = new List> (); + + /// + /// The current IMainLoopDriver in use. + /// + /// The driver. + public IMainLoopDriver Driver { get; } + + /// + /// Creates a new Mainloop. + /// + /// Should match the (one of the implementations UnixMainLoop, NetMainLoop or WindowsMainLoop). + public MainLoop (IMainLoopDriver driver) + { + Driver = driver; + driver.Setup (this); + } + + /// + /// Runs action on the thread that is processing events + /// + /// the action to be invoked on the main processing thread. + public void Invoke (Action action) + { + AddIdle (() => { + action (); + return false; + }); + } + + /// + /// Adds specified idle handler function to mainloop processing. The handler function will be called once per iteration of the main loop after other events have been handled. + /// + /// + /// + /// Remove an idle hander by calling with the token this method returns. + /// + /// + /// If the idleHandler returns false it will be removed and not called subsequently. + /// + /// + /// Token that can be used to remove the idle handler with . + public Func AddIdle (Func idleHandler) + { + lock (idleHandlers) + idleHandlers.Add (idleHandler); + + return idleHandler; + } + + /// + /// Removes an idle handler added with from processing. + /// + /// A token returned by + public void RemoveIdle (Func token) + { + lock (token) + idleHandlers.Remove (token); + } + + void AddTimeout (TimeSpan time, Timeout timeout) + { + timeouts.Add ((DateTime.UtcNow + time).Ticks, timeout); + } + + /// + /// Adds a timeout to the mainloop. + /// + /// + /// When time time specified passes, the callback will be invoked. + /// If the callback returns true, the timeout will be reset, repeating + /// the invocation. If it returns false, the timeout will stop and be removed. + /// + /// The returned value is a token that can be used to stop the timeout + /// by calling . + /// + public object AddTimeout (TimeSpan time, Func callback) + { + if (callback == null) + throw new ArgumentNullException (nameof (callback)); + var timeout = new Timeout () { + Span = time, + Callback = callback + }; + AddTimeout (time, timeout); + return timeout; + } + + /// + /// Removes a previously scheduled timeout + /// + /// + /// The token parameter is the value returned by AddTimeout. + /// + public void RemoveTimeout (object token) + { + var idx = timeouts.IndexOfValue (token as Timeout); + if (idx == -1) + return; + timeouts.RemoveAt (idx); + } + + void RunTimers () + { + long now = DateTime.UtcNow.Ticks; + var copy = timeouts; + timeouts = new SortedList (); + foreach (var k in copy.Keys) { + var timeout = copy [k]; + if (k < now) { + if (timeout.Callback (this)) + AddTimeout (timeout.Span, timeout); + } else + timeouts.Add (k, timeout); + } + } + + void RunIdle () + { + List> iterate; + lock (idleHandlers) { + iterate = idleHandlers; + idleHandlers = new List> (); + } + + foreach (var idle in iterate) { + if (idle ()) + lock (idleHandlers) + idleHandlers.Add (idle); + } + } + + bool running; + + /// + /// Stops the mainloop. + /// + public void Stop () + { + running = false; + Driver.Wakeup (); + } + + /// + /// Determines whether there are pending events to be processed. + /// + /// + /// You can use this method if you want to probe if events are pending. + /// Typically used if you need to flush the input queue while still + /// running some of your own code in your main thread. + /// + public bool EventsPending (bool wait = false) + { + return Driver.EventsPending (wait); + } + + /// + /// Runs one iteration of timers and file watches + /// + /// + /// You use this to process all pending events (timers, idle handlers and file watches). + /// + /// You can use it like this: + /// while (main.EvensPending ()) MainIteration (); + /// + public void MainIteration () + { + if (timeouts.Count > 0) + RunTimers (); + + Driver.MainIteration (); + + lock (idleHandlers) { + if (idleHandlers.Count > 0) + RunIdle (); + } + } + + /// + /// Runs the mainloop. + /// + public void Run () + { + bool prev = running; + running = true; + while (running) { + EventsPending (true); + MainIteration (); + } + running = prev; + } + } +} diff --git a/Terminal.Gui/Core/PosDim.cs b/Terminal.Gui/Core/PosDim.cs new file mode 100644 index 0000000..33e3a5e --- /dev/null +++ b/Terminal.Gui/Core/PosDim.cs @@ -0,0 +1,604 @@ +// +// PosDim.cs: Pos and Dim objects for view dimensions. +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// + +using System; +namespace Terminal.Gui { + /// + /// Describes the position of a which can be an absolute value, a percentage, centered, or + /// relative to the ending dimension. Integer values are implicitly convertible to + /// an absolute . These objects are created using the static methods Percent, + /// AnchorEnd, and Center. The objects can be combined with the addition and + /// subtraction operators. + /// + /// + /// + /// Use the objects on the X or Y properties of a view to control the position. + /// + /// + /// These can be used to set the absolute position, when merely assigning an + /// integer value (via the implicit integer to conversion), and they can be combined + /// to produce more useful layouts, like: Pos.Center - 3, which would shift the postion + /// of the 3 characters to the left after centering for example. + /// + /// + /// It is possible to reference coordinates of another view by using the methods + /// Left(View), Right(View), Bottom(View), Top(View). The X(View) and Y(View) are + /// aliases to Left(View) and Top(View) respectively. + /// + /// + public class Pos { + internal virtual int Anchor (int width) + { + return 0; + } + + class PosFactor : Pos { + float factor; + + public PosFactor (float n) + { + this.factor = n; + } + + internal override int Anchor (int width) + { + return (int)(width * factor); + } + + public override string ToString () + { + return $"Pos.Factor({factor})"; + } + } + + /// + /// Creates a percentage object + /// + /// The percent object. + /// A value between 0 and 100 representing the percentage. + /// + /// This creates a that is centered horizontally, is 50% of the way down, + /// is 30% the height, and is 80% the width of the it added to. + /// + /// var textView = new TextView () { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), + /// }; + /// + /// + public static Pos Percent (float n) + { + if (n < 0 || n > 100) + throw new ArgumentException ("Percent value must be between 0 and 100"); + + return new PosFactor (n / 100); + } + + static PosAnchorEnd endNoMargin; + + class PosAnchorEnd : Pos { + int n; + + public PosAnchorEnd (int n) + { + this.n = n; + } + + internal override int Anchor (int width) + { + return width - n; + } + + public override string ToString () + { + return $"Pos.AnchorEnd(margin={n})"; + } + } + + /// + /// Creates a object that is anchored to the end (right side or bottom) of the dimension, + /// useful to flush the layout from the right or bottom. + /// + /// The object anchored to the end (the bottom or the right side). + /// Optional margin to place to the right or below. + /// + /// This sample shows how align a to the bottom-right of a . + /// + /// // See Issue #502 + /// anchorButton.X = Pos.AnchorEnd () - (Pos.Right (anchorButton) - Pos.Left (anchorButton)); + /// anchorButton.Y = Pos.AnchorEnd (1); + /// + /// + public static Pos AnchorEnd (int margin = 0) + { + if (margin < 0) + throw new ArgumentException ("Margin must be positive"); + + if (margin == 0) { + if (endNoMargin == null) + endNoMargin = new PosAnchorEnd (0); + return endNoMargin; + } + return new PosAnchorEnd (margin); + } + + internal class PosCenter : Pos { + internal override int Anchor (int width) + { + return width / 2; + } + + public override string ToString () + { + return "Pos.Center"; + } + } + + static PosCenter pCenter; + + /// + /// Returns a object that can be used to center the + /// + /// The center Pos. + /// + /// This creates a that is centered horizontally, is 50% of the way down, + /// is 30% the height, and is 80% the width of the it added to. + /// + /// var textView = new TextView () { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), + /// }; + /// + /// + public static Pos Center () + { + if (pCenter == null) + pCenter = new PosCenter (); + return pCenter; + } + + class PosAbsolute : Pos { + int n; + public PosAbsolute (int n) { this.n = n; } + + public override string ToString () + { + return $"Pos.Absolute({n})"; + } + + internal override int Anchor (int width) + { + return n; + } + + public override int GetHashCode () => n.GetHashCode (); + + public override bool Equals (object other) => other is PosAbsolute abs && abs.n == n; + + } + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static implicit operator Pos (int n) + { + return new PosAbsolute (n); + } + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static Pos At (int n) + { + return new PosAbsolute (n); + } + + class PosCombine : Pos { + Pos left, right; + bool add; + public PosCombine (bool add, Pos left, Pos right) + { + this.left = left; + this.right = right; + this.add = add; + } + + internal override int Anchor (int width) + { + var la = left.Anchor (width); + var ra = right.Anchor (width); + if (add) + return la + ra; + else + return la - ra; + } + + public override string ToString () + { + return $"Pos.Combine({left.ToString ()}{(add ? '+' : '-')}{right.ToString ()})"; + } + + } + + static PosCombine posCombine; + + /// + /// Adds a to a , yielding a new . + /// + /// The first to add. + /// The second to add. + /// The that is the sum of the values of left and right. + public static Pos operator + (Pos left, Pos right) + { + PosCombine newPos = new PosCombine (true, left, right); + if (posCombine?.ToString () != newPos.ToString ()) { + var view = left as PosView; + if (view != null) { + view.Target.SetNeedsLayout (); + } + } + return posCombine = newPos; + } + + /// + /// Subtracts a from a , yielding a new . + /// + /// The to subtract from (the minuend). + /// The to subtract (the subtrahend). + /// The that is the left minus right. + public static Pos operator - (Pos left, Pos right) + { + PosCombine newPos = new PosCombine (false, left, right); + if (posCombine?.ToString () != newPos.ToString ()) { + var view = left as PosView; + if (view != null) + view.Target.SetNeedsLayout (); + } + return posCombine = newPos; + } + + internal class PosView : Pos { + public View Target; + int side; + public PosView (View view, int side) + { + Target = view; + this.side = side; + } + internal override int Anchor (int width) + { + switch (side) { + case 0: return Target.Frame.X; + case 1: return Target.Frame.Y; + case 2: return Target.Frame.Right; + case 3: return Target.Frame.Bottom; + default: + return 0; + } + } + + public override string ToString () + { + string tside; + switch (side) { + case 0: tside = "x"; break; + case 1: tside = "y"; break; + case 2: tside = "right"; break; + case 3: tside = "bottom"; break; + default: tside = "unknown"; break; + } + return $"Pos.View(side={tside}, target={Target.ToString ()})"; + } + } + + /// + /// Returns a object tracks the Left (X) position of the specified . + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos Left (View view) => new PosCombine (true, new PosView (view, 0), new Pos.PosAbsolute (0)); + + /// + /// Returns a object tracks the Left (X) position of the specified . + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos X (View view) => new PosCombine (true, new PosView (view, 0), new Pos.PosAbsolute (0)); + + /// + /// Returns a object tracks the Top (Y) position of the specified . + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos Top (View view) => new PosCombine (true, new PosView (view, 1), new Pos.PosAbsolute (0)); + + /// + /// Returns a object tracks the Top (Y) position of the specified . + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos Y (View view) => new PosCombine (true, new PosView (view, 1), new Pos.PosAbsolute (0)); + + /// + /// Returns a object tracks the Right (X+Width) coordinate of the specified . + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos Right (View view) => new PosCombine (true, new PosView (view, 2), new Pos.PosAbsolute (0)); + + /// + /// Returns a object tracks the Bottom (Y+Height) coordinate of the specified + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos Bottom (View view) => new PosCombine (true, new PosView (view, 3), new Pos.PosAbsolute (0)); + } + + /// + /// Dim properties of a to control the position. + /// + /// + /// + /// Use the Dim objects on the Width or Height properties of a to control the position. + /// + /// + /// These can be used to set the absolute position, when merely assigning an + /// integer value (via the implicit integer to Pos conversion), and they can be combined + /// to produce more useful layouts, like: Pos.Center - 3, which would shift the postion + /// of the 3 characters to the left after centering for example. + /// + /// + public class Dim { + internal virtual int Anchor (int width) + { + return 0; + } + + internal class DimFactor : Dim { + float factor; + bool remaining; + + public DimFactor (float n, bool r = false) + { + factor = n; + remaining = r; + } + + internal override int Anchor (int width) + { + return (int)(width * factor); + } + + public bool IsFromRemaining () + { + return remaining; + } + + public override string ToString () + { + return $"Dim.Factor(factor={factor}, remaining={remaining})"; + } + + public override int GetHashCode () => factor.GetHashCode (); + + public override bool Equals (object other) => other is DimFactor f && f.factor == factor && f.remaining == remaining; + + } + + /// + /// Creates a percentage object + /// + /// The percent object. + /// A value between 0 and 100 representing the percentage. + /// If true the Percent is computed based on the remaining space after the X/Y anchor positions. If false is computed based on the whole original space. + /// + /// This initializes a that is centered horizontally, is 50% of the way down, + /// is 30% the height, and is 80% the width of the it added to. + /// + /// var textView = new TextView () { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), + /// }; + /// + /// + public static Dim Percent (float n, bool r = false) + { + if (n < 0 || n > 100) + throw new ArgumentException ("Percent value must be between 0 and 100"); + + return new DimFactor (n / 100, r); + } + + internal class DimAbsolute : Dim { + int n; + public DimAbsolute (int n) { this.n = n; } + + public override string ToString () + { + return $"Dim.Absolute({n})"; + } + + internal override int Anchor (int width) + { + return n; + } + + public override int GetHashCode () => n.GetHashCode (); + + public override bool Equals (object other) => other is DimAbsolute abs && abs.n == n; + + } + + internal class DimFill : Dim { + int margin; + public DimFill (int margin) { this.margin = margin; } + + public override string ToString () + { + return $"Dim.Fill(margin={margin})"; + } + + internal override int Anchor (int width) + { + return width - margin; + } + + public override int GetHashCode () => margin.GetHashCode (); + + public override bool Equals (object other) => other is DimFill fill && fill.margin == margin; + } + + static DimFill zeroMargin; + + /// + /// Initializes a new instance of the class that fills the dimension, but leaves the specified number of colums for a margin. + /// + /// The Fill dimension. + /// Margin to use. + public static Dim Fill (int margin = 0) + { + if (margin == 0) { + if (zeroMargin == null) + zeroMargin = new DimFill (0); + return zeroMargin; + } + return new DimFill (margin); + } + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the pos. + public static implicit operator Dim (int n) + { + return new DimAbsolute (n); + } + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static Dim Sized (int n) + { + return new DimAbsolute (n); + } + + class DimCombine : Dim { + Dim left, right; + bool add; + public DimCombine (bool add, Dim left, Dim right) + { + this.left = left; + this.right = right; + this.add = add; + } + + internal override int Anchor (int width) + { + var la = left.Anchor (width); + var ra = right.Anchor (width); + if (add) + return la + ra; + else + return la - ra; + } + } + + /// + /// Adds a to a , yielding a new . + /// + /// The first to add. + /// The second to add. + /// The that is the sum of the values of left and right. + public static Dim operator + (Dim left, Dim right) + { + return new DimCombine (true, left, right); + } + + /// + /// Subtracts a from a , yielding a new . + /// + /// The to subtract from (the minuend). + /// The to subtract (the subtrahend). + /// The that is the left minus right. + public static Dim operator - (Dim left, Dim right) + { + return new DimCombine (false, left, right); + } + + internal class DimView : Dim { + public View Target; + int side; + public DimView (View view, int side) + { + Target = view; + this.side = side; + } + + public override string ToString () + { + string tside; + switch (side) { + case 0: tside = "Height"; break; + case 1: tside = "Width"; break; + default: tside = "unknown"; break; + } + return $"DimView(side={tside}, target={Target.ToString ()})"; + } + + internal override int Anchor (int width) + { + switch (side) { + case 0: return Target.Frame.Height; + case 1: return Target.Frame.Width; + default: + return 0; + } + } + + public override int GetHashCode () => Target.GetHashCode (); + + public override bool Equals (object other) => other is DimView abs && abs.Target == Target; + + } + /// + /// Returns a object tracks the Width of the specified . + /// + /// The of the other . + /// The view that will be tracked. + public static Dim Width (View view) => new DimView (view, 1); + + /// + /// Returns a object tracks the Height of the specified . + /// + /// The of the other . + /// The view that will be tracked. + public static Dim Height (View view) => new DimView (view, 0); + + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode () => GetHashCode (); + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + /// + /// if the specified object is equal to the current object; otherwise, . + public override bool Equals (object other) => other is Dim abs && abs == this; + } +} diff --git a/Terminal.Gui/Core/Responder.cs b/Terminal.Gui/Core/Responder.cs new file mode 100644 index 0000000..c6fd75c --- /dev/null +++ b/Terminal.Gui/Core/Responder.cs @@ -0,0 +1,185 @@ +// +// Core.cs: The core engine for gui.cs +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// Pending: +// - Check for NeedDisplay on the hierarchy and repaint +// - Layout support +// - "Colors" type or "Attributes" type? +// - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors? +// +// Optimziations +// - Add rendering limitation to the exposed area + +namespace Terminal.Gui { + /// + /// Responder base class implemented by objects that want to participate on keyboard and mouse input. + /// + public class Responder { + /// + /// Gets or sets a value indicating whether this can focus. + /// + /// true if can focus; otherwise, false. + public virtual bool CanFocus { get; set; } + + /// + /// Gets or sets a value indicating whether this has focus. + /// + /// true if has focus; otherwise, false. + public virtual bool HasFocus { get; internal set; } + + // Key handling + /// + /// This method can be overwritten by view that + /// want to provide accelerator functionality + /// (Alt-key for example). + /// + /// + /// + /// Before keys are sent to the subview on the + /// current view, all the views are + /// processed and the key is passed to the widgets + /// to allow some of them to process the keystroke + /// as a hot-key. + /// + /// For example, if you implement a button that + /// has a hotkey ok "o", you would catch the + /// combination Alt-o here. If the event is + /// caught, you must return true to stop the + /// keystroke from being dispatched to other + /// views. + /// + /// + + public virtual bool ProcessHotKey (KeyEvent kb) + { + return false; + } + + /// + /// If the view is focused, gives the view a + /// chance to process the keystroke. + /// + /// + /// + /// Views can override this method if they are + /// interested in processing the given keystroke. + /// If they consume the keystroke, they must + /// return true to stop the keystroke from being + /// processed by other widgets or consumed by the + /// widget engine. If they return false, the + /// keystroke will be passed using the ProcessColdKey + /// method to other views to process. + /// + /// + /// The View implementation does nothing but return false, + /// so it is not necessary to call base.ProcessKey if you + /// derive directly from View, but you should if you derive + /// other View subclasses. + /// + /// + /// Contains the details about the key that produced the event. + public virtual bool ProcessKey (KeyEvent keyEvent) + { + return false; + } + + /// + /// This method can be overwritten by views that + /// want to provide accelerator functionality + /// (Alt-key for example), but without + /// interefering with normal ProcessKey behavior. + /// + /// + /// + /// After keys are sent to the subviews on the + /// current view, all the view are + /// processed and the key is passed to the views + /// to allow some of them to process the keystroke + /// as a cold-key. + /// + /// This functionality is used, for example, by + /// default buttons to act on the enter key. + /// Processing this as a hot-key would prevent + /// non-default buttons from consuming the enter + /// keypress when they have the focus. + /// + /// + /// Contains the details about the key that produced the event. + public virtual bool ProcessColdKey (KeyEvent keyEvent) + { + return false; + } + + /// + /// Method invoked when a key is pressed. + /// + /// Contains the details about the key that produced the event. + /// true if the event was handled + public virtual bool OnKeyDown (KeyEvent keyEvent) + { + return false; + } + + /// + /// Method invoked when a key is released. + /// + /// Contains the details about the key that produced the event. + /// true if the event was handled + public virtual bool OnKeyUp (KeyEvent keyEvent) + { + return false; + } + + + /// + /// Method invoked when a mouse event is generated + /// + /// true, if the event was handled, false otherwise. + /// Contains the details about the mouse event. + public virtual bool MouseEvent (MouseEvent mouseEvent) + { + return false; + } + + /// + /// Method invoked when a mouse event is generated for the first time. + /// + /// + /// true, if the event was handled, false otherwise. + public virtual bool OnMouseEnter (MouseEvent mouseEvent) + { + return false; + } + + /// + /// Method invoked when a mouse event is generated for the last time. + /// + /// + /// true, if the event was handled, false otherwise. + public virtual bool OnMouseLeave (MouseEvent mouseEvent) + { + return false; + } + + /// + /// Method invoked when a view gets focus. + /// + /// true, if the event was handled, false otherwise. + public virtual bool OnEnter () + { + return false; + } + + /// + /// Method invoked when a view loses focus. + /// + /// true, if the event was handled, false otherwise. + public virtual bool OnLeave () + { + return false; + } + } +} diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs new file mode 100644 index 0000000..b912885 --- /dev/null +++ b/Terminal.Gui/Core/Toplevel.cs @@ -0,0 +1,335 @@ +// +// Toplevel.cs: Toplevel views can be modally executed +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace Terminal.Gui { + /// + /// Toplevel views can be modally executed. + /// + /// + /// + /// Toplevels can be modally executing views, started by calling . + /// They return control to the caller when has + /// been called (which sets the property to false). + /// + /// + /// A Toplevel is created when an application initialzies Terminal.Gui by callling . + /// The application Toplevel can be accessed via . Additional Toplevels can be created + /// and run (e.g. s. To run a Toplevel, create the and + /// call . + /// + /// + /// Toplevels can also opt-in to more sophisticated initialization + /// by implementing . When they do + /// so, the and + /// methods will be called + /// before running the view. + /// If first-run-only initialization is preferred, the + /// can be implemented too, in which case the + /// methods will only be called if + /// is . This allows proper inheritance hierarchies + /// to override base class layout code optimally by doing so only on first run, + /// instead of on every run. + /// + /// + public class Toplevel : View { + /// + /// Gets or sets whether the for this is running or not. + /// + /// + /// Setting this property directly is discouraged. Use instead. + /// + public bool Running { get; set; } + + /// + /// Fired once the Toplevel's has started it's first iteration. + /// Subscribe to this event to perform tasks when the has been laid out and focus has been set. + /// changes. A Ready event handler is a good place to finalize initialization after calling `(topLevel)`. + /// + public Action Ready; + + /// + /// Called from after the has entered it's first iteration of the loop. + /// + internal virtual void OnReady () + { + Ready?.Invoke (); + } + + /// + /// Initializes a new instance of the class with the specified absolute layout. + /// + /// A superview-relative rectangle specifying the location and size for the new Toplevel + public Toplevel (Rect frame) : base (frame) + { + Initialize (); + } + + /// + /// Initializes a new instance of the class with layout, defaulting to full screen. + /// + public Toplevel () : base () + { + Initialize (); + Width = Dim.Fill (); + Height = Dim.Fill (); + } + + void Initialize () + { + ColorScheme = Colors.Base; + } + + /// + /// Convenience factory method that creates a new Toplevel with the current terminal dimensions. + /// + /// The create. + public static Toplevel Create () + { + return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows)); + } + + /// + /// Gets or sets a value indicating whether this can focus. + /// + /// true if can focus; otherwise, false. + public override bool CanFocus { + get => true; + } + + /// + /// Determines whether the is modal or not. + /// Causes to propagate keys upwards + /// by default unless set to . + /// + public bool Modal { get; set; } + + /// + /// Gets or sets the menu for this Toplevel + /// + public MenuBar MenuBar { get; set; } + + /// + /// Gets or sets the status bar for this Toplevel + /// + public StatusBar StatusBar { get; set; } + + /// + public override bool ProcessKey (KeyEvent keyEvent) + { + if (base.ProcessKey (keyEvent)) + return true; + + switch (keyEvent.Key) { + case Key.ControlQ: + // FIXED: stop current execution of this container + Application.RequestStop (); + break; + case Key.ControlZ: + Driver.Suspend (); + return true; + +#if false + case Key.F5: + Application.DebugDrawBounds = !Application.DebugDrawBounds; + SetNeedsDisplay (); + return true; +#endif + case Key.Tab: + case Key.CursorRight: + case Key.CursorDown: + case Key.ControlI: // Unix + var old = Focused; + if (!FocusNext ()) + FocusNext (); + if (old != Focused) { + old?.SetNeedsDisplay (); + Focused?.SetNeedsDisplay (); + } else { + FocusNearestView (GetToplevelSubviews (true)); + } + return true; + case Key.CursorLeft: + case Key.CursorUp: + case Key.BackTab: + old = Focused; + if (!FocusPrev ()) + FocusPrev (); + if (old != Focused) { + old?.SetNeedsDisplay (); + Focused?.SetNeedsDisplay (); + } else { + FocusNearestView (GetToplevelSubviews (false)); + } + return true; + + case Key.ControlL: + Application.Refresh (); + return true; + } + return false; + } + + IEnumerable GetToplevelSubviews (bool isForward) + { + if (SuperView == null) { + return null; + } + + HashSet views = new HashSet (); + + foreach (var v in SuperView.Subviews) { + views.Add (v); + } + + return isForward ? views : views.Reverse (); + } + + void FocusNearestView (IEnumerable views) + { + if (views == null) { + return; + } + + bool found = false; + + foreach (var v in views) { + if (v == this) { + found = true; + } + if (found && v != this) { + v.EnsureFocus (); + if (SuperView.Focused != null && SuperView.Focused != this) { + return; + } + } + } + } + + /// + public override void Add (View view) + { + if (this == Application.Top) { + if (view is MenuBar) + MenuBar = view as MenuBar; + if (view is StatusBar) + StatusBar = view as StatusBar; + } + base.Add (view); + } + + /// + public override void Remove (View view) + { + if (this == Application.Top) { + if (view is MenuBar) + MenuBar = null; + if (view is StatusBar) + StatusBar = null; + } + base.Remove (view); + } + + /// + public override void RemoveAll () + { + if (this == Application.Top) { + MenuBar = null; + StatusBar = null; + } + base.RemoveAll (); + } + + internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out int ny) + { + nx = Math.Max (x, 0); + nx = nx + top.Frame.Width > Driver.Cols ? Math.Max (Driver.Cols - top.Frame.Width, 0) : nx; + bool m, s; + if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) + m = Application.Top.MenuBar != null; + else + m = ((Toplevel)SuperView).MenuBar != null; + int l = m ? 1 : 0; + ny = Math.Max (y, l); + if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) + s = Application.Top.StatusBar != null; + else + s = ((Toplevel)SuperView).StatusBar != null; + l = s ? Driver.Rows - 1 : Driver.Rows; + ny = Math.Min (ny, l); + ny = ny + top.Frame.Height > l ? Math.Max (l - top.Frame.Height, m ? 1 : 0) : ny; + } + + internal void PositionToplevels () + { + foreach (var top in Subviews) { + if (top is Toplevel) { + PositionToplevel ((Toplevel)top); + } + } + } + + private void PositionToplevel (Toplevel top) + { + EnsureVisibleBounds (top, top.Frame.X, top.Frame.Y, out int nx, out int ny); + if ((nx != top.Frame.X || ny != top.Frame.Y) && top.LayoutStyle != LayoutStyle.Computed) { + top.X = nx; + top.Y = ny; + } + if (StatusBar != null) { + if (ny + top.Frame.Height > Driver.Rows - 1) { + if (top.Height is Dim.DimFill) + top.Height = Dim.Fill () - 1; + } + if (StatusBar.Frame.Y != Driver.Rows - 1) { + StatusBar.Y = Driver.Rows - 1; + SetNeedsDisplay (); + } + } + } + + /// + public override void Redraw (Rect bounds) + { + Application.CurrentView = this; + + if (IsCurrentTop || this == Application.Top) { + if (NeedDisplay != null && !NeedDisplay.IsEmpty) { + Driver.SetAttribute (Colors.TopLevel.Normal); + + // This is the Application.Top. Clear just the region we're being asked to redraw + // (the bounds passed to us). + Clear (bounds); + Driver.SetAttribute (Colors.Base.Normal); + PositionToplevels (); + } + foreach (var view in Subviews) { + if (view.Frame.IntersectsWith (bounds)) { + view.SetNeedsLayout (); + view.SetNeedsDisplay (view.Bounds); + } + } + + ClearNeedsDisplay (); + } + + base.Redraw (base.Bounds); + } + + /// + /// Invoked by as part of the after + /// the views have been laid out, and before the views are drawn for the first time. + /// + public virtual void WillPresent () + { + FocusFirst (); + } + } +} diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs new file mode 100644 index 0000000..6531b56 --- /dev/null +++ b/Terminal.Gui/Core/View.cs @@ -0,0 +1,1692 @@ +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// Pending: +// - Check for NeedDisplay on the hierarchy and repaint +// - Layout support +// - "Colors" type or "Attributes" type? +// - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors? +// +// Optimziations +// - Add rendering limitation to the exposed area +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NStack; + +namespace Terminal.Gui { + /// + /// Text alignment enumeration, controls how text is displayed. + /// + public enum TextAlignment { + /// + /// Aligns the text to the left of the frame. + /// + Left, + /// + /// Aligns the text to the right side of the frame. + /// + Right, + /// + /// Centers the text in the frame. + /// + Centered, + /// + /// Shows the text as justified text in the frame. + /// + Justified + } + + /// + /// Determines the LayoutStyle for a view, if Absolute, during LayoutSubviews, the + /// value from the Frame will be used, if the value is Computed, then the Frame + /// will be updated from the X, Y Pos objects and the Width and Height Dim objects. + /// + public enum LayoutStyle { + /// + /// The position and size of the view are based on the Frame value. + /// + Absolute, + + /// + /// The position and size of the view will be computed based on the + /// X, Y, Width and Height properties and set on the Frame. + /// + Computed + } + + /// + /// View is the base class for all views on the screen and represents a visible element that can render itself and contains zero or more nested views. + /// + /// + /// + /// The View defines the base functionality for user interface elements in Terminal.Gui. Views + /// can contain one or more subviews, can respond to user input and render themselves on the screen. + /// + /// + /// Views supports two layout styles: Absolute or Computed. The choice as to which layout style is used by the View + /// is determined when the View is initizlied. To create a View using Absolute layout, call a constructor that takes a + /// Rect parameter to specify the absolute position and size (the View.)/. To create a View + /// using Computed layout use a constructor that does not take a Rect parametr and set the X, Y, Width and Height + /// properties on the view. Both approaches use coordinates that are relative to the container they are being added to. + /// + /// + /// To switch between Absolute and Computed layout, use the property. + /// + /// + /// Computed layout is more flexible and supports dynamic console apps where controls adjust layout + /// as the terminal resizes or other Views change size or position. The X, Y, Width and Height + /// properties are Dim and Pos objects that dynamically update the position of a view. + /// The X and Y properties are of type + /// and you can use either absolute positions, percentages or anchor + /// points. The Width and Height properties are of type + /// and can use absolute position, + /// percentages and anchors. These are useful as they will take + /// care of repositioning views when view's frames are resized or + /// if the terminal size changes. + /// + /// + /// Absolute layout requires specifying coordiantes and sizes of Views explicitly, and the + /// View will typcialy stay in a fixed position and size. To change the position and size use the + /// property. + /// + /// + /// Subviews (child views) can be added to a View by calling the method. + /// The container of a View can be accessed with the property. + /// + /// + /// To flag a region of the View's to be redrawn call . To flag the entire view + /// for redraw call . + /// + /// + /// Views have a property that defines the default colors that subviews + /// should use for rendering. This ensures that the views fit in the context where + /// they are being used, and allows for themes to be plugged in. For example, the + /// default colors for windows and toplevels uses a blue background, while it uses + /// a white background for dialog boxes and a red background for errors. + /// + /// + /// Subclasses should not rely on being + /// set at construction time. If a is not set on a view, the view will inherit the + /// value from its and the value might only be valid once a view has been + /// added to a SuperView. + /// + /// + /// By using applications will work both + /// in color as well as black and white displays. + /// + /// + /// Views that are focusable should implement the to make sure that + /// the cursor is placed in a location that makes sense. Unix terminals do not have + /// a way of hiding the cursor, so it can be distracting to have the cursor left at + /// the last focused view. So views should make sure that they place the cursor + /// in a visually sensible place. + /// + /// + /// The method is invoked when the size or layout of a view has + /// changed. The default processing system will keep the size and dimensions + /// for views that use the , and will recompute the + /// frames for the vies that use . + /// + /// + public class View : Responder, IEnumerable { + internal enum Direction { + Forward, + Backward + } + + // container == SuperView + View container = null; + View focused = null; + Direction focusDirection; + + /// + /// Event fired when the view gets focus. + /// + public Action Enter; + + /// + /// Event fired when the view looses focus. + /// + public Action Leave; + + /// + /// Event fired when the view receives the mouse event for the first time. + /// + public Action MouseEnter; + + /// + /// Event fired when the view receives a mouse event for the last time. + /// + public Action MouseLeave; + + /// + /// Event fired when a mouse event is generated. + /// + public Action MouseClick; + + internal Direction FocusDirection { + get => SuperView?.FocusDirection ?? focusDirection; + set { + if (SuperView != null) + SuperView.FocusDirection = value; + else + focusDirection = value; + } + } + + /// + /// Points to the current driver in use by the view, it is a convenience property + /// for simplifying the development of new views. + /// + public static ConsoleDriver Driver { get { return Application.Driver; } } + + static IList empty = new List (0).AsReadOnly (); + + // This is null, and allocated on demand. + List subviews; + + /// + /// This returns a list of the subviews contained by this view. + /// + /// The subviews. + public IList Subviews => subviews == null ? empty : subviews.AsReadOnly (); + + // Internally, we use InternalSubviews rather than subviews, as we do not expect us + // to make the same mistakes our users make when they poke at the Subviews. + internal IList InternalSubviews => subviews ?? empty; + + internal Rect NeedDisplay { get; private set; } = Rect.Empty; + + // The frame for the object. Superview relative. + Rect frame; + + /// + /// Gets or sets an identifier for the view; + /// + /// The identifier. + /// The id should be unique across all Views that share a SuperView. + public ustring Id { get; set; } = ""; + + /// + /// Returns a value indicating if this View is currently on Top (Active) + /// + public bool IsCurrentTop { + get { + return Application.Current == this; + } + } + + /// + /// Gets or sets a value indicating whether this wants mouse position reports. + /// + /// true if want mouse position reports; otherwise, false. + public virtual bool WantMousePositionReports { get; set; } = false; + + /// + /// Gets or sets a value indicating whether this want continuous button pressed event. + /// + public virtual bool WantContinuousButtonPressed { get; set; } = false; + + /// + /// Gets or sets the frame for the view. The frame is relative to the view's container (). + /// + /// The frame. + /// + /// + /// Change the Frame when using the layout style to move or resize views. + /// + /// + /// Altering the Frame of a view will trigger the redrawing of the + /// view as well as the redrawing of the affected regions of the . + /// + /// + public virtual Rect Frame { + get => frame; + set { + if (SuperView != null) { + SuperView.SetNeedsDisplay (frame); + SuperView.SetNeedsDisplay (value); + } + frame = value; + + SetNeedsLayout (); + SetNeedsDisplay (frame); + } + } + + /// + /// Gets an enumerator that enumerates the subviews in this view. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + foreach (var v in InternalSubviews) + yield return v; + } + + LayoutStyle layoutStyle; + + /// + /// Controls how the View's is computed during the LayoutSubviews method, if the style is set to , + /// LayoutSubviews does not change the . If the style is the is updated using + /// the , , , and properties. + /// + /// The layout style. + public LayoutStyle LayoutStyle { + get => layoutStyle; + set { + layoutStyle = value; + SetNeedsLayout (); + } + } + + /// + /// The bounds represent the View-relative rectangle used for this view; the area inside of the view. + /// + /// The bounds. + /// + /// + /// Updates to the Bounds update the , + /// and has the same side effects as updating the . + /// + /// + /// Because coordinates are relative to the upper-left corner of the , + /// the coordinates of the upper-left corner of the rectangle returned by this property are (0,0). + /// Use this property to obtain the size and coordinates of the client area of the + /// control for tasks such as drawing on the surface of the control. + /// + /// + public Rect Bounds { + get => new Rect (Point.Empty, Frame.Size); + set { + Frame = new Rect (frame.Location, value.Size); + } + } + + Pos x, y; + + /// + /// Gets or sets the X position for the view (the column). Only used whe is . + /// + /// The X Position. + /// + /// If is changing this property has no effect and its value is indeterminate. + /// + public Pos X { + get => x; + set { + x = value; + SetNeedsLayout (); + SetNeedsDisplay (frame); + } + } + + /// + /// Gets or sets the Y position for the view (the row). Only used whe is . + /// + /// The y position (line). + /// + /// If is changing this property has no effect and its value is indeterminate. + /// + public Pos Y { + get => y; + set { + y = value; + SetNeedsLayout (); + SetNeedsDisplay (frame); + } + } + + Dim width, height; + + /// + /// Gets or sets the width of the view. Only used whe is . + /// + /// The width. + /// + /// If is changing this property has no effect and its value is indeterminate. + /// + public Dim Width { + get => width; + set { + width = value; + SetNeedsLayout (); + SetNeedsDisplay (frame); + } + } + + /// + /// Gets or sets the height of the view. Only used whe is . + /// + /// The height. + /// If is changing this property has no effect and its value is indeterminate. + public Dim Height { + get => height; + set { + height = value; + SetNeedsLayout (); + SetNeedsDisplay (frame); + } + } + + /// + /// Returns the container for this view, or null if this view has not been added to a container. + /// + /// The super view. + public View SuperView => container; + + /// + /// Initializes a new instance of a class with the absolute + /// dimensions specified in the frame parameter. + /// + /// The region covered by this view. + /// + /// This constructor intitalize a View with a of . Use to + /// initialize a View with of + /// + public View (Rect frame) + { + this.Frame = frame; + CanFocus = false; + LayoutStyle = LayoutStyle.Absolute; + } + + /// + /// Initializes a new instance of class. + /// + /// + /// Use , , , and properties to dynamically control the size and location of the view. + /// + /// + /// This constructor intitalize a View with a of . + /// Use , , , and properties to dynamically control the size and location of the view. + /// + public View () + { + CanFocus = false; + LayoutStyle = LayoutStyle.Computed; + x = Pos.At (0); + y = Pos.At (0); + Height = 0; + Width = 0; + } + + /// + /// Sets a flag indicating this view needs to be redisplayed because its state has changed. + /// + public void SetNeedsDisplay () + { + SetNeedsDisplay (Bounds); + } + + internal bool layoutNeeded = true; + + internal void SetNeedsLayout () + { + if (layoutNeeded) + return; + layoutNeeded = true; + if (SuperView == null) + return; + SuperView.SetNeedsLayout (); + } + + /// + /// Flags the view-relative region on this View as needing to be repainted. + /// + /// The view-relative region that must be flagged for repaint. + public void SetNeedsDisplay (Rect region) + { + if (NeedDisplay == null || NeedDisplay.IsEmpty) + NeedDisplay = region; + else { + var x = Math.Min (NeedDisplay.X, region.X); + var y = Math.Min (NeedDisplay.Y, region.Y); + var w = Math.Max (NeedDisplay.Width, region.Width); + var h = Math.Max (NeedDisplay.Height, region.Height); + NeedDisplay = new Rect (x, y, w, h); + } + if (container != null) + container.ChildNeedsDisplay (); + if (subviews == null) + return; + foreach (var view in subviews) + if (view.Frame.IntersectsWith (region)) { + var childRegion = Rect.Intersect (view.Frame, region); + childRegion.X -= view.Frame.X; + childRegion.Y -= view.Frame.Y; + view.SetNeedsDisplay (childRegion); + } + } + + internal bool childNeedsDisplay; + + /// + /// Indicates that any child views (in the list) need to be repainted. + /// + public void ChildNeedsDisplay () + { + childNeedsDisplay = true; + if (container != null) + container.ChildNeedsDisplay (); + } + + /// + /// Adds a subview (child) to this view. + /// + /// + /// The Views that have been added to this view can be retrieved via the property. See also + /// + public virtual void Add (View view) + { + if (view == null) + return; + if (subviews == null) + subviews = new List (); + subviews.Add (view); + view.container = this; + if (view.CanFocus) + CanFocus = true; + SetNeedsLayout (); + SetNeedsDisplay (); + } + + /// + /// Adds the specified views (children) to the view. + /// + /// Array of one or more views (can be optional parameter). + /// + /// The Views that have been added to this view can be retrieved via the property. See also + /// + public void Add (params View [] views) + { + if (views == null) + return; + foreach (var view in views) + Add (view); + } + + /// + /// Removes all subviews (children) added via or from this View. + /// + public virtual void RemoveAll () + { + if (subviews == null) + return; + + while (subviews.Count > 0) { + Remove (subviews [0]); + } + } + + /// + /// Removes a subview added via or from this View. + /// + /// + /// + public virtual void Remove (View view) + { + if (view == null || subviews == null) + return; + + SetNeedsLayout (); + SetNeedsDisplay (); + var touched = view.Frame; + subviews.Remove (view); + view.container = null; + + if (subviews.Count < 1) + this.CanFocus = false; + + foreach (var v in subviews) { + if (v.Frame.IntersectsWith (touched)) + view.SetNeedsDisplay (); + } + } + + void PerformActionForSubview (View subview, Action action) + { + if (subviews.Contains (subview)) { + action (subview); + } + + SetNeedsDisplay (); + subview.SetNeedsDisplay (); + } + + /// + /// Brings the specified subview to the front so it is drawn on top of any other views. + /// + /// The subview to send to the front + /// + /// . + /// + public void BringSubviewToFront (View subview) + { + PerformActionForSubview (subview, x => { + subviews.Remove (x); + subviews.Add (x); + }); + } + + /// + /// Sends the specified subview to the front so it is the first view drawn + /// + /// The subview to send to the front + /// + /// . + /// + public void SendSubviewToBack (View subview) + { + PerformActionForSubview (subview, x => { + subviews.Remove (x); + subviews.Insert (0, subview); + }); + } + + /// + /// Moves the subview backwards in the hierarchy, only one step + /// + /// The subview to send backwards + /// + /// If you want to send the view all the way to the back use SendSubviewToBack. + /// + public void SendSubviewBackwards (View subview) + { + PerformActionForSubview (subview, x => { + var idx = subviews.IndexOf (x); + if (idx > 0) { + subviews.Remove (x); + subviews.Insert (idx - 1, x); + } + }); + } + + /// + /// Moves the subview backwards in the hierarchy, only one step + /// + /// The subview to send backwards + /// + /// If you want to send the view all the way to the back use SendSubviewToBack. + /// + public void BringSubviewForward (View subview) + { + PerformActionForSubview (subview, x => { + var idx = subviews.IndexOf (x); + if (idx + 1 < subviews.Count) { + subviews.Remove (x); + subviews.Insert (idx + 1, x); + } + }); + } + + /// + /// Clears the view region with the current color. + /// + /// + /// + /// This clears the entire region used by this view. + /// + /// + public void Clear () + { + var h = Frame.Height; + var w = Frame.Width; + for (int line = 0; line < h; line++) { + Move (0, line); + for (int col = 0; col < w; col++) + Driver.AddRune (' '); + } + } + + /// + /// Clears the specified region with the current color. + /// + /// + /// + /// The screen-relative region to clear. + public void Clear (Rect regionScreen) + { + var h = regionScreen.Height; + var w = regionScreen.Width; + for (int line = regionScreen.Y; line < regionScreen.Y + h; line++) { + Driver.Move (regionScreen.X, line); + for (int col = 0; col < w; col++) + Driver.AddRune (' '); + } + } + + /// + /// Converts a view-relative (col,row) position to a screen-relative position (col,row). The values are optionally clamped to the screen dimensions. + /// + /// View-relative column. + /// View-relative row. + /// Absolute column; screen-relative. + /// Absolute row; screen-relative. + /// Whether to clip the result of the ViewToScreen method, if set to true, the rcol, rrow values are clamped to the screen (terminal) dimensions (0..TerminalDim-1). + internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true) + { + // Computes the real row, col relative to the screen. + rrow = row + frame.Y; + rcol = col + frame.X; + var ccontainer = container; + while (ccontainer != null) { + rrow += ccontainer.frame.Y; + rcol += ccontainer.frame.X; + ccontainer = ccontainer.container; + } + + // The following ensures that the cursor is always in the screen boundaries. + if (clipped) { + rrow = Math.Min (rrow, Driver.Rows - 1); + rcol = Math.Min (rcol, Driver.Cols - 1); + } + } + + /// + /// Converts a point from screen-relative coordinates to view-relative coordinates. + /// + /// The mapped point. + /// X screen-coordinate point. + /// Y screen-coordinate point. + public Point ScreenToView (int x, int y) + { + if (SuperView == null) { + return new Point (x - Frame.X, y - frame.Y); + } else { + var parent = SuperView.ScreenToView (x, y); + return new Point (parent.X - frame.X, parent.Y - frame.Y); + } + } + + /// + /// Converts a region in view-relative coordinates to screen-relative coordinates. + /// + internal Rect ViewToScreen (Rect region) + { + ViewToScreen (region.X, region.Y, out var x, out var y, clipped: false); + return new Rect (x, y, region.Width, region.Height); + } + + // Clips a rectangle in screen coordinates to the dimensions currently available on the screen + internal Rect ScreenClip (Rect regionScreen) + { + var x = regionScreen.X < 0 ? 0 : regionScreen.X; + var y = regionScreen.Y < 0 ? 0 : regionScreen.Y; + var w = regionScreen.X + regionScreen.Width >= Driver.Cols ? Driver.Cols - regionScreen.X : regionScreen.Width; + var h = regionScreen.Y + regionScreen.Height >= Driver.Rows ? Driver.Rows - regionScreen.Y : regionScreen.Height; + + return new Rect (x, y, w, h); + } + + /// + /// Sets the 's clip region to the current View's . + /// + /// The existing driver's clip region, which can be then re-eapplied by setting .Clip (). + /// + /// is View-relative. + /// + public Rect ClipToBounds () + { + return SetClip (Bounds); + } + + /// + /// Sets the clip region to the specified view-relative region. + /// + /// The previous screen-relative clip region. + /// View-relative clip region. + public Rect SetClip (Rect region) + { + var previous = Driver.Clip; + Driver.Clip = Rect.Intersect (previous, ViewToScreen (region)); + return previous; + } + + /// + /// Draws a frame in the current view, clipped by the boundary of this view + /// + /// View-relative region for the frame to be drawn. + /// The padding to add around the outside of the drawn frame. + /// If set to true it fill will the contents. + public void DrawFrame (Rect region, int padding = 0, bool fill = false) + { + var scrRect = ViewToScreen (region); + var savedClip = ClipToBounds (); + Driver.DrawWindowFrame (scrRect, padding + 1, padding + 1, padding + 1, padding + 1, border: true, fill: fill); + Driver.Clip = savedClip; + } + + /// + /// Utility function to draw strings that contain a hotkey. + /// + /// String to display, the underscoore before a letter flags the next letter as the hotkey. + /// Hot color. + /// Normal color. + /// + /// The hotkey is any character following an underscore ('_') character. + public void DrawHotString (ustring text, Attribute hotColor, Attribute normalColor) + { + Driver.SetAttribute (normalColor); + foreach (var rune in text) { + if (rune == '_') { + Driver.SetAttribute (hotColor); + continue; + } + Driver.AddRune (rune); + Driver.SetAttribute (normalColor); + } + } + + /// + /// Utility function to draw strings that contains a hotkey using a and the "focused" state. + /// + /// String to display, the underscoore before a letter flags the next letter as the hotkey. + /// If set to true this uses the focused colors from the color scheme, otherwise the regular ones. + /// The color scheme to use. + public void DrawHotString (ustring text, bool focused, ColorScheme scheme) + { + if (focused) + DrawHotString (text, scheme.HotFocus, scheme.Focus); + else + DrawHotString (text, scheme.HotNormal, scheme.Normal); + } + + /// + /// This moves the cursor to the specified column and row in the view. + /// + /// The move. + /// Col. + /// Row. + public void Move (int col, int row) + { + ViewToScreen (col, row, out var rcol, out var rrow); + Driver.Move (rcol, rrow); + } + + /// + /// Positions the cursor in the right position based on the currently focused view in the chain. + /// + /// Views that are focusable should override to ensure + /// the cursor is placed in a location that makes sense. Unix terminals do not have + /// a way of hiding the cursor, so it can be distracting to have the cursor left at + /// the last focused view. Views should make sure that they place the cursor + /// in a visually sensible place. + public virtual void PositionCursor () + { + if (focused != null) + focused.PositionCursor (); + else + Move (frame.X, frame.Y); + } + + /// + public override bool HasFocus { + get { + return base.HasFocus; + } + internal set { + if (base.HasFocus != value) + if (value) + OnEnter (); + else + OnLeave (); + SetNeedsDisplay (); + base.HasFocus = value; + + // Remove focus down the chain of subviews if focus is removed + if (!value && focused != null) { + focused.OnLeave (); + focused.HasFocus = false; + focused = null; + } + } + } + + /// + /// Defines the event arguments for + /// + public class FocusEventArgs : EventArgs { + /// + /// Constructs. + /// + public FocusEventArgs () { } + /// + /// Indicates if the current focus event has already been processed and the driver should stop notifying any other event subscriber. + /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. + /// + public bool Handled { get; set; } + } + + /// + public override bool OnEnter () + { + FocusEventArgs args = new FocusEventArgs (); + Enter?.Invoke (args); + if (args.Handled) + return true; + if (base.OnEnter ()) + return true; + + return false; + } + + /// + public override bool OnLeave () + { + FocusEventArgs args = new FocusEventArgs (); + Leave?.Invoke (args); + if (args.Handled) + return true; + if (base.OnLeave ()) + return true; + + return false; + } + + /// + /// Returns the currently focused view inside this view, or null if nothing is focused. + /// + /// The focused. + public View Focused => focused; + + /// + /// Returns the most focused view in the chain of subviews (the leaf view that has the focus). + /// + /// The most focused. + public View MostFocused { + get { + if (Focused == null) + return null; + var most = Focused.MostFocused; + if (most != null) + return most; + return Focused; + } + } + + /// + /// The color scheme for this view, if it is not defined, it returns the 's + /// color scheme. + /// + public ColorScheme ColorScheme { + get { + if (colorScheme == null) + return SuperView?.ColorScheme; + return colorScheme; + } + set { + colorScheme = value; + } + } + + ColorScheme colorScheme; + + /// + /// Displays the specified character in the specified column and row of the View. + /// + /// Column (view-relative). + /// Row (view-relative). + /// Ch. + public void AddRune (int col, int row, Rune ch) + { + if (row < 0 || col < 0) + return; + if (row > frame.Height - 1 || col > frame.Width - 1) + return; + Move (col, row); + Driver.AddRune (ch); + } + + /// + /// Removes the and the setting on this view. + /// + protected void ClearNeedsDisplay () + { + NeedDisplay = Rect.Empty; + childNeedsDisplay = false; + } + + /// + /// Redraws this view and its subviews; only redraws the views that have been flagged for a re-display. + /// + /// The bounds (view-relative region) to redraw. + /// + /// + /// Always use (view-relative) when calling , NOT (superview-relative). + /// + /// + /// Views should set the color that they want to use on entry, as otherwise this will inherit + /// the last color that was set globaly on the driver. + /// + /// + /// Overrides of must ensure they do not set Driver.Clip to a clip region + /// larger than the region parameter. + /// + /// + public virtual void Redraw (Rect bounds) + { + var clipRect = new Rect (Point.Empty, frame.Size); + + // Invoke DrawContentEvent + OnDrawContent (bounds); + + if (subviews != null) { + foreach (var view in subviews) { + if (view.NeedDisplay != null && (!view.NeedDisplay.IsEmpty || view.childNeedsDisplay)) { + if (view.Frame.IntersectsWith (clipRect) && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) { + + // FIXED: optimize this by computing the intersection of region and view.Bounds + if (view.layoutNeeded) + view.LayoutSubviews (); + Application.CurrentView = view; + + // Draw the subview + // Use the view's bounds (view-relative; Location will always be (0,0) because + view.Redraw (view.Bounds); + + } + view.NeedDisplay = Rect.Empty; + view.childNeedsDisplay = false; + } + } + } + ClearNeedsDisplay (); + } + + /// + /// Event invoked when the content area of the View is to be drawn. + /// + /// + /// + /// Will be invoked before any subviews added with have been drawn. + /// + /// + /// Rect provides the view-relative rectangle describing the currently visible viewport into the . + /// + /// + public Action DrawContent; + + /// + /// Enables overrides to draw infinitely scrolled content and/or a background behind added controls. + /// + /// The view-relative rectangle describing the currently visible viewport into the + /// + /// This method will be called before any subviews added with have been drawn. + /// + public virtual void OnDrawContent (Rect viewport) + { + DrawContent?.Invoke (viewport); + } + + /// + /// Causes the specified subview to have focus. + /// + /// View. + public void SetFocus (View view) + { + if (view == null) + return; + //Console.WriteLine ($"Request to focus {view}"); + if (!view.CanFocus) + return; + if (focused == view) + return; + + // Make sure that this view is a subview + View c; + for (c = view.container; c != null; c = c.container) + if (c == this) + break; + if (c == null) + throw new ArgumentException ("the specified view is not part of the hierarchy of this view"); + + if (focused != null) + focused.HasFocus = false; + + focused = view; + focused.HasFocus = true; + focused.EnsureFocus (); + + // Send focus upwards + SuperView?.SetFocus (this); + } + + /// + /// Defines the event arguments for + /// + public class KeyEventEventArgs : EventArgs { + /// + /// Constructs. + /// + /// + public KeyEventEventArgs (KeyEvent ke) => KeyEvent = ke; + /// + /// The for the event. + /// + public KeyEvent KeyEvent { get; set; } + /// + /// Indicates if the current Key event has already been processed and the driver should stop notifying any other event subscriber. + /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. + /// + public bool Handled { get; set; } = false; + } + + /// + /// Invoked when a character key is pressed and occurs after the key up event. + /// + public Action KeyPress; + + /// + public override bool ProcessKey (KeyEvent keyEvent) + { + + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyPress?.Invoke (args); + if (args.Handled) + return true; + if (Focused?.ProcessKey (keyEvent) == true) + return true; + + return false; + } + + /// + public override bool ProcessHotKey (KeyEvent keyEvent) + { + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyPress?.Invoke (args); + if (args.Handled) + return true; + if (subviews == null || subviews.Count == 0) + return false; + foreach (var view in subviews) + if (view.SuperView.IsCurrentTop && view.ProcessHotKey (keyEvent)) + return true; + return false; + } + + /// + public override bool ProcessColdKey (KeyEvent keyEvent) + { + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyPress?.Invoke (args); + if (args.Handled) + return true; + if (subviews == null || subviews.Count == 0) + return false; + foreach (var view in subviews) + if (view.SuperView.IsCurrentTop && view.ProcessColdKey (keyEvent)) + return true; + return false; + } + + /// + /// Invoked when a key is pressed + /// + public Action KeyDown; + + /// Contains the details about the key that produced the event. + public override bool OnKeyDown (KeyEvent keyEvent) + { + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyDown?.Invoke (args); + if (args.Handled) + return true; + if (subviews == null || subviews.Count == 0) + return false; + foreach (var view in subviews) + if (view.SuperView.IsCurrentTop && view.OnKeyDown (keyEvent)) + return true; + + return false; + } + + /// + /// Invoked when a key is released + /// + public Action KeyUp; + + /// Contains the details about the key that produced the event. + public override bool OnKeyUp (KeyEvent keyEvent) + { + KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); + KeyUp?.Invoke (args); + if (args.Handled) + return true; + if (subviews == null || subviews.Count == 0) + return false; + foreach (var view in subviews) + if (view.SuperView.IsCurrentTop && view.OnKeyUp (keyEvent)) + return true; + + return false; + } + + /// + /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, it does nothing. + /// + public void EnsureFocus () + { + if (focused == null) + if (FocusDirection == Direction.Forward) + FocusFirst (); + else + FocusLast (); + } + + /// + /// Focuses the first focusable subview if one exists. + /// + public void FocusFirst () + { + if (subviews == null) { + SuperView?.SetFocus (this); + return; + } + + foreach (var view in subviews) { + if (view.CanFocus) { + SetFocus (view); + return; + } + } + } + + /// + /// Focuses the last focusable subview if one exists. + /// + public void FocusLast () + { + if (subviews == null) { + SuperView?.SetFocus (this); + return; + } + + for (int i = subviews.Count; i > 0;) { + i--; + + View v = subviews [i]; + if (v.CanFocus) { + SetFocus (v); + return; + } + } + } + + /// + /// Focuses the previous view. + /// + /// true, if previous was focused, false otherwise. + public bool FocusPrev () + { + FocusDirection = Direction.Backward; + if (subviews == null || subviews.Count == 0) + return false; + + if (focused == null) { + FocusLast (); + return focused != null; + } + int focused_idx = -1; + for (int i = subviews.Count; i > 0;) { + i--; + View w = subviews [i]; + + if (w.HasFocus) { + if (w.FocusPrev ()) + return true; + focused_idx = i; + continue; + } + if (w.CanFocus && focused_idx != -1) { + focused.HasFocus = false; + + if (w != null && w.CanFocus) + w.FocusLast (); + + SetFocus (w); + return true; + } + } + if (focused != null) { + focused.HasFocus = false; + focused = null; + } + return false; + } + + /// + /// Focuses the next view. + /// + /// true, if next was focused, false otherwise. + public bool FocusNext () + { + FocusDirection = Direction.Forward; + if (subviews == null || subviews.Count == 0) + return false; + + if (focused == null) { + FocusFirst (); + return focused != null; + } + int n = subviews.Count; + int focused_idx = -1; + for (int i = 0; i < n; i++) { + View w = subviews [i]; + + if (w.HasFocus) { + if (w.FocusNext ()) + return true; + focused_idx = i; + continue; + } + if (w.CanFocus && focused_idx != -1) { + focused.HasFocus = false; + + if (w != null && w.CanFocus) + w.FocusFirst (); + + SetFocus (w); + return true; + } + } + if (focused != null) { + focused.HasFocus = false; + focused = null; + } + return false; + } + + /// + /// Sets the View's to the relative coordinates if its container, given the for its container. + /// + /// The screen-relative frame for the host. + /// + /// Reminder: is superview-relative; is view-relative. + /// + internal void SetRelativeLayout (Rect hostFrame) + { + int w, h, _x, _y; + + if (x is Pos.PosCenter) { + if (width == null) + w = hostFrame.Width; + else + w = width.Anchor (hostFrame.Width); + _x = x.Anchor (hostFrame.Width - w); + } else { + if (x == null) + _x = 0; + else + _x = x.Anchor (hostFrame.Width); + if (width == null) + w = hostFrame.Width; + else if (width is Dim.DimFactor && !((Dim.DimFactor)width).IsFromRemaining ()) + w = width.Anchor (hostFrame.Width); + else + w = Math.Max (width.Anchor (hostFrame.Width - _x), 0); + } + + if (y is Pos.PosCenter) { + if (height == null) + h = hostFrame.Height; + else + h = height.Anchor (hostFrame.Height); + _y = y.Anchor (hostFrame.Height - h); + } else { + if (y == null) + _y = 0; + else + _y = y.Anchor (hostFrame.Height); + if (height == null) + h = hostFrame.Height; + else if (height is Dim.DimFactor && !((Dim.DimFactor)height).IsFromRemaining ()) + h = height.Anchor (hostFrame.Height); + else + h = Math.Max (height.Anchor (hostFrame.Height - _y), 0); + } + Frame = new Rect (_x, _y, w, h); + } + + // https://en.wikipedia.org/wiki/Topological_sorting + List TopologicalSort (HashSet nodes, HashSet<(View From, View To)> edges) + { + var result = new List (); + + // Set of all nodes with no incoming edges + var S = new HashSet (nodes.Where (n => edges.All (e => e.To.Equals (n) == false))); + + while (S.Any ()) { + // remove a node n from S + var n = S.First (); + S.Remove (n); + + // add n to tail of L + if (n != this?.SuperView) + result.Add (n); + + // for each node m with an edge e from n to m do + foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) { + var m = e.To; + + // remove edge e from the graph + edges.Remove (e); + + // if m has no other incoming edges then + if (edges.All (me => me.To.Equals (m) == false) && m != this?.SuperView) { + // insert m into S + S.Add (m); + } + } + } + + if (edges.Any ()) { + if (!object.ReferenceEquals(edges.First ().From, edges.First ().To)) { + throw new InvalidOperationException ($"TopologicalSort (for Pos/Dim) cannot find {edges.First ().From}. Did you forget to add it to {this}?"); + } else { + throw new InvalidOperationException ("TopologicalSort encountered a recursive cycle in the relative Pos/Dim in the views of " + this); + } + } else { + // return L (a topologically sorted order) + return result; + } + } + + /// + /// Event arguments for the event. + /// + public class LayoutEventArgs : EventArgs { + /// + /// The view-relative bounds of the before it was laid out. + /// + public Rect OldBounds { get; set; } + } + + /// + /// Fired after the Views's method has completed. + /// + /// + /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed. + /// + public Action LayoutComplete; + + /// + /// Raises the event. Called from after all sub-views have been laid out. + /// + internal virtual void OnLayoutComplete (LayoutEventArgs args) + { + LayoutComplete?.Invoke (args); + } + + /// + /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in + /// response to the container view or terminal resizing. + /// + /// + /// Calls (which raises the event) before it returns. + /// + public virtual void LayoutSubviews () + { + if (!layoutNeeded) + return; + + Rect oldBounds = Bounds; + + // Sort out the dependencies of the X, Y, Width, Height properties + var nodes = new HashSet (); + var edges = new HashSet<(View, View)> (); + + foreach (var v in InternalSubviews) { + nodes.Add (v); + if (v.LayoutStyle == LayoutStyle.Computed) { + if (v.X is Pos.PosView vX) + edges.Add ((vX.Target, v)); + if (v.Y is Pos.PosView vY) + edges.Add ((vY.Target, v)); + if (v.Width is Dim.DimView vWidth) + edges.Add ((vWidth.Target, v)); + if (v.Height is Dim.DimView vHeight) + edges.Add ((vHeight.Target, v)); + } + } + + var ordered = TopologicalSort (nodes, edges); + + foreach (var v in ordered) { + if (v.LayoutStyle == LayoutStyle.Computed) + v.SetRelativeLayout (Frame); + + v.LayoutSubviews (); + v.layoutNeeded = false; + + } + + if (SuperView == Application.Top && layoutNeeded && ordered.Count == 0 && LayoutStyle == LayoutStyle.Computed) { + SetRelativeLayout (Frame); + } + + layoutNeeded = false; + + OnLayoutComplete (new LayoutEventArgs () { OldBounds = oldBounds }); + } + + /// + /// A generic virtual method at the level of View to manipulate any hot-keys. + /// + /// The text to manipulate. + /// The hot-key to look for. + /// The returning hot-key position. + /// The character immediately to the right relative to the hot-key position + /// It aims to facilitate the preparation for procedures. + public virtual ustring GetTextFromHotKey (ustring text, Rune hotKey, out int hotPos, out Rune showHotKey) + { + Rune hot_key = (Rune)0; + int hot_pos = -1; + ustring shown_text = text; + + // Use first hot_key char passed into 'hotKey'. + int i = 0; + foreach (Rune c in shown_text) { + if ((char)c != 0xFFFD) { + if (c == hotKey) { + hot_pos = i; + } else if (hot_pos > -1) { + hot_key = c; + break; + } + } + i++; + } + + if (hot_pos == -1) { + // Use first upper-case char if there are no hot-key in the text. + i = 0; + foreach (Rune c in shown_text) { + if ((char)c != 0xFFFD) { + if (Rune.IsUpper (c)) { + hot_key = c; + hot_pos = i; + break; + } + } + i++; + } + } else { + // Use char after 'hotKey' + ustring start = ""; + i = 0; + foreach (Rune c in shown_text) { + start += ustring.Make (c); + i++; + if (i == hot_pos) + break; + } + var st = shown_text; + shown_text = start; + i = 0; + foreach (Rune c in st) { + i++; + if (i > hot_pos + 1) { + shown_text += ustring.Make (c); + } + } + } + hotPos = hot_pos; + showHotKey = hot_key; + return shown_text; + } + + /// + /// A generic virtual method at the level of View to manipulate any hot-keys with process. + /// + /// The text to manipulate to align. + /// The passed in hot-key position. + /// The returning hot-key position. + /// The to align to. + /// It performs the process to the caller. + public virtual ustring GetTextAlignment (ustring shown_text, int hot_pos, out int c_hot_pos, TextAlignment textAlignment) + { + int start; + var caption = shown_text; + c_hot_pos = hot_pos; + + if (Frame.Width > shown_text.Length + 1) { + switch (textAlignment) { + case TextAlignment.Left: + caption += new string (' ', Frame.Width - caption.RuneCount); + break; + case TextAlignment.Right: + start = Frame.Width - caption.RuneCount; + caption = $"{new string (' ', Frame.Width - caption.RuneCount)}{caption}"; + if (c_hot_pos > -1) { + c_hot_pos += start; + } + break; + case TextAlignment.Centered: + start = Frame.Width / 2 - caption.RuneCount / 2; + caption = $"{new string (' ', start)}{caption}{new string (' ', Frame.Width - caption.RuneCount - start)}"; + if (c_hot_pos > -1) { + c_hot_pos += start; + } + break; + case TextAlignment.Justified: + var words = caption.Split (" "); + var wLen = GetWordsLength (words, c_hot_pos, out int runeCount, out int w_hot_pos); + var space = (Frame.Width - runeCount) / (caption.Length - wLen); + caption = ""; + for (int i = 0; i < words.Length; i++) { + if (i == words.Length - 1) { + caption += new string (' ', Frame.Width - caption.RuneCount - 1); + caption += words [i]; + } else { + caption += words [i]; + } + if (i < words.Length - 1) { + caption += new string (' ', space); + } + } + if (c_hot_pos > -1) { + c_hot_pos += w_hot_pos * space - space - w_hot_pos + 1; + } + break; + } + } + + return caption; + } + + int GetWordsLength (ustring [] words, int hotPos, out int runeCount, out int wordHotPos) + { + int length = 0; + int rCount = 0; + int wHotPos = -1; + for (int i = 0; i < words.Length; i++) { + if (wHotPos == -1 && rCount + words [i].RuneCount >= hotPos) + wHotPos = i; + length += words [i].Length; + rCount += words [i].RuneCount; + } + if (wHotPos == -1 && hotPos > -1) + wHotPos = words.Length; + runeCount = rCount; + wordHotPos = wHotPos; + return length; + } + + /// + /// Pretty prints the View + /// + /// + public override string ToString () + { + return $"{GetType ().Name}({Id})({Frame})"; + } + + /// + /// Specifies the event arguments for + /// + public class MouseEventArgs : EventArgs { + /// + /// Constructs. + /// + /// + public MouseEventArgs (MouseEvent me) => MouseEvent = me; + /// + /// The for the event. + /// + public MouseEvent MouseEvent { get; set; } + /// + /// Indicates if the current mouse event has already been processed and the driver should stop notifying any other event subscriber. + /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. + /// + public bool Handled { get; set; } + } + + /// + public override bool OnMouseEnter (MouseEvent mouseEvent) + { + MouseEventArgs args = new MouseEventArgs (mouseEvent); + MouseEnter?.Invoke (args); + if (args.Handled) + return true; + if (base.OnMouseEnter (mouseEvent)) + return true; + + return false; + } + + /// + public override bool OnMouseLeave (MouseEvent mouseEvent) + { + MouseEventArgs args = new MouseEventArgs (mouseEvent); + MouseLeave?.Invoke (args); + if (args.Handled) + return true; + if (base.OnMouseLeave (mouseEvent)) + return true; + + return false; + } + + /// + /// Method invoked when a mouse event is generated + /// + /// + /// true, if the event was handled, false otherwise. + public virtual bool OnMouseEvent (MouseEvent mouseEvent) + { + MouseEventArgs args = new MouseEventArgs (mouseEvent); + MouseClick?.Invoke (args); + if (args.Handled) + return true; + if (MouseEvent (mouseEvent)) + return true; + + return false; + } + } +} diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs new file mode 100644 index 0000000..1cd4be5 --- /dev/null +++ b/Terminal.Gui/Core/Window.cs @@ -0,0 +1,267 @@ +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System.Collections; +using NStack; + +namespace Terminal.Gui { + /// + /// A that draws a border around its with a at the top. + /// + /// + /// The 'client area' of a is a rectangle deflated by one or more rows/columns from . A this time there is no + /// API to determine this rectangle. + /// + public class Window : Toplevel, IEnumerable { + View contentView; + ustring title; + + /// + /// The title to be displayed for this window. + /// + /// The title + public ustring Title { + get => title; + set { + title = value; + SetNeedsDisplay (); + } + } + + + /// + /// ContentView is an internal implementation detail of Window. It is used to host Views added with . + /// Its ONLY reason for being is to provide a simple way for Window to expose to those SubViews that the Window's Bounds + /// are actually deflated due to the border. + /// + class ContentView : View { + public ContentView (Rect frame) : base (frame) { } + public ContentView () : base () { } +#if false + public override void Redraw (Rect bounds) + { + Driver.SetAttribute (ColorScheme.Focus); + + for (int y = 0; y < Frame.Height; y++) { + Move (0, y); + for (int x = 0; x < Frame.Width; x++) { + + Driver.AddRune ('x'); + } + } + base.Redraw (region); + } +#endif + } + + /// + /// Initializes a new instance of the class with an optional title using positioning. + /// + /// Superview-relatie rectangle specifying the location and size + /// Title + /// + /// This constructor intitalizes a Window with a of . Use constructors + /// that do not take Rect parameters to initialize a Window with . + /// + public Window (Rect frame, ustring title = null) : this (frame, title, padding: 0) + { + } + + /// + /// Initializes a new instance of the class with an optional title using positioning. + /// + /// Title. + /// + /// This constructor intitalize a View with a of . + /// Use , , , and properties to dynamically control the size and location of the view. + /// + public Window (ustring title = null) : this (title, padding: 0) + { + } + + /// + /// Initializes a new instance of the class using positioning. + /// + public Window () : this (title: null) { } + + int padding; + /// + /// Initializes a new instance of the using positioning with the specified frame for its location, with the specified frame padding, + /// and an optional title. + /// + /// Superview-relatie rectangle specifying the location and size + /// Number of characters to use for padding of the drawn frame. + /// Title + /// + /// This constructor intitalizes a Window with a of . Use constructors + /// that do not take Rect parameters to initialize a Window with of + /// + public Window (Rect frame, ustring title = null, int padding = 0) : base (frame) + { + this.Title = title; + int wb = 2 * (1 + padding); + this.padding = padding; + var cFrame = new Rect (1 + padding, 1 + padding, frame.Width - wb, frame.Height - wb); + contentView = new ContentView (cFrame); + base.Add (contentView); + } + + /// + /// Initializes a new instance of the using positioning with the specified frame for its location, with the specified frame padding, + /// and an optional title. + /// + /// Number of characters to use for padding of the drawn frame. + /// Title. + /// + /// This constructor intitalize a View with a of . + /// Use , , , and properties to dynamically control the size and location of the view. + /// + public Window (ustring title = null, int padding = 0) : base () + { + this.Title = title; + int wb = 1 + padding; + this.padding = padding; + contentView = new ContentView () { + X = wb, + Y = wb, + Width = Dim.Fill (wb), + Height = Dim.Fill (wb) + }; + base.Add (contentView); + } + + /// + /// Enumerates the various s in the embedded . + /// + /// The enumerator. + public new IEnumerator GetEnumerator () + { + return contentView.GetEnumerator (); + } + + /// + public override void Add (View view) + { + contentView.Add (view); + if (view.CanFocus) + CanFocus = true; + } + + + /// + public override void Remove (View view) + { + if (view == null) + return; + + SetNeedsDisplay (); + var touched = view.Frame; + contentView.Remove (view); + + if (contentView.InternalSubviews.Count < 1) + this.CanFocus = false; + } + + /// + public override void RemoveAll () + { + contentView.RemoveAll (); + } + + /// + public override void Redraw (Rect bounds) + { + //var padding = 0; + Application.CurrentView = this; + var scrRect = ViewToScreen (new Rect (0, 0, Frame.Width, Frame.Height)); + + // BUGBUG: Why do we draw the frame twice? This call is here to clear the content area, I think. Why not just clear that area? + if (NeedDisplay != null && !NeedDisplay.IsEmpty) { + Driver.SetAttribute (ColorScheme.Normal); + Driver.DrawWindowFrame (scrRect, padding + 1, padding + 1, padding + 1, padding + 1, border: true, fill: true); + } + + var savedClip = ClipToBounds (); + + // Redraw our contenetView + // TODO: smartly constrict contentView.Bounds to just be what intersects with the 'bounds' we were passed + contentView.Redraw (contentView.Bounds); + Driver.Clip = savedClip; + + ClearNeedsDisplay (); + Driver.SetAttribute (ColorScheme.Normal); + Driver.DrawWindowFrame (scrRect, padding + 1, padding + 1, padding + 1, padding + 1, border: true, fill: false); + + if (HasFocus) + Driver.SetAttribute (ColorScheme.HotNormal); + Driver.DrawWindowTitle (scrRect, Title, padding, padding, padding, padding); + Driver.SetAttribute (ColorScheme.Normal); + } + + // + // FIXED:It does not look like the event is raised on clicked-drag + // need to figure that out. + // + internal static Point? dragPosition; + Point start; + + /// + public override bool MouseEvent (MouseEvent mouseEvent) + { + // FIXED:The code is currently disabled, because the + // Driver.UncookMouse does not seem to have an effect if there is + // a pending mouse event activated. + + int nx, ny; + if ((mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) || + mouseEvent.Flags == MouseFlags.Button3Pressed)) { + if (dragPosition.HasValue) { + if (SuperView == null) { + Application.Top.SetNeedsDisplay (Frame); + // Redraw the entire app window using just our Frame. Since we are + // Application.Top, and our Frame always == our Bounds (Location is always (0,0)) + // our Frame is actually view-relative (which is what Redraw takes). + Application.Top.Redraw (Frame); + } else { + SuperView.SetNeedsDisplay (Frame); + } + EnsureVisibleBounds (this, mouseEvent.X + mouseEvent.OfX - start.X, + mouseEvent.Y + mouseEvent.OfY, out nx, out ny); + + dragPosition = new Point (nx, ny); + Frame = new Rect (nx, ny, Frame.Width, Frame.Height); + X = nx; + Y = ny; + //Demo.ml2.Text = $"{dx},{dy}"; + + // FIXED: optimize, only SetNeedsDisplay on the before/after regions. + SetNeedsDisplay (); + return true; + } else { + // Only start grabbing if the user clicks on the title bar. + if (mouseEvent.Y == 0) { + start = new Point (mouseEvent.X, mouseEvent.Y); + dragPosition = new Point (); + nx = mouseEvent.X - mouseEvent.OfX; + ny = mouseEvent.Y - mouseEvent.OfY; + dragPosition = new Point (nx, ny); + Application.GrabMouse (this); + } + + //Demo.ml2.Text = $"Starting at {dragPosition}"; + return true; + } + } + + if (mouseEvent.Flags == MouseFlags.Button1Released && dragPosition.HasValue) { + Application.UngrabMouse (); + Driver.UncookMouse (); + dragPosition = null; + } + + //Demo.ml.Text = me.ToString (); + return false; + } + } +} diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md new file mode 100644 index 0000000..61f411e --- /dev/null +++ b/Terminal.Gui/README.md @@ -0,0 +1,25 @@ +# Terminal.Gui Project + +Contains all files required to build the **Terminal.Gui** library (and nuget package). + +## Project Folder Structure + +- `Terminal.Gui.sln` - The Visual Studio 2019 solution +- `Core/` - Source files for all types that comprise the core building blocks of **Terminal-Gui** + - `Application` - A `static` class that provides the base 'application driver'. Given it defines a **Terminal.Gui** application it is both logically and literally (because `static`) a singleton. It has direct dependencies on `MainLoop`, `Events.cs` `NetDriver`, `CursesDriver`, `WindowsDriver`, `Responder`, `View`, and `TopLevel` (and nothing else). + - `MainLoop` - Defines `IMainLoopDriver` and implements the and `MainLoop` class. + - `ConsoleDriver` - Definition for the Console Driver API. + - `Events.cs` - Defines keyboard and mouse related structs & classes. + - `PosDim.cs` - Implements **Terminal-Gui's** *Computed Layout* system. These classes have deep dependencies on `View`. + - `Responder` - Base class for the windowing class heirachy. Implements support for keyboard & mouse input. + - `View` - Derived from `Responder`, the base class for non-modal visual elements such as controls. + - `Toplevel` - Drived from `View`, the base class for modal visual elements such as top-level windows and dialogs. Supports the concept of `MenuBar` and `StatusBar`. + - `Window` - Drived from `TopLevel`, implements Toplevel views with a visible frame and Title. +- `Types/` - A folder (not namespace) containing implementations of `Point`, `Rect`, and `Size` which are ancient versions of the modern `System.Drawing.Point`, `System.Drawing.Size`, and `System.Drawning.Rectangle`. +- `ConsoleDrivers/` - Source files for the three `ConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. +- `Views/` - A folder (not namespace) containing the source for all built-in classes that drive from `View` (non-modals). +- `Windows/` - A folder (not namespace) containing the source all built-in classes that derive from `Window`. + +## Contributing + +See [CONTRIBUTING.md](https://github.com/migueldeicaza/gui.cs/blob/master/CONTRIBUTING.md). diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj new file mode 100644 index 0000000..ee37a1e --- /dev/null +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -0,0 +1,195 @@ + + + net472;netstandard2.1;netcoreapp3.1 + Terminal.Gui + Terminal.Gui + bin\Release\Terminal.Gui.xml + true + 0.90.0.0 + + + true + Terminal.Gui + 0.90 + Miguel de Icaza, Charlie Kindel (@tig), @BDisp + MIT + https://github.com/migueldeicaza/gui.cs/ + csharp, terminal, c#, f#, gui, toolkit, console + Console-based user interface toolkit for .NET applications. + Miguel de Icaza + Application framework for creating modern console applications using .NET + Terminal.Gui is a framework for creating console user interfaces + + 0.90: (Still Under Construction - Will be Feature Complete release for 1.0) + * API documentation completely revamped and updated. Readme upated. Contributors guide added (Thanks @tig!) + * New sample/demo app - UI Catalog - Replaces demo.cs with an easy to use and extend set of demo scenarios. (Thanks @tig!) + * MenuBar can now have MenuItems directly (enables top-level menu items with no submenu). (Thanks @tig!) + * Apps can now get KeyUp/KeyDown events. (Thanks @tig!) + * Fixes #396 - Text alignnment issues. (Thanks @tig!) + * Fixes #423 - Fix putting results of ocgv on command line erases cursor. (Thanks @tig!) + * Example/Designer csproj files updated to latest Visual Studio model. (Thanks @tig!) + * Adjusted the default colors for Windows to make more readable. (Thanks @tig!) + * Toplevel.Ready event - Fired once the Toplevel's MainLoop has started (#445). (Thanks @tig!) + * Refactored several events to use event vs. Action. (BREAKING CHANGE) (Thanks @tig!) + * All compile warnings fixed. (Thanks @tig!) + * Fixed a crash in EnsureVisibleBounds. (Thanks @tig!) + * Application.Init/Shutdown are more robust. (Thanks @tig!) + * New "Draw Window Frame" code; consistent across Window, FrameView, and Menu. Fixes many drawing bugs. (Thanks @tig!) + * The project has been refactored an reorganized to reduce risk of bugs and make it easier to contribute #541. (Thanks @tig!) + * Fixes #522 - Last view of Frameview not drawn. (Thanks @tig!) + * Clipping has been fixed/restored - it now works properly. (#586) (Thanks @tig!) + * Added a View.LayoutComplete event (#569). (Thanks @tig!) + * Fixes #299 - MessageBox now auto sizes. (Thanks @tig!) + * Fixes #557 - MessageBoxes on small screens. (Thanks @tig!) + * Fixes #432 - MessageBox does not deal with long text; width/height params are goofy. (Thanks @tig!) + * Fixes #35 - Dialog should have 1 char padding. (Thanks @tig!) + * `MessageBox.Query` called with `width` and `height` == 0 get auto-size behavior. A new constructor is added making this easy to use. (Thanks @tig!) + * Multi-line `MessageBox`es are now supported. Just use `\n` to add lines. The height of the MessageBox will adjust automatically. (Thanks @tig!) + * The `MessageBoxes` Scenario in UI Catalog provides a full demo/test-case. (Thanks @tig!) + * `Dialog` called with `width` and `height` == 0 are sized to 85% container. A new constructor is added making this easy to use. (Thanks @tig!) + * Dialog (and MessageBox `Buttons` are now dynamically laid out using Computed layout. (Thanks @tig!) + * A `Dialogs` Scenario has been added to UI Catalog making it easy to test the API. (Thanks @tig!) + * `Button` now supports BOTH specifying a hotkey with '_' and the old behavior of using the first uppercase char (if '_' is not found). (Thanks @tig!) + * All UI Catalog scenarios that use `Dialog` or `MessageBox` now use the simplified API. (Thanks @tig!) + * `Terminal.Gui.dll` now has version metadata and UI Catalog's about box displays it as a test case. (Thanks @tig!) + * Button, Dialog, and MessageBox API documentation has been updated/revised. (Thanks @tig!) + * `View`, `Window`, `FrameView`, and `Dialog` have been upgraded to use the new `ConsoleDriver.DrawFrameWindow` API directly. (Thanks @tig!) + * New ComboBox control (Thanks @fergusonr!) + * ConsoleDriver now supports improved KeyModifers (shift keys) with an expanded Keys Sceanrio in UI Catalog. (Thanks @bdisp!) + * Tons of mouse handling improvements. (Thanks @bdisp!) + * Fsharp sample updated. (Thanks @bdisp!) + * Fixes #562 - Background drawing issue. (Thanks @bdisp!) + * Fixes #517 - Focus and mouse handlers enahced (BREAKING CHANGE). (Thanks @bdisp!) + * Fixed resizing update and correct Toplevel colors without colors. (Thanks @bdisp!) + * Fixed #515, #518, #536, #540. (Thanks @bdisp!) + * Added Threading Scenario to UI catalog. (Thanks @bdisp!) + * Added support for F11 and F12 keys. (Thanks @bdisp!) + * Multiple improvements to Date/TimeField. (Thanks @bdisp!) + * Fixes #409 - Invoke does not cause Wakeup #501. (Thanks @bdisp!) + * Fixed Label text alignemnt. (Thanks @bdisp!) + * Added mouse features in the Unix version. Supports xterm-1006. (Thanks @bdisp!) + * Several StatusBar fixes. (Thanks @bdisp!) + * Tons of mouse improvements including mouse wheel support (e.g. #404, #409). (Thanks @bdisp!) + * Added a CloseFile method to the TextView as stated in #452. (Thanks @bdisp) + * Added a OpenSelectedItem event to the ListView #429. (Thanks @bdisp!) + * Fixes the return value of the position cursor in the TextField. (Thanks @bdisp!) + * Updates screen on Unix window resizing. (Thanks @bdisp!) + * Fixes the functions of the Edit->Copy-Cut-Paste menu for the TextField that was not working well. (Thanks @bdisp!) + * More robust error handing in Pos/Dim. Fixes #355 stack overflow with Pos based on the size of windows at startup. Added a OnResized action to set the Pos after the terminal are resized. (Thanks @bdisp!) + * Fixes #389 Window layouting breaks when resizing. (Thanks @bdisp!) + * Fixes #557 MessageBox needs to take ustrings (BREAKING CHANGE). (Thanks @tig!) + * Fixes ScrollView in several key ways. (Thanks @tig!) + * Now supports Computed layout and has constructors that don't require parameters. + * ScrollBarViews are now positioned using Computed layout versus error prone absoulte + * ScrollBarViews now correctly position themselves when one, either, or both are on/off. + * IsVertical is now a public property that does the expected thing when changed + * Mouse handling is better; there's still a bug where the mouse doesn't get grabbed by the ScrollView initially but I think this is a broader problem. I need @BDisp's help on this. + * Supports "infinite scrolling" via the new OnDrawContent/DrawContent event on the View class. + * The Scrolling Scenario was enhanced to demo dynamically adding/removing horizontal/vertical scrollbars (and to prove it was working right). + * The Checkbox.Toggled event is now an EventHandler event and passes previous state. (Thanks @tig!) + * Fixes #102 All Views now support parameterless constructors. (Thanks @Bdisp and @tig!) + * Fixes #583 Button can now be sized. Button now supports TextAlignment. (Thanks @Bdisp!) + * Fixes #447 All events are now defined in terms of Action instead of EventHanlder. BREAKING CHANGE. (Thanks @bdisp and @tig!) + * Fixes #421 Now builds on Linux with "dotnet build". (Thanks @AArnott!) + * MenuItem now supports checked/selected items. (Thanks @tig!) + * Label no longer incorreclty displays formfeed char. (Thanks @tig!) + * Fixes #645 - RadioGroup now supports unicode. (Thanks @tig!) + * Fixes #573 - RadioGroup supports Computed Layout. (Thanks @tig!) + * RadioGroup now uses a single, good looking, glyph. (Thanks @tig!) + * RadioGroup now supportrs the Action-based event pattern correctly. BREAKING CHANGE. (Thanks @tig!) + * ConsoleDriver and Drivers have new standard glyph definitions for things like right arrow. (Thanks @tig!) + * ScrollView updated to use pretty glyphs. (Thanks @tig!) + * Menubar now uses pretty arrow glyph for sub-menus. (Thanks @tig!) + + 0.81: + * Fix ncurses engine for macOS/Linux, it works again + * Fixes an issue with referencing views that have not been allocated yet causing a stack overflow + * New OnCloseMenu event on menus + * Button cursor position looks better + * Listview in single-selection mode uses a radio-button look + * Fixes a couple of crashes (356) + * Default the samples to work on Catalina + + 0.80: Jumbo update from BDisp: + * Fixed key events traversal for modal dialogs + * Fixes culture info of DataField from pr + * Fixes the rectangle drawing issue + * Redraw issue when setting coordinates of label + * Added sub menus into menu bar with mouse and key navigation + * Added Colors.Menu.Disabled to CursesDriver.cs + * Mouse text selection with cut, copy and paste on text fields + * Change sepChar from char to string in DateField + * Adding a disabled menu item in the demo file + * Fixes Button repainting issue when changing the text length to one smaller + * Fixes Redraw issue when setting coordinates of label + * Fixes ScrollView does not render some content + * Fixed bug in Button that caused a loop redraw calling TerminalResized + * Fixes a repaint issue (282) + * Mouse features added to FileDialog including wheel support. + * Switch netcoreapp target to netstandard2.0 + * Added TextView.TextChanged event + * Fixes issue #306 async/await hang (#312) + * Replaced static driver initialization with property getter for reference passing in Core.cs::View class, this allows the library to be reinitialized at any time. + * Made the Shutdown method on Core.cs::Application class public, since there is no reason to keep it private. Applications can shutdown the library and revert the console to the initial stage by calling it. + * Fixed a memory-leak on Drivers/WindowsDriver class by destroying the generated screen buffers at library shutdown by calling CloseHandle. + * Minor change to Core.cs::Application.Init(Func) for better initialization status tracking, via backend property instead of relying on the Top field. + * Moved `ListView.ListWrapper` out of `ListView` migueldeicaza/gui.cs#313` (#315) + * Resizing the MessageBox width to accommodate all message text (#299) + * Timefield format with bounds values (#303) + * Implemented lower and upper bounds to TimeField + * Passing old text to the Changed event handler + * Extract methods on ListView to make it controlable from other controls + + 0.70: Bug fixes (320, 321, 306, 304, 291, 299, 303); Surface ListView.ListWrapper, surface various internal methods for use in ListView; Allow list item selection; ; 0.65: Added new TimeField from Jörg Preiß; Fixes for Backtab by Martin Björkström; ListView now supports simple selection; Bug fixes by giladlevi, Daniel Cazzulino and Marius Ungureanu; New Application.Run of T entry point by Daniel Cazzulino; Added various View methods to bring forward, backwards and move views in the hierarchy; Switch to Portable PDBs by Daniel Cazzulino; Dims can now be compared by Daniel Cazzulino; OnMenuOpen handler by giladlevi; Various memory usage optimizations by giladlevi; FileDialog.FilePath is now a full path by Yanwei Wang; ISupportInitialize/ISupportInitializeNotification is now supported thanks to the work from Daniel Cazzulino; Support for non-modal TopLevels by Daniel Cazzulino and Adrian Alonso; 0.24: the Windows driver implements WakeUp, allowing some scenarios like bug #207 to be fixed; + 0.23: Better support for disabled menu items; Raises text changed event after the internals have been updated; Fix Caps-NumLock; Alt-HotKey now work on menus + 0.22: Correct vertical scrollview behavior, Small curses driver fix for terminals without mouse support, TextView support for scrolling, Surface Used property on TextField, Surface Cursor on RadioGroup. + + 0.21: Introudce Attribute.Make to more easily create attributes, and fix a bug in the file panel. + 0.20: Expose some of the CursesDriver APIs + 0.19: PageUpDown updates (GaikwadPratik); Fixes in multi-line labels (hlfrye@gmail.com); Support Delete char in TextView (Greg Amidon); Prevent empty TextViews from crashing; Allow TextFields to be updated on the Changed event. + 0.18: Fixes hotkeys for menus (Sang Kil); Fixes RemoveAll for all containers; Allows Toplevels with no views; Fixes FileDialog layout; Prevent crash in TextView + 0.17: Unix, dynamically load ncurses library to support different configurations, and not require -dev on Linux, various bug fixes. + + 0.16: Support for Shift-Tab on Windows (fix by @mholo65) + + 0.15: WindowsDriver fix for Legacy Console keyboard input (issue #105) + + 0.14: WindowsDriver fix for EventType size. + + 0.13: Fixes keyboard input for Alt-Gr and numbers. + + 0.12: Fixes the text editor merge line command. + + 0.11: Simplify TextField implementation, fixes a couple of editing bugs. + + 0.10: Fix unicode rendering for TextField, and bring F# example + + 0.9: File Open/File Save dialogs, HexView, Windows Driver allows resizing, mouse events, faster (thanks to Nick Van Dyck, nickvdyck for the contribution!), smaller bug fixes, + + 0.8: Completes keyboard support on Windows; Fixes delete on Windows, some layout fixes. + 0.7: terminal resizing on Linux propagates sizes with new layout system, and new features on the layout system (documented) + 0.6: new layout system, multi-line textview editor, Linux bug fix for .NET Core + 0.5: support Linux with .NET Core, Windows driver fixes. + 0.4: hotkey handling fix for RadioButtons + 0.3: Fix Windows key input to not echo characters on console, proper Fkey mapping + 0.2: Auto-detect the best console + + + + + + + + + + + + + + + + diff --git a/Terminal.Gui/Types/Point.cs b/Terminal.Gui/Types/Point.cs new file mode 100644 index 0000000..a3263fe --- /dev/null +++ b/Terminal.Gui/Types/Point.cs @@ -0,0 +1,258 @@ +// +// System.Drawing.Point.cs +// +// Author: +// Mike Kestner (mkestner@speakeasy.net) +// +// Copyright (C) 2001 Mike Kestner +// Copyright (C) 2004 Novell, Inc. http://www.novell.com +// + +using System; +using System.Globalization; + +namespace Terminal.Gui +{ + /// + /// Represents an ordered pair of integer x- and y-coordinates that defines a point in a two-dimensional plane. + /// + public struct Point + { + /// + /// Gets or sets the x-coordinate of this Point. + /// + public int X; + + /// + /// Gets or sets the y-coordinate of this Point. + /// + public int Y; + + // ----------------------- + // Public Shared Members + // ----------------------- + + /// + /// Empty Shared Field + /// + /// + /// + /// An uninitialized Point Structure. + /// + + public static readonly Point Empty; + + /// + /// Addition Operator + /// + /// + /// + /// Translates a Point using the Width and Height + /// properties of the given Size. + /// + + public static Point operator + (Point pt, Size sz) + { + return new Point (pt.X + sz.Width, pt.Y + sz.Height); + } + + /// + /// Equality Operator + /// + /// + /// + /// Compares two Point objects. The return value is + /// based on the equivalence of the X and Y properties + /// of the two points. + /// + + public static bool operator == (Point left, Point right) + { + return ((left.X == right.X) && (left.Y == right.Y)); + } + + /// + /// Inequality Operator + /// + /// + /// + /// Compares two Point objects. The return value is + /// based on the equivalence of the X and Y properties + /// of the two points. + /// + + public static bool operator != (Point left, Point right) + { + return ((left.X != right.X) || (left.Y != right.Y)); + } + + /// + /// Subtraction Operator + /// + /// + /// + /// Translates a Point using the negation of the Width + /// and Height properties of the given Size. + /// + + public static Point operator - (Point pt, Size sz) + { + return new Point (pt.X - sz.Width, pt.Y - sz.Height); + } + + /// + /// Point to Size Conversion + /// + /// + /// + /// Returns a Size based on the Coordinates of a given + /// Point. Requires explicit cast. + /// + + public static explicit operator Size (Point p) + { + if (p.X < 0 || p.Y < 0) + throw new ArgumentException ("Either Width and Height must be greater or equal to 0."); + + return new Size (p.X, p.Y); + } + + // ----------------------- + // Public Constructors + // ----------------------- + /// + /// Point Constructor + /// + /// + /// + /// Creates a Point from a Size value. + /// + + public Point (Size sz) + { + X = sz.Width; + Y = sz.Height; + } + + /// + /// Point Constructor + /// + /// + /// + /// Creates a Point from a specified x,y coordinate pair. + /// + + public Point (int x, int y) + { + this.X = x; + this.Y = y; + } + + // ----------------------- + // Public Instance Members + // ----------------------- + + /// + /// IsEmpty Property + /// + /// + /// + /// Indicates if both X and Y are zero. + /// + public bool IsEmpty { + get { + return ((X == 0) && (Y == 0)); + } + } + + /// + /// Equals Method + /// + /// + /// + /// Checks equivalence of this Point and another object. + /// + + public override bool Equals (object obj) + { + if (!(obj is Point)) + return false; + + return (this == (Point) obj); + } + + /// + /// GetHashCode Method + /// + /// + /// + /// Calculates a hashing value. + /// + + public override int GetHashCode () + { + return X^Y; + } + + /// + /// Offset Method + /// + /// + /// + /// Moves the Point a specified distance. + /// + + public void Offset (int dx, int dy) + { + X += dx; + Y += dy; + } + + /// + /// ToString Method + /// + /// + /// + /// Formats the Point as a string in coordinate notation. + /// + + public override string ToString () + { + return string.Format ("{{X={0},Y={1}}}", X.ToString (CultureInfo.InvariantCulture), + Y.ToString (CultureInfo.InvariantCulture)); + } + + /// + /// Adds the specified Size to the specified Point. + /// + /// The Point that is the result of the addition operation. + /// The Point to add. + /// The Size to add. + public static Point Add (Point pt, Size sz) + { + return new Point (pt.X + sz.Width, pt.Y + sz.Height); + } + + /// + /// Translates this Point by the specified Point. + /// + /// The offset. + /// The Point used offset this Point. + public void Offset (Point p) + { + Offset (p.X, p.Y); + } + + /// + /// Returns the result of subtracting specified Size from the specified Point. + /// + /// The Point that is the result of the subtraction operation. + /// The Point to be subtracted from. + /// The Size to subtract from the Point. + public static Point Subtract (Point pt, Size sz) + { + return new Point (pt.X - sz.Width, pt.Y - sz.Height); + } + + } +} diff --git a/Terminal.Gui/Types/Rect.cs b/Terminal.Gui/Types/Rect.cs new file mode 100644 index 0000000..eb24d71 --- /dev/null +++ b/Terminal.Gui/Types/Rect.cs @@ -0,0 +1,497 @@ +// +// System.Drawing.Rectangle.cs +// +// Author: +// Mike Kestner (mkestner@speakeasy.net) +// +// Copyright (C) 2001 Mike Kestner +// Copyright (C) 2004 Novell, Inc. http://www.novell.com +// + +using System; + +namespace Terminal.Gui +{ + /// + /// Stores a set of four integers that represent the location and size of a rectangle + /// + public struct Rect + { + int width; + int height; + + /// + /// Gets or sets the x-coordinate of the upper-left corner of this Rectangle structure. + /// + public int X; + /// + /// Gets or sets the y-coordinate of the upper-left corner of this Rectangle structure. + /// + public int Y; + + /// + /// Gets or sets the width of this Rect structure. + /// + public int Width { + get { return width; } + set { + if (value < 0) + throw new ArgumentException ("Width must be greater or equal to 0."); + width = value; + } + } + + /// + /// Gets or sets the height of this Rectangle structure. + /// + public int Height { + get { return height; } + set { + if (value < 0) + throw new ArgumentException ("Height must be greater or equal to 0."); + height = value; + } + } + + /// + /// Empty Shared Field + /// + /// + /// + /// An uninitialized Rectangle Structure. + /// + + public static readonly Rect Empty; + + /// + /// FromLTRB Shared Method + /// + /// + /// + /// Produces a Rectangle structure from left, top, right + /// and bottom coordinates. + /// + + public static Rect FromLTRB (int left, int top, + int right, int bottom) + { + return new Rect (left, top, right - left, + bottom - top); + } + + /// + /// Inflate Shared Method + /// + /// + /// + /// Produces a new Rectangle by inflating an existing + /// Rectangle by the specified coordinate values. + /// + + public static Rect Inflate (Rect rect, int x, int y) + { + Rect r = new Rect (rect.Location, rect.Size); + r.Inflate (x, y); + return r; + } + + /// + /// Inflate Method + /// + /// + /// + /// Inflates the Rectangle by a specified width and height. + /// + + public void Inflate (int width, int height) + { + Inflate (new Size (width, height)); + } + + /// + /// Inflate Method + /// + /// + /// + /// Inflates the Rectangle by a specified Size. + /// + + public void Inflate (Size size) + { + X -= size.Width; + Y -= size.Height; + Width += size.Width * 2; + Height += size.Height * 2; + } + + /// + /// Intersect Shared Method + /// + /// + /// + /// Produces a new Rectangle by intersecting 2 existing + /// Rectangles. Returns null if there is no intersection. + /// + + public static Rect Intersect (Rect a, Rect b) + { + // MS.NET returns a non-empty rectangle if the two rectangles + // touch each other + if (!a.IntersectsWithInclusive (b)) + return Empty; + + return Rect.FromLTRB ( + Math.Max (a.Left, b.Left), + Math.Max (a.Top, b.Top), + Math.Min (a.Right, b.Right), + Math.Min (a.Bottom, b.Bottom)); + } + + /// + /// Intersect Method + /// + /// + /// + /// Replaces the Rectangle with the intersection of itself + /// and another Rectangle. + /// + + public void Intersect (Rect rect) + { + this = Rect.Intersect (this, rect); + } + + /// + /// Union Shared Method + /// + /// + /// + /// Produces a new Rectangle from the union of 2 existing + /// Rectangles. + /// + + public static Rect Union (Rect a, Rect b) + { + return FromLTRB (Math.Min (a.Left, b.Left), + Math.Min (a.Top, b.Top), + Math.Max (a.Right, b.Right), + Math.Max (a.Bottom, b.Bottom)); + } + + /// + /// Equality Operator + /// + /// + /// + /// Compares two Rectangle objects. The return value is + /// based on the equivalence of the Location and Size + /// properties of the two Rectangles. + /// + + public static bool operator == (Rect left, Rect right) + { + return ((left.Location == right.Location) && + (left.Size == right.Size)); + } + + /// + /// Inequality Operator + /// + /// + /// + /// Compares two Rectangle objects. The return value is + /// based on the equivalence of the Location and Size + /// properties of the two Rectangles. + /// + + public static bool operator != (Rect left, Rect right) + { + return ((left.Location != right.Location) || + (left.Size != right.Size)); + } + + // ----------------------- + // Public Constructors + // ----------------------- + + /// + /// Rectangle Constructor + /// + /// + /// + /// Creates a Rectangle from Point and Size values. + /// + + public Rect (Point location, Size size) + { + X = location.X; + Y = location.Y; + width = size.Width; + height = size.Height; + Width = width; + Height = height; + } + + /// + /// Rectangle Constructor + /// + /// + /// + /// Creates a Rectangle from a specified x,y location and + /// width and height values. + /// + + public Rect (int x, int y, int width, int height) + { + X = x; + Y = y; + this.width = width; + this.height = height; + Width = this.width; + Height = this.height; + } + + + + /// + /// Bottom Property + /// + /// + /// + /// The Y coordinate of the bottom edge of the Rectangle. + /// Read only. + /// + public int Bottom { + get { + return Y + Height; + } + } + + /// + /// IsEmpty Property + /// + /// + /// + /// Indicates if the width or height are zero. Read only. + /// + public bool IsEmpty { + get { + return ((X == 0) && (Y == 0) && (Width == 0) && (Height == 0)); + } + } + + /// + /// Left Property + /// + /// + /// + /// The X coordinate of the left edge of the Rectangle. + /// Read only. + /// + + public int Left { + get { + return X; + } + } + + /// + /// Location Property + /// + /// + /// + /// The Location of the top-left corner of the Rectangle. + /// + + public Point Location { + get { + return new Point (X, Y); + } + set { + X = value.X; + Y = value.Y; + } + } + + /// + /// Right Property + /// + /// + /// + /// The X coordinate of the right edge of the Rectangle. + /// Read only. + /// + + public int Right { + get { + return X + Width; + } + } + + /// + /// Size Property + /// + /// + /// + /// The Size of the Rectangle. + /// + + public Size Size { + get { + return new Size (Width, Height); + } + set { + Width = value.Width; + Height = value.Height; + } + } + + /// + /// Top Property + /// + /// + /// + /// The Y coordinate of the top edge of the Rectangle. + /// Read only. + /// + + public int Top { + get { + return Y; + } + } + + /// + /// Contains Method + /// + /// + /// + /// Checks if an x,y coordinate lies within this Rectangle. + /// + + public bool Contains (int x, int y) + { + return ((x >= Left) && (x < Right) && + (y >= Top) && (y < Bottom)); + } + + /// + /// Contains Method + /// + /// + /// + /// Checks if a Point lies within this Rectangle. + /// + + public bool Contains (Point pt) + { + return Contains (pt.X, pt.Y); + } + + /// + /// Contains Method + /// + /// + /// + /// Checks if a Rectangle lies entirely within this + /// Rectangle. + /// + + public bool Contains (Rect rect) + { + return (rect == Intersect (this, rect)); + } + + /// + /// Equals Method + /// + /// + /// + /// Checks equivalence of this Rectangle and another object. + /// + + public override bool Equals (object obj) + { + if (!(obj is Rect)) + return false; + + return (this == (Rect) obj); + } + + /// + /// GetHashCode Method + /// + /// + /// + /// Calculates a hashing value. + /// + + public override int GetHashCode () + { + return (Height + Width) ^ X + Y; + } + + /// + /// IntersectsWith Method + /// + /// + /// + /// Checks if a Rectangle intersects with this one. + /// + + public bool IntersectsWith (Rect rect) + { + return !((Left >= rect.Right) || (Right <= rect.Left) || + (Top >= rect.Bottom) || (Bottom <= rect.Top)); + } + + bool IntersectsWithInclusive (Rect r) + { + return !((Left > r.Right) || (Right < r.Left) || + (Top > r.Bottom) || (Bottom < r.Top)); + } + + /// + /// Offset Method + /// + /// + /// + /// Moves the Rectangle a specified distance. + /// + + public void Offset (int x, int y) + { + this.X += x; + this.Y += y; + } + + /// + /// Offset Method + /// + /// + /// + /// Moves the Rectangle a specified distance. + /// + + public void Offset (Point pos) + { + X += pos.X; + Y += pos.Y; + } + + /// + /// ToString Method + /// + /// + /// + /// Formats the Rectangle as a string in (x,y,w,h) notation. + /// + + public override string ToString () + { + return String.Format ("{{X={0},Y={1},Width={2},Height={3}}}", + X, Y, Width, Height); + } + + } +} diff --git a/Terminal.Gui/Types/Size.cs b/Terminal.Gui/Types/Size.cs new file mode 100644 index 0000000..1f7e741 --- /dev/null +++ b/Terminal.Gui/Types/Size.cs @@ -0,0 +1,251 @@ +// +// System.Drawing.Size.cs +// +// Author: +// Mike Kestner (mkestner@speakeasy.net) +// +// Copyright (C) 2001 Mike Kestner +// Copyright (C) 2004 Novell, Inc. http://www.novell.com +// + +using System; + +namespace Terminal.Gui { + /// + /// Stores an ordered pair of integers, which specify a Height and Width. + /// + public struct Size + { + int width, height; + + /// + /// Gets a Size structure that has a Height and Width value of 0. + /// + public static readonly Size Empty; + + /// + /// Addition Operator + /// + /// + /// + /// Addition of two Size structures. + /// + + public static Size operator + (Size sz1, Size sz2) + { + return new Size (sz1.Width + sz2.Width, + sz1.Height + sz2.Height); + } + + /// + /// Equality Operator + /// + /// + /// + /// Compares two Size objects. The return value is + /// based on the equivalence of the Width and Height + /// properties of the two Sizes. + /// + + public static bool operator == (Size sz1, Size sz2) + { + return ((sz1.Width == sz2.Width) && + (sz1.Height == sz2.Height)); + } + + /// + /// Inequality Operator + /// + /// + /// + /// Compares two Size objects. The return value is + /// based on the equivalence of the Width and Height + /// properties of the two Sizes. + /// + + public static bool operator != (Size sz1, Size sz2) + { + return ((sz1.Width != sz2.Width) || + (sz1.Height != sz2.Height)); + } + + /// + /// Subtraction Operator + /// + /// + /// + /// Subtracts two Size structures. + /// + + public static Size operator - (Size sz1, Size sz2) + { + return new Size (sz1.Width - sz2.Width, + sz1.Height - sz2.Height); + } + + /// + /// Size to Point Conversion + /// + /// + /// + /// Returns a Point based on the dimensions of a given + /// Size. Requires explicit cast. + /// + + public static explicit operator Point (Size size) + { + return new Point (size.Width, size.Height); + } + + /// + /// Size Constructor + /// + /// + /// + /// Creates a Size from a Point value. + /// + + public Size (Point pt) + { + width = pt.X; + height = pt.Y; + } + + /// + /// Size Constructor + /// + /// + /// + /// Creates a Size from specified dimensions. + /// + + public Size (int width, int height) + { + if (width < 0 || height < 0) + throw new ArgumentException ("Either Width and Height must be greater or equal to 0."); + + this.width = width; + this.height = height; + } + + /// + /// IsEmpty Property + /// + /// + /// + /// Indicates if both Width and Height are zero. + /// + + public bool IsEmpty { + get { + return ((width == 0) && (height == 0)); + } + } + + /// + /// Width Property + /// + /// + /// + /// The Width coordinate of the Size. + /// + + public int Width { + get { + return width; + } + set { + if (value < 0) + throw new ArgumentException ("Width must be greater or equal to 0."); + width = value; + } + } + + /// + /// Height Property + /// + /// + /// + /// The Height coordinate of the Size. + /// + + public int Height { + get { + return height; + } + set { + if (value < 0) + throw new ArgumentException ("Height must be greater or equal to 0."); + height = value; + } + } + + /// + /// Equals Method + /// + /// + /// + /// Checks equivalence of this Size and another object. + /// + + public override bool Equals (object obj) + { + if (!(obj is Size)) + return false; + + return (this == (Size) obj); + } + + /// + /// GetHashCode Method + /// + /// + /// + /// Calculates a hashing value. + /// + + public override int GetHashCode () + { + return width^height; + } + + /// + /// ToString Method + /// + /// + /// + /// Formats the Size as a string in coordinate notation. + /// + + public override string ToString () + { + return String.Format ("{{Width={0}, Height={1}}}", width, height); + } + + /// + /// Adds the width and height of one Size structure to the width and height of another Size structure. + /// + /// The add. + /// The first Size structure to add. + /// The second Size structure to add. + public static Size Add (Size sz1, Size sz2) + { + return new Size (sz1.Width + sz2.Width, + sz1.Height + sz2.Height); + + } + + /// + /// Subtracts the width and height of one Size structure to the width and height of another Size structure. + /// + /// The subtract. + /// The first Size structure to subtract. + /// The second Size structure to subtract. + public static Size Subtract (Size sz1, Size sz2) + { + return new Size (sz1.Width - sz2.Width, + sz1.Height - sz2.Height); + } + + } +} diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs new file mode 100644 index 0000000..c542d3d --- /dev/null +++ b/Terminal.Gui/Views/Button.cs @@ -0,0 +1,263 @@ +// +// Button.cs: Button control +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// + +using System; +using NStack; + +namespace Terminal.Gui { + /// + /// Button is a that provides an item that invokes an when activated by the user. + /// + /// + /// + /// Provides a button showing text invokes an when clicked on with a mouse + /// or when the user presses SPACE, ENTER, or hotkey. The hotkey is specified by the first uppercase + /// letter in the button. + /// + /// + /// When the button is configured as the default () and the user presses + /// the ENTER key, if no other processes the , the 's + /// will be invoked. + /// + /// + public class Button : View { + ustring text; + ustring shown_text; + Rune hot_key; + int hot_pos = -1; + bool is_default; + TextAlignment textAlignment = TextAlignment.Centered; + + /// + /// Gets or sets whether the is the default action to activate in a dialog. + /// + /// true if is default; otherwise, false. + public bool IsDefault { + get => is_default; + set { + is_default = value; + SetWidthHeight (Text, is_default); + Update (); + } + } + + /// + /// Clicked , raised when the button is clicked. + /// + /// + /// Client code can hook up to this event, it is + /// raised when the button is activated either with + /// the mouse or the keyboard. + /// + public Action Clicked; + + /// + /// Initializes a new instance of using layout. + /// + /// + /// The width of the is computed based on the + /// text length. The height will always be 1. + /// + public Button () : this (text: string.Empty, is_default: false) { } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// The width of the is computed based on the + /// text length. The height will always be 1. + /// + /// The button's text + /// + /// If true, a special decoration is used, and the user pressing the enter key + /// in a will implicitly activate this button. + /// + public Button (ustring text, bool is_default = false) : base () + { + Init (text, is_default); + } + + /// + /// Initializes a new instance of using layout, based on the given text + /// + /// + /// The width of the is computed based on the + /// text length. The height will always be 1. + /// + /// X position where the button will be shown. + /// Y position where the button will be shown. + /// The button's text + public Button (int x, int y, ustring text) : this (x, y, text, false) { } + + /// + /// Initializes a new instance of using layout, based on the given text. + /// + /// + /// The width of the is computed based on the + /// text length. The height will always be 1. + /// + /// X position where the button will be shown. + /// Y position where the button will be shown. + /// The button's text + /// + /// If true, a special decoration is used, and the user pressing the enter key + /// in a will implicitly activate this button. + /// + public Button (int x, int y, ustring text, bool is_default) + : base (new Rect (x, y, text.Length + 4 + (is_default ? 2 : 0), 1)) + { + Init (text, is_default); + } + + Rune _leftBracket; + Rune _rightBracket; + Rune _leftDefault; + Rune _rightDefault; + + void Init (ustring text, bool is_default) + { + _leftBracket = new Rune (Driver != null ? Driver.LeftBracket : '['); + _rightBracket = new Rune (Driver != null ? Driver.RightBracket : ']'); + _leftDefault = new Rune (Driver != null ? Driver.LeftDefaultIndicator : '<'); + _rightDefault = new Rune (Driver != null ? Driver.RightDefaultIndicator : '>'); + + CanFocus = true; + Text = text ?? string.Empty; + this.IsDefault = is_default; + int w = SetWidthHeight (text, is_default); + Frame = new Rect (Frame.Location, new Size (w, 1)); + } + + int SetWidthHeight (ustring text, bool is_default) + { + int w = text.Length + 4 + (is_default ? 2 : 0); + Width = w; + Height = 1; + Frame = new Rect (Frame.Location, new Size (w, 1)); + return w; + } + + /// + /// The text displayed by this . + /// + public ustring Text { + get { + return text; + } + + set { + SetWidthHeight (value, is_default); + text = value; + Update (); + } + } + + /// + /// Sets or gets the text alignment for the . + /// + public TextAlignment TextAlignment { + get => textAlignment; + set { + textAlignment = value; + Update (); + } + } + + internal void Update () + { + if (IsDefault) + shown_text = ustring.Make (_leftBracket) + ustring.Make (_leftDefault) + " " + text + " " + ustring.Make (_rightDefault) + ustring.Make (_rightBracket); + else + shown_text = ustring.Make (_leftBracket) + " " + text + " " + ustring.Make (_rightBracket); + + shown_text = GetTextFromHotKey (shown_text, '_', out hot_pos, out hot_key); + + SetNeedsDisplay (); + } + + int c_hot_pos; + + /// + public override void Redraw (Rect bounds) + { + Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); + Move (0, 0); + + var caption = GetTextAlignment (shown_text, hot_pos, out int s_hot_pos, TextAlignment); + c_hot_pos = s_hot_pos; + + Driver.AddStr (caption); + + if (c_hot_pos != -1) { + Move (c_hot_pos, 0); + Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); + Driver.AddRune (hot_key); + } + } + + /// + public override void PositionCursor () + { + Move (c_hot_pos == -1 ? 1 : c_hot_pos, 0); + } + + bool CheckKey (KeyEvent key) + { + if ((char)key.KeyValue == hot_key) { + this.SuperView.SetFocus (this); + Clicked?.Invoke (); + return true; + } + return false; + } + + /// + public override bool ProcessHotKey (KeyEvent kb) + { + if (kb.IsAlt) + return CheckKey (kb); + + return false; + } + + /// + public override bool ProcessColdKey (KeyEvent kb) + { + if (IsDefault && kb.KeyValue == '\n') { + Clicked?.Invoke (); + return true; + } + return CheckKey (kb); + } + + /// + public override bool ProcessKey (KeyEvent kb) + { + var c = kb.KeyValue; + if (c == '\n' || c == ' ' || Rune.ToUpper ((uint)c) == hot_key) { + Clicked?.Invoke (); + return true; + } + return base.ProcessKey (kb); + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (me.Flags == MouseFlags.Button1Clicked) { + if (!HasFocus) { + SuperView.SetFocus (this); + SetNeedsDisplay (); + } + + Clicked?.Invoke (); + return true; + } + return false; + } + } +} diff --git a/Terminal.Gui/Views/Checkbox.cs b/Terminal.Gui/Views/Checkbox.cs new file mode 100644 index 0000000..80f5505 --- /dev/null +++ b/Terminal.Gui/Views/Checkbox.cs @@ -0,0 +1,162 @@ +// +// Checkbox.cs: Checkbox control +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; +using NStack; + +namespace Terminal.Gui { + + /// + /// The shows an on/off toggle that the user can set + /// + public class CheckBox : View { + ustring text; + int hot_pos = -1; + Rune hot_key; + + /// + /// Toggled event, raised when the is toggled. + /// + /// + /// Client code can hook up to this event, it is + /// raised when the is activated either with + /// the mouse or the keyboard. The passed bool contains the previous state. + /// + public Action Toggled; + + /// + /// Called when the property changes. Invokes the event. + /// + public virtual void OnToggled (bool previousChecked) + { + Toggled?.Invoke (previousChecked); + } + + /// + /// Initializes a new instance of based on the given text, using layout. + /// + public CheckBox () : this (string.Empty) { } + + /// + /// Initializes a new instance of based on the given text, using layout. + /// + /// S. + /// If set to true is checked. + public CheckBox (ustring s, bool is_checked = false) : base () + { + Checked = is_checked; + Text = s; + CanFocus = true; + Height = 1; + Width = s.Length + 4; + } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// The size of is computed based on the + /// text length. This is not toggled. + /// + public CheckBox (int x, int y, ustring s) : this (x, y, s, false) + { + } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// The size of is computed based on the + /// text length. + /// + public CheckBox (int x, int y, ustring s, bool is_checked) : base (new Rect (x, y, s.Length + 4, 1)) + { + Checked = is_checked; + Text = s; + + CanFocus = true; + } + + /// + /// The state of the + /// + public bool Checked { get; set; } + + /// + /// The text displayed by this + /// + public ustring Text { + get { + return text; + } + + set { + text = value; + + int i = 0; + hot_pos = -1; + hot_key = (char)0; + foreach (Rune c in text) { + if (Rune.IsUpper (c)) { + hot_key = c; + hot_pos = i; + break; + } + i++; + } + } + } + + /// + public override void Redraw (Rect bounds) + { + Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); + Move (0, 0); + Driver.AddStr (Checked ? "[x] " : "[ ] "); + Move (4, 0); + Driver.AddStr (Text); + if (hot_pos != -1) { + Move (4 + hot_pos, 0); + Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); + Driver.AddRune (hot_key); + } + } + + /// + public override void PositionCursor () + { + Move (1, 0); + } + + /// + public override bool ProcessKey (KeyEvent kb) + { + if (kb.KeyValue == ' ') { + var previousChecked = Checked; + Checked = !Checked; + OnToggled (previousChecked); + SetNeedsDisplay (); + return true; + } + return base.ProcessKey (kb); + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) + return false; + + SuperView.SetFocus (this); + var previousChecked = Checked; + Checked = !Checked; + OnToggled (previousChecked); + SetNeedsDisplay (); + + return true; + } + } +} diff --git a/Terminal.Gui/Views/Clipboard.cs b/Terminal.Gui/Views/Clipboard.cs new file mode 100644 index 0000000..e24e795 --- /dev/null +++ b/Terminal.Gui/Views/Clipboard.cs @@ -0,0 +1,15 @@ +using System; +using NStack; + +namespace Terminal.Gui { + /// + /// Provides cut, copy, and paste support for the clipboard. + /// NOTE: Currently not implemented. + /// + public static class Clipboard { + /// + /// + /// + public static ustring Contents { get; set; } + } +} diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs new file mode 100644 index 0000000..99e3064 --- /dev/null +++ b/Terminal.Gui/Views/ComboBox.cs @@ -0,0 +1,372 @@ +// +// ComboBox.cs: ComboBox control +// +// Authors: +// Ross Ferguson (ross.c.ferguson@btinternet.com) +// +// TODO: +// LayoutComplete() resize Height implement +// Cursor rolls of end of list when Height = Dim.Fill() and list fills frame +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NStack; + +namespace Terminal.Gui { + /// + /// ComboBox control + /// + public class ComboBox : View { + + IListDataSource source; + /// + /// Gets or sets the backing this , enabling custom rendering. + /// + /// The source. + /// + /// Use to set a new source. + /// + public IListDataSource Source { + get => source; + set { + source = value; + SetNeedsDisplay (); + } + } + + /// + /// Sets the source of the to an . + /// + /// An object implementing the IList interface. + /// + /// Use the property to set a new source and use custome rendering. + /// + public void SetSource (IList source) + { + if (source == null) + Source = null; + else { + Source = MakeWrapper (source); + } + } + + /// + /// Changed event, raised when the selection has been confirmed. + /// + /// + /// Client code can hook up to this event, it is + /// raised when the selection has been confirmed. + /// + public event EventHandler SelectedItemChanged; + + IList searchset; + ustring text = ""; + readonly TextField search; + readonly ListView listview; + int height; + int width; + bool autoHide = true; + + /// + /// Public constructor + /// + public ComboBox () : base() + { + search = new TextField (""); + listview = new ListView () { LayoutStyle = LayoutStyle.Computed, CanFocus = true }; + + Initialize (); + } + + /// + /// Public constructor + /// + /// + /// + public ComboBox (Rect rect, IList source) : base (rect) + { + SetSource (source); + this.height = rect.Height; + this.width = rect.Width; + + search = new TextField ("") { Width = width }; + listview = new ListView (rect, source) { LayoutStyle = LayoutStyle.Computed }; + + Initialize (); + } + + static IListDataSource MakeWrapper (IList source) + { + return new ListWrapper (source); + } + + private void Initialize() + { + ColorScheme = Colors.Base; + + search.TextChanged += Search_Changed; + + // On resize + LayoutComplete += (LayoutEventArgs a) => { + + search.Width = Bounds.Width; + listview.Width = autoHide ? Bounds.Width - 1 : Bounds.Width; + }; + + listview.SelectedItemChanged += (ListViewItemEventArgs e) => { + + if(searchset.Count > 0) + SetValue ((string)searchset [listview.SelectedItem]); + }; + + Application.Loaded += (Application.ResizedEventArgs a) => { + // Determine if this view is hosted inside a dialog + for (View view = this.SuperView; view != null; view = view.SuperView) { + if (view is Dialog) { + autoHide = false; + break; + } + } + + ResetSearchSet (); + + ColorScheme = autoHide ? Colors.Base : ColorScheme = null; + + // Needs to be re-applied for LayoutStyle.Computed + // If Dim or Pos are null, these are the from the parametrized constructor + listview.Y = 1; + + if (Width == null) { + listview.Width = CalculateWidth (); + search.Width = width; + } else { + width = GetDimAsInt (Width, vertical: false); + search.Width = width; + listview.Width = CalculateWidth (); + } + + if (Height == null) { + var h = CalculatetHeight (); + listview.Height = h; + this.Height = h + 1; // adjust view to account for search box + } else { + if (height == 0) + height = GetDimAsInt (Height, vertical: true); + + listview.Height = CalculatetHeight (); + this.Height = height + 1; // adjust view to account for search box + } + + if (this.Text != null) + Search_Changed (Text); + + if (autoHide) + listview.ColorScheme = Colors.Menu; + else + search.ColorScheme = Colors.Menu; + }; + + search.MouseClick += Search_MouseClick; + + this.Add(listview, search); + this.SetFocus(search); + } + + private void Search_MouseClick (MouseEventArgs e) + { + if (e.MouseEvent.Flags != MouseFlags.Button1Clicked) + return; + + SuperView.SetFocus (search); + } + + /// + public override bool OnEnter () + { + if (!search.HasFocus) + this.SetFocus (search); + + search.CursorPosition = search.Text.Length; + + return true; + } + + /// + /// Invokes the SelectedChanged event if it is defined. + /// + /// + public virtual bool OnSelectedChanged () + { + // Note: Cannot rely on "listview.SelectedItem != lastSelectedItem" because the list is dynamic. + // So we cannot optimize. Ie: Don't call if not changed + SelectedItemChanged?.Invoke (this, search.Text); + + return true; + } + + /// + public override bool ProcessKey(KeyEvent e) + { + if (e.Key == Key.Tab) { + base.ProcessKey(e); + return false; // allow tab-out to next control + } + + if (e.Key == Key.Enter && listview.HasFocus) { + if (listview.Source.Count == 0 || searchset.Count == 0) { + text = ""; + return true; + } + + SetValue((string)searchset [listview.SelectedItem]); + search.CursorPosition = search.Text.Length; + Search_Changed (search.Text); + OnSelectedChanged (); + + searchset.Clear(); + listview.Clear (); + listview.Height = 0; + this.SetFocus(search); + + return true; + } + + if (e.Key == Key.CursorDown && search.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) { // jump to list + this.SetFocus (listview); + SetValue ((string)searchset [listview.SelectedItem]); + return true; + } + + if (e.Key == Key.CursorUp && search.HasFocus) // stop odd behavior on KeyUp when search has focus + return true; + + if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) // jump back to search + { + search.CursorPosition = search.Text.Length; + this.SetFocus (search); + return true; + } + + if (e.Key == Key.Esc) { + this.SetFocus (search); + search.Text = text = ""; + OnSelectedChanged (); + return true; + } + + // Unix emulation + if (e.Key == Key.ControlU) + { + Reset(); + return true; + } + + return base.ProcessKey(e); + } + + /// + /// The currently selected list item + /// + public ustring Text + { + get + { + return text; + } + set { + search.Text = text = value; + } + } + + private void SetValue(ustring text) + { + search.TextChanged -= Search_Changed; + this.text = search.Text = text; + search.CursorPosition = 0; + search.TextChanged += Search_Changed; + } + + /// + /// Reset to full original list + /// + private void Reset() + { + search.Text = text = ""; + OnSelectedChanged(); + + ResetSearchSet (); + + listview.SetSource(searchset); + listview.Height = CalculatetHeight (); + + this.SetFocus(search); + } + + private void ResetSearchSet() + { + if (autoHide) { + if (searchset == null) + searchset = new List (); + else + searchset.Clear (); + } else + searchset = source.ToList (); + } + + private void Search_Changed (ustring text) + { + if (source == null) // Object initialization + return; + + if (string.IsNullOrEmpty (search.Text.ToString ())) + ResetSearchSet (); + else + searchset = source.ToList().Cast().Where (x => x.StartsWith (search.Text.ToString (), StringComparison.CurrentCultureIgnoreCase)).ToList(); + + listview.SetSource (searchset); + listview.Height = CalculatetHeight (); + + listview.Redraw (new Rect (0, 0, width, height)); // for any view behind this + this.SuperView?.BringSubviewToFront (this); + } + + /// + /// Internal height of dynamic search list + /// + /// + private int CalculatetHeight () + { + return Math.Min (height, searchset.Count); + } + + /// + /// Internal width of search list + /// + /// + private int CalculateWidth () + { + return autoHide ? Math.Max (1, width - 1) : width; + } + + /// + /// Get Dim as integer value + /// + /// + /// + /// n + private int GetDimAsInt (Dim dim, bool vertical) + { + if (dim is Dim.DimAbsolute) + return dim.Anchor (0); + else { // Dim.Fill Dim.Factor + if(autoHide) + return vertical ? dim.Anchor (SuperView.Bounds.Height) : dim.Anchor (SuperView.Bounds.Width); + else + return vertical ? dim.Anchor (Bounds.Height) : dim.Anchor (Bounds.Width); + } + } + } +} diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs new file mode 100644 index 0000000..2354405 --- /dev/null +++ b/Terminal.Gui/Views/DateField.cs @@ -0,0 +1,414 @@ +// +// DateField.cs: text entry for date +// +// Author: Barry Nolte +// +// Licensed under the MIT license +// +using System; +using System.Globalization; +using System.Linq; +using NStack; + +namespace Terminal.Gui { + /// + /// Simple Date editing + /// + /// + /// The provides date editing functionality with mouse support. + /// + public class DateField : TextField { + DateTime date; + bool isShort; + int longFieldLen = 10; + int shortFieldLen = 8; + string sepChar; + string longFormat; + string shortFormat; + + int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } } + string Format { get { return isShort ? shortFormat : longFormat; } } + + /// + /// DateChanged event, raised when the property has changed. + /// + /// + /// This event is raised when the property changes. + /// + /// + /// The passed event arguments containing the old value, new value, and format string. + /// + public Action> DateChanged; + + /// + /// Initializes a new instance of using layout. + /// + /// The x coordinate. + /// The y coordinate. + /// Initial date contents. + /// If true, shows only two digits for the year. + public DateField (int x, int y, DateTime date, bool isShort = false) : base (x, y, isShort ? 10 : 12, "") + { + this.isShort = isShort; + Initialize (date); + } + + /// + /// Initializes a new instance of using layout. + /// + public DateField () : this (DateTime.MinValue) { } + + /// + /// Initializes a new instance of using layout. + /// + /// + public DateField (DateTime date) : base ("") + { + this.isShort = true; + Width = FieldLen + 2; + Initialize (date); + } + + void Initialize (DateTime date) + { + CultureInfo cultureInfo = CultureInfo.CurrentCulture; + sepChar = cultureInfo.DateTimeFormat.DateSeparator; + longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern); + shortFormat = GetShortFormat (longFormat); + CursorPosition = 1; + Date = date; + TextChanged += DateField_Changed; + } + + void DateField_Changed (ustring e) + { + try { + if (!DateTime.TryParseExact (GetDate (Text).ToString (), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) + Text = e; + } catch (Exception) { + Text = e; + } + } + + string GetInvarianteFormat () + { + return $"MM{sepChar}dd{sepChar}yyyy"; + } + + string GetLongFormat (string lf) + { + ustring [] frm = ustring.Make (lf).Split (ustring.Make (sepChar)); + for (int i = 0; i < frm.Length; i++) { + if (frm [i].Contains ("M") && frm [i].Length < 2) + lf = lf.Replace ("M", "MM"); + if (frm [i].Contains ("d") && frm [i].Length < 2) + lf = lf.Replace ("d", "dd"); + if (frm [i].Contains ("y") && frm [i].Length < 4) + lf = lf.Replace ("yy", "yyyy"); + } + return $" {lf}"; + } + + string GetShortFormat (string lf) + { + return lf.Replace ("yyyy", "yy"); + } + + /// + /// Gets or sets the date of the . + /// + /// + /// + public DateTime Date { + get { + return date; + } + set { + if (ReadOnly) + return; + + var oldData = date; + date = value; + this.Text = value.ToString (Format); + var args = new DateTimeEventArgs (oldData, value, Format); + if (oldData != value) { + OnDateChanged (args); + } + } + } + + /// + /// Get or set the date format for the widget. + /// + public bool IsShortFormat { + get => isShort; + set { + isShort = value; + if (isShort) + Width = 10; + else + Width = 12; + var ro = ReadOnly; + if (ro) + ReadOnly = false; + SetText (Text); + ReadOnly = ro; + SetNeedsDisplay (); + } + } + + bool SetText (Rune key) + { + var text = TextModel.ToRunes (Text); + var newText = text.GetRange (0, CursorPosition); + newText.Add (key); + if (CursorPosition < FieldLen) + newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); + return SetText (ustring.Make (newText)); + } + + bool SetText (ustring text) + { + if (text.IsEmpty) { + return false; + } + + ustring [] vals = text.Split (ustring.Make (sepChar)); + ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar)); + bool isValidDate = true; + int idx = GetFormatIndex (frm, "y"); + int year = Int32.Parse (vals [idx].ToString ()); + int month; + int day; + idx = GetFormatIndex (frm, "M"); + if (Int32.Parse (vals [idx].ToString ()) < 1) { + isValidDate = false; + month = 1; + vals [idx] = "1"; + } else if (Int32.Parse (vals [idx].ToString ()) > 12) { + isValidDate = false; + month = 12; + vals [idx] = "12"; + } else + month = Int32.Parse (vals [idx].ToString ()); + idx = GetFormatIndex (frm, "d"); + if (Int32.Parse (vals [idx].ToString ()) < 1) { + isValidDate = false; + day = 1; + vals [idx] = "1"; + } else if (Int32.Parse (vals [idx].ToString ()) > 31) { + isValidDate = false; + day = DateTime.DaysInMonth (year, month); + vals [idx] = day.ToString (); + } else + day = Int32.Parse (vals [idx].ToString ()); + string d = GetDate (month, day, year, frm); + + if (!DateTime.TryParseExact (d, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) || + !isValidDate) + return false; + Date = result; + return true; + } + + string GetDate (int month, int day, int year, ustring [] fm) + { + string date = " "; + for (int i = 0; i < fm.Length; i++) { + if (fm [i].Contains ("M")) { + date += $"{month,2:00}"; + } else if (fm [i].Contains ("d")) { + date += $"{day,2:00}"; + } else { + if (!isShort && year.ToString ().Length == 2) { + var y = DateTime.Now.Year.ToString (); + date += y.Substring (0, 2) + year.ToString (); + } else if (isShort && year.ToString ().Length == 4) { + date += $"{year.ToString ().Substring (2, 2)}"; + } else { + date += $"{year,2:00}"; + } + } + if (i < 2) + date += $"{sepChar}"; + } + return date; + } + + ustring GetDate (ustring text) + { + ustring [] vals = text.Split (ustring.Make (sepChar)); + ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar)); + ustring [] date = { null, null, null }; + + for (int i = 0; i < frm.Length; i++) { + if (frm [i].Contains ("M")) { + date [0] = vals [i].TrimSpace (); + } else if (frm [i].Contains ("d")) { + date [1] = vals [i].TrimSpace (); + } else { + var year = vals [i].TrimSpace (); + if (year.Length == 2) { + var y = DateTime.Now.Year.ToString (); + date [2] = y.Substring (0, 2) + year.ToString (); + } else { + date [2] = vals [i].TrimSpace (); + } + } + } + return date [0] + ustring.Make (sepChar) + date [1] + ustring.Make (sepChar) + date [2]; + + } + + int GetFormatIndex (ustring [] fm, string t) + { + int idx = -1; + for (int i = 0; i < fm.Length; i++) { + if (fm [i].Contains (t)) { + idx = i; + break; + } + } + return idx; + } + + void IncCursorPosition () + { + if (CursorPosition == FieldLen) + return; + if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) + CursorPosition++; + } + + void DecCursorPosition () + { + if (CursorPosition == 1) + return; + if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) + CursorPosition--; + } + + void AdjCursorPosition () + { + if (Text [CursorPosition] == sepChar.ToCharArray () [0]) + CursorPosition++; + } + + /// + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.DeleteChar: + case Key.ControlD: + if (ReadOnly) + return true; + + SetText ('0'); + break; + + case Key.Delete: + case Key.Backspace: + if (ReadOnly) + return true; + + SetText ('0'); + DecCursorPosition (); + break; + + // Home, C-A + case Key.Home: + case Key.ControlA: + CursorPosition = 1; + break; + + case Key.CursorLeft: + case Key.ControlB: + DecCursorPosition (); + break; + + case Key.End: + case Key.ControlE: // End + CursorPosition = FieldLen; + break; + + case Key.CursorRight: + case Key.ControlF: + IncCursorPosition (); + break; + + default: + // Ignore non-numeric characters. + if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9')) + return false; + + if (ReadOnly) + return true; + + if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ())) + IncCursorPosition (); + return true; + } + return true; + } + + /// + public override bool MouseEvent (MouseEvent ev) + { + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) + return false; + if (!HasFocus) + SuperView.SetFocus (this); + + var point = ev.X; + if (point > FieldLen) + point = FieldLen; + if (point < 1) + point = 1; + CursorPosition = point; + AdjCursorPosition (); + return true; + } + + /// + /// Event firing method for the event. + /// + /// Event arguments + public virtual void OnDateChanged (DateTimeEventArgs args) + { + DateChanged?.Invoke (args); + } + } + + /// + /// Defines the event arguments for and events. + /// + public class DateTimeEventArgs : EventArgs { + /// + /// The old or value. + /// + public T OldValue {get;} + + /// + /// The new or value. + /// + public T NewValue { get; } + + /// + /// The or format. + /// + public string Format { get; } + + /// + /// Initializes a new instance of + /// + /// The old or value. + /// The new or value. + /// The or format string. + public DateTimeEventArgs (T oldValue, T newValue, string format) + { + OldValue = oldValue; + NewValue = newValue; + Format = format; + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs new file mode 100644 index 0000000..5b7f49a --- /dev/null +++ b/Terminal.Gui/Views/FrameView.cs @@ -0,0 +1,162 @@ +// +// FrameView.cs: Frame control +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; +using System.Collections; +using System.Collections.Generic; +using NStack; + +namespace Terminal.Gui { + /// + /// The FrameView is a container frame that draws a frame around the contents. It is similar to + /// a GroupBox in Windows. + /// + public class FrameView : View { + View contentView; + ustring title; + + /// + /// The title to be displayed for this . + /// + /// The title. + public ustring Title { + get => title; + set { + title = value; + SetNeedsDisplay (); + } + } + + class ContentView : View { + public ContentView (Rect frame) : base (frame) { } + public ContentView () : base () { } + } + + /// + /// Initializes a new instance of the class using layout. + /// + /// Frame. + /// Title. + public FrameView (Rect frame, ustring title) : base (frame) + { + var cFrame = new Rect (1, 1, frame.Width - 2, frame.Height - 2); + this.title = title; + contentView = new ContentView (cFrame); + Initialize (); + } + + /// + /// Initializes a new instance of the class using layout. + /// + /// Frame. + /// Title. + /// /// Views. + public FrameView (Rect frame, ustring title, View [] views) : this (frame, title) + { + foreach (var view in views) { + contentView.Add (view); + } + Initialize (); + } + + /// + /// Initializes a new instance of the class using layout. + /// + /// Title. + public FrameView (ustring title) + { + this.title = title; + contentView = new ContentView () { + X = 1, + Y = 1, + Width = Dim.Fill (1), + Height = Dim.Fill (1) + }; + Initialize (); + } + + /// + /// Initializes a new instance of the class using layout. + /// + public FrameView () : this (title: string.Empty) { } + + void Initialize () + { + base.Add (contentView); + } + + void DrawFrame () + { + DrawFrame (new Rect (0, 0, Frame.Width, Frame.Height), 0, fill: true); + } + + /// + /// Add the specified to this container. + /// + /// to add to this container + public override void Add (View view) + { + contentView.Add (view); + if (view.CanFocus) + CanFocus = true; + } + + + /// + /// Removes a from this container. + /// + /// + /// + public override void Remove (View view) + { + if (view == null) + return; + + SetNeedsDisplay (); + var touched = view.Frame; + contentView.Remove (view); + + if (contentView.InternalSubviews.Count < 1) + this.CanFocus = false; + } + + /// + /// Removes all s from this container. + /// + /// + /// + public override void RemoveAll () + { + contentView.RemoveAll (); + } + + /// + public override void Redraw (Rect bounds) + { + var padding = 0; + Application.CurrentView = this; + var scrRect = ViewToScreen (new Rect (0, 0, Frame.Width, Frame.Height)); + + if (NeedDisplay != null && !NeedDisplay.IsEmpty) { + Driver.SetAttribute (ColorScheme.Normal); + Driver.DrawWindowFrame (scrRect, padding + 1, padding + 1, padding + 1, padding + 1, border: true, fill: true); + } + + var savedClip = ClipToBounds (); + contentView.Redraw (contentView.Bounds); + Driver.Clip = savedClip; + + ClearNeedsDisplay (); + Driver.SetAttribute (ColorScheme.Normal); + Driver.DrawWindowFrame (scrRect, padding + 1, padding + 1, padding + 1, padding + 1, border: true, fill: false); + + if (HasFocus) + Driver.SetAttribute (ColorScheme.HotNormal); + Driver.DrawWindowTitle (scrRect, Title, padding, padding, padding, padding); + Driver.SetAttribute (ColorScheme.Normal); + } + } +} diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs new file mode 100644 index 0000000..af9d55e --- /dev/null +++ b/Terminal.Gui/Views/HexView.cs @@ -0,0 +1,399 @@ +// +// HexView.cs: A hexadecimal viewer +// +// TODO: +// - Support searching and highlighting of the search result +// - Bug showing the last line +// +using System; +using System.Collections.Generic; +using System.IO; + +namespace Terminal.Gui { + /// + /// An hex viewer and editor over a + /// + /// + /// + /// provides a hex editor on top of a seekable with the left side showing an hex + /// dump of the values in the and the right side showing the contents (filterd to + /// non-control sequence ASCII characters). + /// + /// + /// Users can switch from one side to the other by using the tab key. + /// + /// + /// To enable editing, set to true. When is true + /// the user can make changes to the hexadecimal values of the . Any changes are tracked + /// in the property (a ) indicating + /// the position where the changes were made and the new values. A convenience method, + /// will apply the edits to the . + /// + /// + /// Control the first byte shown by setting the property + /// to an offset in the stream. + /// + /// + public class HexView : View { + SortedDictionary edits = new SortedDictionary (); + Stream source; + long displayStart, position; + bool firstNibble, leftSide; + + /// + /// Initialzies a class using layout. + /// + /// The to view and edit as hex, this must support seeking, or an exception will be thrown. + public HexView (Stream source) : base () + { + Source = source; + this.source = source; + CanFocus = true; + leftSide = true; + firstNibble = true; + } + + /// + /// Initialzies a class using layout. + /// + public HexView () : this (source: new MemoryStream ()) { } + + /// + /// Sets or gets the the is operating on; the stream must support seeking ( == true). + /// + /// The source. + public Stream Source { + get => source; + set { + if (value == null) + throw new ArgumentNullException ("source"); + if (!value.CanSeek) + throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source"); + source = value; + + SetNeedsDisplay (); + } + } + + internal void SetDisplayStart (long value) + { + if (value >= source.Length) + displayStart = source.Length - 1; + else if (value < 0) + displayStart = 0; + else + displayStart = value; + SetNeedsDisplay (); + } + + /// + /// Sets or gets the offset into the that will displayed at the top of the + /// + /// The display start. + public long DisplayStart { + get => displayStart; + set { + position = value; + + SetDisplayStart (value); + } + } + + const int displayWidth = 9; + const int bsize = 4; + int bytesPerLine; + + /// + public override Rect Frame { + get => base.Frame; + set { + base.Frame = value; + + // Small buffers will just show the position, with 4 bytes + bytesPerLine = 4; + if (value.Width - displayWidth > 17) + bytesPerLine = 4 * ((value.Width - displayWidth) / 18); + } + } + + // + // This is used to support editing of the buffer on a peer List<>, + // the offset corresponds to an offset relative to DisplayStart, and + // the buffer contains the contents of a screenful of data, so the + // offset is relative to the buffer. + // + // + byte GetData (byte [] buffer, int offset, out bool edited) + { + var pos = DisplayStart + offset; + if (edits.TryGetValue (pos, out byte v)) { + edited = true; + return v; + } + edited = false; + return buffer [offset]; + } + + /// + public override void Redraw (Rect bounds) + { + Attribute currentAttribute; + var current = ColorScheme.Focus; + Driver.SetAttribute (current); + Move (0, 0); + + var frame = Frame; + + var nblocks = bytesPerLine / 4; + var data = new byte [nblocks * 4 * frame.Height]; + Source.Position = displayStart; + var n = source.Read (data, 0, data.Length); + + int activeColor = ColorScheme.HotNormal; + int trackingColor = ColorScheme.HotFocus; + + for (int line = 0; line < frame.Height; line++) { + var lineRect = new Rect (0, line, frame.Width, 1); + if (!bounds.Contains (lineRect)) + continue; + + Move (0, line); + Driver.SetAttribute (ColorScheme.HotNormal); + Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * 4)); + + currentAttribute = ColorScheme.HotNormal; + SetAttribute (ColorScheme.Normal); + + for (int block = 0; block < nblocks; block++) { + for (int b = 0; b < 4; b++) { + var offset = (line * nblocks * 4) + block * 4 + b; + bool edited; + var value = GetData (data, offset, out edited); + if (offset + displayStart == position || edited) + SetAttribute (leftSide ? activeColor : trackingColor); + else + SetAttribute (ColorScheme.Normal); + + Driver.AddStr (offset >= n ? " " : string.Format ("{0:x2}", value)); + SetAttribute (ColorScheme.Normal); + Driver.AddRune (' '); + } + Driver.AddStr (block + 1 == nblocks ? " " : "| "); + } + + + for (int bitem = 0; bitem < nblocks * 4; bitem++) { + var offset = line * nblocks * 4 + bitem; + + bool edited = false; + Rune c = ' '; + if (offset >= n) + c = ' '; + else { + var b = GetData (data, offset, out edited); + if (b < 32) + c = '.'; + else if (b > 127) + c = '.'; + else + c = b; + } + if (offset + displayStart == position || edited) + SetAttribute (leftSide ? trackingColor : activeColor); + else + SetAttribute (ColorScheme.Normal); + + Driver.AddRune (c); + } + } + + void SetAttribute (Attribute attribute) + { + if (currentAttribute != attribute) { + currentAttribute = attribute; + Driver.SetAttribute (attribute); + } + } + + } + + /// + public override void PositionCursor () + { + var delta = (int)(position - displayStart); + var line = delta / bytesPerLine; + var item = delta % bytesPerLine; + var block = item / 4; + var column = (item % 4) * 3; + + if (leftSide) + Move (displayWidth + block * 14 + column + (firstNibble ? 0 : 1), line); + else + Move (displayWidth + (bytesPerLine / 4) * 14 + item - 1, line); + } + + void RedisplayLine (long pos) + { + var delta = (int)(pos - DisplayStart); + var line = delta / bytesPerLine; + + SetNeedsDisplay (new Rect (0, line, Frame.Width, 1)); + } + + void CursorRight () + { + RedisplayLine (position); + if (leftSide) { + if (firstNibble) { + firstNibble = false; + return; + } else + firstNibble = true; + } + if (position < source.Length) + position++; + if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { + SetDisplayStart (DisplayStart + bytesPerLine); + SetNeedsDisplay (); + } else + RedisplayLine (position); + } + + void MoveUp (int bytes) + { + RedisplayLine (position); + position -= bytes; + if (position < 0) + position = 0; + if (position < DisplayStart) { + SetDisplayStart (DisplayStart - bytes); + SetNeedsDisplay (); + } else + RedisplayLine (position); + + } + + void MoveDown (int bytes) + { + RedisplayLine (position); + if (position + bytes < source.Length) + position += bytes; + if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { + SetDisplayStart (DisplayStart + bytes); + SetNeedsDisplay (); + } else + RedisplayLine (position); + } + + /// + public override bool ProcessKey (KeyEvent keyEvent) + { + switch (keyEvent.Key) { + case Key.CursorLeft: + RedisplayLine (position); + if (leftSide) { + if (!firstNibble) { + firstNibble = true; + return true; + } + firstNibble = false; + } + if (position == 0) + return true; + if (position - 1 < DisplayStart) { + SetDisplayStart (displayStart - bytesPerLine); + SetNeedsDisplay (); + } else + RedisplayLine (position); + position--; + break; + case Key.CursorRight: + CursorRight (); + break; + case Key.CursorDown: + MoveDown (bytesPerLine); + break; + case Key.CursorUp: + MoveUp (bytesPerLine); + break; + case Key.Enter: + leftSide = !leftSide; + RedisplayLine (position); + firstNibble = true; + break; + case ((int)'v' + Key.AltMask): + case Key.PageUp: + MoveUp (bytesPerLine * Frame.Height); + break; + case Key.ControlV: + case Key.PageDown: + MoveDown (bytesPerLine * Frame.Height); + break; + case Key.Home: + DisplayStart = 0; + SetNeedsDisplay (); + break; + default: + if (leftSide) { + int value = -1; + var k = (char)keyEvent.Key; + if (k >= 'A' && k <= 'F') + value = k - 'A' + 10; + else if (k >= 'a' && k <= 'f') + value = k - 'a' + 10; + else if (k >= '0' && k <= '9') + value = k - '0'; + else + return false; + + byte b; + if (!edits.TryGetValue (position, out b)) { + source.Position = position; + b = (byte)source.ReadByte (); + } + RedisplayLine (position); + if (firstNibble) { + firstNibble = false; + b = (byte)(b & 0xf | (value << 4)); + edits [position] = b; + } else { + b = (byte)(b & 0xf0 | value); + edits [position] = b; + CursorRight (); + } + return true; + } else + return false; + } + PositionCursor (); + return true; + } + + /// + /// Gets or sets whether this allow editing of the + /// of the underlying . + /// + /// true if allow edits; otherwise, false. + public bool AllowEdits { get; set; } + + /// + /// Gets a describing the edits done to the . + /// Each Key indicates an offset where an edit was made and the Value is the changed byte. + /// + /// The edits. + public IReadOnlyDictionary Edits => edits; + + /// + /// This method applies andy edits made to the and resets the + /// contents of the property + /// + public void ApplyEdits () + { + foreach (var kv in edits) { + source.Position = kv.Key; + source.WriteByte (kv.Value); + } + edits = new SortedDictionary (); + } + } +} diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs new file mode 100644 index 0000000..1bfa7a3 --- /dev/null +++ b/Terminal.Gui/Views/Label.cs @@ -0,0 +1,364 @@ +// +// Label.cs: Label control +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NStack; + +namespace Terminal.Gui { + /// + /// The Label displays a string at a given position and supports multiple lines separted by newline characters. Multi-line Labels support word wrap. + /// + public class Label : View { + List lines = new List (); + bool recalcPending = true; + ustring text; + TextAlignment textAlignment; + + static Rect CalcRect (int x, int y, ustring s) + { + int mw = 0; + int ml = 1; + + int cols = 0; + foreach (var rune in s) { + if (rune == '\n') { + ml++; + if (cols > mw) + mw = cols; + cols = 0; + } else + cols++; + } + if (cols > mw) + mw = cols; + + return new Rect (x, y, mw, ml); + } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// + /// The will be created at the given + /// coordinates with the given string. The size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// No line wrapping is provided. + /// + /// + /// column to locate the Label. + /// row to locate the Label. + /// text to initialize the property with. + public Label (int x, int y, ustring text) : this (CalcRect (x, y, text), text) + { + } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// + /// The will be created at the given + /// coordinates with the given string. The initial size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// If rect.Height is greater than one, word wrapping is provided. + /// + /// + /// Location. + /// text to initialize the property with. + public Label (Rect rect, ustring text) : base (rect) + { + this.text = text; + } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// + /// The will be created using + /// coordinates with the given string. The initial size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// If Height is greater than one, word wrapping is provided. + /// + /// + /// text to initialize the property with. + public Label (ustring text) : base () + { + this.text = text; + var r = CalcRect (0, 0, text); + Width = r.Width; + Height = r.Height; + } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// + /// The will be created using + /// coordinates. The initial size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// If Height is greater than one, word wrapping is provided. + /// + /// + public Label () : this (text: string.Empty) { } + + static char [] whitespace = new char [] { ' ', '\t' }; + + static ustring ClipAndJustify (ustring str, int width, TextAlignment talign) + { + int slen = str.RuneCount; + if (slen > width) { + var uints = str.ToRunes (width); + var runes = new Rune [uints.Length]; + for (int i = 0; i < uints.Length; i++) + runes [i] = uints [i]; + return ustring.Make (runes); + } else { + if (talign == TextAlignment.Justified) { + // TODO: ustring needs this + var words = str.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries); + int textCount = words.Sum (arg => arg.Length); + + var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0; + var extras = words.Length > 1 ? (width - textCount) % words.Length : 0; + + var s = new System.Text.StringBuilder (); + //s.Append ($"tc={textCount} sp={spaces},x={extras} - "); + for (int w = 0; w < words.Length; w++) { + var x = words [w]; + s.Append (x); + if (w + 1 < words.Length) + for (int i = 0; i < spaces; i++) + s.Append (' '); + if (extras > 0) { + //s.Append ('_'); + extras--; + } + } + return ustring.Make (s.ToString ()); + } + return str; + } + } + + void Recalc () + { + recalcPending = false; + Recalc (text, lines, Frame.Width, textAlignment, Bounds.Height > 1); + } + + static ustring StripCRLF (ustring str) + { + var runes = new List (); + foreach (var r in str.ToRunes ()) { + if (r != '\r' && r != '\n') { + runes.Add (r); + } + } + return ustring.Make (runes); ; + } + static ustring ReplaceCRLFWithSpace (ustring str) + { + var runes = new List (); + foreach (var r in str.ToRunes ()) { + if (r == '\r' || r == '\n') { + runes.Add (new Rune (' ')); // r + 0x2400)); // U+25A1 □ WHITE SQUARE + } else { + runes.Add (r); + } + } + return ustring.Make (runes); ; + } + + static List WordWrap (ustring text, int margin) + { + int start = 0, end; + var lines = new List (); + + text = StripCRLF (text); + + while ((end = start + margin) < text.Length) { + while (text [end] != ' ' && end > start) + end -= 1; + + if (end == start) + end = start + margin; + + lines.Add (text [start, end]); + start = end + 1; + } + + if (start < text.Length) + lines.Add (text.Substring (start)); + + return lines; + } + + static void Recalc (ustring textStr, List lineResult, int width, TextAlignment talign, bool wordWrap) + { + lineResult.Clear (); + + if (wordWrap == false) { + textStr = ReplaceCRLFWithSpace (textStr); + lineResult.Add (ClipAndJustify (textStr, width, talign)); + return; + } + + int textLen = textStr.Length; + int lp = 0; + for (int i = 0; i < textLen; i++) { + Rune c = textStr [i]; + if (c == '\n') { + var wrappedLines = WordWrap (textStr [lp, i], width); + foreach (var line in wrappedLines) { + lineResult.Add (ClipAndJustify (line, width, talign)); + } + if (wrappedLines.Count == 0) { + lineResult.Add (ustring.Empty); + } + lp = i + 1; + } + } + foreach (var line in WordWrap (textStr [lp, textLen], width)) { + lineResult.Add (ClipAndJustify (line, width, talign)); + } + } + + /// + public override void LayoutSubviews () + { + recalcPending = true; + } + + /// + public override void Redraw (Rect bounds) + { + if (recalcPending) + Recalc (); + + if (TextColor != -1) + Driver.SetAttribute (TextColor); + else + Driver.SetAttribute (ColorScheme.Normal); + + Clear (); + for (int line = 0; line < lines.Count; line++) { + if (line < bounds.Top || line >= bounds.Bottom) + continue; + var str = lines [line]; + int x; + switch (textAlignment) { + case TextAlignment.Left: + x = 0; + break; + case TextAlignment.Justified: + x = Bounds.Left; + break; + case TextAlignment.Right: + x = Bounds.Right - str.Length; + break; + case TextAlignment.Centered: + x = Bounds.Left + (Bounds.Width - str.Length) / 2; + break; + default: + throw new ArgumentOutOfRangeException (); + } + Move (x, line); + Driver.AddStr (str); + } + } + + /// + /// Computes the number of lines needed to render the specified text by the view + /// + /// Number of lines. + /// Text, may contain newlines. + /// The width for the text. + public static int MeasureLines (ustring text, int width) + { + var result = new List (); + Recalc (text, result, width, TextAlignment.Left, true); + return result.Count; + } + + /// + /// Computes the max width of a line or multilines needed to render by the Label control + /// + /// Max width of lines. + /// Text, may contain newlines. + /// The width for the text. + public static int MaxWidth (ustring text, int width) + { + var result = new List (); + Recalc (text, result, width, TextAlignment.Left, true); + return result.Max (s => s.RuneCount); + } + + /// + /// Computes the max height of a line or multilines needed to render by the Label control + /// + /// Max height of lines. + /// Text, may contain newlines. + /// The width for the text. + public static int MaxHeight (ustring text, int width) + { + var result = new List (); + Recalc (text, result, width, TextAlignment.Left, true); + return result.Count; + } + + /// + /// The text displayed by the . + /// + public virtual ustring Text { + get => text; + set { + text = value; + recalcPending = true; + SetNeedsDisplay (); + } + } + + /// + /// Controls the text-alignment property of the label, changing it will redisplay the . + /// + /// The text alignment. + public TextAlignment TextAlignment { + get => textAlignment; + set { + textAlignment = value; + SetNeedsDisplay (); + } + } + + Attribute textColor = -1; + /// + /// The color used for the . + /// + public Attribute TextColor { + get => textColor; + set { + textColor = value; + SetNeedsDisplay (); + } + } + } + +} diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs new file mode 100644 index 0000000..e6add2e --- /dev/null +++ b/Terminal.Gui/Views/ListView.cs @@ -0,0 +1,665 @@ +// +// ListView.cs: ListView control +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// +// TODO: +// - Should we support multiple columns, if so, how should that be done? +// - Show mark for items that have been marked. +// - Mouse support +// - Scrollbars? +// +// Column considerations: +// - Would need a way to specify widths +// - Should it automatically extract data out of structs/classes based on public fields/properties? +// - It seems that this would be useful just for the "simple" API, not the IListDAtaSource, as that one has full support for it. +// - Should a function be specified that retrieves the individual elements? +// +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NStack; + +namespace Terminal.Gui { + /// + /// Implement to provide custom rendering for a . + /// + public interface IListDataSource { + /// + /// Returns the number of elements to display + /// + int Count { get; } + + /// + /// This method is invoked to render a specified item, the method should cover the entire provided width. + /// + /// The render. + /// The list view to render. + /// The console driver to render. + /// Describes whether the item being rendered is currently selected by the user. + /// The index of the item to render, zero for the first item and so on. + /// The column where the rendering will start + /// The line where the rendering will be done. + /// The width that must be filled out. + /// + /// The default color will be set before this method is invoked, and will be based on whether the item is selected or not. + /// + void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width); + + /// + /// Should return whether the specified item is currently marked. + /// + /// true, if marked, false otherwise. + /// Item index. + bool IsMarked (int item); + + /// + /// Flags the item as marked. + /// + /// Item index. + /// If set to true value. + void SetMark (int item, bool value); + + /// + /// Return the source as IList. + /// + /// + IList ToList (); + } + + /// + /// ListView renders a scrollable list of data where each item can be activated to perform an action. + /// + /// + /// + /// The displays lists of data and allows the user to scroll through the data. + /// Items in the can be activated firing an event (with the ENTER key or a mouse double-click). + /// If the property is true, elements of the list can be marked by the user. + /// + /// + /// By default uses to render the items of any + /// object (e.g. arrays, , + /// and other collections). Alternatively, an object that implements the + /// interface can be provided giving full control of what is rendered. + /// + /// + /// can display any object that implements the interface. + /// values are converted into values before rendering, and other values are + /// converted into by calling and then converting to . + /// + /// + /// To change the contents of the ListView, set the property (when + /// providing custom rendering via ) or call + /// an is being used. + /// + /// + /// When is set to true the rendering will prefix the rendered items with + /// [x] or [ ] and bind the SPACE key to toggle the selection. To implement a different + /// marking style set to false and implement custom rendering. + /// + /// + public class ListView : View { + int top; + int selected; + + IListDataSource source; + /// + /// Gets or sets the backing this , enabling custom rendering. + /// + /// The source. + /// + /// Use to set a new source. + /// + public IListDataSource Source { + get => source; + set { + source = value; + top = 0; + selected = 0; + SetNeedsDisplay (); + } + } + + /// + /// Sets the source of the to an . + /// + /// An object implementing the IList interface. + /// + /// Use the property to set a new source and use custome rendering. + /// + public void SetSource (IList source) + { + if (source == null) + Source = null; + else { + Source = MakeWrapper (source); + } + } + + /// + /// Sets the source to an value asynchronously. + /// + /// An item implementing the IList interface. + /// + /// Use the property to set a new source and use custome rendering. + /// + public Task SetSourceAsync (IList source) + { + return Task.Factory.StartNew (() => { + if (source == null) + Source = null; + else + Source = MakeWrapper (source); + return source; + }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + + bool allowsMarking; + /// + /// Gets or sets whether this allows items to be marked. + /// + /// true if allows marking elements of the list; otherwise, false. + /// + /// + /// If set to true, will render items marked items with "[x]", and unmarked items with "[ ]" + /// spaces. SPACE key will toggle marking. + /// + public bool AllowsMarking { + get => allowsMarking; + set { + allowsMarking = value; + SetNeedsDisplay (); + } + } + + /// + /// If set to true allows more than one item to be selected. If false only allow one item selected. + /// + public bool AllowsMultipleSelection { get; set; } = true; + + /// + /// Gets or sets the item that is displayed at the top of the . + /// + /// The top item. + public int TopItem { + get => top; + set { + if (source == null) + return; + + if (top < 0 || top >= source.Count) + throw new ArgumentException ("value"); + top = value; + SetNeedsDisplay (); + } + } + + /// + /// Gets or sets the index of the currently selected item. + /// + /// The selected item. + public int SelectedItem { + get => selected; + set { + if (source == null) + return; + if (selected < 0 || selected >= source.Count) + throw new ArgumentException ("value"); + selected = value; + if (selected < top) + top = selected; + else if (selected >= top + Frame.Height) + top = selected; + } + } + + + static IListDataSource MakeWrapper (IList source) + { + return new ListWrapper (source); + } + + /// + /// Initializes a new instance of that will display the contents of the object implementing the interface, + /// with relative positioning. + /// + /// An data source, if the elements are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result. + public ListView (IList source) : this (MakeWrapper (source)) + { + } + + /// + /// Initializes a new instance of that will display the provided data source, using relative positioning. + /// + /// object that provides a mechanism to render the data. + /// The number of elements on the collection should not change, if you must change, set + /// the "Source" property to reset the internal settings of the ListView. + public ListView (IListDataSource source) : base () + { + Source = source; + CanFocus = true; + } + + /// + /// Initializes a new instance of . Set the property to display something. + /// + public ListView () : base () + { + } + + /// + /// Initializes a new instance of that will display the contents of the object implementing the interface with an absolute position. + /// + /// Frame for the listview. + /// An IList data source, if the elements of the IList are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result. + public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source)) + { + } + + /// + /// Initializes a new instance of with the provided data source and an absolute position + /// + /// Frame for the listview. + /// IListDataSource object that provides a mechanism to render the data. The number of elements on the collection should not change, if you must change, set the "Source" property to reset the internal settings of the ListView. + public ListView (Rect rect, IListDataSource source) : base (rect) + { + Source = source; + CanFocus = true; + } + + /// + public override void Redraw (Rect bounds) + { + var current = ColorScheme.Focus; + Driver.SetAttribute (current); + Move (0, 0); + var f = Frame; + var item = top; + bool focused = HasFocus; + int col = allowsMarking ? 4 : 0; + + for (int row = 0; row < f.Height; row++, item++) { + bool isSelected = item == selected; + + var newcolor = focused ? (isSelected ? ColorScheme.Focus : ColorScheme.Normal) : (isSelected ? ColorScheme.HotNormal : ColorScheme.Normal); + if (newcolor != current) { + Driver.SetAttribute (newcolor); + current = newcolor; + } + + Move (0, row); + if (source == null || item >= source.Count) { + for (int c = 0; c < f.Width; c++) + Driver.AddRune (' '); + } else { + if (allowsMarking) { + Driver.AddStr (source.IsMarked (item) ? (AllowsMultipleSelection ? "[x] " : "(o)") : (AllowsMultipleSelection ? "[ ] " : "( )")); + } + Source.Render (this, Driver, isSelected, item, col, row, f.Width - col); + } + } + } + + /// + /// This event is raised when the selected item in the has changed. + /// + public Action SelectedItemChanged; + + /// + /// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item. + /// + public Action OpenSelectedItem; + + /// + public override bool ProcessKey (KeyEvent kb) + { + if (source == null) + return base.ProcessKey (kb); + + switch (kb.Key) { + case Key.CursorUp: + case Key.ControlP: + return MoveUp (); + + case Key.CursorDown: + case Key.ControlN: + return MoveDown (); + + case Key.ControlV: + case Key.PageDown: + return MovePageDown (); + + case Key.PageUp: + return MovePageUp (); + + case Key.Space: + if (MarkUnmarkRow ()) + return true; + else + break; + + case Key.Enter: + OnOpenSelectedItem (); + break; + + } + return base.ProcessKey (kb); + } + + /// + /// Prevents marking if it's not allowed mark and if it's not allows multiple selection. + /// + /// + public virtual bool AllowsAll () + { + if (!allowsMarking) + return false; + if (!AllowsMultipleSelection) { + for (int i = 0; i < Source.Count; i++) { + if (Source.IsMarked (i) && i != selected) { + Source.SetMark (i, false); + return true; + } + } + } + return true; + } + + /// + /// Marks an unmarked row. + /// + /// + public virtual bool MarkUnmarkRow () + { + if (AllowsAll ()) { + Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); + SetNeedsDisplay (); + return true; + } + + return false; + } + + /// + /// Moves the selected item index to the next page. + /// + /// + public virtual bool MovePageUp () + { + int n = (selected - Frame.Height); + if (n < 0) + n = 0; + if (n != selected) { + selected = n; + top = selected; + OnSelectedChanged (); + SetNeedsDisplay (); + } + + return true; + } + + /// + /// Moves the selected item index to the previous page. + /// + /// + public virtual bool MovePageDown () + { + var n = (selected + Frame.Height); + if (n > source.Count) + n = source.Count - 1; + if (n != selected) { + selected = n; + if (source.Count >= Frame.Height) + top = selected; + else + top = 0; + OnSelectedChanged (); + SetNeedsDisplay (); + } + + return true; + } + + /// + /// Moves the selected item index to the next row. + /// + /// + public virtual bool MoveDown () + { + if (selected + 1 < source.Count) { + selected++; + if (selected >= top + Frame.Height) + top++; + OnSelectedChanged (); + SetNeedsDisplay (); + } + + return true; + } + + /// + /// Moves the selected item index to the previous row. + /// + /// + public virtual bool MoveUp () + { + if (selected > 0) { + selected--; + if (selected < top) + top = selected; + OnSelectedChanged (); + SetNeedsDisplay (); + } + + return true; + } + + int lastSelectedItem = -1; + + /// + /// Invokes the SelectedChanged event if it is defined. + /// + /// + public virtual bool OnSelectedChanged () + { + if (selected != lastSelectedItem) { + var value = source.ToList () [selected]; + SelectedItemChanged?.Invoke (new ListViewItemEventArgs (selected, value)); + lastSelectedItem = selected; + return true; + } + + return false; + } + + /// + /// Invokes the OnOpenSelectedItem event if it is defined. + /// + /// + public virtual bool OnOpenSelectedItem () + { + var value = source.ToList () [selected]; + OpenSelectedItem?.Invoke (new ListViewItemEventArgs (selected, value)); + + return true; + } + + /// + public override void PositionCursor () + { + if (allowsMarking) + Move (1, selected - top); + else + Move (0, selected - top); + } + + /// + public override bool MouseEvent(MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && + me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp) + return false; + + if (!HasFocus) + SuperView.SetFocus (this); + + if (source == null) + return false; + + if (me.Flags == MouseFlags.WheeledDown) { + MoveDown (); + return true; + } else if (me.Flags == MouseFlags.WheeledUp) { + MoveUp (); + return true; + } + + if (me.Y + top >= source.Count) + return true; + + selected = top + me.Y; + if (AllowsAll ()) { + Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); + SetNeedsDisplay (); + return true; + } + OnSelectedChanged (); + SetNeedsDisplay (); + if (me.Flags == MouseFlags.Button1DoubleClicked) + OnOpenSelectedItem (); + return true; + } + } + + /// + /// Implements an that renders arbitrary instances for . + /// + /// Implements support for rendering marked items. + public class ListWrapper : IListDataSource { + IList src; + BitArray marks; + int count; + + /// + /// Initializes a new instance of given an + /// + /// + public ListWrapper (IList source) + { + count = source.Count; + marks = new BitArray (count); + this.src = source; + } + + /// + /// Gets the number of items in the . + /// + public int Count => src.Count; + + void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) + { + int byteLen = ustr.Length; + int used = 0; + for (int i = 0; i < byteLen;) { + (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen); + var count = Rune.ColumnWidth (rune); + if (used + count > width) + break; + driver.AddRune (rune); + used += count; + i += size; + } + for (; used < width; used++) { + driver.AddRune (' '); + } + } + + /// + /// Renders a item to the appropriate type. + /// + /// The ListView. + /// The driver used by the caller. + /// Informs if it's marked or not. + /// The item. + /// The col where to move. + /// The line where to move. + /// The item width. + public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width) + { + container.Move (col, line); + var t = src [item]; + if (t == null) { + RenderUstr (driver, ustring.Make(""), col, line, width); + } else { + if (t is ustring) { + RenderUstr (driver, (ustring)t, col, line, width); + } else if (t is string) { + RenderUstr (driver, (string)t, col, line, width); + } else + RenderUstr (driver, t.ToString (), col, line, width); + } + } + + /// + /// Returns true if the item is marked, false otherwise. + /// + /// The item. + /// trueIf is marked.falseotherwise. + public bool IsMarked (int item) + { + if (item >= 0 && item < count) + return marks [item]; + return false; + } + + /// + /// Sets the item as marked or unmarked based on the value is true or false, respectively. + /// + /// The item + /// Marks the item.Unmarked the item.The value. + public void SetMark (int item, bool value) + { + if (item >= 0 && item < count) + marks [item] = value; + } + + /// + /// Returns the source as IList. + /// + /// + public IList ToList () + { + return src; + } + } + + /// + /// for events. + /// + public class ListViewItemEventArgs : EventArgs { + /// + /// The index of the item. + /// + public int Item { get; } + /// + /// The the item. + /// + public object Value { get; } + + /// + /// Initializes a new instance of + /// + /// The index of the the item. + /// The item + public ListViewItemEventArgs (int item, object value) + { + Item = item; + Value = value; + } + } +} diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs new file mode 100644 index 0000000..b0da907 --- /dev/null +++ b/Terminal.Gui/Views/Menu.cs @@ -0,0 +1,1277 @@ +// +// Menu.cs: application menus and submenus +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// TODO: +// Add accelerator support, but should also support chords (ShortCut in MenuItem) +// Allow menus inside menus + +using System; +using NStack; +using System.Linq; +using System.Collections.Generic; + +namespace Terminal.Gui { + + /// + /// Specifies how a shows selection state. + /// + [Flags] + public enum MenuItemCheckStyle { + /// + /// The menu item will be shown normally, with no check indicator. + /// + NoCheck = 0b_0000_0000, + + /// + /// The menu item will indicate checked/un-checked state (see . + /// + Checked = 0b_0000_0001, + + /// + /// The menu item is part of a menu radio group (see and will indicate selected state. + /// + Radio = 0b_0000_0010, + }; + + /// + /// A has a title, an associated help text, and an action to execute on activation. + /// + public class MenuItem { + + /// + /// Initializes a new instance of + /// + public MenuItem () + { + Title = ""; + Help = ""; + } + + /// + /// Initializes a new instance of . + /// + /// Title for the menu item. + /// Help text to display. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executred. + public MenuItem (ustring title, string help, Action action, Func canExecute = null) + { + Title = title ?? ""; + Help = help ?? ""; + Action = action; + CanExecute = canExecute; + bool nextIsHot = false; + foreach (var x in Title) { + if (x == '_') + nextIsHot = true; + else { + if (nextIsHot) { + HotKey = Char.ToUpper ((char)x); + break; + } + nextIsHot = false; + } + } + } + + /// + /// Initializes a new instance of + /// + /// Title for the menu item. + /// The menu sub-menu. + public MenuItem (ustring title, MenuBarItem subMenu) : this (title, "", null) + { + SubMenu = subMenu; + IsFromSubMenu = true; + } + + /// + /// The HotKey is used when the menu is active, the shortcut can be triggered when the menu is not active. + /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry + /// if the ShortCut is set to "Control-N", this would be a global hotkey that would trigger as well + /// + public Rune HotKey; + + /// + /// This is the global setting that can be used as a global shortcut to invoke the action on the menu. + /// + public Key ShortCut; + + /// + /// Gets or sets the title. + /// + /// The title. + public ustring Title { get; set; } + + /// + /// Gets or sets the help text for the menu item. + /// + /// The help text. + public ustring Help { get; set; } + + /// + /// Gets or sets the action to be invoked when the menu is triggered + /// + /// Method to invoke. + public Action Action { get; set; } + + /// + /// Gets or sets the action to be invoked if the menu can be triggered + /// + /// Function to determine if action is ready to be executed. + public Func CanExecute { get; set; } + + /// + /// Shortcut to check if the menu item is enabled + /// + public bool IsEnabled () + { + return CanExecute == null ? true : CanExecute (); + } + + internal int Width => Title.Length + Help.Length + 1 + 2 + + (Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0); + + /// + /// Sets or gets whether the shows a check indicator or not. See . + /// + public bool Checked { set; get; } + + /// + /// Sets or gets the type selection indicator the menu item will be displayed with. + /// + public MenuItemCheckStyle CheckType { get; set; } + + /// + /// Gets or sets the parent for this + /// + /// The parent. + internal MenuBarItem SubMenu { get; set; } + internal bool IsFromSubMenu { get; set; } + + /// + /// Merely a debugging aid to see the interaction with main + /// + public MenuItem GetMenuItem () + { + return this; + } + + /// + /// Merely a debugging aid to see the interaction with main + /// + public bool GetMenuBarItem () + { + return IsFromSubMenu; + } + } + + /// + /// A contains s or s. + /// + public class MenuBarItem : MenuItem { + /// + /// Initializes a new as a . + /// + /// Title for the menu item. + /// Help text to display. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executred. + public MenuBarItem (ustring title, string help, Action action, Func canExecute = null) : base (title, help, action, canExecute) + { + SetTitle (title ?? ""); + Children = null; + } + + /// + /// Initializes a new . + /// + /// Title for the menu item. + /// The items in the current menu. + public MenuBarItem (ustring title, MenuItem [] children) + { + if (children == null) + throw new ArgumentNullException (nameof (children), "The parameter cannot be null. Use an empty array instead."); + + SetTitle (title ?? ""); + Children = children; + } + + /// + /// Initializes a new . + /// + /// The items in the current menu. + public MenuBarItem (MenuItem [] children) : this (new string (' ', GetMaxTitleLength (children)), children) { } + + /// + /// Initializes a new . + /// + public MenuBarItem () : this (children: new MenuItem [] { }) { } + + static int GetMaxTitleLength (MenuItem [] children) + { + int maxLength = 0; + foreach (var item in children) { + int len = GetMenuBarItemLength (item.Title); + if (len > maxLength) + maxLength = len; + item.IsFromSubMenu = true; + } + + return maxLength; + } + + void SetTitle (ustring title) + { + if (title == null) + title = ""; + Title = title; + TitleLength = GetMenuBarItemLength (Title); + } + + static int GetMenuBarItemLength (ustring title) + { + int len = 0; + foreach (var ch in title) { + if (ch == '_') + continue; + len++; + } + + return len; + } + + ///// + ///// Gets or sets the title to display. + ///// + ///// The title. + //public ustring Title { get; set; } + + /// + /// Gets or sets an array of objects that are the children of this + /// + /// The children. + public MenuItem [] Children { get; set; } + internal int TitleLength { get; private set; } + + internal bool IsTopLevel { get => (Children == null || Children.Length == 0); } + + } + + class Menu : View { + internal MenuBarItem barItems; + MenuBar host; + internal int current; + internal View previousSubFocused; + + static Rect MakeFrame (int x, int y, MenuItem [] items) + { + if (items == null || items.Length == 0) { + return new Rect (); + } + int maxW = items.Max (z => z?.Width) ?? 0; + + return new Rect (x, y, maxW + 2, items.Length + 2); + } + + public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children)) + { + this.barItems = barItems; + this.host = host; + if (barItems.IsTopLevel) { + // This is a standalone MenuItem on a MenuBar + ColorScheme = Colors.Menu; + CanFocus = true; + } else { + + current = -1; + for (int i = 0; i < barItems.Children.Length; i++) { + if (barItems.Children [i] != null) { + current = i; + break; + } + } + ColorScheme = Colors.Menu; + CanFocus = true; + WantMousePositionReports = host.WantMousePositionReports; + } + + } + + internal Attribute DetermineColorSchemeFor (MenuItem item, int index) + { + if (item != null) { + if (index == current) return ColorScheme.Focus; + if (!item.IsEnabled ()) return ColorScheme.Disabled; + } + return ColorScheme.Normal; + } + + public override void Redraw (Rect bounds) + { + Driver.SetAttribute (ColorScheme.Normal); + DrawFrame (bounds, padding: 0, fill: true); + + for (int i = 0; i < barItems.Children.Length; i++) { + var item = barItems.Children [i]; + Driver.SetAttribute (item == null ? ColorScheme.Normal : i == current ? ColorScheme.Focus : ColorScheme.Normal); + if (item == null) { + Move (0, i + 1); + Driver.AddRune (Driver.LeftTee); + } else + Move (1, i + 1); + + Driver.SetAttribute (DetermineColorSchemeFor (item, i)); + for (int p = 0; p < Frame.Width - 2; p++) + if (item == null) + Driver.AddRune (Driver.HLine); + else if (p == Frame.Width - 3 && barItems.Children [i].SubMenu != null) + Driver.AddRune (Driver.RightArrow); + else + Driver.AddRune (' '); + + if (item == null) { + Move (Frame.Right - 1, i + 1); + Driver.AddRune (Driver.RightTee); + continue; + } + + ustring textToDraw; + var checkChar = Driver.Selected; + var uncheckedChar = Driver.UnSelected; + + if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) { + checkChar = Driver.Checked; + uncheckedChar = Driver.UnChecked; + } + + // Support Checked even though CHeckType wasn't set + if (item.Checked) { + textToDraw = ustring.Make(new Rune [] { checkChar, ' ' }) + item.Title; + } else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || + item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) { + textToDraw = ustring.Make (new Rune [] { uncheckedChar, ' ' }) + item.Title; + } else { + textToDraw = item.Title; + } + + Move (2, i + 1); + if (!item.IsEnabled ()) + DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled); + else + DrawHotString (textToDraw, + i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, + i == current ? ColorScheme.Focus : ColorScheme.Normal); + + // The help string + var l = item.Help.Length; + Move (Frame.Width - l - 2, 1 + i); + Driver.AddStr (item.Help); + } + PositionCursor (); + } + + public override void PositionCursor () + { + if (host == null || host.IsMenuOpen) + if (barItems.IsTopLevel) { + host.PositionCursor (); + } else + Move (2, 1 + current); + else + host.PositionCursor (); + } + + public void Run (Action action) + { + if (action == null) + return; + + Application.UngrabMouse (); + host.CloseAllMenus (); + Application.Refresh (); + + Application.MainLoop.AddIdle (() => { + action (); + return false; + }); + } + + public override bool OnKeyDown (KeyEvent keyEvent) + { + if (keyEvent.IsAlt) { + host.CloseAllMenus (); + return true; + } + + return false; + } + + public override bool ProcessHotKey (KeyEvent keyEvent) + { + // To ncurses simulate a AltMask key pressing Alt+Space because + // it can�t detect an alone special key down was pressed. + if (keyEvent.IsAlt && keyEvent.Key == Key.AltMask) { + OnKeyDown (keyEvent); + return true; + } + + return false; + } + + public override bool ProcessKey (KeyEvent kb) + { + bool disabled; + switch (kb.Key) { + case Key.CursorUp: + if (barItems.IsTopLevel || current == -1) + break; + do { + disabled = false; + current--; + if (host.UseKeysUpDownAsKeysLeftRight) { + if (current == -1 && barItems.Children [current + 1].IsFromSubMenu && host.selectedSub > -1) { + current++; + host.PreviousMenu (true); + break; + } + } + if (current < 0) + current = barItems.Children.Length - 1; + var item = barItems.Children [current]; + if (item == null || !item.IsEnabled ()) disabled = true; + } while (barItems.Children [current] == null || disabled); + SetNeedsDisplay (); + break; + case Key.CursorDown: + if (barItems.IsTopLevel) { + break; + } + + do { + current++; + disabled = false; + if (current == barItems.Children.Length) + current = 0; + var item = barItems.Children [current]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (host.UseKeysUpDownAsKeysLeftRight && barItems.Children [current]?.SubMenu != null && + !disabled && host.IsMenuOpen) { + CheckSubMenu (); + break; + } + if (!host.IsMenuOpen) + host.OpenMenu (host.selected); + } while (barItems.Children [current] == null || disabled); + SetNeedsDisplay (); + break; + case Key.CursorLeft: + host.PreviousMenu (true); + break; + case Key.CursorRight: + host.NextMenu (barItems.IsTopLevel || barItems.Children [current].IsFromSubMenu ? true : false); + break; + case Key.Esc: + Application.UngrabMouse (); + host.CloseAllMenus (); + break; + case Key.Enter: + if (barItems.IsTopLevel) { + Run (barItems.Action); + } else { + CheckSubMenu (); + Run (barItems.Children [current].Action); + } + break; + default: + // TODO: rune-ify + if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) { + var x = Char.ToUpper ((char)kb.KeyValue); + foreach (var item in barItems.Children) { + if (item == null) continue; + if (item.IsEnabled () && item.HotKey == x) { + host.CloseMenu (); + Run (item.Action); + return true; + } + } + } + break; + } + return true; + } + + public override bool MouseEvent (MouseEvent me) + { + if (!host.handled && !host.HandleGrabView (me, this)) { + return false; + } + host.handled = false; + bool disabled; + if (me.Flags == MouseFlags.Button1Clicked) { + disabled = false; + if (me.Y < 1) + return true; + var meY = me.Y - 1; + if (meY >= barItems.Children.Length) + return true; + var item = barItems.Children [meY]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (item != null && !disabled) + Run (barItems.Children [meY].Action); + return true; + } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || + me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.ReportMousePosition || + me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + disabled = false; + if (me.Y < 1) + return true; + if (me.Y - 1 >= barItems.Children.Length) + return true; + var item = barItems.Children [me.Y - 1]; + if (item == null || !item.IsEnabled ()) disabled = true; + if (item != null && !disabled) + current = me.Y - 1; + HasFocus = true; + SetNeedsDisplay (); + CheckSubMenu (); + return true; + } + return false; + } + + internal void CheckSubMenu () + { + if (barItems.Children [current] == null) + return; + var subMenu = barItems.Children [current].SubMenu; + if (subMenu != null) { + int pos = -1; + if (host.openSubMenu != null) + pos = host.openSubMenu.FindIndex (o => o?.barItems == subMenu); + host.Activate (host.selected, pos, subMenu); + } else if (host.openSubMenu != null && !barItems.Children [current].IsFromSubMenu) + host.CloseMenu (false, true); + } + + int GetSubMenuIndex (MenuBarItem subMenu) + { + int pos = -1; + if (this != null && Subviews.Count > 0) { + Menu v = null; + foreach (var menu in Subviews) { + if (((Menu)menu).barItems == subMenu) + v = (Menu)menu; + } + if (v != null) + pos = Subviews.IndexOf (v); + } + + return pos; + } + } + + + + /// + /// The MenuBar provides a menu for Terminal.Gui applications. + /// + /// + /// + /// The appears on the first row of the terminal. + /// + /// + /// The provides global hotkeys for the application. + /// + /// + public class MenuBar : View { + /// + /// Gets or sets the array of s for the menu. Only set this when the is vislble. + /// + /// The menu array. + public MenuBarItem [] Menus { get; set; } + internal int selected; + internal int selectedSub; + + Action action; + + /// + /// Used for change the navigation key style. + /// + public bool UseKeysUpDownAsKeysLeftRight { get; set; } = true; + + /// + /// Initializes a new instance of the . + /// + public MenuBar () : this (new MenuBarItem [] { }) { } + + /// + /// Initializes a new instance of the class with the specified set of toplevel menu items. + /// + /// Individual menu items; a null item will result in a separator being drawn. + public MenuBar (MenuBarItem [] menus) : base () + { + X = 0; + Y = 0; + Width = Dim.Fill (); + Height = 1; + Menus = menus; + //CanFocus = true; + selected = -1; + selectedSub = -1; + ColorScheme = Colors.Menu; + WantMousePositionReports = true; + IsMenuOpen = false; + } + + bool openedByAltKey; + + /// + public override bool OnKeyDown (KeyEvent keyEvent) + { + if (keyEvent.IsAlt) { + openedByAltKey = true; + SetNeedsDisplay (); + openedByHotKey = false; + } + return false; + } + + /// + public override bool OnKeyUp (KeyEvent keyEvent) + { + if (keyEvent.IsAlt) { + // User pressed Alt - this may be a precursor to a menu accelerator (e.g. Alt-F) + if (!keyEvent.IsCtrl && openedByAltKey && !IsMenuOpen && openMenu == null && ((uint)keyEvent.Key & (uint)Key.CharMask) == 0) { + // There's no open menu, the first menu item should be highlight. + // The right way to do this is to SetFocus(MenuBar), but for some reason + // that faults. + + //Activate (0); + //StartMenu (); + IsMenuOpen = true; + selected = 0; + CanFocus = true; + lastFocused = SuperView.MostFocused; + SuperView.SetFocus (this); + SetNeedsDisplay (); + Application.GrabMouse (this); + } else if (!openedByHotKey) { + // There's an open menu. If this Alt key-up is a pre-cursor to an accelerator + // we don't want to close the menu because it'll flash. + // How to deal with that? + + if (openMenu != null) + CloseAllMenus (); + openedByAltKey = false; + IsMenuOpen = false; + selected = -1; + CanFocus = false; + if (lastFocused != null) + SuperView?.SetFocus (lastFocused); + SetNeedsDisplay (); + Application.UngrabMouse (); + } + + return true; + } + return false; + } + + /// + public override void Redraw (Rect bounds) + { + Move (0, 0); + Driver.SetAttribute (Colors.Menu.Normal); + for (int i = 0; i < Frame.Width; i++) + Driver.AddRune (' '); + + Move (1, 0); + int pos = 1; + + for (int i = 0; i < Menus.Length; i++) { + var menu = Menus [i]; + Move (pos, 0); + Attribute hotColor, normalColor; + if (i == selected) { + hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal; + normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal; + } else if (openedByAltKey) { + hotColor = ColorScheme.HotNormal; + normalColor = ColorScheme.Normal; + } else { + hotColor = ColorScheme.Normal; + normalColor = ColorScheme.Normal; + } + DrawHotString ($" {menu.Title} ", hotColor, normalColor); + pos += 1 + menu.TitleLength + 2; + } + PositionCursor (); + } + + /// + public override void PositionCursor () + { + int pos = 0; + for (int i = 0; i < Menus.Length; i++) { + if (i == selected) { + pos++; + if (IsMenuOpen) + Move (pos + 1, 0); + else + Move (pos + 1, 0); + return; + } else { + if (IsMenuOpen) + pos += 1 + Menus [i].TitleLength + 2; + else + pos += 2 + Menus [i].TitleLength + 1; + } + } + //Move (0, 0); + } + + void Selected (MenuItem item) + { + // TODO: Running = false; + action = item.Action; + } + + /// + /// Raised as a menu is opening. + /// + public Action MenuOpening; + + /// + /// Raised when a menu is closing. + /// + public Action MenuClosing; + + internal Menu openMenu; + Menu openCurrentMenu; + internal List openSubMenu; + View previousFocused; + internal bool isMenuOpening; + internal bool isMenuClosing; + + /// + /// True if the menu is open; otherwise false. + /// + public bool IsMenuOpen { get; protected set; } + + /// + /// Virtual method that will invoke the + /// + public virtual void OnMenuOpening () + { + MenuOpening?.Invoke (); + } + + /// + /// Virtual method that will invoke the + /// + public virtual void OnMenuClosing () + { + MenuClosing?.Invoke (); + } + + View lastFocused; + + /// + /// Get the lasted focused view before open the menu. + /// + public View LastFocused { get; private set; } + + internal void OpenMenu (int index, int sIndex = -1, MenuBarItem subMenu = null) + { + isMenuOpening = true; + OnMenuOpening (); + int pos = 0; + switch (subMenu) { + case null: + lastFocused = lastFocused ?? SuperView.MostFocused; + if (openSubMenu != null) + CloseMenu (false, true); + if (openMenu != null) + SuperView.Remove (openMenu); + + for (int i = 0; i < index; i++) + pos += Menus [i].Title.Length + 2; + openMenu = new Menu (this, pos, 1, Menus [index]); + openCurrentMenu = openMenu; + openCurrentMenu.previousSubFocused = openMenu; + + SuperView.Add (openMenu); + SuperView.SetFocus (openMenu); + break; + default: + if (openSubMenu == null) + openSubMenu = new List (); + if (sIndex > -1) { + RemoveSubMenu (sIndex); + } else { + var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu; + openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu); + openCurrentMenu.previousSubFocused = last.previousSubFocused; + openSubMenu.Add (openCurrentMenu); + SuperView.Add (openCurrentMenu); + } + selectedSub = openSubMenu.Count - 1; + SuperView?.SetFocus (openCurrentMenu); + break; + } + isMenuOpening = false; + IsMenuOpen = true; + } + + /// + /// Opens the current Menu programatically. + /// + public void OpenMenu () + { + if (openMenu != null) + return; + selected = 0; + SetNeedsDisplay (); + + previousFocused = SuperView.Focused; + OpenMenu (selected); + Application.GrabMouse (this); + } + + // Activates the menu, handles either first focus, or activating an entry when it was already active + // For mouse events. + internal void Activate (int idx, int sIdx = -1, MenuBarItem subMenu = null) + { + selected = idx; + selectedSub = sIdx; + if (openMenu == null) + previousFocused = SuperView.Focused; + + OpenMenu (idx, sIdx, subMenu); + SetNeedsDisplay (); + } + + /// + /// Closes the current Menu programatically, if open. + /// + public void CloseMenu () + { + CloseMenu (false, false); + } + + internal void CloseMenu (bool reopen = false, bool isSubMenu = false) + { + isMenuClosing = true; + OnMenuClosing (); + switch (isSubMenu) { + case false: + if (openMenu != null) + SuperView.Remove (openMenu); + SetNeedsDisplay (); + if (previousFocused != null && openMenu != null && previousFocused.ToString () != openCurrentMenu.ToString ()) + previousFocused?.SuperView?.SetFocus (previousFocused); + openMenu = null; + if (lastFocused is Menu) { + lastFocused = null; + } + LastFocused = lastFocused; + lastFocused = null; + if (LastFocused != null) { + if (!reopen) + selected = -1; + LastFocused.SuperView?.SetFocus (LastFocused); + } else { + SuperView.SetFocus (this); + PositionCursor (); + } + IsMenuOpen = false; + break; + + case true: + selectedSub = -1; + SetNeedsDisplay (); + RemoveAllOpensSubMenus (); + openCurrentMenu.previousSubFocused?.SuperView?.SetFocus (openCurrentMenu.previousSubFocused); + openSubMenu = null; + IsMenuOpen = true; + break; + } + isMenuClosing = false; + } + + void RemoveSubMenu (int index) + { + if (openSubMenu == null) + return; + for (int i = openSubMenu.Count - 1; i > index; i--) { + isMenuClosing = true; + if (openSubMenu.Count - 1 > 0) + SuperView.SetFocus (openSubMenu [i - 1]); + else + SuperView.SetFocus (openMenu); + if (openSubMenu != null) { + SuperView.Remove (openSubMenu [i]); + openSubMenu.Remove (openSubMenu [i]); + } + RemoveSubMenu (i); + } + if (openSubMenu.Count > 0) + openCurrentMenu = openSubMenu.Last (); + + //if (openMenu.Subviews.Count == 0) + // return; + //if (index == 0) { + // //SuperView.SetFocus (previousSubFocused); + // FocusPrev (); + // return; + //} + + //for (int i = openMenu.Subviews.Count - 1; i > index; i--) { + // isMenuClosing = true; + // if (openMenu.Subviews.Count - 1 > 0) + // SuperView.SetFocus (openMenu.Subviews [i - 1]); + // else + // SuperView.SetFocus (openMenu); + // if (openMenu != null) { + // Remove (openMenu.Subviews [i]); + // openMenu.Remove (openMenu.Subviews [i]); + // } + // RemoveSubMenu (i); + //} + isMenuClosing = false; + } + + internal void RemoveAllOpensSubMenus () + { + if (openSubMenu != null) { + foreach (var item in openSubMenu) { + SuperView.Remove (item); + } + } + } + + internal void CloseAllMenus () + { + if (!isMenuOpening && !isMenuClosing) { + if (openSubMenu != null) + CloseMenu (false, true); + CloseMenu (); + if (LastFocused != null && LastFocused != this) + selected = -1; + } + IsMenuOpen = false; + openedByHotKey = false; + openedByAltKey = false; + } + + View FindDeepestMenu (View view, ref int count) + { + count = count > 0 ? count : 0; + foreach (var menu in view.Subviews) { + if (menu is Menu) { + count++; + return FindDeepestMenu ((Menu)menu, ref count); + } + } + return view; + } + + internal void PreviousMenu (bool isSubMenu = false) + { + switch (isSubMenu) { + case false: + if (selected <= 0) + selected = Menus.Length - 1; + else + selected--; + + if (selected > -1) + CloseMenu (true, false); + OpenMenu (selected); + break; + case true: + if (selectedSub > -1) { + selectedSub--; + RemoveSubMenu (selectedSub); + SetNeedsDisplay (); + } else + PreviousMenu (); + + break; + } + } + + internal void NextMenu (bool isSubMenu = false) + { + switch (isSubMenu) { + case false: + if (selected == -1) + selected = 0; + else if (selected + 1 == Menus.Length) + selected = 0; + else + selected++; + + if (selected > -1) + CloseMenu (true); + OpenMenu (selected); + break; + case true: + if (UseKeysUpDownAsKeysLeftRight) { + CloseMenu (false, true); + NextMenu (); + } else { + if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && openCurrentMenu.barItems.Children [openCurrentMenu.current].SubMenu == null) { + if (openSubMenu != null) + CloseMenu (false, true); + NextMenu (); + } else if (openCurrentMenu.barItems.Children [openCurrentMenu.current].SubMenu != null || + !openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu) + selectedSub++; + else + return; + SetNeedsDisplay (); + openCurrentMenu.CheckSubMenu (); + } + break; + } + } + + bool openedByHotKey; + internal bool FindAndOpenMenuByHotkey (KeyEvent kb) + { + //int pos = 0; + var c = ((uint)kb.Key & (uint)Key.CharMask); + for (int i = 0; i < Menus.Length; i++) { + // TODO: this code is duplicated, hotkey should be part of the MenuBarItem + var mi = Menus [i]; + int p = mi.Title.IndexOf ('_'); + if (p != -1 && p + 1 < mi.Title.Length) { + if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) { + ProcessMenu (i, mi); + return true; + } + } + } + return false; + } + + private void ProcessMenu (int i, MenuBarItem mi) + { + if (mi.IsTopLevel) { + var menu = new Menu (this, i, 0, mi); + menu.Run (mi.Action); + } else { + openedByHotKey = true; + Application.GrabMouse (this); + selected = i; + OpenMenu (i); + } + } + + /// + public override bool ProcessHotKey (KeyEvent kb) + { + if (kb.Key == Key.F9) { + if (!IsMenuOpen) + OpenMenu (); + else + CloseAllMenus (); + return true; + } + + // To ncurses simulate a AltMask key pressing Alt+Space because + // it can�t detect an alone special key down was pressed. + if (kb.IsAlt && kb.Key == Key.AltMask && openMenu == null) { + OnKeyDown (kb); + OnKeyUp (kb); + return true; + } else if (kb.IsAlt) { + if (FindAndOpenMenuByHotkey (kb)) return true; + } + //var kc = kb.KeyValue; + + return base.ProcessHotKey (kb); + } + + /// + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.CursorLeft: + selected--; + if (selected < 0) + selected = Menus.Length - 1; + break; + case Key.CursorRight: + selected = (selected + 1) % Menus.Length; + break; + + case Key.Esc: + case Key.ControlC: + //TODO: Running = false; + CloseMenu (); + if (openedByAltKey) { + openedByAltKey = false; + LastFocused.SuperView?.SetFocus (LastFocused); + } + break; + + case Key.CursorDown: + case Key.Enter: + ProcessMenu (selected, Menus [selected]); + break; + + default: + var key = kb.KeyValue; + if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) { + char c = Char.ToUpper ((char)key); + + if (selected == -1 || Menus [selected].IsTopLevel) + return false; + + foreach (var mi in Menus [selected].Children) { + if (mi == null) + continue; + int p = mi.Title.IndexOf ('_'); + if (p != -1 && p + 1 < mi.Title.Length) { + if (mi.Title [p + 1] == c) { + Selected (mi); + return true; + } + } + } + } + + return false; + } + SetNeedsDisplay (); + return true; + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (!handled && !HandleGrabView (me, this)) { + return false; + } + handled = false; + + if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.Button1Clicked || + (me.Flags == MouseFlags.ReportMousePosition && selected > -1) || + (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && selected > -1)) { + int pos = 1; + int cx = me.X; + for (int i = 0; i < Menus.Length; i++) { + if (cx > pos && me.X < pos + 1 + Menus [i].TitleLength) { + if (me.Flags == MouseFlags.Button1Clicked) { + if (Menus [i].IsTopLevel) { + var menu = new Menu (this, i, 0, Menus [i]); + menu.Run (Menus [i].Action); + } + } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked) { + if (IsMenuOpen) { + CloseAllMenus (); + } else { + Activate (i); + } + } else if (selected != i && selected > -1 && (me.Flags == MouseFlags.ReportMousePosition || + me.Flags == MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition)) { + if (IsMenuOpen) { + CloseMenu (); + Activate (i); + } + } else { + if (IsMenuOpen) + Activate (i); + } + return true; + } + pos += 2 + Menus [i].TitleLength + 1; + } + } + return false; + } + + internal bool handled; + + internal bool HandleGrabView (MouseEvent me, View current) + { + if (Application.mouseGrabView != null) { + if (me.View is MenuBar || me.View is Menu) { + if (me.View != current) { + Application.UngrabMouse (); + Application.GrabMouse (me.View); + me.View.MouseEvent (me); + } + } else if (!(me.View is MenuBar || me.View is Menu) && (me.Flags.HasFlag (MouseFlags.Button1Clicked) || + me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked)) { + Application.UngrabMouse (); + CloseAllMenus (); + handled = false; + return false; + } else { + handled = false; + return false; + } + } else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { + Application.GrabMouse (current); + } else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) { + Application.GrabMouse (me.View); + } else { + handled = false; + return false; + } + //if (me.View != this && me.Flags != MouseFlags.Button1Pressed) + // return true; + //else if (me.View != this && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { + // Application.UngrabMouse (); + // host.CloseAllMenus (); + // return true; + //} + + + //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Pressed)) + // return false; + + //if (Application.mouseGrabView != null) { + // if (me.View is MenuBar || me.View is Menu) { + // me.X -= me.OfX; + // me.Y -= me.OfY; + // me.View.MouseEvent (me); + // return true; + // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { + // Application.UngrabMouse (); + // CloseAllMenus (); + // } + //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { + // Application.GrabMouse (this); + // return true; + //} + + //if (Application.mouseGrabView != null) { + // if (Application.mouseGrabView == me.View && me.View == current) { + // me.X -= me.OfX; + // me.Y -= me.OfY; + // } else if (me.View != current && me.View is MenuBar && me.View is Menu) { + // Application.UngrabMouse (); + // Application.GrabMouse (me.View); + // } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { + // Application.UngrabMouse (); + // CloseMenu (); + // } + //} else if ((!isMenuClosed && selected > -1)) { + // Application.GrabMouse (current); + //} + + handled = true; + + return true; + } + } + +} diff --git a/Terminal.Gui/Views/ProgressBar.cs b/Terminal.Gui/Views/ProgressBar.cs new file mode 100644 index 0000000..18a04c5 --- /dev/null +++ b/Terminal.Gui/Views/ProgressBar.cs @@ -0,0 +1,106 @@ +using System; +namespace Terminal.Gui { + + /// + /// A Progress Bar view that can indicate progress of an activity visually. + /// + /// + /// + /// can operate in two modes, percentage mode, or + /// activity mode. The progress bar starts in percentage mode and + /// setting the Fraction property will reflect on the UI the progress + /// made so far. Activity mode is used when the application has no + /// way of knowing how much time is left, and is started when the method is called. + /// Call repeatedly as progress is made. + /// + /// + public class ProgressBar : View { + bool isActivity; + int activityPos, delta; + + /// + /// Initializes a new instance of the class, starts in percentage mode with an absolute position and size. + /// + /// Rect. + public ProgressBar (Rect rect) : base (rect) + { + CanFocus = false; + fraction = 0; + } + + /// + /// Initializes a new instance of the class, starts in percentage mode and uses relative layout. + /// + public ProgressBar () : base () + { + CanFocus = false; + fraction = 0; + } + + float fraction; + + /// + /// Gets or sets the fraction to display, must be a value between 0 and 1. + /// + /// The fraction representing the progress. + public float Fraction { + get => fraction; + set { + fraction = value; + isActivity = false; + SetNeedsDisplay (); + } + } + + /// + /// Notifies the that some progress has taken place. + /// + /// + /// If the is is percentage mode, it switches to activity + /// mode. If is in activity mode, the marker is moved. + /// + public void Pulse () + { + if (!isActivity) { + isActivity = true; + activityPos = 0; + delta = 1; + } else { + activityPos += delta; + if (activityPos < 0) { + activityPos = 1; + delta = 1; + } else if (activityPos >= Frame.Width) { + activityPos = Frame.Width - 2; + delta = -1; + } + } + + SetNeedsDisplay (); + } + + /// + public override void Redraw(Rect region) + { + Driver.SetAttribute (ColorScheme.Normal); + + int top = Frame.Width; + if (isActivity) { + Move (0, 0); + for (int i = 0; i < top; i++) + if (i == activityPos) + Driver.AddRune (Driver.Stipple); + else + Driver.AddRune (' '); + } else { + Move (0, 0); + int mid = (int)(fraction * top); + int i; + for (i = 0; i < mid; i++) + Driver.AddRune (Driver.Stipple); + for (; i < top; i++) + Driver.AddRune (' '); + } + } + } +} diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs new file mode 100644 index 0000000..7bcd7aa --- /dev/null +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -0,0 +1,280 @@ +using NStack; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal.Gui { + /// + /// shows a group of radio labels, only one of those can be selected at a given time + /// + public class RadioGroup : View { + int selected = -1; + int cursor; + + void Init(Rect rect, ustring [] radioLabels, int selected) + { + if (radioLabels == null) { + this.radioLabels = new List(); + } else { + this.radioLabels = radioLabels.ToList (); + } + + this.selected = selected; + SetWidthHeight (this.radioLabels); + CanFocus = true; + } + + + /// + /// Initializes a new instance of the class using layout. + /// + public RadioGroup () : this (radioLabels: new ustring [] { }) { } + + /// + /// Initializes a new instance of the class using layout. + /// + /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. + /// The index of the item to be selected, the value is clamped to the number of items. + public RadioGroup (ustring [] radioLabels, int selected = 0) : base () + { + Init (Rect.Empty, radioLabels, selected); + } + + /// + /// Initializes a new instance of the class using layout. + /// + /// Boundaries for the radio group. + /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. + /// The index of item to be selected, the value is clamped to the number of items. + public RadioGroup (Rect rect, ustring [] radioLabels, int selected = 0) : base (rect) + { + Init (rect, radioLabels, selected); + } + + /// + /// Initializes a new instance of the class using layout. + /// The frame is computed from the provided radio labels. + /// + /// The x coordinate. + /// The y coordinate. + /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. + /// The item to be selected, the value is clamped to the number of items. + public RadioGroup (int x, int y, ustring [] radioLabels, int selected = 0) : + this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList() : null), radioLabels, selected) { } + + /// + /// The location of the cursor in the + /// + public int Cursor { + get => cursor; + set { + if (cursor < 0 || cursor >= radioLabels.Count) + return; + cursor = value; + SetNeedsDisplay (); + } + } + + void SetWidthHeight (List radioLabels) + { + var r = MakeRect(0, 0, radioLabels); + if (LayoutStyle == LayoutStyle.Computed) { + Width = r.Width; + Height = radioLabels.Count; + } else { + Frame = new Rect (Frame.Location, new Size (r.Width, radioLabels.Count)); + } + } + + static Rect MakeRect (int x, int y, List radioLabels) + { + int width = 0; + + if (radioLabels == null) { + return new Rect (x, y, width, 0); + } + + foreach (var s in radioLabels) + width = Math.Max (s.Length + 3, width); + return new Rect (x, y, width, radioLabels.Count); + } + + + List radioLabels = new List (); + + /// + /// The radio labels to display + /// + /// The radio labels. + public ustring [] RadioLabels { + get => radioLabels.ToArray(); + set { + var prevCount = radioLabels.Count; + radioLabels = value.ToList (); + if (prevCount != radioLabels.Count) { + SetWidthHeight (radioLabels); + } + SelectedItem = 0; + cursor = 0; + SetNeedsDisplay (); + } + } + + //// Redraws the RadioGroup + //void Update(List newRadioLabels) + //{ + // for (int i = 0; i < radioLabels.Count; i++) { + // Move(0, i); + // Driver.SetAttribute(ColorScheme.Normal); + // Driver.AddStr(ustring.Make(new string (' ', radioLabels[i].Length + 4))); + // } + // if (newRadioLabels.Count != radioLabels.Count) { + // SetWidthHeight(newRadioLabels); + // } + //} + + /// + public override void Redraw (Rect bounds) + { + Driver.SetAttribute (ColorScheme.Normal); + Clear (); + for (int i = 0; i < radioLabels.Count; i++) { + Move (0, i); + Driver.SetAttribute (ColorScheme.Normal); + Driver.AddStr (ustring.Make(new Rune[] { (i == selected ? Driver.Selected : Driver.UnSelected), ' '})); + DrawHotString (radioLabels [i], HasFocus && i == cursor, ColorScheme); + } + base.Redraw (bounds); + } + + /// + public override void PositionCursor () + { + Move (0, cursor); + } + + // TODO: Make this a global class + /// + /// Event arguments for the SelectedItemChagned event. + /// + public class SelectedItemChangedArgs : EventArgs { + /// + /// Gets the index of the item that was previously selected. -1 if there was no previous selection. + /// + public int PreviousSelectedItem { get; } + + /// + /// Gets the index of the item that is now selected. -1 if there is no selection. + /// + public int SelectedItem { get; } + + /// + /// Initializes a new class. + /// + /// + /// + public SelectedItemChangedArgs(int selectedItem, int previousSelectedItem) + { + PreviousSelectedItem = previousSelectedItem; + SelectedItem = selectedItem; + } + } + + /// + /// Invoked when the selected radio label has changed. + /// + public Action SelectedItemChanged; + + /// + /// The currently selected item from the list of radio labels + /// + /// The selected. + public int SelectedItem { + get => selected; + set { + OnSelectedItemChanged (value, SelectedItem); + SetNeedsDisplay (); + } + } + + /// + /// Called whenever the current selected item changes. Invokes the event. + /// + /// + /// + public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem) + { + selected = selectedItem; + SelectedItemChanged?.Invoke (new SelectedItemChangedArgs (selectedItem, previousSelectedItem)); + } + + /// + public override bool ProcessColdKey (KeyEvent kb) + { + var key = kb.KeyValue; + if (key < Char.MaxValue && Char.IsLetterOrDigit ((char)key)) { + int i = 0; + key = Char.ToUpper ((char)key); + foreach (var l in radioLabels) { + bool nextIsHot = false; + foreach (var c in l) { + if (c == '_') + nextIsHot = true; + else { + if (nextIsHot && c == key) { + SelectedItem = i; + cursor = i; + if (!HasFocus) + SuperView.SetFocus (this); + return true; + } + nextIsHot = false; + } + } + i++; + } + } + return false; + } + + /// + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.CursorUp: + if (cursor > 0) { + cursor--; + SetNeedsDisplay (); + return true; + } + break; + case Key.CursorDown: + if (cursor + 1 < radioLabels.Count) { + cursor++; + SetNeedsDisplay (); + return true; + } + break; + case Key.Space: + SelectedItem = cursor; + return true; + } + return base.ProcessKey (kb); + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) + return false; + + SuperView.SetFocus (this); + + if (me.Y < radioLabels.Count) { + cursor = SelectedItem = me.Y; + SetNeedsDisplay (); + } + return true; + } + } +} diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs new file mode 100644 index 0000000..ad4ccb6 --- /dev/null +++ b/Terminal.Gui/Views/ScrollView.cs @@ -0,0 +1,649 @@ +// +// ScrollView.cs: ScrollView and ScrollBarView views. +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// +// TODO: +// - focus in scrollview +// - focus handling in scrollview to auto scroll to focused view +// - Raise events +// - Perhaps allow an option to not display the scrollbar arrow indicators? + +using System; +using System.Reflection; + +namespace Terminal.Gui { + /// + /// ScrollBarViews are views that display a 1-character scrollbar, either horizontal or vertical + /// + /// + /// + /// The scrollbar is drawn to be a representation of the Size, assuming that the + /// scroll position is set at Position. + /// + /// + /// If the region to display the scrollbar is larger than three characters, + /// arrow indicators are drawn. + /// + /// + public class ScrollBarView : View { + bool vertical = false; + int size = 0, position = 0; + + /// + /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. + /// + public bool IsVertical { + get => vertical; + set { + vertical = value; + SetNeedsDisplay (); + } + } + + /// + /// The size of content the scrollbar represents. + /// + /// The size. + /// The is typically the size of the virtual content. E.g. when a Scrollbar is + /// part of a the Size is set to the appropriate dimension of . + public int Size { + get => size; + set { + size = value; + SetNeedsDisplay (); + } + } + + /// + /// This event is raised when the position on the scrollbar has changed. + /// + public Action ChangedPosition; + + /// + /// The position, relative to , to set the scrollbar at. + /// + /// The position. + public int Position { + get => position; + set { + position = value; + SetNeedsDisplay (); + } + } + + void SetPosition (int newPos) + { + Position = newPos; + ChangedPosition?.Invoke (); + } + + /// + /// Initializes a new instance of the class using layout. + /// + /// Frame for the scrollbar. + public ScrollBarView (Rect rect) : this (rect, 0, 0, false) { } + + /// + /// Initializes a new instance of the class using layout. + /// + /// Frame for the scrollbar. + /// The size that this scrollbar represents. Sets the property. + /// The position within this scrollbar. Sets the property. + /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. Sets the property. + public ScrollBarView (Rect rect, int size, int position, bool isVertical) : base (rect) + { + Init (size, position, isVertical); + } + + /// + /// Initializes a new instance of the class using layout. + /// + public ScrollBarView () : this (0, 0, false) { } + + /// + /// Initializes a new instance of the class using layout. + /// + /// The size that this scrollbar represents. + /// The position within this scrollbar. + /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. + public ScrollBarView (int size, int position, bool isVertical) : base () + { + Init (size, position, isVertical); + } + + void Init (int size, int position, bool isVertical) + { + vertical = isVertical; + this.position = position; + this.size = size; + WantContinuousButtonPressed = true; + } + + int posTopTee; + int posLeftTee; + int posBottomTee; + int posRightTee; + + /// + public override void Redraw (Rect region) + { + if (ColorScheme == null || Size == 0) + return; + + Driver.SetAttribute (ColorScheme.Normal); + + if (vertical) { + if (region.Right < Bounds.Width - 1) + return; + + var col = Bounds.Width - 1; + var bh = Bounds.Height; + Rune special; + + if (bh < 4) { + var by1 = position * bh / Size; + var by2 = (position + bh) * bh / Size; + + Move (col, 0); + Driver.AddRune (Driver.UpArrow); + Move (col, Bounds.Height - 1); + Driver.AddRune (Driver.DownArrow); + } else { + bh -= 2; + var by1 = position * bh / Size; + var by2 = (position + bh) * bh / Size; + + Move (col, 0); + Driver.AddRune (Driver.UpArrow); + Move (col, Bounds.Height - 1); + Driver.AddRune (Driver.DownArrow); + + bool hasTopTee = false; + bool hasDiamond = false; + bool hasBottomTee = false; + for (int y = 0; y < bh; y++) { + Move (col, y + 1); + if ((y < by1 || y > by2) && ((position > 0 && !hasTopTee) || (hasTopTee && hasBottomTee))) { + special = Driver.Stipple; + } else { + if (y != by2 && y > 1 && by2 - by1 == 0 && by1 < bh - 1 && hasTopTee && !hasDiamond) { + hasDiamond = true; + special = Driver.Diamond; + } else { + if (y == by1 && !hasTopTee) { + hasTopTee = true; + posTopTee = y; + special = Driver.TopTee; + } else if ((y >= by2 || by2 == 0) && !hasBottomTee) { + hasBottomTee = true; + posBottomTee = y; + special = Driver.BottomTee; + } else { + special = Driver.VLine; + } + } + } + Driver.AddRune (special); + } + if (!hasTopTee) { + Move (col, Bounds.Height - 2); + Driver.AddRune (Driver.TopTee); + } + } + } else { + if (region.Bottom < Bounds.Height - 1) + return; + + var row = Bounds.Height - 1; + var bw = Bounds.Width; + Rune special; + + if (bw < 4) { + var bx1 = position * bw / Size; + var bx2 = (position + bw) * bw / Size; + + Move (0, row); + Driver.AddRune (Driver.LeftArrow); + Driver.AddRune (Driver.RightArrow); + } else { + bw -= 2; + var bx1 = position * bw / Size; + var bx2 = (position + bw) * bw / Size; + + Move (0, row); + Driver.AddRune (Driver.LeftArrow); + + bool hasLeftTee = false; + bool hasDiamond = false; + bool hasRightTee = false; + for (int x = 0; x < bw; x++) { + if ((x < bx1 || x >= bx2 + 1) && ((position > 0 && !hasLeftTee) || (hasLeftTee && hasRightTee))) { + special = Driver.Stipple; + } else { + if (x != bx2 && x > 1 && bx2 - bx1 == 0 && bx1 < bw - 1 && hasLeftTee && !hasDiamond) { + hasDiamond = true; + special = Driver.Diamond; + } else { + if (x == bx1 && !hasLeftTee) { + hasLeftTee = true; + posLeftTee = x; + special = Driver.LeftTee; + } else if ((x >= bx2 || bx2 == 0) && !hasRightTee) { + hasRightTee = true; + posRightTee = x; + special = Driver.RightTee; + } else { + special = Driver.HLine; + } + } + } + Driver.AddRune (special); + } + if (!hasLeftTee) { + Move (Bounds.Width -2, row); + Driver.AddRune (Driver.LeftTee); + } + + Driver.AddRune (Driver.RightArrow); + } + } + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && + !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) + return false; + + int location = vertical ? me.Y : me.X; + int barsize = vertical ? Bounds.Height : Bounds.Width; + int posTopLeftTee = vertical ? posTopTee : posLeftTee; + int posBottomRightTee = vertical ? posBottomTee : posRightTee; + + barsize -= 2; + var pos = Position; + if (location == 0) { + if (pos > 0) + SetPosition (pos - 1); + } else if (location == barsize + 1) { + if (pos + 1 < Size) + SetPosition (pos + 1); + } else { + var b1 = pos * barsize / Size; + var b2 = (pos + barsize) * barsize / Size; + + if (b1 == 0 && location == 1 && pos == 0 || (location >= posTopLeftTee + 1 && location <= posBottomRightTee + 1 && (pos != 0 || pos != Size - 1) && location != 1 && location != barsize) || + (b2 == barsize + (b2 - b1 - 1) && location == barsize && pos == Size - 1)) { + return true; + } else if (location <= barsize) { + if (location > 1 && location > posTopLeftTee && location > posBottomRightTee) + SetPosition (Math.Min (pos + (Size / location), Size - 1)); + else if (location <= b2 && pos > 0 || pos > 0) + SetPosition (Math.Max (pos - (Size / barsize), 0)); + } + } + + return true; + } + } + + /// + /// Scrollviews are views that present a window into a virtual space where subviews are added. Similar to the iOS UIScrollView. + /// + /// + /// + /// The subviews that are added to this are offset by the + /// property. The view itself is a window into the + /// space represented by the . + /// + /// + /// Use the + /// + /// + public class ScrollView : View { + View contentView = null; + ScrollBarView vertical, horizontal; + + /// + /// Initializes a new instance of the class using positioning. + /// + /// + public ScrollView (Rect frame) : base (frame) + { + Init (frame); + } + + + /// + /// Initializes a new instance of the class using positioning. + /// + public ScrollView () : base () + { + Init (new Rect (0, 0, 0, 0)); + } + + void Init (Rect frame) + { + contentView = new View (frame); + vertical = new ScrollBarView (1, 0, isVertical: true) { + X = Pos.AnchorEnd (1), + Y = 0, + Width = 1, + Height = Dim.Fill (showHorizontalScrollIndicator ? 1 : 0) + }; + vertical.ChangedPosition += delegate { + ContentOffset = new Point (ContentOffset.X, vertical.Position); + }; + horizontal = new ScrollBarView (1, 0, isVertical: false) { + X = 0, + Y = Pos.AnchorEnd (1), + Width = Dim.Fill (showVerticalScrollIndicator ? 1 : 0), + Height = 1 + }; + horizontal.ChangedPosition += delegate { + ContentOffset = new Point (horizontal.Position, ContentOffset.Y); + }; + base.Add (contentView); + CanFocus = true; + + MouseEnter += View_MouseEnter; + MouseLeave += View_MouseLeave; + } + + Size contentSize; + Point contentOffset; + bool showHorizontalScrollIndicator; + bool showVerticalScrollIndicator; + + /// + /// Represents the contents of the data shown inside the scrolview + /// + /// The size of the content. + public Size ContentSize { + get { + return contentSize; + } + set { + contentSize = value; + contentView.Frame = new Rect (contentOffset, value); + vertical.Size = contentSize.Height; + horizontal.Size = contentSize.Width; + SetNeedsDisplay (); + } + } + + /// + /// Represents the top left corner coordinate that is displayed by the scrollview + /// + /// The content offset. + public Point ContentOffset { + get { + return contentOffset; + } + set { + contentOffset = new Point (-Math.Abs (value.X), -Math.Abs (value.Y)); + contentView.Frame = new Rect (contentOffset, contentSize); + vertical.Position = Math.Max (0, -contentOffset.Y); + horizontal.Position = Math.Max (0, -contentOffset.X); + SetNeedsDisplay (); + } + } + + /// + /// Adds the view to the scrollview. + /// + /// The view to add to the scrollview. + public override void Add (View view) + { + if (!IsOverridden (view)) { + view.MouseEnter += View_MouseEnter; + view.MouseLeave += View_MouseLeave; + } + contentView.Add (view); + SetNeedsLayout (); + } + + void View_MouseLeave (MouseEventArgs e) + { + Application.UngrabMouse (); + } + + void View_MouseEnter (MouseEventArgs e) + { + Application.GrabMouse (this); + } + + bool IsOverridden (View view) + { + Type t = view.GetType (); + MethodInfo m = t.GetMethod ("MouseEvent"); + + return m.DeclaringType == t && m.GetBaseDefinition ().DeclaringType == typeof (Responder); + } + + /// + /// Gets or sets the visibility for the horizontal scroll indicator. + /// + /// true if show vertical scroll indicator; otherwise, false. + public bool ShowHorizontalScrollIndicator { + get => showHorizontalScrollIndicator; + set { + if (value == showHorizontalScrollIndicator) + return; + + showHorizontalScrollIndicator = value; + SetNeedsLayout (); + if (value) { + base.Add (horizontal); + horizontal.MouseEnter += View_MouseEnter; + horizontal.MouseLeave += View_MouseLeave; + } else { + Remove (horizontal); + horizontal.MouseEnter -= View_MouseEnter; + horizontal.MouseLeave -= View_MouseLeave; + } + vertical.Height = Dim.Fill (showHorizontalScrollIndicator ? 1 : 0); + } + } + + /// + /// Removes all widgets from this container. + /// + /// + /// + public override void RemoveAll () + { + contentView.RemoveAll (); + } + + /// + /// /// Gets or sets the visibility for the vertical scroll indicator. + /// + /// true if show vertical scroll indicator; otherwise, false. + public bool ShowVerticalScrollIndicator { + get => showVerticalScrollIndicator; + set { + if (value == showVerticalScrollIndicator) + return; + + showVerticalScrollIndicator = value; + SetNeedsLayout (); + if (value) { + base.Add (vertical); + vertical.MouseEnter += View_MouseEnter; + vertical.MouseLeave += View_MouseLeave; + } else { + Remove (vertical); + vertical.MouseEnter -= View_MouseEnter; + vertical.MouseLeave -= View_MouseLeave; + } + horizontal.Width = Dim.Fill (showVerticalScrollIndicator ? 1 : 0); + } + } + + /// + public override void Redraw (Rect region) + { + Driver.SetAttribute (ColorScheme.Normal); + SetViewsNeedsDisplay (); + Clear (); + + var savedClip = ClipToBounds (); + OnDrawContent (new Rect (ContentOffset, + new Size (Bounds.Width - (ShowVerticalScrollIndicator ? 1 : 0), + Bounds.Height - (ShowHorizontalScrollIndicator ? 1 : 0)))); + contentView.Redraw (contentView.Frame); + Driver.Clip = savedClip; + + if (ShowVerticalScrollIndicator) { + vertical.Redraw (vertical.Bounds); + } + + if (ShowHorizontalScrollIndicator) { + horizontal.Redraw (horizontal.Bounds); + } + + // Fill in the bottom left corner + if (ShowVerticalScrollIndicator && ShowHorizontalScrollIndicator) { + AddRune (Bounds.Width - 1, Bounds.Height - 1, ' '); + } + Driver.SetAttribute (ColorScheme.Normal); + } + + void SetViewsNeedsDisplay () + { + foreach (View view in contentView) { + view.SetNeedsDisplay (); + } + } + + /// + public override void PositionCursor () + { + if (InternalSubviews.Count == 0) + Move (0, 0); + else + base.PositionCursor (); + } + + /// + /// Scrolls the view up. + /// + /// true, if left was scrolled, false otherwise. + /// Number of lines to scroll. + public bool ScrollUp (int lines) + { + if (contentOffset.Y < 0) { + ContentOffset = new Point (contentOffset.X, Math.Min (contentOffset.Y + lines, 0)); + return true; + } + return false; + } + + /// + /// Scrolls the view to the left + /// + /// true, if left was scrolled, false otherwise. + /// Number of columns to scroll by. + public bool ScrollLeft (int cols) + { + if (contentOffset.X < 0) { + ContentOffset = new Point (Math.Min (contentOffset.X + cols, 0), contentOffset.Y); + return true; + } + return false; + } + + /// + /// Scrolls the view down. + /// + /// true, if left was scrolled, false otherwise. + /// Number of lines to scroll. + public bool ScrollDown (int lines) + { + var ny = Math.Max (-contentSize.Height, contentOffset.Y - lines); + if (ny == contentOffset.Y) + return false; + ContentOffset = new Point (contentOffset.X, ny); + return true; + } + + /// + /// Scrolls the view to the right. + /// + /// true, if right was scrolled, false otherwise. + /// Number of columns to scroll by. + public bool ScrollRight (int cols) + { + var nx = Math.Max (-contentSize.Width, contentOffset.X - cols); + if (nx == contentOffset.X) + return false; + + ContentOffset = new Point (nx, contentOffset.Y); + return true; + } + + /// + public override bool ProcessKey (KeyEvent kb) + { + if (base.ProcessKey (kb)) + return true; + + switch (kb.Key) { + case Key.CursorUp: + return ScrollUp (1); + case (Key)'v' | Key.AltMask: + case Key.PageUp: + return ScrollUp (Bounds.Height); + + case Key.ControlV: + case Key.PageDown: + return ScrollDown (Bounds.Height); + + case Key.CursorDown: + return ScrollDown (1); + + case Key.CursorLeft: + return ScrollLeft (1); + + case Key.CursorRight: + return ScrollRight (1); + + case Key.Home: + return ScrollUp (contentSize.Height); + + case Key.End: + return ScrollDown (contentSize.Height); + + } + return false; + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp && + me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && + !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) + return false; + + if (me.Flags == MouseFlags.WheeledDown) + ScrollDown (1); + else if (me.Flags == MouseFlags.WheeledUp) + ScrollUp (1); + else if (me.X == vertical.Frame.X) + vertical.MouseEvent (me); + else if (me.Y == horizontal.Frame.Y) + horizontal.MouseEvent (me); + else if (IsOverridden (me.View)) { + Application.UngrabMouse (); + return false; + } + return true; + } + } +} diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs new file mode 100644 index 0000000..d2def20 --- /dev/null +++ b/Terminal.Gui/Views/StatusBar.cs @@ -0,0 +1,196 @@ +// +// StatusBar.cs: a statusbar for an application +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// TODO: +// Add mouse support +using System; +using NStack; + +namespace Terminal.Gui { + /// + /// objects are contained by s. + /// Each has a title, a shortcut (hotkey), and an that will be invoked when the + /// is pressed. + /// The will be a global hotkey for the application in the current context of the screen. + /// The colour of the will be changed after each ~. + /// A set to `~F1~ Help` will render as *F1* using and + /// *Help* as . + /// + public class StatusItem { + /// + /// Initializes a new . + /// + /// Shortcut to activate the . + /// Title for the . + /// Action to invoke when the is activated. + public StatusItem (Key shortcut, ustring title, Action action) + { + Title = title ?? ""; + Shortcut = shortcut; + Action = action; + } + + /// + /// Gets the global shortcut to invoke the action on the menu. + /// + public Key Shortcut { get; } + + /// + /// Gets or sets the title. + /// + /// The title. + /// + /// The colour of the will be changed after each ~. + /// A set to `~F1~ Help` will render as *F1* using and + /// *Help* as . + /// + public ustring Title { get; set; } + + /// + /// Gets or sets the action to be invoked when the statusbar item is triggered + /// + /// Action to invoke. + public Action Action { get; } + }; + + /// + /// A status bar is a that snaps to the bottom of a displaying set of s. + /// The should be context sensitive. This means, if the main menu and an open text editor are visible, the items probably shown will + /// be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. + /// So for each context must be a new instance of a statusbar. + /// + public class StatusBar : View { + /// + /// The items that compose the + /// + public StatusItem [] Items { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public StatusBar () : this (items: new StatusItem [] { }) { } + + /// + /// Initializes a new instance of the class with the specified set of s. + /// The will be drawn on the lowest line of the terminal or (if not null). + /// + /// A list of statusbar items. + public StatusBar (StatusItem [] items) : base () + { + Width = Dim.Fill (); + Height = 1; + Items = items; + CanFocus = false; + ColorScheme = Colors.Menu; + X = 0; + Y = Driver.Rows - 1; + Width = Dim.Fill (); + Height = 1; + + LayoutComplete += (e) => { + X = 0; + Height = 1; + if (SuperView == null || SuperView == Application.Top) { + Y = Driver.Rows - 1; + } else { + //Y = Pos.Bottom (SuperView); + } + }; + } + + Attribute ToggleScheme (Attribute scheme) + { + var result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; + Driver.SetAttribute (result); + return result; + } + + /// + public override void Redraw (Rect bounds) + { + //if (Frame.Y != Driver.Rows - 1) { + // Frame = new Rect (Frame.X, Driver.Rows - 1, Frame.Width, Frame.Height); + // Y = Driver.Rows - 1; + // SetNeedsDisplay (); + //} + + Move (0, 0); + Driver.SetAttribute (ColorScheme.Normal); + for (int i = 0; i < Frame.Width; i++) + Driver.AddRune (' '); + + Move (1, 0); + var scheme = ColorScheme.Normal; + Driver.SetAttribute (scheme); + for (int i = 0; i < Items.Length; i++) { + var title = Items [i].Title; + for (int n = 0; n < title.Length; n++) { + if (title [n] == '~') { + scheme = ToggleScheme (scheme); + continue; + } + Driver.AddRune (title [n]); + } + if (i + 1 < Items.Length) { + Driver.AddRune (' '); + Driver.AddRune (Driver.VLine); + Driver.AddRune (' '); + } + } + } + + /// + public override bool ProcessHotKey (KeyEvent kb) + { + foreach (var item in Items) { + if (kb.Key == item.Shortcut) { + item.Action?.Invoke (); + return true; + } + } + return false; + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (me.Flags != MouseFlags.Button1Clicked) + return false; + + int pos = 1; + for (int i = 0; i < Items.Length; i++) { + if (me.X >= pos && me.X < pos + GetItemTitleLength (Items [i].Title)) { + Run (Items [i].Action); + } + pos += GetItemTitleLength (Items [i].Title) + 3; + } + return true; + } + + int GetItemTitleLength (ustring title) + { + int len = 0; + foreach (var ch in title) { + if (ch == '~') + continue; + len++; + } + + return len; + } + + void Run (Action action) + { + if (action == null) + return; + + Application.MainLoop.AddIdle (() => { + action (); + return false; + }); + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs new file mode 100644 index 0000000..6f709af --- /dev/null +++ b/Terminal.Gui/Views/TextField.cs @@ -0,0 +1,804 @@ +// +// TextField.cs: single-line text editor with Emacs keybindings +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// + +using System; +using System.Collections.Generic; +using System.Linq; +using NStack; + +namespace Terminal.Gui { + /// + /// Single-line text entry + /// + /// + /// The provides editing functionality and mouse support. + /// + public class TextField : View { + List text; + int first, point; + bool used; + + /// + /// Tracks whether the text field should be considered "used", that is, that the user has moved in the entry, so new input should be appended at the cursor position, rather than clearing the entry + /// + public bool Used { get => used; set { used = value; } } + + /// + /// If set to true its not allow any changes in the text. + /// + public bool ReadOnly { get; set; } = false; + + /// + /// Changed event, raised when the text has clicked. + /// + /// + /// This event is raised when the changes. + /// + /// + /// The passed is a containing the old value. + /// + public Action TextChanged; + + /// + /// Initializes a new instance of the class using positioning. + /// + /// Initial text contents. + public TextField (string text) : this (ustring.Make (text)) + { + Height = 1; + } + + /// + /// Initializes a new instance of the class using positioning. + /// + public TextField () : this (string.Empty) { } + + /// + /// Initializes a new instance of the class using positioning. + /// + /// Initial text contents. + public TextField (ustring text) + { + Initialize (text, Frame.Width); + } + + /// + /// Initializes a new instance of the class using positioning. + /// + /// The x coordinate. + /// The y coordinate. + /// The width. + /// Initial text contents. + public TextField (int x, int y, int w, ustring text) : base (new Rect (x, y, w, 1)) + { + Initialize (text, w); + } + + void Initialize (ustring text, int w) + { + if (text == null) + text = ""; + + this.text = TextModel.ToRunes (text); + point = text.Length; + first = point > w ? point - w : 0; + CanFocus = true; + Used = true; + WantMousePositionReports = true; + } + + /// + public override bool OnLeave () + { + if (Application.mouseGrabView != null && Application.mouseGrabView == this) + Application.UngrabMouse (); + if (SelectedLength != 0 && !(Application.mouseGrabView is MenuBar)) + ClearAllSelection (); + + return base.OnLeave (); + } + + /// + public override Rect Frame { + get => base.Frame; + set { + base.Frame = value; + var w = base.Frame.Width; + first = point > w ? point - w : 0; + Adjust (); + } + } + + List historyText; + int idxhistoryText; + bool isFromHistory; + + /// + /// Sets or gets the text held by the view. + /// + /// + /// + public ustring Text { + get { + return ustring.Make (text); + } + + set { + if (ReadOnly) + return; + + var oldText = ustring.Make (text); + + if (oldText == value) + return; + + text = TextModel.ToRunes (value); + if (!Secret && !isFromHistory) { + if (historyText == null) + historyText = new List () { oldText }; + if (idxhistoryText > 0 && idxhistoryText + 1 < historyText.Count) + historyText.RemoveRange (idxhistoryText + 1, historyText.Count - idxhistoryText - 1); + historyText.Add (ustring.Make (text)); + idxhistoryText++; + } + TextChanged?.Invoke (oldText); + + if (point > text.Count) + point = Math.Max (DisplaySize (text, 0) - 1, 0); + + // FIXME: this needs to be updated to use Rune.ColumnWidth + //first = point > Frame.Width ? point - Frame.Width : 0; + Adjust (); + SetNeedsDisplay (); + } + } + + /// + /// Sets the secret property. + /// + /// + /// This makes the text entry suitable for entering passwords. + /// + public bool Secret { get; set; } + + /// + /// Sets or gets the current cursor position. + /// + public int CursorPosition { + get { return point; } + set { + point = value; + Adjust (); + SetNeedsDisplay (); + } + } + + /// + /// Sets the cursor position. + /// + public override void PositionCursor () + { + var col = 0; + for (int idx = first < 0 ? 0 : first; idx < text.Count; idx++) { + if (idx == point) + break; + var cols = Rune.ColumnWidth (text [idx]); + col += cols; + } + Move (col, 0); + } + + /// + public override void Redraw (Rect bounds) + { + ColorScheme color = Colors.Menu; + SetSelectedStartSelectedLength (); + + Driver.SetAttribute (ColorScheme.Focus); + Move (0, 0); + + int p = first; + int col = 0; + int width = Frame.Width; + var tcount = text.Count; + var roc = new Attribute (Color.DarkGray, Color.Gray); + for (int idx = 0; idx < tcount; idx++) { + var rune = text [idx]; + if (idx < p) + continue; + var cols = Rune.ColumnWidth (rune); + if (col == point && HasFocus && !Used && SelectedLength == 0 && !ReadOnly) + Driver.SetAttribute (Colors.Menu.HotFocus); + else if (ReadOnly) + Driver.SetAttribute (idx >= start && length > 0 && idx < start + length ? color.Focus : roc); + else + Driver.SetAttribute (idx >= start && length > 0 && idx < start + length ? color.Focus : ColorScheme.Focus); + if (col + cols <= width) + Driver.AddRune ((Rune)(Secret ? '*' : rune)); + col += cols; + } + + Driver.SetAttribute (ColorScheme.Focus); + for (int i = col; i < Frame.Width; i++) + Driver.AddRune (' '); + + PositionCursor (); + } + + // Returns the size of the string starting at position start + int DisplaySize (List t, int start) + { + int size = 0; + int tcount = t.Count; + for (int i = start; i < tcount; i++) { + var rune = t [i]; + size += Rune.ColumnWidth (rune); + } + return size; + } + + void Adjust () + { + int offB = 0; + if (SuperView != null && SuperView.Frame.Right - Frame.Right < 0) + offB = SuperView.Frame.Right - Frame.Right - 1; + if (point < first) + first = point; + else if (first + point >= Frame.Width + offB) { + first = point - (Frame.Width - 1 + offB); + } + SetNeedsDisplay (); + } + + void SetText (List newText) + { + Text = ustring.Make (newText); + } + + void SetText (IEnumerable newText) + { + SetText (newText.ToList ()); + } + + /// + public override bool CanFocus { + get => true; + set { base.CanFocus = value; } + } + + void SetClipboard (IEnumerable text) + { + if (!Secret) + Clipboard.Contents = ustring.Make (text.ToList ()); + } + + /// + /// Processes key presses for the . + /// + /// + /// + /// + /// The control responds to the following keys: + /// + /// + /// Keys + /// Function + /// + /// + /// , + /// Deletes the character before cursor. + /// + /// + /// + public override bool ProcessKey (KeyEvent kb) + { + // remember current cursor position + // because the new calculated cursor position is needed to be set BEFORE the change event is triggest + // Needed for the Elmish Wrapper issue https://github.com/DieselMeister/Terminal.Gui.Elmish/issues/2 + var oldCursorPos = point; + + switch (kb.Key) { + case Key.DeleteChar: + case Key.ControlD: + if (ReadOnly) + return true; + + if (SelectedLength == 0) { + if (text.Count == 0 || text.Count == point) + return true; + + SetText (text.GetRange (0, point).Concat (text.GetRange (point + 1, text.Count - (point + 1)))); + Adjust (); + + } else { + DeleteSelectedText (); + } + break; + + case Key.Delete: + case Key.Backspace: + if (ReadOnly) + return true; + + if (SelectedLength == 0) { + if (point == 0) + return true; + + point--; + SetText (text.GetRange (0, oldCursorPos - 1).Concat (text.GetRange (oldCursorPos, text.Count - (oldCursorPos)))); + Adjust (); + } else { + DeleteSelectedText (); + } + break; + + case Key.Home | Key.ShiftMask: + if (point > 0) { + int x = point; + point = 0; + PrepareSelection (x, point - x); + } + break; + + case Key.End | Key.ShiftMask: + if (point < text.Count) { + int x = point; + point = text.Count; + PrepareSelection (x, point - x); + } + break; + + // Home, C-A + case Key.Home: + case Key.ControlA: + ClearAllSelection (); + point = 0; + Adjust (); + break; + + case Key.CursorLeft | Key.ShiftMask: + case Key.CursorUp | Key.ShiftMask: + if (point > 0) { + PrepareSelection (point--, -1); + } + break; + + case Key.CursorRight | Key.ShiftMask: + case Key.CursorDown | Key.ShiftMask: + if (point < text.Count) { + PrepareSelection (point++, 1); + } + break; + + case Key.CursorLeft | Key.ShiftMask | Key.CtrlMask: + case Key.CursorUp | Key.ShiftMask | Key.CtrlMask: + if (point > 0) { + int x = start > -1 ? start : point; + int sbw = WordBackward (point); + if (sbw != -1) + point = sbw; + PrepareSelection (x, sbw - x); + } + break; + + case Key.CursorRight | Key.ShiftMask | Key.CtrlMask: + case Key.CursorDown | Key.ShiftMask | Key.CtrlMask: + if (point < text.Count) { + int x = start > -1 ? start : point; + int sfw = WordForward (point); + if (sfw != -1) + point = sfw; + PrepareSelection (x, sfw - x); + } + break; + + case Key.CursorLeft: + case Key.ControlB: + ClearAllSelection (); + if (point > 0) { + point--; + Adjust (); + } + break; + + case Key.End: + case Key.ControlE: // End + ClearAllSelection (); + point = text.Count; + Adjust (); + break; + + case Key.CursorRight: + case Key.ControlF: + ClearAllSelection (); + if (point == text.Count) + break; + point++; + Adjust (); + break; + + case Key.ControlK: // kill-to-end + if (ReadOnly) + return true; + + ClearAllSelection (); + if (point >= text.Count) + return true; + SetClipboard (text.GetRange (point, text.Count - point)); + SetText (text.GetRange (0, point)); + Adjust (); + break; + + // Undo + case Key.ControlZ: + if (ReadOnly) + return true; + + if (historyText != null && historyText.Count > 0) { + isFromHistory = true; + if (idxhistoryText > 0) + idxhistoryText--; + if (idxhistoryText > -1) + Text = historyText [idxhistoryText]; + point = text.Count; + isFromHistory = false; + } + break; + + //Redo + case Key.ControlY: // Control-y, yank + if (ReadOnly) + return true; + + if (historyText != null && historyText.Count > 0) { + isFromHistory = true; + if (idxhistoryText < historyText.Count - 1) { + idxhistoryText++; + if (idxhistoryText < historyText.Count) { + Text = historyText [idxhistoryText]; + } else if (idxhistoryText == historyText.Count - 1) { + Text = historyText [historyText.Count - 1]; + } + point = text.Count; + } + isFromHistory = false; + } + + //if (Clipboard.Contents == null) + // return true; + //var clip = TextModel.ToRunes (Clipboard.Contents); + //if (clip == null) + // return true; + + //if (point == text.Count) { + // point = text.Count; + // SetText(text.Concat(clip).ToList()); + //} else { + // point += clip.Count; + // SetText(text.GetRange(0, oldCursorPos).Concat(clip).Concat(text.GetRange(oldCursorPos, text.Count - oldCursorPos))); + //} + //Adjust (); + + break; + + case Key.CursorLeft | Key.CtrlMask: + case (Key)((int)'b' + Key.AltMask): + ClearAllSelection (); + int bw = WordBackward (point); + if (bw != -1) + point = bw; + Adjust (); + break; + + case Key.CursorRight | Key.CtrlMask: + case (Key)((int)'f' + Key.AltMask): + ClearAllSelection (); + int fw = WordForward (point); + if (fw != -1) + point = fw; + Adjust (); + break; + + case Key.InsertChar: + Used = !Used; + SetNeedsDisplay (); + break; + + case Key.ControlC: + Copy (); + break; + + case Key.ControlX: + if (ReadOnly) + return true; + + Cut (); + break; + + case Key.ControlV: + Paste (); + break; + + // MISSING: + // Alt-D, Alt-backspace + // Alt-Y + // Delete adding to kill buffer + + default: + // Ignore other control characters. + if (kb.Key < Key.Space || kb.Key > Key.CharMask) + return false; + + if (ReadOnly) + return true; + + if (SelectedLength != 0) { + DeleteSelectedText (); + oldCursorPos = point; + } + var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key)); + if (used) { + point++; + if (point == text.Count + 1) { + SetText (text.Concat (kbstr).ToList ()); + } else { + SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.Count)))); + } + } else { + SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (Math.Min (oldCursorPos + 1, text.Count), Math.Max (text.Count - oldCursorPos - 1, 0)))); + point++; + } + Adjust (); + return true; + } + return true; + } + + int WordForward (int p) + { + if (p >= text.Count) + return -1; + + int i = p; + if (Rune.IsPunctuation (text [p]) || Rune.IsWhiteSpace (text [p])) { + for (; i < text.Count; i++) { + var r = text [i]; + if (Rune.IsLetterOrDigit (r)) + break; + } + for (; i < text.Count; i++) { + var r = text [i]; + if (!Rune.IsLetterOrDigit (r)) + break; + } + } else { + for (; i < text.Count; i++) { + var r = text [i]; + if (!Rune.IsLetterOrDigit (r)) + break; + } + } + if (i != p) + return i; + return -1; + } + + int WordBackward (int p) + { + if (p == 0) + return -1; + + int i = p - 1; + if (i == 0) + return 0; + + var ti = text [i]; + if (Rune.IsPunctuation (ti) || Rune.IsSymbol (ti) || Rune.IsWhiteSpace (ti)) { + for (; i >= 0; i--) { + if (Rune.IsLetterOrDigit (text [i])) + break; + } + for (; i >= 0; i--) { + if (!Rune.IsLetterOrDigit (text [i])) + break; + } + } else { + for (; i >= 0; i--) { + if (!Rune.IsLetterOrDigit (text [i])) + break; + } + } + i++; + + if (i != p) + return i; + + return -1; + } + + /// + /// Start position of the selected text. + /// + public int SelectedStart { get; set; } = -1; + + /// + /// Length of the selected text. + /// + public int SelectedLength { get; set; } = 0; + + /// + /// The selected text. + /// + public ustring SelectedText { get; set; } + + int start, length; + bool isButtonReleased = true; + + /// + public override bool MouseEvent (MouseEvent ev) + { + if (!ev.Flags.HasFlag (MouseFlags.Button1Pressed) && !ev.Flags.HasFlag (MouseFlags.ReportMousePosition) && + !ev.Flags.HasFlag (MouseFlags.Button1Released) && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && + !ev.Flags.HasFlag (MouseFlags.Button1TripleClicked)) + return false; + + if (ev.Flags == MouseFlags.Button1Pressed) { + if (!HasFocus) + SuperView.SetFocus (this); + PositionCursor (ev); + if (isButtonReleased) + ClearAllSelection (); + isButtonReleased = true; + } else if (ev.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + int x = PositionCursor (ev); + isButtonReleased = false; + PrepareSelection (x); + if (Application.mouseGrabView == null) { + Application.GrabMouse (this); + } + } else if (ev.Flags == MouseFlags.Button1Released) { + isButtonReleased = true; + Application.UngrabMouse (); + } else if (ev.Flags == MouseFlags.Button1DoubleClicked) { + int x = PositionCursor (ev); + int sbw = x; + if (x > 0 && (char)Text [x - 1] != ' ') + sbw = WordBackward (x); + if (sbw != -1) { + x = sbw; + PositionCursor (x); + } + int sfw = WordForward (x); + ClearAllSelection (); + PrepareSelection (sbw, sfw - sbw); + } else if (ev.Flags == MouseFlags.Button1TripleClicked) { + PositionCursor (0); + ClearAllSelection (); + PrepareSelection (0, text.Count); + } + + SetNeedsDisplay (); + return true; + } + + int PositionCursor (MouseEvent ev) + { + // We could also set the cursor position. + int x; + if (text.Count == 0) + x = ev.X - ev.OfX; + else + x = ev.X; + + return PositionCursor (x); + } + + private int PositionCursor (int x) + { + point = first + x; + if (point > text.Count) + point = text.Count; + if (point < first) + point = 0; + return point; + } + + void PrepareSelection (int x, int direction = 0) + { + x = x + first < 0 ? 0 : x; + SelectedStart = SelectedStart == -1 && text.Count > 0 && x >= 0 && x <= text.Count ? x : SelectedStart; + if (SelectedStart > -1) { + SelectedLength = x + direction <= text.Count ? x + direction - SelectedStart : text.Count - SelectedStart; + SetSelectedStartSelectedLength (); + SelectedText = length > 0 ? ustring.Make (text).ToString ().Substring ( + start < 0 ? 0 : start, length > text.Count ? text.Count : length) : ""; + } + Adjust (); + } + + /// + /// Clear the selected text. + /// + public void ClearAllSelection () + { + if (SelectedStart == -1 && SelectedLength == 0) + return; + SelectedStart = -1; + SelectedLength = 0; + SelectedText = ""; + start = 0; + } + + void SetSelectedStartSelectedLength () + { + if (SelectedLength < 0) { + start = SelectedLength + SelectedStart; + length = Math.Abs (SelectedLength); + } else { + start = SelectedStart; + length = SelectedLength; + } + } + + /// + /// Copy the selected text to the clipboard. + /// + public virtual void Copy () + { + if (Secret) + return; + + if (SelectedLength != 0) { + Clipboard.Contents = SelectedText; + } + } + + /// + /// Cut the selected text to the clipboard. + /// + public virtual void Cut () + { + if (SelectedLength != 0) { + Clipboard.Contents = SelectedText; + DeleteSelectedText (); + } + } + + void DeleteSelectedText () + { + string actualText = Text.ToString (); + int selStart = SelectedLength < 0 ? SelectedLength + SelectedStart : SelectedStart; + int selLength = Math.Abs (SelectedLength); + Text = actualText.Substring (0, selStart) + + actualText.Substring (selStart + selLength, actualText.Length - selStart - selLength); + ClearAllSelection (); + CursorPosition = selStart >= Text.Length ? Text.Length : selStart; + SetNeedsDisplay (); + } + + /// + /// Paste the selected text from the clipboard. + /// + public virtual void Paste () + { + if (ReadOnly) + return; + + string actualText = Text.ToString (); + int start = SelectedStart == -1 ? CursorPosition : SelectedStart; + ustring cbTxt = Clipboard.Contents?.ToString () ?? ""; + Text = actualText.Substring (0, start) + + cbTxt + + actualText.Substring (start + SelectedLength, actualText.Length - start - SelectedLength); + point = start + cbTxt.Length; + SelectedLength = 0; + ClearAllSelection (); + SetNeedsDisplay (); + } + + } +} diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs new file mode 100644 index 0000000..6825331 --- /dev/null +++ b/Terminal.Gui/Views/TextView.cs @@ -0,0 +1,1234 @@ +// +// TextView.cs: multi-line text editing +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// +// TODO: +// In ReadOnly mode backspace/space behave like pageup/pagedown +// Attributed text on spans +// Replace insertion with Insert method +// String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes +// Alt-D, Alt-Backspace +// API to set the cursor position +// API to scroll to a particular place +// keybindings to go to top/bottom +// public API to insert, remove ranges +// Add word forward/word backwards commands +// Save buffer API +// Mouse +// +// Desirable: +// Move all the text manipulation into the TextModel + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NStack; +using Rune = System.Rune; + +namespace Terminal.Gui { + class TextModel { + List> lines = new List> (); + + public bool LoadFile (string file) + { + if (file == null) + throw new ArgumentNullException (nameof (file)); + try { + FilePath = file; + var stream = File.OpenRead (file); + } catch { + return false; + } + LoadStream (File.OpenRead (file)); + return true; + } + + public bool CloseFile () + { + if (FilePath == null) + throw new ArgumentNullException (nameof (FilePath)); + try { + FilePath = null; + lines = new List> (); + } catch { + return false; + } + return true; + } + + // Turns the ustring into runes, this does not split the + // contents on a newline if it is present. + internal static List ToRunes (ustring str) + { + List runes = new List (); + foreach (var x in str.ToRunes ()) { + runes.Add (x); + } + return runes; + } + + // Splits a string into a List that contains a List for each line + public static List> StringToRunes (ustring content) + { + var lines = new List> (); + int start = 0, i = 0; + for (; i < content.Length; i++) { + if (content [i] == 10) { + if (i - start > 0) + lines.Add (ToRunes (content [start, i])); + else + lines.Add (ToRunes (ustring.Empty)); + start = i + 1; + } + } + if (i - start >= 0) + lines.Add (ToRunes (content [start, null])); + return lines; + } + + void Append (List line) + { + var str = ustring.Make (line.ToArray ()); + lines.Add (ToRunes (str)); + } + + public void LoadStream (Stream input) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + lines = new List> (); + var buff = new BufferedStream (input); + int v; + var line = new List (); + while ((v = buff.ReadByte ()) != -1) { + if (v == 10) { + Append (line); + line.Clear (); + continue; + } + line.Add ((byte)v); + } + if (line.Count > 0) + Append (line); + } + + public void LoadString (ustring content) + { + lines = StringToRunes (content); + } + + public override string ToString () + { + var sb = new StringBuilder (); + foreach (var line in lines) + { + sb.Append (ustring.Make(line)); + sb.AppendLine (); + } + return sb.ToString (); + } + + public string FilePath { get; set; } + + /// + /// The number of text lines in the model + /// + public int Count => lines.Count; + + /// + /// Returns the specified line as a List of Rune + /// + /// The line. + /// Line number to retrieve. + public List GetLine (int line) => line < Count ? lines [line]: lines[Count-1]; + + /// + /// Adds a line to the model at the specified position. + /// + /// Line number where the line will be inserted. + /// The line of text, as a List of Rune. + public void AddLine (int pos, List runes) + { + lines.Insert (pos, runes); + } + + /// + /// Removes the line at the specified position + /// + /// Position. + public void RemoveLine (int pos) + { + lines.RemoveAt (pos); + } + } + + /// + /// Multi-line text editing + /// + /// + /// + /// provides a multi-line text editor. Users interact + /// with it with the standard Emacs commands for movement or the arrow + /// keys. + /// + /// + /// + /// Shortcut + /// Action performed + /// + /// + /// Left cursor, Control-b + /// + /// Moves the editing point left. + /// + /// + /// + /// Right cursor, Control-f + /// + /// Moves the editing point right. + /// + /// + /// + /// Alt-b + /// + /// Moves one word back. + /// + /// + /// + /// Alt-f + /// + /// Moves one word forward. + /// + /// + /// + /// Up cursor, Control-p + /// + /// Moves the editing point one line up. + /// + /// + /// + /// Down cursor, Control-n + /// + /// Moves the editing point one line down + /// + /// + /// + /// Home key, Control-a + /// + /// Moves the cursor to the beginning of the line. + /// + /// + /// + /// End key, Control-e + /// + /// Moves the cursor to the end of the line. + /// + /// + /// + /// Delete, Control-d + /// + /// Deletes the character in front of the cursor. + /// + /// + /// + /// Backspace + /// + /// Deletes the character behind the cursor. + /// + /// + /// + /// Control-k + /// + /// Deletes the text until the end of the line and replaces the kill buffer + /// with the deleted text. You can paste this text in a different place by + /// using Control-y. + /// + /// + /// + /// Control-y + /// + /// Pastes the content of the kill ring into the current position. + /// + /// + /// + /// Alt-d + /// + /// Deletes the word above the cursor and adds it to the kill ring. You + /// can paste the contents of the kill ring with Control-y. + /// + /// + /// + /// Control-q + /// + /// Quotes the next input character, to prevent the normal processing of + /// key handling to take place. + /// + /// + /// + /// + public class TextView : View { + TextModel model = new TextModel (); + int topRow; + int leftColumn; + int currentRow; + int currentColumn; + int selectionStartColumn, selectionStartRow; + bool selecting; + //bool used; + + /// + /// Raised when the of the changes. + /// + public Action TextChanged; + +#if false + /// + /// Changed event, raised when the text has clicked. + /// + /// + /// Client code can hook up to this event, it is + /// raised when the text in the entry changes. + /// + public Action Changed; +#endif + /// + /// Initalizes a on the specified area, with absolute position and size. + /// + /// + /// + public TextView (Rect frame) : base (frame) + { + CanFocus = true; + } + + /// + /// Initalizes a on the specified area, + /// with dimensions controlled with the X, Y, Width and Height properties. + /// + public TextView () : base () + { + CanFocus = true; + } + + void ResetPosition () + { + topRow = leftColumn = currentRow = currentColumn = 0; + } + + /// + /// Sets or gets the text in the . + /// + /// + /// + public ustring Text { + get { + return model.ToString (); + } + + set { + ResetPosition (); + model.LoadString (value); + TextChanged?.Invoke (); + SetNeedsDisplay (); + } + } + + /// + /// Loads the contents of the file into the . + /// + /// true, if file was loaded, false otherwise. + /// Path to the file to load. + public bool LoadFile (string path) + { + if (path == null) + throw new ArgumentNullException (nameof (path)); + ResetPosition (); + var res = model.LoadFile (path); + SetNeedsDisplay (); + return res; + } + + /// + /// Loads the contents of the stream into the . + /// + /// true, if stream was loaded, false otherwise. + /// Stream to load the contents from. + public void LoadStream (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + ResetPosition (); + model.LoadStream(stream); + SetNeedsDisplay (); + } + + /// + /// Closes the contents of the stream into the . + /// + /// true, if stream was closed, false otherwise. + public bool CloseFile() + { + ResetPosition (); + var res = model.CloseFile (); + SetNeedsDisplay (); + return res; + } + + /// + /// Gets the current cursor row. + /// + public int CurrentRow => currentRow; + + /// + /// Gets the cursor column. + /// + /// The cursor column. + public int CurrentColumn => currentColumn; + + /// + /// Positions the cursor on the current row and column + /// + public override void PositionCursor () + { + if (selecting) { + var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow)-topRow, 0), Frame.Height); + var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Frame.Height); + + SetNeedsDisplay (new Rect (0, minRow, Frame.Width, maxRow)); + } + Move (CurrentColumn - leftColumn, CurrentRow - topRow); + } + + void ClearRegion (int left, int top, int right, int bottom) + { + for (int row = top; row < bottom; row++) { + Move (left, row); + for (int col = left; col < right; col++) + AddRune (col, row, ' '); + } + } + + void ColorNormal () + { + Driver.SetAttribute (ColorScheme.Normal); + } + + void ColorSelection () + { + if (HasFocus) + Driver.SetAttribute (ColorScheme.Focus); + else + Driver.SetAttribute (ColorScheme.Normal); + } + + bool isReadOnly = false; + + /// + /// Gets or sets whether the is in read-only mode or not + /// + /// Boolean value(Default false) + public bool ReadOnly { + get => isReadOnly; + set { + isReadOnly = value; + } + } + + // Returns an encoded region start..end (top 32 bits are the row, low32 the column) + void GetEncodedRegionBounds (out long start, out long end) + { + long selection = ((long)(uint)selectionStartRow << 32) | (uint)selectionStartColumn; + long point = ((long)(uint)currentRow << 32) | (uint)currentColumn; + if (selection > point) { + start = point; + end = selection; + } else { + start = selection; + end = point; + } + } + + bool PointInSelection (int col, int row) + { + long start, end; + GetEncodedRegionBounds (out start, out end); + var q = ((long)(uint)row << 32) | (uint)col; + return q >= start && q <= end; + } + + // + // Returns a ustring with the text in the selected + // region. + // + ustring GetRegion () + { + long start, end; + GetEncodedRegionBounds (out start, out end); + int startRow = (int)(start >> 32); + var maxrow = ((int)(end >> 32)); + int startCol = (int)(start & 0xffffffff); + var endCol = (int)(end & 0xffffffff); + var line = model.GetLine (startRow); + + if (startRow == maxrow) + return StringFromRunes (line.GetRange (startCol, endCol)); + + ustring res = StringFromRunes (line.GetRange (startCol, line.Count - startCol)); + + for (int row = startRow+1; row < maxrow; row++) { + res = res + ustring.Make ((Rune)10) + StringFromRunes (model.GetLine (row)); + } + line = model.GetLine (maxrow); + res = res + ustring.Make ((Rune)10) + StringFromRunes (line.GetRange (0, endCol)); + return res; + } + + // + // Clears the contents of the selected region + // + void ClearRegion () + { + long start, end; + long currentEncoded = ((long)(uint)currentRow << 32) | (uint)currentColumn; + GetEncodedRegionBounds (out start, out end); + int startRow = (int)(start >> 32); + var maxrow = ((int)(end >> 32)); + int startCol = (int)(start & 0xffffffff); + var endCol = (int)(end & 0xffffffff); + var line = model.GetLine (startRow); + + if (startRow == maxrow) { + line.RemoveRange (startCol, endCol - startCol); + currentColumn = startCol; + SetNeedsDisplay (new Rect (0, startRow - topRow, Frame.Width, startRow - topRow + 1)); + return; + } + + line.RemoveRange (startCol, line.Count - startCol); + var line2 = model.GetLine (maxrow); + line.AddRange (line2.Skip (endCol)); + for (int row = startRow + 1; row <= maxrow; row++) { + model.RemoveLine (startRow+1); + } + if (currentEncoded == end) { + currentRow -= maxrow - (startRow); + } + currentColumn = startCol; + + SetNeedsDisplay (); + } + + /// + public override void Redraw (Rect bounds) + { + ColorNormal (); + + int bottom = bounds.Bottom; + int right = bounds.Right; + for (int row = bounds.Top; row < bottom; row++) + { + int textLine = topRow + row; + if (textLine >= model.Count) + { + ColorNormal (); + ClearRegion (bounds.Left, row, bounds.Right, row + 1); + continue; + } + var line = model.GetLine (textLine); + int lineRuneCount = line.Count; + if (line.Count < bounds.Left) + { + ClearRegion (bounds.Left, row, bounds.Right, row + 1); + continue; + } + + Move (bounds.Left, row); + for (int col = bounds.Left; col < right; col++) + { + var lineCol = leftColumn + col; + var rune = lineCol >= lineRuneCount ? ' ' : line [lineCol]; + if (selecting && PointInSelection (col, row)) + ColorSelection (); + else + ColorNormal (); + + AddRune (col, row, rune); + } + } + PositionCursor (); + } + + /// + public override bool CanFocus { + get => true; + set { base.CanFocus = value; } + } + + void SetClipboard (ustring text) + { + Clipboard.Contents = text; + } + + void AppendClipboard (ustring text) + { + Clipboard.Contents = Clipboard.Contents + text; + } + + void Insert (Rune rune) + { + var line = GetCurrentLine (); + line.Insert (currentColumn, rune); + var prow = currentRow - topRow; + + SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1)); + } + + ustring StringFromRunes (List runes) + { + if (runes == null) + throw new ArgumentNullException (nameof (runes)); + int size = 0; + foreach (var rune in runes) { + size += Utf8.RuneLen (rune); + } + var encoded = new byte [size]; + int offset = 0; + foreach (var rune in runes) { + offset += Utf8.EncodeRune (rune, encoded, offset); + } + return ustring.Make (encoded); + } + + List GetCurrentLine () => model.GetLine (currentRow); + + void InsertText (ustring text) + { + var lines = TextModel.StringToRunes (text); + + if (lines.Count == 0) + return; + + var line = GetCurrentLine (); + + // Optmize single line + if (lines.Count == 1) { + line.InsertRange (currentColumn, lines [0]); + currentColumn += lines [0].Count; + if (currentColumn - leftColumn > Frame.Width) + leftColumn = currentColumn - Frame.Width + 1; + SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, currentRow - topRow + 1)); + return; + } + + // Keep a copy of the rest of the line + var restCount = line.Count - currentColumn; + var rest = line.GetRange (currentColumn, restCount); + line.RemoveRange (currentColumn, restCount); + + // First line is inserted at the current location, the rest is appended + line.InsertRange (currentColumn, lines [0]); + + for (int i = 1; i < lines.Count; i++) + model.AddLine (currentRow + i, lines [i]); + + var last = model.GetLine (currentRow + lines.Count-1); + var lastp = last.Count; + last.InsertRange (last.Count, rest); + + // Now adjjust column and row positions + currentRow += lines.Count-1; + currentColumn = lastp; + if (currentRow - topRow > Frame.Height) { + topRow = currentRow - Frame.Height + 1; + if (topRow < 0) + topRow = 0; + } + if (currentColumn < leftColumn) + leftColumn = currentColumn; + if (currentColumn-leftColumn >= Frame.Width) + leftColumn = currentColumn - Frame.Width + 1; + SetNeedsDisplay (); + } + + // The column we are tracking, or -1 if we are not tracking any column + int columnTrack = -1; + + // Tries to snap the cursor to the tracking column + void TrackColumn () + { + // Now track the column + var line = GetCurrentLine (); + if (line.Count < columnTrack) + currentColumn = line.Count; + else if (columnTrack != -1) + currentColumn = columnTrack; + else if (currentColumn > line.Count) + currentColumn = line.Count; + Adjust (); + } + + void Adjust () + { + bool need = false; + if (currentColumn < leftColumn) { + currentColumn = leftColumn; + need = true; + } + if (currentColumn - leftColumn > Frame.Width) { + leftColumn = currentColumn - Frame.Width + 1; + need = true; + } + if (currentRow < topRow) { + topRow = currentRow; + need = true; + } + if (currentRow - topRow > Frame.Height) { + topRow = currentRow - Frame.Height + 1; + need = true; + } + if (need) + SetNeedsDisplay (); + else + PositionCursor (); + } + + /// + /// Will scroll the to display the specified row at the top + /// + /// Row that should be displayed at the top, if the value is negative it will be reset to zero + public void ScrollTo (int row) + { + if (row < 0) + row = 0; + topRow = row > model.Count ? model.Count - 1 : row; + SetNeedsDisplay (); + } + + bool lastWasKill; + + /// + public override bool ProcessKey (KeyEvent kb) + { + int restCount; + List rest; + + // Handle some state here - whether the last command was a kill + // operation and the column tracking (up/down) + switch (kb.Key) { + case Key.ControlN: + case Key.CursorDown: + case Key.ControlP: + case Key.CursorUp: + lastWasKill = false; + break; + case Key.ControlK: + break; + default: + lastWasKill = false; + columnTrack = -1; + break; + } + + // Dispatch the command. + switch (kb.Key) { + case Key.PageDown: + case Key.ControlV: + int nPageDnShift = Frame.Height - 1; + if (currentRow < model.Count) { + if (columnTrack == -1) + columnTrack = currentColumn; + currentRow = (currentRow + nPageDnShift) > model.Count ? model.Count : currentRow + nPageDnShift; + if (topRow < currentRow - nPageDnShift) { + topRow = currentRow >= model.Count ? currentRow - nPageDnShift : topRow + nPageDnShift; + SetNeedsDisplay (); + } + TrackColumn (); + PositionCursor (); + } + break; + + case Key.PageUp: + case ((int)'v' + Key.AltMask): + int nPageUpShift = Frame.Height - 1; + if (currentRow > 0) { + if (columnTrack == -1) + columnTrack = currentColumn; + currentRow = currentRow - nPageUpShift < 0 ? 0 : currentRow - nPageUpShift; + if (currentRow < topRow) { + topRow = topRow - nPageUpShift < 0 ? 0 : topRow - nPageUpShift; + SetNeedsDisplay (); + } + TrackColumn (); + PositionCursor (); + } + break; + + case Key.ControlN: + case Key.CursorDown: + MoveDown (); + break; + + case Key.ControlP: + case Key.CursorUp: + MoveUp (); + break; + + case Key.ControlF: + case Key.CursorRight: + var currentLine = GetCurrentLine (); + if (currentColumn < currentLine.Count) { + currentColumn++; + if (currentColumn >= leftColumn + Frame.Width) { + leftColumn++; + SetNeedsDisplay (); + } + PositionCursor (); + } else { + if (currentRow + 1 < model.Count) { + currentRow++; + currentColumn = 0; + leftColumn = 0; + if (currentRow >= topRow + Frame.Height) { + topRow++; + } + SetNeedsDisplay (); + PositionCursor (); + } + break; + } + break; + + case Key.ControlB: + case Key.CursorLeft: + if (currentColumn > 0) { + currentColumn--; + if (currentColumn < leftColumn) { + leftColumn--; + SetNeedsDisplay (); + } + PositionCursor (); + } else { + if (currentRow > 0) { + currentRow--; + if (currentRow < topRow) { + topRow--; + SetNeedsDisplay (); + } + currentLine = GetCurrentLine (); + currentColumn = currentLine.Count; + int prev = leftColumn; + leftColumn = currentColumn - Frame.Width + 1; + if (leftColumn < 0) + leftColumn = 0; + if (prev != leftColumn) + SetNeedsDisplay (); + PositionCursor (); + } + } + break; + + case Key.Delete: + case Key.Backspace: + if (isReadOnly) + break; + if (currentColumn > 0) { + // Delete backwards + currentLine = GetCurrentLine (); + currentLine.RemoveAt (currentColumn - 1); + currentColumn--; + if (currentColumn < leftColumn) { + leftColumn--; + SetNeedsDisplay (); + } else + SetNeedsDisplay (new Rect (0, currentRow - topRow, 1, Frame.Width)); + } else { + // Merges the current line with the previous one. + if (currentRow == 0) + return true; + var prowIdx = currentRow - 1; + var prevRow = model.GetLine (prowIdx); + var prevCount = prevRow.Count; + model.GetLine (prowIdx).AddRange (GetCurrentLine ()); + model.RemoveLine (currentRow); + currentRow--; + currentColumn = prevCount; + leftColumn = currentColumn - Frame.Width + 1; + if (leftColumn < 0) + leftColumn = 0; + SetNeedsDisplay (); + } + break; + + // Home, C-A + case Key.Home: + case Key.ControlA: + currentColumn = 0; + if (currentColumn < leftColumn) { + leftColumn = 0; + SetNeedsDisplay (); + } else + PositionCursor (); + break; + case Key.DeleteChar: + case Key.ControlD: // Delete + if (isReadOnly) + break; + currentLine = GetCurrentLine (); + if (currentColumn == currentLine.Count) { + if (currentRow + 1 == model.Count) + break; + var nextLine = model.GetLine (currentRow + 1); + currentLine.AddRange (nextLine); + model.RemoveLine (currentRow + 1); + var sr = currentRow - topRow; + SetNeedsDisplay (new Rect (0, sr, Frame.Width, sr + 1)); + } else { + currentLine.RemoveAt (currentColumn); + var r = currentRow - topRow; + SetNeedsDisplay (new Rect (currentColumn - leftColumn, r, Frame.Width, r + 1)); + } + break; + + case Key.End: + case Key.ControlE: // End + currentLine = GetCurrentLine (); + currentColumn = currentLine.Count; + int pcol = leftColumn; + leftColumn = currentColumn - Frame.Width + 1; + if (leftColumn < 0) + leftColumn = 0; + if (pcol != leftColumn) + SetNeedsDisplay (); + PositionCursor (); + break; + + case Key.ControlK: // kill-to-end + if (isReadOnly) + break; + currentLine = GetCurrentLine (); + if (currentLine.Count == 0) { + model.RemoveLine (currentRow); + var val = ustring.Make ((Rune)'\n'); + if (lastWasKill) + AppendClipboard (val); + else + SetClipboard (val); + } else { + restCount = currentLine.Count - currentColumn; + rest = currentLine.GetRange (currentColumn, restCount); + var val = StringFromRunes (rest); + if (lastWasKill) + AppendClipboard (val); + else + SetClipboard (val); + currentLine.RemoveRange (currentColumn, restCount); + } + SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); + lastWasKill = true; + break; + + case Key.ControlY: // Control-y, yank + if (isReadOnly) + break; + InsertText (Clipboard.Contents); + selecting = false; + break; + + case Key.ControlSpace: + selecting = true; + selectionStartColumn = currentColumn; + selectionStartRow = currentRow; + break; + + case ((int)'w' + Key.AltMask): + SetClipboard (GetRegion ()); + selecting = false; + break; + + case Key.ControlW: + SetClipboard (GetRegion ()); + if (!isReadOnly) + ClearRegion (); + selecting = false; + break; + + case (Key)((int)'b' + Key.AltMask): + var newPos = WordBackward (currentColumn, currentRow); + if (newPos.HasValue) { + currentColumn = newPos.Value.col; + currentRow = newPos.Value.row; + } + Adjust (); + + break; + + case (Key)((int)'f' + Key.AltMask): + newPos = WordForward (currentColumn, currentRow); + if (newPos.HasValue) { + currentColumn = newPos.Value.col; + currentRow = newPos.Value.row; + } + Adjust (); + break; + + case Key.Enter: + if (isReadOnly) + break; + var orow = currentRow; + currentLine = GetCurrentLine (); + restCount = currentLine.Count - currentColumn; + rest = currentLine.GetRange (currentColumn, restCount); + currentLine.RemoveRange (currentColumn, restCount); + model.AddLine (currentRow + 1, rest); + currentRow++; + bool fullNeedsDisplay = false; + if (currentRow >= topRow + Frame.Height) { + topRow++; + fullNeedsDisplay = true; + } + currentColumn = 0; + if (currentColumn < leftColumn) { + fullNeedsDisplay = true; + leftColumn = 0; + } + + if (fullNeedsDisplay) + SetNeedsDisplay (); + else + SetNeedsDisplay (new Rect (0, currentRow - topRow, 2, Frame.Height)); + break; + + case Key.CtrlMask | Key.End: + currentRow = model.Count; + TrackColumn (); + PositionCursor (); + break; + + case Key.CtrlMask | Key.Home: + currentRow = 0; + TrackColumn (); + PositionCursor (); + break; + + default: + // Ignore control characters and other special keys + if (kb.Key < Key.Space || kb.Key > Key.CharMask) + return false; + //So that special keys like tab can be processed + if (isReadOnly) + return true; + Insert ((uint)kb.Key); + currentColumn++; + if (currentColumn >= leftColumn + Frame.Width) { + leftColumn++; + SetNeedsDisplay (); + } + PositionCursor (); + return true; + } + return true; + } + + private void MoveUp () + { + if (currentRow > 0) { + if (columnTrack == -1) + columnTrack = currentColumn; + currentRow--; + if (currentRow < topRow) { + topRow--; + SetNeedsDisplay (); + } + TrackColumn (); + PositionCursor (); + } + } + + private void MoveDown () + { + if (currentRow + 1 < model.Count) { + if (columnTrack == -1) + columnTrack = currentColumn; + currentRow++; + if (currentRow >= topRow + Frame.Height) { + topRow++; + SetNeedsDisplay (); + } + TrackColumn (); + PositionCursor (); + } + } + + IEnumerable<(int col, int row, Rune rune)> ForwardIterator (int col, int row) + { + if (col < 0 || row < 0) + yield break; + if (row >= model.Count) + yield break; + var line = GetCurrentLine (); + if (col >= line.Count) + yield break; + + while (row < model.Count) { + for (int c = col; c < line.Count; c++) { + yield return (c, row, line [c]); + } + col = 0; + row++; + line = GetCurrentLine (); + } + } + + Rune RuneAt (int col, int row) => model.GetLine (row) [col]; + + bool MoveNext (ref int col, ref int row, out Rune rune) + { + var line = model.GetLine (row); + if (col + 1 < line.Count) { + col++; + rune = line [col]; + return true; + } + while (row + 1 < model.Count){ + col = 0; + row++; + line = model.GetLine (row); + if (line.Count > 0) { + rune = line [0]; + return true; + } + } + rune = 0; + return false; + } + + bool MovePrev (ref int col, ref int row, out Rune rune) + { + var line = model.GetLine (row); + + if (col > 0) { + col--; + rune = line [col]; + return true; + } + if (row == 0) { + rune = 0; + return false; + } + while (row > 0) { + row--; + line = model.GetLine (row); + col = line.Count - 1; + if (col >= 0) { + rune = line [col]; + return true; + } + } + rune = 0; + return false; + } + + (int col, int row)? WordForward (int fromCol, int fromRow) + { + var col = fromCol; + var row = fromRow; + var line = GetCurrentLine (); + var rune = RuneAt (col, row); + + var srow = row; + if (Rune.IsPunctuation (rune) || Rune.IsWhiteSpace (rune)) { + while (MoveNext (ref col, ref row, out rune)){ + if (Rune.IsLetterOrDigit (rune)) + break; + } + while (MoveNext (ref col, ref row, out rune)) { + if (!Rune.IsLetterOrDigit (rune)) + break; + } + } else { + while (MoveNext (ref col, ref row, out rune)) { + if (!Rune.IsLetterOrDigit (rune)) + break; + } + } + if (fromCol != col || fromRow != row) + return (col, row); + return null; + } + + (int col, int row)? WordBackward (int fromCol, int fromRow) + { + if (fromRow == 0 && fromCol == 0) + return null; + + var col = fromCol; + var row = fromRow; + var line = GetCurrentLine (); + var rune = RuneAt (col, row); + + if (Rune.IsPunctuation (rune) || Rune.IsSymbol (rune) || Rune.IsWhiteSpace (rune)) { + while (MovePrev (ref col, ref row, out rune)){ + if (Rune.IsLetterOrDigit (rune)) + break; + } + while (MovePrev (ref col, ref row, out rune)){ + if (!Rune.IsLetterOrDigit (rune)) + break; + } + } else { + while (MovePrev (ref col, ref row, out rune)) { + if (!Rune.IsLetterOrDigit (rune)) + break; + } + } + if (fromCol != col || fromRow != row) + return (col, row); + return null; + } + + /// + public override bool MouseEvent (MouseEvent ev) + { + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked) && + !ev.Flags.HasFlag (MouseFlags.WheeledDown) && !ev.Flags.HasFlag (MouseFlags.WheeledUp)) { + return false; + } + + if (!HasFocus) + SuperView.SetFocus (this); + + if (ev.Flags == MouseFlags.Button1Clicked) { + if (model.Count > 0) { + var maxCursorPositionableLine = (model.Count - 1) - topRow; + if (ev.Y > maxCursorPositionableLine) { + currentRow = maxCursorPositionableLine; + } else { + currentRow = ev.Y + topRow; + } + var r = GetCurrentLine (); + if (ev.X - leftColumn >= r.Count) + currentColumn = r.Count - leftColumn; + else + currentColumn = ev.X - leftColumn; + } + PositionCursor (); + } else if (ev.Flags == MouseFlags.WheeledDown) { + lastWasKill = false; + MoveDown (); + } else if (ev.Flags == MouseFlags.WheeledUp) { + lastWasKill = false; + MoveUp (); + } + + return true; + } + } + +} diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs new file mode 100644 index 0000000..efe8e6e --- /dev/null +++ b/Terminal.Gui/Views/TimeField.cs @@ -0,0 +1,299 @@ +// +// TimeField.cs: text entry for time +// +// Author: Jörg Preiß +// +// Licensed under the MIT license +using System; +using System.Globalization; +using System.Linq; +using NStack; + +namespace Terminal.Gui { + /// + /// Time editing + /// + /// + /// The provides time editing functionality with mouse support. + /// + public class TimeField : TextField { + TimeSpan time; + bool isShort; + + int longFieldLen = 8; + int shortFieldLen = 5; + string sepChar; + string longFormat; + string shortFormat; + + int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } } + string Format { get { return isShort ? shortFormat : longFormat; } } + + /// + /// TimeChanged event, raised when the Date has changed. + /// + /// + /// This event is raised when the changes. + /// + /// + /// The passed is a containing the old value, new value, and format string. + /// + public Action> TimeChanged; + + /// + /// Initializes a new instance of using positioning. + /// + /// The x coordinate. + /// The y coordinate. + /// Initial time. + /// If true, the seconds are hidden. Sets the property. + public TimeField (int x, int y, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "") + { + this.isShort = isShort; + Initialize (time); + } + + /// + /// Initializes a new instance of using positioning. + /// + /// Initial time + public TimeField (TimeSpan time) : base (string.Empty) + { + this.isShort = true; + Width = FieldLen + 2; + Initialize (time); + } + + /// + /// Initializes a new instance of using positioning. + /// + public TimeField () : this (time: TimeSpan.MinValue) { } + + void Initialize (TimeSpan time) + { + CultureInfo cultureInfo = CultureInfo.CurrentCulture; + sepChar = cultureInfo.DateTimeFormat.TimeSeparator; + longFormat = $" hh\\{sepChar}mm\\{sepChar}ss"; + shortFormat = $" hh\\{sepChar}mm"; + CursorPosition = 1; + Time = time; + TextChanged += TimeField_Changed; + } + + void TimeField_Changed (ustring e) + { + try { + if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result)) + Text = e; + } catch (Exception) { + Text = e; + } + } + + /// + /// Gets or sets the time of the . + /// + /// + /// + public TimeSpan Time { + get { + return time; + } + set { + if (ReadOnly) + return; + + var oldTime = time; + time = value; + this.Text = " " + value.ToString (Format.Trim ()); + var args = new DateTimeEventArgs (oldTime, value, Format); + if (oldTime != value) { + OnTimeChanged (args); + } + } + } + + /// + /// Get or sets whether uses the short or long time format. + /// + public bool IsShortFormat { + get => isShort; + set { + isShort = value; + if (isShort) + Width = 7; + else + Width = 10; + var ro = ReadOnly; + if (ro) + ReadOnly = false; + SetText (Text); + ReadOnly = ro; + SetNeedsDisplay (); + } + } + + bool SetText (Rune key) + { + var text = TextModel.ToRunes (Text); + var newText = text.GetRange (0, CursorPosition); + newText.Add (key); + if (CursorPosition < FieldLen) + newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); + return SetText (ustring.Make (newText)); + } + + bool SetText (ustring text) + { + if (text.IsEmpty) { + return false; + } + + ustring [] vals = text.Split (ustring.Make (sepChar)); + bool isValidTime = true; + int hour = Int32.Parse (vals [0].ToString ()); + int minute = Int32.Parse (vals [1].ToString ()); + int second = isShort ? 0 : vals.Length > 2 ? Int32.Parse (vals [2].ToString ()) : 0; + if (hour < 0) { + isValidTime = false; + hour = 0; + vals [0] = "0"; + } else if (hour > 23) { + isValidTime = false; + hour = 23; + vals [0] = "23"; + } + if (minute < 0) { + isValidTime = false; + minute = 0; + vals [1] = "0"; + } else if (minute > 59) { + isValidTime = false; + minute = 59; + vals [1] = "59"; + } + if (second < 0) { + isValidTime = false; + second = 0; + vals [2] = "0"; + } else if (second > 59) { + isValidTime = false; + second = 59; + vals [2] = "59"; + } + string t = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}"; + + if (!TimeSpan.TryParseExact (t.Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) || + !isValidTime) + return false; + Time = result; + return true; + } + + void IncCursorPosition () + { + if (CursorPosition == FieldLen) + return; + if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) + CursorPosition++; + } + + void DecCursorPosition () + { + if (CursorPosition == 1) + return; + if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) + CursorPosition--; + } + + void AdjCursorPosition () + { + if (Text [CursorPosition] == sepChar.ToCharArray () [0]) + CursorPosition++; + } + + /// + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.DeleteChar: + case Key.ControlD: + if (ReadOnly) + return true; + + SetText ('0'); + break; + + case Key.Delete: + case Key.Backspace: + if (ReadOnly) + return true; + + SetText ('0'); + DecCursorPosition (); + break; + + // Home, C-A + case Key.Home: + case Key.ControlA: + CursorPosition = 1; + break; + + case Key.CursorLeft: + case Key.ControlB: + DecCursorPosition (); + break; + + case Key.End: + case Key.ControlE: // End + CursorPosition = FieldLen; + break; + + case Key.CursorRight: + case Key.ControlF: + IncCursorPosition (); + break; + + default: + // Ignore non-numeric characters. + if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9')) + return false; + + if (ReadOnly) + return true; + + if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ())) + IncCursorPosition (); + return true; + } + return true; + } + + /// + public override bool MouseEvent (MouseEvent ev) + { + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) + return false; + if (!HasFocus) + SuperView.SetFocus (this); + + var point = ev.X; + if (point > FieldLen) + point = FieldLen; + if (point < 1) + point = 1; + CursorPosition = point; + AdjCursorPosition (); + return true; + } + + /// + /// Event firing method that invokes the event. + /// + /// The event arguments + public virtual void OnTimeChanged (DateTimeEventArgs args) + { + TimeChanged?.Invoke (args); + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Windows/Dialog.cs b/Terminal.Gui/Windows/Dialog.cs new file mode 100644 index 0000000..2d16048 --- /dev/null +++ b/Terminal.Gui/Windows/Dialog.cs @@ -0,0 +1,140 @@ +// +// Dialog.cs: Dialog box +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +using System; +using System.Collections.Generic; +using System.Linq; +using NStack; + +namespace Terminal.Gui { + /// + /// The is a that by default is centered and contains one + /// or more s. It defaults to the color scheme and has a 1 cell padding around the edges. + /// + /// + /// To run the modally, create the , and pass it to . + /// This will execute the dialog until it terminates via the [ESC] or [CTRL-Q] key, or when one of the views + /// or buttons added to the dialog calls . + /// + public class Dialog : Window { + List