diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 000000000..28273f4e9 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,5 @@ +# Terminal.Gui - Cursor AI Rules + +> **πŸ“˜ Source of Truth: [CONTRIBUTING.md](CONTRIBUTING.md)** + +This project uses [CONTRIBUTING.md](CONTRIBUTING.md) as the single source of truth for contribution guidelines. AI agents, including CoPilot and Cursor **MUST** follow the guidelines in [CONTRIBUTING.md](CONTRIBUTING.md)/ diff --git a/.editorconfig b/.editorconfig index 95013c249..fa758e3bb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -82,8 +82,8 @@ dotnet_diagnostic.cs0464.severity = warning dotnet_diagnostic.cs0465.severity = warning dotnet_diagnostic.cs0469.severity = warning dotnet_diagnostic.cs0472.severity = warning -dotnet_diagnostic.cs0612.severity = warning -dotnet_diagnostic.cs0618.severity = warning +dotnet_diagnostic.cs0612.severity = none +dotnet_diagnostic.cs0618.severity = none dotnet_diagnostic.cs0628.severity = warning dotnet_diagnostic.cs0642.severity = warning dotnet_diagnostic.cs0649.severity = warning @@ -94,7 +94,7 @@ dotnet_diagnostic.cs0659.severity = warning dotnet_diagnostic.cs0660.severity = warning dotnet_diagnostic.cs0661.severity = warning dotnet_diagnostic.cs0665.severity = warning -dotnet_diagnostic.cs0672.severity = warning +dotnet_diagnostic.cs0672.severity = none dotnet_diagnostic.cs0675.severity = warning dotnet_diagnostic.cs0693.severity = warning dotnet_diagnostic.cs0728.severity = warning diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2eda8e051..43e9ecc25 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,41 +1,140 @@ --- name: Bug report -about: Create a report to help us improve +about: Create a report to help us improve Terminal.Gui title: '' labels: bug assignees: '' --- -**Describe the bug** +## Describe the bug + A clear and concise description of what the bug is. -**To Reproduce** +## To Reproduce + Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error -**Expected behavior** -A clear and concise description of what you expected to happen. +1. Run the following code: + ```csharp + // Paste your minimal reproduction code here + ``` -**Screenshots** -If applicable, add screenshots to help explain your problem. +2. Expected behavior: (describe what should happen) -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +3. Actual behavior: (describe what actually happens) -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] +## Environment -**Additional context** -Add any other context about the problem here. +Please run the following commands in your terminal and paste the output: -**Set Project & Milestone** -If you have access, please don't forget to set the right Project and Milestone. +### OS Information + +**Windows (PowerShell):** +```powershell +"OS: $(Get-CimInstance Win32_OperatingSystem | Select-Object -ExpandProperty Caption) $(Get-CimInstance Win32_OperatingSystem | Select-Object -ExpandProperty Version)" +``` + +**macOS/Linux:** +```bash +echo "OS: $(uname -s) $(uname -r)" +``` + +**Output:** +``` +(paste output here) +``` + +### Terminal Information + +**Windows Terminal:** +```powershell +"Terminal: Windows Terminal $(Get-AppxPackage -Name Microsoft.WindowsTerminal | Select-Object -ExpandProperty Version)" +``` + +**Other terminals:** +```bash +echo $TERM +``` + +**Output:** +``` +(paste output here) +``` + +### PowerShell Version + +```powershell +$PSVersionTable.PSVersion +``` + +**Output:** +``` +(paste output here) +``` + +### .NET Information + +```bash +dotnet --version +dotnet --info +``` + +**Output:** +``` +(paste output here) +``` + +### Terminal.Gui Version + +**Option 1 - Run UICatalog (easiest):** + +UICatalog displays the Terminal.Gui version in its About box and status bar. + +```bash +dotnet run --project Examples/UICatalog/UICatalog.csproj +``` + +**Option 2 - NuGet Package Version:** +``` +(e.g., 2.0.0-alpha.1, 2.0.0-develop.123, etc.) +``` + +**Option 3 - Building from source:** +```bash +git rev-parse HEAD +git describe --tags --always --dirty +``` + +**Version:** +``` +(paste version here) +``` + +## Screenshots, GIFs, or Terminal Output + +If applicable, add screenshots, animated GIFs, or copy/paste terminal output to help explain your problem. + +**Animated GIFs are especially helpful for showing behavior!** + +- **Windows**: [ShareX](https://getsharex.com/) (free, captures screen to GIF) +- **macOS**: [Kap](https://getkap.co/) (free, open source) +- **Linux**: [Peek](https://github.com/phw/peek) (free) + +**For terminal output, use code blocks:** + +``` +(paste terminal output here) +``` + +## Additional context + +Add any other context about the problem here, such as: +- Does this happen consistently or intermittently? +- Did this work in a previous version? +- Are there any error messages in the console? +- Terminal configuration or settings that might be relevant? + +## For Maintainers + +**Set Project & Milestone:** If you have access, please don't forget to set the right Project and Milestone. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..c91e56652 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +# Terminal.Gui - CoPilot AI Rules + +> **πŸ“˜ Source of Truth: [../CONTRIBUTING.md](../CONTRIBUTING.md)** + +This project uses [../CONTRIBUTING.md](../CONTRIBUTING.md) as the single source of truth for contribution guidelines. AI agents, including CoPilot and Cursor **MUST** follow the guidelines in [../CONTRIBUTING.md](CONTRIBUTING.md)/ + +**CRITICAL** - CoPilot you MUST internalize [../CONTRIBUTING.md](../CONTRIBUTING.md) and follow the guidance for any work you do on Terminal.Gui. This includes Github Copilot in Agent mode. \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-validation.yml similarity index 51% rename from .github/workflows/build-release.yml rename to .github/workflows/build-validation.yml index a95896a28..9f49a7375 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-validation.yml @@ -1,4 +1,4 @@ -name: Ensure that Release Build of Solution Builds Correctly +name: Build Validation on: push: @@ -11,11 +11,14 @@ on: - '**.md' jobs: - build_release: - # Ensure that RELEASE builds are not broken + build-validation: + name: Build All Configurations runs-on: ubuntu-latest + + timeout-minutes: 10 steps: - - name: Checkout ${{ github.ref_name }} + + - name: Checkout code uses: actions/checkout@v4 - name: Setup .NET Core @@ -24,11 +27,19 @@ jobs: dotnet-version: 8.x dotnet-quality: 'ga' + - name: Restore dependencies + run: dotnet restore + + # Suppress CS0618 (member is obsolete) and CS0612 (member is obsolete without message) + # Using -property: syntax with URL-encoded semicolon (%3B) to avoid shell interpretation issues + - name: Build Debug + run: dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612 + - name: Build Release Terminal.Gui - run: dotnet build Terminal.Gui/Terminal.Gui.csproj --no-incremental --nologo --force --configuration Release + run: dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612 - name: Pack Release Terminal.Gui - run: dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages + run: dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612 - name: Restore AOT and Self-Contained projects run: | @@ -40,8 +51,8 @@ jobs: - name: Build Release AOT and Self-Contained run: | - dotnet build ./Examples/NativeAot/NativeAot.csproj --configuration Release - dotnet build ./Examples/SelfContained/SelfContained.csproj --configuration Release + dotnet build ./Examples/NativeAot/NativeAot.csproj --configuration Release -property:NoWarn=0618%3B0612 + dotnet build ./Examples/SelfContained/SelfContained.csproj --configuration Release -property:NoWarn=0618%3B0612 - - name: Build Release Solution without restore - run: dotnet build --configuration Release --no-restore + - name: Build Release Solution + run: dotnet build --configuration Release --no-restore -property:NoWarn=0618%3B0612 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e0cb43025..ba315db00 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,5 +1,4 @@ name: Build & Run Integration Tests - on: push: branches: [ v2_release, v2_develop ] @@ -9,52 +8,99 @@ on: branches: [ v2_release, v2_develop ] paths-ignore: - '**.md' - -jobs: - build_and_test_debug: +jobs: + build: + uses: ./.github/workflows/quick-build.yml + + integration_tests: + name: Integration Tests runs-on: ${{ matrix.os }} + needs: build strategy: - # Turn off fail-fast to let all runners run even if there are errors - fail-fast: true + fail-fast: false # Let all OSes finish even if one fails matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] + timeout-minutes: 15 - timeout-minutes: 10 steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Checkout code - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + dotnet-quality: ga - - name: Setup .NET Core - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.x - dotnet-quality: 'ga' + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: test-build-artifacts + path: . - - name: Install dependencies - run: | - dotnet restore + - name: Restore NuGet packages + run: dotnet restore - - name: Build IntegrationTests - run: dotnet build Tests/IntegrationTests --configuration Debug --no-restore + - name: Disable Windows Defender (Windows only) + if: runner.os == 'Windows' + shell: powershell + run: | + Add-MpPreference -ExclusionPath "${{ github.workspace }}" + Add-MpPreference -ExclusionProcess "dotnet.exe" + Add-MpPreference -ExclusionProcess "testhost.exe" + Add-MpPreference -ExclusionProcess "VSTest.Console.exe" - - name: Set VSTEST_DUMP_PATH - shell: bash - run: echo "{VSTEST_DUMP_PATH}={logs/${{ runner.os }}/}" >> $GITHUB_ENV + - name: Set VSTEST_DUMP_PATH + shell: bash + run: echo "VSTEST_DUMP_PATH=logs/IntegrationTests/${{ runner.os }}/" >> $GITHUB_ENV - - name: Run IntegrationTests - run: | - dotnet test Tests/IntegrationTests --no-build --verbosity normal --diag:logs/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true - - # mv -v Tests/IntegrationTests/TestResults/*/*.* TestResults/IntegrationTests/ + - name: Run IntegrationTests + shell: bash + run: | + if [ "${{ runner.os }}" == "Linux" ]; then + # Run with coverage on Linux only + dotnet test Tests/IntegrationTests \ + --no-build \ + --verbosity minimal \ + --collect:"XPlat Code Coverage" \ + --settings Tests/IntegrationTests/runsettings.coverage.xml \ + --diag:logs/IntegrationTests/${{ runner.os }}/logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always + else + # Run without coverage on Windows/macOS for speed + dotnet test Tests/IntegrationTests \ + --no-build \ + --verbosity minimal \ + --settings Tests/IntegrationTests/runsettings.xml \ + --diag:logs/IntegrationTests/${{ runner.os }}/logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always + fi - - name: Upload Test Logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: integration-test-logs-${{ runner.os }} - path: | - logs/ - TestResults/IntegrationTests/ + - name: Upload Integration Test Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration_tests-logs-${{ runner.os }} + path: | + logs/IntegrationTests/ + TestResults/ + + - name: Upload Integration Tests Coverage to Codecov + if: matrix.os == 'ubuntu-latest' && always() + uses: codecov/codecov-action@v4 + with: + files: TestResults/**/coverage.cobertura.xml + flags: integrationtests + name: IntegrationTests-${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/quick-build.yml b/.github/workflows/quick-build.yml new file mode 100644 index 000000000..01695d8c3 --- /dev/null +++ b/.github/workflows/quick-build.yml @@ -0,0 +1,43 @@ +name: Quick Build for Tests + +on: + workflow_call: + outputs: + artifact-name: + description: "Name of the build artifacts" + value: ${{ jobs.quick-build.outputs.artifact-name }} + +jobs: + quick-build: + name: Build Debug Only + runs-on: ubuntu-latest + outputs: + artifact-name: test-build-artifacts + + timeout-minutes: 5 + steps: + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + dotnet-quality: 'ga' + + - name: Restore dependencies + run: dotnet restore + + # Suppress CS0618 (member is obsolete) and CS0612 (member is obsolete without message) + - name: Build Debug + run: dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: test-build-artifacts + path: | + **/bin/Debug/** + **/obj/Debug/** + retention-days: 1 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6fb66961c..4488bb645 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -11,16 +11,21 @@ on: - '**.md' jobs: + # Call the quick-build workflow to build Debug configuration only + build: + uses: ./.github/workflows/quick-build.yml + non_parallel_unittests: name: Non-Parallel Unit Tests runs-on: ${{ matrix.os }} + needs: build strategy: # Turn off fail-fast to let all runners run even if there are errors - fail-fast: true + fail-fast: false matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - timeout-minutes: 10 + timeout-minutes: 15 # Increased from 10 for Windows steps: - name: Checkout code @@ -32,27 +37,59 @@ jobs: dotnet-version: 8.x dotnet-quality: 'ga' - - name: Install dependencies + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: test-build-artifacts + path: . + + # KEEP THIS - It's needed for --no-build to work + - name: Restore NuGet packages + run: dotnet restore + + # Optimize Windows performance + - name: Disable Windows Defender (Windows only) + if: runner.os == 'Windows' + shell: powershell run: | - dotnet restore - - - name: Build Solution Debug - run: dotnet build --configuration Debug --no-restore - -# Test - # Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac - # See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed. - # Without it, the test runner crashes on ubuntu (but not Windows or mac) + Add-MpPreference -ExclusionPath "${{ github.workspace }}" + Add-MpPreference -ExclusionProcess "dotnet.exe" + Add-MpPreference -ExclusionProcess "testhost.exe" + Add-MpPreference -ExclusionProcess "VSTest.Console.exe" - name: Set VSTEST_DUMP_PATH shell: bash - run: echo "{VSTEST_DUMP_PATH}={logs/UnitTests/${{ runner.os }}/}" >> $GITHUB_ENV + run: echo "VSTEST_DUMP_PATH=logs/UnitTests/${{ runner.os }}/" >> $GITHUB_ENV - name: Run UnitTests + shell: bash run: | - dotnet test Tests/UnitTests --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTests/coverlet.runsettings --diag:logs/UnitTests/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true - - # mv -v Tests/UnitTests/TestResults/*/*.* TestResults/UnitTests/ + if [ "${{ runner.os }}" == "Linux" ]; then + # Run with coverage on Linux only + dotnet test Tests/UnitTests \ + --no-build \ + --verbosity normal \ + --collect:"XPlat Code Coverage" \ + --settings Tests/UnitTests/runsettings.xml \ + --diag:logs/UnitTests/${{ runner.os }}/logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always + else + # Run without coverage on Windows/macOS for speed + dotnet test Tests/UnitTests \ + --no-build \ + --verbosity normal \ + --settings Tests/UnitTests/runsettings.xml \ + --diag:logs/UnitTests/${{ runner.os }}/logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 120s \ + --blame-crash-collect-always + fi - name: Upload Test Logs if: always() @@ -61,18 +98,29 @@ jobs: name: non_parallel_unittests-logs-${{ runner.os }} path: | logs/UnitTests - TestResults/UnitTests/ + TestResults/ + - name: Upload Non-Parallel UnitTests Coverage to Codecov + if: matrix.os == 'ubuntu-latest' && always() + uses: codecov/codecov-action@v4 + with: + files: TestResults/**/coverage.cobertura.xml + flags: unittests-nonparallel + name: UnitTests-${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + parallel_unittests: name: Parallel Unit Tests runs-on: ${{ matrix.os }} + needs: build strategy: # Turn off fail-fast to let all runners run even if there are errors - fail-fast: true + fail-fast: false matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Checkout code @@ -84,27 +132,57 @@ jobs: dotnet-version: 8.x dotnet-quality: 'ga' - - name: Install dependencies + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: test-build-artifacts + path: . + + - name: Restore NuGet packages + run: dotnet restore + + - name: Disable Windows Defender (Windows only) + if: runner.os == 'Windows' + shell: powershell run: | - dotnet restore - - - name: Build Solution Debug - run: dotnet build --configuration Debug --no-restore - -# Test - # Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac - # See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed. - # Without it, the test runner crashes on ubuntu (but not Windows or mac) + Add-MpPreference -ExclusionPath "${{ github.workspace }}" + Add-MpPreference -ExclusionProcess "dotnet.exe" + Add-MpPreference -ExclusionProcess "testhost.exe" + Add-MpPreference -ExclusionProcess "VSTest.Console.exe" - name: Set VSTEST_DUMP_PATH shell: bash - run: echo "{VSTEST_DUMP_PATH}={logs/UnitTestsParallelizable/${{ runner.os }}/}" >> $GITHUB_ENV + run: echo "VSTEST_DUMP_PATH=logs/UnitTestsParallelizable/${{ runner.os }}/" >> $GITHUB_ENV - name: Run UnitTestsParallelizable + shell: bash run: | - dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTestsParallelizable/coverlet.runsettings --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true - - # mv -v Tests/UnitTestsParallelizable/TestResults/*/*.* TestResults/UnitTestsParallelizable/ + if [ "${{ runner.os }}" == "Linux" ]; then + # Run with coverage on Linux only + dotnet test Tests/UnitTestsParallelizable \ + --no-build \ + --verbosity normal \ + --collect:"XPlat Code Coverage" \ + --settings Tests/UnitTests/runsettings.coverage.xml \ + --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always + else + # Run without coverage on Windows/macOS for speed + dotnet test Tests/UnitTestsParallelizable \ + --no-build \ + --verbosity normal \ + --settings Tests/UnitTestsParallelizable/runsettings.xml \ + --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \ + --blame \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 60s \ + --blame-crash-collect-always + fi - name: Upload UnitTestsParallelizable Logs if: always() @@ -113,4 +191,14 @@ jobs: name: parallel_unittests-logs-${{ runner.os }} path: | logs/UnitTestsParallelizable/ - TestResults/UnitTestsParallelizable/ + TestResults/ + + - name: Upload Parallelizable UnitTests Coverage to Codecov + if: matrix.os == 'ubuntu-latest' && always() + uses: codecov/codecov-action@v4 + with: + files: TestResults/**/coverage.cobertura.xml + flags: unittests-parallel + name: UnitTestsParallelizable-${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index e2b23118e..cdec09ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,8 @@ BenchmarkDotNet.Artifacts/ *.log.* log.* + +/Tests/coverage/ +!/Tests/coverage/.gitkeep # keep folder in repo +/Tests/report/ +*.cobertura.xml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..96c71615b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,14 @@ +# Terminal.Gui - AI Agent Instructions + +> **πŸ“˜ For complete contributor guidelines (humans and AI agents), see [CONTRIBUTING.md](CONTRIBUTING.md).** + +This repository uses [CONTRIBUTING.md](CONTRIBUTING.md) as the single source of truth for code style, testing, CI/CD, and contribution workflow. GitHub Copilot and other AI coding agents should also refer to [.github/copilot-instructions.md](.github/copilot-instructions.md) for a curated summary of non-negotiable rules. + +**Key highlights for AI agents:** +- Always use explicit types (no `var` except for built-in simple types) +- Always use target-typed `new()` syntax +- Add new tests to `Tests/UnitTestsParallelizable/` when possible +- Never decrease code coverage +- Follow `.editorconfig` and `Terminal.sln.DotSettings` for formatting + +See [CONTRIBUTING.md](CONTRIBUTING.md) for complete details. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76defbb7f..2e985e560 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,191 +1,428 @@ # Contributing to Terminal.Gui -We welcome contributions from the community. See [Issues](https://github.com/gui-cs/Terminal.Gui/issues) for a list of open [bugs](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3Abug) and [enhancements](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement). Contributors looking for something fun to work on should look at issues tagged as: +> **πŸ“˜ This document is the single source of truth for all contributors (humans and AI agents) to Terminal.Gui.** -- [good first issue](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) -- [up for grabs](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3Aup-for-grabs) -- [help wanted](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3Aup-for-grabs) +Welcome! This guide provides everything you need to know to contribute effectively to Terminal.Gui, including project structure, build instructions, coding conventions, testing requirements, and CI/CD workflows. -## Forking and Submitting Changes +## Table of Contents -Terminal.Gui uses the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. +- [Project Overview](#project-overview) +- [Building and Testing](#building-and-testing) +- [Coding Conventions](#coding-conventions) +- [Testing Requirements](#testing-requirements) +- [API Documentation Requirements](#api-documentation-requirements) +- [Pull Request Guidelines](#pull-request-guidelines) +- [CI/CD Workflows](#cicd-workflows) +- [Repository Structure](#repository-structure) +- [Branching Model](#branching-model) +- [Key Architecture Concepts](#key-architecture-concepts) +- [What NOT to Do](#what-not-to-do) +- [Additional Resources](#additional-resources) -* The `v1_release` and `v2_release` branches are always stable, and always match the most recently released Nuget package. -* The `v1_develop` and `v2_develop` branches are where new development and bug-fixes happen. `v2_develop` is the default Github branch. +--- -### Forking Terminal.Gui +## Project Overview -1. Use GitHub to fork the `Terminal.Gui` repo to your account (https://github.com/gui-cs/Terminal.Gui/fork). +**Terminal.Gui** is a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. It's a large codebase (~1,050 C# files) providing a comprehensive framework for building interactive console applications with support for keyboard and mouse input, customizable views, and a robust event system. -2. Clone your fork to your local machine +**Key characteristics:** +- **Language**: C# (net8.0) +- **Platform**: Cross-platform (Windows, macOS, Linux) +- **Architecture**: Console UI toolkit with driver-based architecture +- **Version**: v2 (Alpha), v1 (maintenance mode) +- **Branching**: GitFlow model (v2_develop is default/active development) -``` -git clone https://github.com//Terminal.Gui +--- + +## Building and Testing + +### Required Tools + +- **.NET SDK**: 8.0.0 (see `global.json`) +- **Runtime**: .NET 8.x (latest GA) +- **Optional**: ReSharper/Rider for code formatting (honor `.editorconfig` and `Terminal.sln.DotSettings`) + +### Build Commands (In Order) + +**ALWAYS run these commands from the repository root:** + +1. **Restore packages** (required first, ~15-20 seconds): + ```bash + dotnet restore + ``` + +2. **Build solution** (Debug, ~50 seconds): + ```bash + dotnet build --configuration Debug --no-restore + ``` + - Expect ~326 warnings (nullable reference warnings, unused variables, etc.) - these are normal + - 0 errors expected + +3. **Build Release** (for packaging): + ```bash + dotnet build --configuration Release --no-restore + ``` + +### Test Commands + +**Two test projects exist:** + +1. **Non-parallel tests** (depend on static state, ~10 min timeout): + ```bash + dotnet test Tests/UnitTests --no-build --verbosity normal + ``` + - Uses `Application.Init` and static state + - Cannot run in parallel + - Includes `--blame` flags for crash diagnostics + +2. **Parallel tests** (can run concurrently, ~10 min timeout): + ```bash + dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal + ``` + - No dependencies on static state + - **Preferred for new tests** + +3. **Integration tests**: + ```bash + dotnet test Tests/IntegrationTests --no-build --verbosity normal + ``` + +### Common Build Issues + +#### Issue: Build Warnings +- **Expected**: None warnings (~100 currently). +- **Action**: Don't add new warnings; fix warnings in code you modify + +#### Issue: NativeAot/SelfContained Build +- **Solution**: Restore these projects explicitly: + ```bash + dotnet restore ./Examples/NativeAot/NativeAot.csproj -f + dotnet restore ./Examples/SelfContained/SelfContained.csproj -f + ``` + +### Running Examples + +**UICatalog** (comprehensive demo app): +```bash +dotnet run --project Examples/UICatalog/UICatalog.csproj ``` -Now, your local repo will have an `origin` remote pointing to `https://github.com//Terminal.Gui`. +--- -3. Add a remote for `upstream`: -``` -git remote add upstream https://github.com/gui-cs/Terminal.Gui -``` -You now have your own fork and a local repo that references it as `origin`. Your local repo also now references the orignal Terminal.Gui repo as `upstream`. +## Coding Conventions -### Starting to Make a Change +### Code Style Tenets -Ensure your local `v1_develop` (for v1) or `v2_develop` (for v2) branch is up-to-date with `upstream` (`github.com/gui-cs/Terminal.Gui`): -```powershell -cd ./Terminal.Gui -git checkout v2_develop -git pull upstream v2_develop +1. **Six-Year-Old Reading Level** - Readability over terseness +2. **Consistency, Consistency, Consistency** - Follow existing patterns ruthlessly +3. **Don't be Weird** - Follow Microsoft/.NET conventions +4. **Set and Forget** - Rely on automated tooling +5. **Documentation is the Spec** - API docs are source of truth + +### Code Formatting + +**⚠️ CRITICAL - These rules MUST be followed in ALL new or modified code:** + +- **Do NOT add formatting tools** - Use existing `.editorconfig` and `Terminal.sln.DotSettings` +- Format code with: + 1. ReSharper/Rider (`Ctrl-E-C`) + 2. JetBrains CleanupCode CLI tool (free) + 3. Visual Studio (`Ctrl-K-D`) as fallback +- **Only format files you modify** +- Follow `.editorconfig` settings (e.g., braces on new lines, spaces after keywords) +- 4-space indentation +- No trailing whitespace +- File-scoped namespaces +- **ALWAYS use explicit types** - Never use `var` except for built-in simple types (`int`, `string`, `bool`, `double`, `float`, `decimal`, `char`, `byte`) + ```csharp + // βœ… CORRECT - Explicit types + View view = new () { Width = 10 }; + MouseEventArgs args = new () { Position = new Point(5, 5) }; + List views = new (); + var count = 0; // OK - int is a built-in type + var name = "test"; // OK - string is a built-in type + + // ❌ WRONG - Using var for non-built-in types + var view = new View { Width = 10 }; + var args = new MouseEventArgs { Position = new Point(5, 5) }; + var views = new List(); + ``` + +- **ALWAYS use target-typed `new ()`** - Use `new ()` instead of `new TypeName()` when the type is already declared + ```csharp + // βœ… CORRECT - Target-typed new + View view = new () { Width = 10 }; + MouseEventArgs args = new (); + + // ❌ WRONG - Redundant type name + View view = new View() { Width = 10 }; + MouseEventArgs args = new MouseEventArgs(); + ``` + +**⚠️ CRITICAL - These conventions apply to ALL code - production code, test code, examples, and samples.** + +--- + +## Testing Requirements + +### Code Coverage + +- **Never decrease code coverage** - PRs must maintain or increase coverage +- Target: 70%+ coverage for new code +- **Coverage collection**: +- Centralized in `TestResults/` directory at repository root +- Collected only on Linux (ubuntu-latest) runners in CI for performance +- Windows and macOS runners skip coverage collection to reduce execution time +- Coverage reports uploaded to Codecov automatically from Linux runner +- CI monitors coverage on each PR + +### Test Patterns + +- **Parallelizable tests preferred** - Add new tests to `UnitTestsParallelizable` when possible +- **Avoid static dependencies** - Don't use `Application.Init`, `ConfigurationManager` in tests +- **Don't use `[AutoInitShutdown]`** - Legacy pattern, being phased out +- **Make tests granular** - Each test should cover smallest area possible +- Follow existing test patterns in respective test projects + +### Test Configuration + +- `xunit.runner.json` - xUnit configuration +- `coverlet.runsettings` - Coverage settings (OpenCover format) + +--- + +## API Documentation Requirements + +**All public APIs MUST have XML documentation:** + +- Clear, concise `` tags +- Use `` for cross-references +- Add `` for context +- Include `` for non-obvious usage +- Complex topics β†’ `docfx/docs/*.md` files +- Proper English and grammar - Clear, concise, complete. Use imperative mood. + +--- + +## Pull Request Guidelines + +### PR Requirements + +- **Title**: "Fixes #issue. Terse description". If multiple issues, list all, separated by commas (e.g. "Fixes #123, #456. Terse description") +- **Description**: + - Include "- Fixes #issue" for each issue near the top + - **ALWAYS** include instructions for pulling down locally at end of Description + - Suggest user setup a remote named `copilot` pointing to your fork + - Example: + ```markdown + # To pull down this PR locally: + git remote add copilot + git fetch copilot + git checkout copilot/ + ``` +- **Coding Style**: Follow all coding conventions in this document for new and modified code +- **Tests**: Add tests for new functionality (see [Testing Requirements](#testing-requirements)) +- **Coverage**: Maintain or increase code coverage +- **Scenarios**: Update UICatalog scenarios when adding features +- **Warnings**: **CRITICAL - PRs must not introduce any new warnings** + - Any file modified in a PR that currently generates warnings **MUST** be fixed to remove those warnings + - Exception: Warnings caused by `[Obsolete]` attributes can remain + - Expected baseline: ~326 warnings (mostly nullable reference warnings, unused variables, xUnit suggestions) + - Action: Before submitting a PR, verify your changes don't add new warnings and fix any warnings in files you modify + +--- + +## CI/CD Workflows + +The repository uses multiple GitHub Actions workflows. What runs and when: + +### 1) Build Solution (`.github/workflows/build.yml`) + +- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`); supports `workflow_call` +- **Runner/timeout**: `ubuntu-latest`, 10 minutes +- **Steps**: +- Checkout and setup .NET 8.x GA +- `dotnet restore` +- Build Debug: `dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612` +- Build Release (library): `dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612` +- Pack Release: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612` +- Restore NativeAot/SelfContained examples, then restore solution again +- Build Release for `Examples/NativeAot` and `Examples/SelfContained` +- Build Release solution +- Upload artifacts named `build-artifacts`, retention 1 day + +### 2) Build & Run Unit Tests (`.github/workflows/unit-tests.yml`) + +- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`) +- **Matrix**: Ubuntu/Windows/macOS +- **Timeout**: 15 minutes per job +- **Process**: +1. Calls build workflow to build solution once +2. Downloads build artifacts +3. Runs `dotnet restore` (required for `--no-build` to work) +4. **Performance optimizations**: + - Disables Windows Defender on Windows runners (significant speedup) + - Collects code coverage **only on Linux** (ubuntu-latest) for performance + - Windows and macOS skip coverage collection to reduce test time + - Increased blame-hang-timeout to 120s for Windows/macOS (60s for Linux) +5. Runs two test jobs: + - **Non-parallel UnitTests**: `Tests/UnitTests` with blame/diag flags; `xunit.stopOnFail=false` + - **Parallel UnitTestsParallelizable**: `Tests/UnitTestsParallelizable` with blame/diag flags; `xunit.stopOnFail=false` +6. Uploads test logs and diagnostic data from all runners +7. **Uploads code coverage to Codecov only from Linux runner** + +**Test results**: All tests output to unified `TestResults/` directory at repository root + +### 3) Build & Run Integration Tests (`.github/workflows/integration-tests.yml`) + +- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`) +- **Matrix**: Ubuntu/Windows/macOS +- **Timeout**: 15 minutes +- **Process**: +1. Calls build workflow +2. Downloads build artifacts +3. Runs `dotnet restore` +4. **Performance optimizations** (same as unit tests): + - Disables Windows Defender on Windows runners + - Collects code coverage **only on Linux** + - Increased blame-hang-timeout to 120s for Windows/macOS +5. Runs IntegrationTests with blame/diag flags; `xunit.stopOnFail=true` +6. Uploads logs per-OS +7. **Uploads coverage to Codecov only from Linux runner** + +### 4) Publish to NuGet (`.github/workflows/publish.yml`) + +- **Triggers**: push to `v2_release`, `v2_develop`, and tags `v*`(ignores `**.md`) +- Uses GitVersion to compute SemVer, builds Release, packs with symbols, and pushes to NuGet.org using `NUGET_API_KEY` + +### 5) Build and publish API docs (`.github/workflows/api-docs.yml`) + +- **Triggers**: push to `v1_release` and `v2_develop` +- Builds DocFX site on Windows and deploys to GitHub Pages when `ref_name` is `v2_release` or `v2_develop` + + +### Replicating CI Locally + +```bash +# Full CI sequence: +dotnet restore +dotnet build --configuration Debug --no-restore +dotnet test Tests/UnitTests --no-build --verbosity normal +dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal +dotnet build --configuration Release --no-restore ``` -Create a new local branch: -```powershell -git checkout -b my_new_branch -``` +--- -### Making Changes -Follow all the guidelines below. +## Repository Structure -* [Coding Style](#TerminalGui-Coding-Style) -* [Unit Tests](#Unit-Tests) -* [Sample Code](#Sample-Code) -* API Documentation -* etc... +### Root Directory Files -When you're ready, commit your changes: +- `Terminal.sln` - Main solution file +- `Terminal.sln.DotSettings` - ReSharper code style settings +- `.editorconfig` - Code formatting rules (111KB, extensive) +- `global.json` - .NET SDK version pinning +- `Directory.Build.props` - Common MSBuild properties +- `Directory.Packages.props` - Central package version management +- `GitVersion.yml` - Version numbering configuration +- `CONTRIBUTING.md` - This file - contribution guidelines (source of truth) +- `AGENTS.md` - Pointer to this file for AI agents +- `README.md` - Project documentation -```powershell -git add . -git commit -m "Fixes #1234. Some bug" -``` +### Main Directories -### Submitting a Pull Request +**`/Terminal.Gui/`** - Core library (496 C# files): +- `App/` - Application lifecycle (`Application.cs` static class, `SessionToken`, `MainLoop`) +- `Configuration/` - `ConfigurationManager` for settings +- `Drivers/` - Console driver implementations (`Dotnet`, `Windows`, `Unix`, `Fake`) +- `Drawing/` - Rendering system (attributes, colors, glyphs) +- `Input/` - Keyboard and mouse input handling +- `ViewBase/` - Core `View` class hierarchy and layout +- `Views/` - Specific View subclasses (Window, Dialog, Button, ListView, etc.) +- `Text/` - Text manipulation and formatting +- `FileServices/` - File operations and services -1. Push your local branch to your fork (`origin`): +**`/Tests/`**: +- `UnitTests/` - Non-parallel tests (use `Application.Init`, static state) +- `UnitTestsParallelizable/` - Parallel tests (no static dependencies) - **Preferred** +- `IntegrationTests/` - Integration tests +- `StressTests/` - Long-running stress tests (scheduled daily) +- `coverlet.runsettings` - Code coverage configuration -```powershell -git push --set-upstream origin my_new_branch -``` +**`/Examples/`**: +- `UICatalog/` - Comprehensive demo app for manual testing +- `Example/` - Basic example +- `NativeAot/`, `SelfContained/` - Deployment examples +- `ReactiveExample/`, `CommunityToolkitExample/` - Integration examples -2. Create the Pull Request: +**`/docfx/`** - Documentation source: +- `docs/` - Conceptual documentation (deep dives) +- `api/` - Generated API docs (gitignored) +- `docfx.json` - DocFX configuration -In the output of the `git push` command you'll see instructions with a link to the Pull Request: +**`/Scripts/`** - PowerShell build utilities (requires PowerShell 7.4+) -```powershell - $ git push --set-upstream origin my_new_branch -Enumerating objects: 8, done. -... -remote: -remote: Create a pull request for 'my_new_branch' on GitHub by visiting: -remote: https://github.com//Terminal.Gui/pull/new/more_doc_fixes -remote: -... -``` +**`/.github/workflows/`** - CI/CD pipelines (see [CI/CD Workflows](#cicd-workflows)) -3. Go to that URL and create the Pull Request: +--- -(in Windows Terminal, just CTRL-Click on the URL) +## Branching Model -Follow the template instructions found on Github. +### GitFlow Model -## Tenets for [gui-cs](www.github.com/gui-cs) Code Style (Unless you have better ones) +- `v2_develop` - Default branch, active development +- `v2_release` - Stable releases, matches NuGet +- `v1_develop`, `v1_release` - Legacy v1 (maintenance only) -* **Six-Year-Old Reading Level** - Our code style is biased towards code readability and away from terseness. This is *Systems Software* and needs to stand the test of time. Code should be structured and use variable names that make it readable by a 6-year-old, and comments in code are encouraged. -* **Consistency, Consistency, Consistency** - We adopt and document our standards for code style and then enforce them ruthlessly. For example, we require code reviews to pay attention to code style, not just functionality. -* **Don't be Weird** - Like all developers we have opinions, but our opinions on code style are tempered by existing standards. We are biased towards code style that used by Microsoft and other leading dotnet developers. For example, we choose 4 spaces for indentation instead of 8. -* **Set and Forget** - We embrace and encourage the use of technology that makes it easy for contributors to apply best-practice code-style, such as ReSharper. As we do so we are mindful that tools can cause hidden issues and merge hell. -* **Documentation is the Spec** - We care deeply about providing delightful developer documentation and are sticklers for grammar and clarity. If the code and the docs conflict, we are biased to believe what we wrote in the API documentation. This drives a virtuous cycle of clear thinking. +--- -**Terminal.Gui** uses a derivative of the [Microsoft C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions), with any deviations from those (somewhat older) conventions codified in the .editorconfig for the solution, as well as even more specific definitions in team-shared dotsettings files, used by ReSharper and Rider.\ -Before you commit code, please run the formatting rules on **only the code file(s) you have modified**, in one of the following ways, in order of most preferred to least preferred: +## Key Architecture Concepts - 1. `Ctrl-E-C` if using ReSharper or Rider - 2. Running the free [CleanupCode](https://www.jetbrains.com/help/resharper/CleanupCode.html) tool from JetBrains (this applies the same formatting rules as if you had used ReSharper or Rider, but is free for all users, if you don't have a license for those products) - - Run at the command line, from the solution root directory, as: `cleanupcode.exe relative/path/to/your/file.cs` - 3. If you are unable to use either of those options, the last resort is to use `Ctrl-K-D` in Visual Studio (with default C# developer key bindings), to apply the subset of the formatting rules that Visual Studio can apply. +**⚠️ CRITICAL - Contributors should understand these concepts before starting work.** -## User Experience Tenets +See `/docfx/docs/` for deep dives on: -**Terminal.Gui**, as a UI framework, heavily influences how console graphical user interfaces (GUIs) work. We use the following [tenets](https://ceklog.kindel.com/2020/02/10/tenets/) to guide us: +- **Application Lifecycle** - How `Application.Init`, `Application.Run`, and `Application.Shutdown` work +- **View Hierarchy** - Understanding `View`, `Toplevel`, `Window`, and view containment +- **Layout System** - Pos, Dim, and automatic layout +- **Event System** - How keyboard, mouse, and application events flow +- **Driver Architecture** - How console drivers abstract platform differences +- **Drawing Model** - How rendering works with Attributes, Colors, and Glyphs -*NOTE: Like all tenets, these are up for debate. If you disagree, have questions, or suggestions about these tenets and guidelines submit an Issue using the [design](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3Adesign) tag.* +Key documentation: +- [View Documentation](https://gui-cs.github.io/Terminal.Gui/docs/View.html) +- [Events Deep Dive](https://gui-cs.github.io/Terminal.Gui/docs/events.html) +- [Layout System](https://gui-cs.github.io/Terminal.Gui/docs/layout.html) +- [Keyboard Handling](https://gui-cs.github.io/Terminal.Gui/docs/keyboard.html) +- [Mouse Support](https://gui-cs.github.io/Terminal.Gui/docs/mouse.html) +- [Drivers](https://gui-cs.github.io/Terminal.Gui/docs/drivers.html) -1. **Honor What's Come Before**. The Mac and Windows OS's have well-established GUI idioms that are mostly consistent. We adhere to these versus inventing new ways for users to do things. For example, **Terminal.Gui** adopts the `ctrl/command-c`, `ctrl/command-v`, and `ctrl/command-x` keyboard shortcuts for cut, copy, and paste versus defining new shortcuts. -2. **Consistency Matters**. Common UI idioms should be consistent across the GUI framework. For example, `ctrl/command-q` quits/exits all modal views. See [Issue #456](https://github.com/gui-cs/Terminal.Gui/issues/456) as a counter-example that should be fixed. -3. **Honor the OS, but Work Everywhere**. **Terminal.Gui** is cross-platform, but we support taking advantage of a platform's unique advantages. For example, the Windows Console API is richer than the Unix API in terms of keyboard handling. Thus, in Windows pressing the `alt` key in a **Terminal.Gui** app will activate the `MenuBar`, but in Unix, the user has to press the full hotkey (e.g. `alt-f`) or `F9`. -4. **Keyboard first, Mouse also**. Users use consoles primarily with the keyboard; **Terminal.Gui** is optimized for getting stuff done without using the Mouse. However, as a GUI framework, the Mouse is essential thus we strive to ensure that everything also works via the Mouse. +--- -## Public API Tenets & Guidelines +## What NOT to Do -**Terminal.Gui** provides an API that is used by many. As the project evolves, contributors should follow these [tenets](https://ceklog.kindel.com/2020/02/10/tenets/) to ensure Consistency and backward compatibility. +- ❌ Don't add new linters/formatters (use existing) +- ❌ Don't modify unrelated code +- ❌ Don't remove/edit unrelated tests +- ❌ Don't break existing functionality +- ❌ Don't add tests to `UnitTests` if they can be parallelizable +- ❌ Don't use `Application.Init` in new tests +- ❌ Don't decrease code coverage +- ❌ **Don't use `var` for anything but built-in simple types** (use explicit types) +- ❌ **Don't use redundant type names with `new`** (**ALWAYS PREFER** target-typed `new ()`) +- ❌ **Don't introduce new warnings** (fix warnings in files you modify; exception: `[Obsolete]` warnings) -*NOTE: Like all tenets, these are up for debate. If you disagree, have questions, or suggestions about these tenets and guidelines submit an Issue using the [design](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3Adesign) tag.* +--- -1. **Stand on the shoulders of giants.** Follow the [Microsoft .NET Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/) where appropriate. -2. **Don't Break Existing Stuff.** Avoid breaking changes to user behavior or the public API; instead, figure out how to implement new functionality in a similar way. If a breaking change can't be avoided, follow the guidelines below. -3. **Fail-fast.** Fail-fast makes bugs and failures appear sooner, leading to a higher-quality framework and API. -4. **Standards Reduce Complexity**. We strive to adopt standard API idoms because doing so reduces complexity for users of the API. For example, see Tenet #1 above. A counterexample is [Issue #447](https://github.com/gui-cs/Terminal.Gui/issues/447). +## Additional Resources -### Include API Documentation +- **Full Documentation**: https://gui-cs.github.io/Terminal.Gui +- **API Reference**: https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.App.html +- **Deep Dives**: `/docfx/docs/` directory +- **Getting Started**: https://gui-cs.github.io/Terminal.Gui/docs/getting-started.html +- **Migrating from v1 to v2**: https://gui-cs.github.io/Terminal.Gui/docs/migratingfromv1.html +- **Showcase**: https://gui-cs.github.io/Terminal.Gui/docs/showcase.html -Great care has been provided thus far in ensuring **Terminal.Gui** has great [API Documentation](https://gui-cs.github.io/Terminal.Gui). Contributors have the responsibility of continuously improving the API Documentation. +--- -- All public APIs must have clear, concise, and complete documentation in the form of [XML Documentation](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/). -- Keep the `` terse. -- Use `` liberally to cross-link topics. -- Use `` to add more context and explanation. -- For complex topics, provide conceptual documentation in the `docfx/articles` folder as a `.md` file. It will automatically get picked up and be added to [Conceptual Documentation](https://gui-cs.github.io/Terminal.Gui/docs/index.html). -- Use proper English and good grammar. - -### Defining Events - -See https://gui-cs.github.io/Terminal.Gui/docs/events.html - - -### Defining new `View` classes - -- Support parameterless constructors (see [Issue 102](Parameterless constructors #102)). Do not require callers to use a parameterized constructor except when forcing `Absolute Layout`). -- Avoid doing initialization via constructors. Instead use a property so consumers can use object initialization (e.g. `var foo = new Foo() { a = b };`). -- Ensure the `UICatalog` demo for the new class illustrates both `Absolutle Layout` and `Computed Layout`. - -## Breaking Changes to User Behavior or the Public API - -- Tag all pull requests that cause breaking changes to user behavior or the public API with the [breaking-change](https://github.com/gui-cs/Terminal.Gui/issues?q=is%3Aopen+is%3Aissue+label%3Abreaking-change) tag. This will help project maintainers track and document these. -- Add a `` to the XML Documentation to the code describing the breaking change. These will get picked up in the [API Documentation](https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.html). - -## Unit Tests - -PRs should never cause code coverage to go down. Ideally, every PR will get the project closer to 100%. PRs that include new functionality (e.g. a new control) should have at least 70% code coverage for the new functionality. - -**Terminal.Gui** has an automated unit or regression test suite. See the [Testing wiki](https://github.com/gui-cs/Terminal.Gui/wiki/Testing). - -We analyze unit tests and code coverage on each PR push. - -The code coverage of the latest released build (on NuGet) is shown as a badge at the top of `README.md`. Here as well: - -![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/migueldeicaza/90ef67a684cb71db1817921a970f8d27/raw/code-coverage.json) - -The project uses Fine Code Coverage to allow easy access to code coverage info on a per-component basis. - -Use the following command to generate the same CC info that the Publish Github Action uses to publish the results to the badge: - -``` -dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --settings UnitTests/coverlet.runsettings -``` - -Then open up the resulting `coverage.opencover.xml` file and you'll see the `sequenceCoverage` value: - -```xml - - - - -``` - -## Sample Code - -[UI Catalog](https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog) is a great sample app for manual testing. - -When adding new functionality, fixing bugs, or changing things, please either add a new `Scenario` to **UICatalog** or update an existing `Scenario` to fully illustrate your work and provide a test-case. +**Thank you for contributing to Terminal.Gui!** πŸŽ‰ diff --git a/Directory.Packages.props b/Directory.Packages.props index 836970b60..d62a1d298 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,14 +11,14 @@ - + - + - - + + diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index d3e02a342..82cd60d6e 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -11,9 +11,11 @@ using Terminal.Gui.Views; using Attribute = Terminal.Gui.Drawing.Attribute; // Override the default configuration for the application to use the Light theme -ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }"""; +//ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }"""; ConfigurationManager.Enable(ConfigLocations.All); + + Application.Run ().Dispose (); // Before the application exits, reset Terminal.Gui for clean shutdown @@ -89,5 +91,22 @@ public class ExampleWindow : Window // Add the views to the Window Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin); + + ListView lv = new ListView () + { + Y = Pos.AnchorEnd(), + Height= Dim.Auto(), + Width = Dim.Auto() + }; + lv.SetSource (["One", "Two", "Three", "Four"]); + Add (lv); + } + + public override void EndInit () + { + base.EndInit (); + // Set the theme to "Anders" if it exists, otherwise use "Default" + ThemeManager.Theme = ThemeManager.GetThemeNames ().FirstOrDefault (x => x == "Anders") ?? "Default"; } } + diff --git a/Examples/NativeAot/Program.cs b/Examples/NativeAot/Program.cs index bfc566c6d..3de9bfeec 100644 --- a/Examples/NativeAot/Program.cs +++ b/Examples/NativeAot/Program.cs @@ -12,8 +12,8 @@ namespace NativeAot; public static class Program { - [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")] - [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")] + [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(IDriver, String)")] + [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(IDriver, String)")] private static void Main (string [] args) { ConfigurationManager.Enable(ConfigLocations.All); diff --git a/Examples/SelfContained/Program.cs b/Examples/SelfContained/Program.cs index 2cfdb8cc9..02109bf3a 100644 --- a/Examples/SelfContained/Program.cs +++ b/Examples/SelfContained/Program.cs @@ -12,7 +12,7 @@ namespace SelfContained; public static class Program { - [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, IConsoleDriver)")] + [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, IDriver)")] private static void Main (string [] args) { ConfigurationManager.Enable (ConfigLocations.All); diff --git a/Examples/UICatalog/Properties/launchSettings.json b/Examples/UICatalog/Properties/launchSettings.json index 02062518f..3da4193e1 100644 --- a/Examples/UICatalog/Properties/launchSettings.json +++ b/Examples/UICatalog/Properties/launchSettings.json @@ -4,25 +4,13 @@ "commandName": "Project", "commandLineArgs": "--debug-log-level Debug" }, - "UICatalog --driver NetDriver": { + "UICatalog --driver windows": { "commandName": "Project", - "commandLineArgs": "--driver NetDriver" + "commandLineArgs": "--driver windows -dl Trace" }, - "UICatalog --driver WindowsDriver": { + "UICatalog --driver dotnet": { "commandName": "Project", - "commandLineArgs": "--driver WindowsDriver" - }, - "UICatalog --driver v2": { - "commandName": "Project", - "commandLineArgs": "--driver v2 -dl Trace" - }, - "UICatalog --driver v2win": { - "commandName": "Project", - "commandLineArgs": "--driver v2win -dl Trace" - }, - "UICatalog --driver v2net": { - "commandName": "Project", - "commandLineArgs": "--driver v2net -dl Trace" + "commandLineArgs": "--driver dotnet -dl Trace" }, "WSL: UICatalog": { "commandName": "Executable", @@ -30,28 +18,16 @@ "commandLineArgs": "dotnet UICatalog.dll", "distributionName": "" }, - "WSL: UICatalog --driver NetDriver": { + "WSL: UICatalog --driver dotnet": { "commandName": "Executable", "executablePath": "wsl", - "commandLineArgs": "dotnet UICatalog.dll --driver NetDriver", + "commandLineArgs": "dotnet UICatalog.dll --driver dotnet", "distributionName": "" }, - "WSL: UICatalog --driver v2": { + "WSL: UICatalog --driver unix": { "commandName": "Executable", "executablePath": "wsl", - "commandLineArgs": "dotnet UICatalog.dll --driver v2", - "distributionName": "" - }, - "WSL: UICatalog --driver v2unix": { - "commandName": "Executable", - "executablePath": "wsl", - "commandLineArgs": "dotnet UICatalog.dll --driver v2unix", - "distributionName": "" - }, - "WSL: UICatalog --driver v2net": { - "commandName": "Executable", - "executablePath": "wsl", - "commandLineArgs": "dotnet UICatalog.dll --driver v2net", + "commandLineArgs": "dotnet UICatalog.dll --driver unix", "distributionName": "" }, "WSL-Gnome: UICatalog": { @@ -60,45 +36,29 @@ "commandLineArgs": "bash -c 'while [ ! -e \"$XDG_RUNTIME_DIR/bus\" ]; do sleep 0.1; done; gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll; exec bash\"'", "distributionName": "" }, - "WSL-Gnome: UICatalog --driver NetDriver": { + "WSL-Gnome: UICatalog --driver dotnet": { "commandName": "Executable", "executablePath": "wsl", - "commandLineArgs": "bash -c 'while [ ! -e \"$XDG_RUNTIME_DIR/bus\" ]; do sleep 0.1; done; gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver NetDriver; exec bash\"'", + "commandLineArgs": "bash -c 'while [ ! -e \"$XDG_RUNTIME_DIR/bus\" ]; do sleep 0.1; done; gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver dotnet; exec bash\"'", "distributionName": "" }, - "WSL-Gnome: UICatalog --driver v2": { + "WSL-Gnome: UICatalog --driver unix": { "commandName": "Executable", "executablePath": "wsl", - "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2; exec bash\"'", - "distributionName": "" - }, - "WSL-Gnome: UICatalog --driver v2unix": { - "commandName": "Executable", - "executablePath": "wsl", - "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2unix; exec bash\"'", - "distributionName": "" - }, - "WSL-Gnome: UICatalog --driver v2net": { - "commandName": "Executable", - "executablePath": "wsl", - "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2net; exec bash\"'", + "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver unix; exec bash\"'", "distributionName": "" }, "Benchmark All": { "commandName": "Project", "commandLineArgs": "--benchmark" }, - "Benchmark All --driver NetDriver": { + "Benchmark All --driver dotnet": { "commandName": "Project", - "commandLineArgs": "--driver NetDriver --benchmark" + "commandLineArgs": "--driver dotnet --benchmark" }, - "Benchmark All --driver v2win": { + "Benchmark All --driver windows": { "commandName": "Project", - "commandLineArgs": "--driver v2win --benchmark" - }, - "Benchmark All --driver v2net": { - "commandName": "Project", - "commandLineArgs": "--driver v2net --benchmark" + "commandLineArgs": "--driver windows --benchmark" }, "WSL: Benchmark All": { "commandName": "Executable", @@ -106,22 +66,16 @@ "commandLineArgs": "dotnet UICatalog.dll --benchmark", "distributionName": "" }, - "WSL: Benchmark All --driver v2": { + "WSL: Benchmark All --driver unix": { "commandName": "Executable", "executablePath": "wsl", - "commandLineArgs": "dotnet UICatalog.dll --driver v2 --benchmark", + "commandLineArgs": "dotnet UICatalog.dll --driver unix --benchmark", "distributionName": "" }, - "WSL: Benchmark All --driver v2unix": { + "WSL: Benchmark All --driver dotnet": { "commandName": "Executable", "executablePath": "wsl", - "commandLineArgs": "dotnet UICatalog.dll --driver v2unix --benchmark", - "distributionName": "" - }, - "WSL: Benchmark All --driver v2net": { - "commandName": "Executable", - "executablePath": "wsl", - "commandLineArgs": "dotnet UICatalog.dll --driver v2net --benchmark", + "commandLineArgs": "dotnet UICatalog.dll --driver dotnet --benchmark", "distributionName": "" }, "Docker": { @@ -135,9 +89,9 @@ "commandName": "Project", "commandLineArgs": "--disable-cm\r\n" }, - "UICatalog --disable-cm --driver v2win": { + "UICatalog --disable-cm --driver windows": { "commandName": "Project", - "commandLineArgs": "--disable-cm --driver v2win" + "commandLineArgs": "--disable-cm --driver windows" }, "Themes": { "commandName": "Project", diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index b0a6e5f36..76fc5dc2a 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -189,32 +189,25 @@ public class Scenario : IDisposable } Application.Iteration += OnApplicationOnIteration; - Application.Driver!.ClearedContents += (sender, args) => BenchmarkResults.ClearedContentCount++; - if (Application.Driver is ConsoleDriver cd) - { - cd.Refreshed += (sender, args) => - { - BenchmarkResults.RefreshedCount++; - if (args.Value) - { - BenchmarkResults.UpdatedCount++; - } - }; - - } - Application.NotifyNewRunState += OnApplicationNotifyNewRunState; + Application.Driver!.ClearedContents += OnClearedContents; + Application.SessionBegun += OnApplicationSessionBegun; _stopwatch = Stopwatch.StartNew (); } else { - Application.NotifyNewRunState -= OnApplicationNotifyNewRunState; + Application.Driver!.ClearedContents -= OnClearedContents; + Application.SessionBegun -= OnApplicationSessionBegun; Application.Iteration -= OnApplicationOnIteration; BenchmarkResults.Duration = _stopwatch!.Elapsed; _stopwatch?.Stop (); } + + return; + + void OnClearedContents (object? sender, EventArgs args) => BenchmarkResults.ClearedContentCount++; } private void OnApplicationOnIteration (object? s, IterationEventArgs a) @@ -226,7 +219,7 @@ public class Scenario : IDisposable } } - private void OnApplicationNotifyNewRunState (object? sender, RunStateEventArgs e) + private void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e) { SubscribeAllSubViews (Application.Top!); diff --git a/Examples/UICatalog/Scenarios/Adornments.cs b/Examples/UICatalog/Scenarios/Adornments.cs index 5e0efc628..938d23a53 100644 --- a/Examples/UICatalog/Scenarios/Adornments.cs +++ b/Examples/UICatalog/Scenarios/Adornments.cs @@ -26,7 +26,7 @@ public class Adornments : Scenario X = Pos.AnchorEnd () }; - editor.Border.Thickness = new (1, 2, 1, 1); + editor.Border!.Thickness = new (1, 2, 1, 1); app.Add (editor); @@ -71,7 +71,7 @@ public class Adornments : Scenario Width = 40, Height = 6 // TODO: Use Dim.Auto }; - label.Border.Thickness = new (1, 3, 1, 1); + label.Border!.Thickness = new (1, 3, 1, 1); var btnButtonInWindow = new Button { X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Text = "Button" }; @@ -84,13 +84,13 @@ public class Adornments : Scenario SchemeName = "Dialog" }; - window.Margin.Data = "Margin"; - window.Margin.Text = "Margin Text"; - window.Margin.Thickness = new (0); + window.Margin!.Data = "Margin"; + window.Margin!.Text = "Margin Text"; + window.Margin!.Thickness = new (0); - window.Border.Data = "Border"; - window.Border.Text = "Border Text"; - window.Border.Thickness = new (0); + window.Border!.Data = "Border"; + window.Border!.Text = "Border Text"; + window.Border!.Thickness = new (0); window.Padding.Data = "Padding"; window.Padding.Text = "Padding Text line 1\nPadding Text line 3\nPadding Text line 3\nPadding Text line 4\nPadding Text line 5"; @@ -134,14 +134,14 @@ public class Adornments : Scenario }; btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "Hi", "Button in Padding Pressed!", "Ok"); btnButtonInPadding.BorderStyle = LineStyle.Dashed; - btnButtonInPadding.Border.Thickness = new (1, 1, 1, 1); + btnButtonInPadding.Border!.Thickness = new (1, 1, 1, 1); window.Padding.Add (btnButtonInPadding); #if SUBVIEW_BASED_BORDER - btnButtonInPadding.Border.CloseButton.Visible = true; + btnButtonInPadding.Border!.CloseButton.Visible = true; - view.Border.CloseButton.Visible = true; - view.Border.CloseButton.Accept += (s, e) => + view.Border!.CloseButton.Visible = true; + view.Border!.CloseButton.Accept += (s, e) => { MessageBox.Query (20, 7, "Hi", "Window Close Button Pressed!", "Ok"); e.Handled = true; diff --git a/Examples/UICatalog/Scenarios/AllViewsTester.cs b/Examples/UICatalog/Scenarios/AllViewsTester.cs index f967742bc..c3695121e 100644 --- a/Examples/UICatalog/Scenarios/AllViewsTester.cs +++ b/Examples/UICatalog/Scenarios/AllViewsTester.cs @@ -38,7 +38,7 @@ public class AllViewsTester : Scenario // Set the BorderStyle we use for all subviews, but disable the app border thickness app.Border!.LineStyle = LineStyle.Heavy; - app.Border.Thickness = new (0); + app.Border!.Thickness = new (0); _viewClasses = GetAllViewClassesCollection () @@ -158,16 +158,13 @@ public class AllViewsTester : Scenario _eventLog = new () { - // X = Pos.Right(_layoutEditor), + X = Pos.AnchorEnd () - 1, + Y = 0, + Width = 30, + Height = Dim.Fill (), SuperViewRendersLineCanvas = true }; _eventLog.Border!.Thickness = new (1); - _eventLog.X = Pos.AnchorEnd () - 1; - _eventLog.Y = 0; - - _eventLog.Height = Dim.Height (_classListView); - - //_eventLog.Width = 30; _layoutEditor.Width = Dim.Fill ( Dim.Func ( @@ -194,7 +191,6 @@ public class AllViewsTester : Scenario Height = Dim.Fill (), CanFocus = true, TabStop = TabBehavior.TabStop, - //SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base), Arrangement = ViewArrangement.LeftResizable | ViewArrangement.BottomResizable | ViewArrangement.RightResizable, BorderStyle = LineStyle.Double, SuperViewRendersLineCanvas = true @@ -228,7 +224,7 @@ public class AllViewsTester : Scenario if (type.IsGenericType) { // For each of the arguments - List typeArguments = new (); + List typeArguments = []; // use or the original type if applicable foreach (Type arg in type.GetGenericArguments ()) diff --git a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs index bb3daaa7c..e058ea4bf 100644 --- a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs +++ b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs @@ -40,7 +40,10 @@ public class AnimationScenario : Scenario var lbl2 = new Label { - X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Text = "https://commons.wikimedia.org/wiki/File:Spinning_globe.gif" + // This ensures the URL that has an underscore is drawn correctly + HotKeySpecifier = new Rune ('\xFFFF'), + X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), + Text = "https://commons.wikimedia.org/wiki/File:Spinning_globe.gif" }; win.Add (lbl2); diff --git a/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs b/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs index 9a68b2567..ad4f881ba 100644 --- a/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -112,7 +112,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario break; case "CSI_ReportTerminalSizeInChars": - selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportTerminalSizeInChars; + selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportWindowSizeInChars; break; case "CSI_RequestCursorPositionReport": diff --git a/Examples/UICatalog/Scenarios/Arrangement.cs b/Examples/UICatalog/Scenarios/Arrangement.cs index 867e36f1d..7b261db6b 100644 --- a/Examples/UICatalog/Scenarios/Arrangement.cs +++ b/Examples/UICatalog/Scenarios/Arrangement.cs @@ -99,7 +99,6 @@ public class Arrangement : Scenario progressBar.Fraction += 0.01f; - Application.Wakeup (); progressBar.SetNeedsDraw (); }; diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 226e18e26..50153f07d 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -40,7 +40,7 @@ public class Bars : Scenario SchemeName = "Toplevel", Source = new ListWrapper (eventSource) }; - eventLog.Border.Thickness = new (0, 1, 0, 0); + eventLog.Border!.Thickness = new (0, 1, 0, 0); Application.Top.Add (eventLog); FrameView menuBarLikeExamples = new () @@ -185,9 +185,9 @@ public class Bars : Scenario menuLikeExamples.Add (popOverMenu); - menuLikeExamples.MouseClick += MenuLikeExamplesMouseClick; + menuLikeExamples.MouseEvent += MenuLikeExamplesMouseEvent; - void MenuLikeExamplesMouseClick (object sender, MouseEventArgs e) + void MenuLikeExamplesMouseEvent (object _, MouseEventArgs e) { if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) { diff --git a/Examples/UICatalog/Scenarios/Buttons.cs b/Examples/UICatalog/Scenarios/Buttons.cs index b4c921437..f2ea4572f 100644 --- a/Examples/UICatalog/Scenarios/Buttons.cs +++ b/Examples/UICatalog/Scenarios/Buttons.cs @@ -243,17 +243,17 @@ public class Buttons : Scenario }; main.Add (label); - var radioGroup = new RadioGroup + OptionSelector osAlignment = new () { X = 4, Y = Pos.Bottom (label) + 1, - SelectedItem = 2, - RadioLabels = new [] { "_Start", "_End", "_Center", "_Fill" }, - Title = "_9 RadioGroup", + Value = Alignment.Center, + AssignHotKeys = true, + Title = "_9 OptionSelector", BorderStyle = LineStyle.Dotted, // CanFocus = false }; - main.Add (radioGroup); + main.Add (osAlignment); // Demo changing hotkey string MoveHotkey (string txt) @@ -292,7 +292,7 @@ public class Buttons : Scenario var moveHotKeyBtn = new Button { X = 2, - Y = Pos.Bottom (radioGroup) + 1, + Y = Pos.Bottom (osAlignment) + 1, Width = Dim.Width (computedFrame) - 2, SchemeName = "TopLevel", Text = mhkb @@ -309,7 +309,7 @@ public class Buttons : Scenario var moveUnicodeHotKeyBtn = new Button { X = Pos.Left (absoluteFrame) + 1, - Y = Pos.Bottom (radioGroup) + 1, + Y = Pos.Bottom (osAlignment) + 1, Width = Dim.Width (absoluteFrame) - 2, SchemeName = "TopLevel", Text = muhkb @@ -321,48 +321,21 @@ public class Buttons : Scenario }; main.Add (moveUnicodeHotKeyBtn); - radioGroup.SelectedItemChanged += (s, args) => - { - switch (args.SelectedItem) - { - case 0: - moveBtn.TextAlignment = Alignment.Start; - sizeBtn.TextAlignment = Alignment.Start; - moveBtnA.TextAlignment = Alignment.Start; - sizeBtnA.TextAlignment = Alignment.Start; - moveHotKeyBtn.TextAlignment = Alignment.Start; - moveUnicodeHotKeyBtn.TextAlignment = Alignment.Start; + osAlignment.ValueChanged += (s, args) => + { + if (args.Value is null) + { + return; + } - break; - case 1: - moveBtn.TextAlignment = Alignment.End; - sizeBtn.TextAlignment = Alignment.End; - moveBtnA.TextAlignment = Alignment.End; - sizeBtnA.TextAlignment = Alignment.End; - moveHotKeyBtn.TextAlignment = Alignment.End; - moveUnicodeHotKeyBtn.TextAlignment = Alignment.End; - - break; - case 2: - moveBtn.TextAlignment = Alignment.Center; - sizeBtn.TextAlignment = Alignment.Center; - moveBtnA.TextAlignment = Alignment.Center; - sizeBtnA.TextAlignment = Alignment.Center; - moveHotKeyBtn.TextAlignment = Alignment.Center; - moveUnicodeHotKeyBtn.TextAlignment = Alignment.Center; - - break; - case 3: - moveBtn.TextAlignment = Alignment.Fill; - sizeBtn.TextAlignment = Alignment.Fill; - moveBtnA.TextAlignment = Alignment.Fill; - sizeBtnA.TextAlignment = Alignment.Fill; - moveHotKeyBtn.TextAlignment = Alignment.Fill; - moveUnicodeHotKeyBtn.TextAlignment = Alignment.Fill; - - break; - } - }; + Alignment newValue = args.Value.Value; + moveBtn.TextAlignment = newValue; + sizeBtn.TextAlignment = newValue; + moveBtnA.TextAlignment = newValue; + sizeBtnA.TextAlignment = newValue; + moveHotKeyBtn.TextAlignment = newValue; + moveUnicodeHotKeyBtn.TextAlignment = newValue; + }; label = new () { diff --git a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs index 26eb08072..3028eb4ee 100644 --- a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs +++ b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -85,7 +85,7 @@ public class CharacterMap : Scenario X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 17, - Caption = "e.g. 01BE3 or ✈" + Title = "e.g. 01BE3 or ✈" //SchemeName = "Dialog" }; @@ -365,40 +365,16 @@ public class CharacterMap : Scenario options [0] = "All"; Array.Copy (allCategoryNames, 0, options, 1, allCategoryNames.Length); - // TODO: When #4126 is merged update this to use OptionSelector - var selector = new OptionSelector - { - AssignHotKeysToCheckBoxes = true, - Options = options - }; + // TODO: Add a "None" option + OptionSelector selector = new (); _unicodeCategorySelector = selector; - // Default to "All" - selector.SelectedItem = 0; + selector.Value = null; _charMap!.ShowUnicodeCategory = null; - selector.SelectedItemChanged += (s, e) => - { - int? idx = selector.SelectedItem; + selector.ValueChanged += (_, e) => _charMap.ShowUnicodeCategory = e.Value; - if (idx is null) - { - return; - } - - if (idx.Value == 0) - { - _charMap.ShowUnicodeCategory = null; - } - else - { - // Map index to UnicodeCategory (offset by 1 because 0 is "All") - UnicodeCategory cat = Enum.GetValues () [idx.Value - 1]; - _charMap.ShowUnicodeCategory = cat; - } - }; - - return new() { CommandView = selector }; + return new () { CommandView = selector }; } } diff --git a/Examples/UICatalog/Scenarios/Clipping.cs b/Examples/UICatalog/Scenarios/Clipping.cs index 986a466f2..b9bb47528 100644 --- a/Examples/UICatalog/Scenarios/Clipping.cs +++ b/Examples/UICatalog/Scenarios/Clipping.cs @@ -100,7 +100,6 @@ public class Clipping : Scenario { tiledProgressBar1.Pulse (); tiledProgressBar2.Pulse (); - Application.Wakeup (); }; progressTimer.Start (); @@ -152,7 +151,7 @@ public class Clipping : Scenario //tiled.Padding.Thickness = new (1); //tiled.Padding.Diagnostics = ViewDiagnosticFlags.Thickness; - //tiled.Margin.Thickness = new (1); + //tiled.Margin!.Thickness = new (1); FrameView fv = new () { diff --git a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs index d1dec203c..2c2dc89ed 100644 --- a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -129,7 +129,7 @@ public class CollectionNavigatorTester : Scenario _items = new (_items.OrderBy (i => i, StringComparer.OrdinalIgnoreCase)); CreateListView (); - var vsep = new LineView (Orientation.Vertical) { X = Pos.Right (_listView), Y = 1, Height = Dim.Fill () }; + var vsep = new Line { Orientation = Orientation.Vertical, X = Pos.Right (_listView), Y = 1, Height = Dim.Fill () }; top.Add (vsep); CreateTreeView (); diff --git a/Examples/UICatalog/Scenarios/ColorPicker.cs b/Examples/UICatalog/Scenarios/ColorPicker.cs index 568e6824e..61b71d093 100644 --- a/Examples/UICatalog/Scenarios/ColorPicker.cs +++ b/Examples/UICatalog/Scenarios/ColorPicker.cs @@ -125,26 +125,25 @@ public class ColorPickers : Scenario app.Add (_demoView); - // Radio for switching color models - var rgColorModel = new RadioGroup () + var osColorModel = new OptionSelector () { Y = Pos.Bottom (_demoView), Width = Dim.Auto (), Height = Dim.Auto (), - RadioLabels = new [] - { + Labels = + [ "_RGB", "_HSV", "H_SL", "_16 Colors" - }, - SelectedItem = (int)foregroundColorPicker.Style.ColorModel, + ], + Value = (int)foregroundColorPicker.Style.ColorModel, }; - rgColorModel.SelectedItemChanged += (_, e) => + osColorModel.ValueChanged += (_, e) => { // 16 colors - if (e.SelectedItem == 3) + if (e.Value == 3) { foregroundColorPicker16.Visible = true; @@ -161,12 +160,17 @@ public class ColorPickers : Scenario { foregroundColorPicker16.Visible = false; foregroundColorPicker.Visible = true; - foregroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem; - foregroundColorPicker.ApplyStyleChanges (); - backgroundColorPicker16.Visible = false; - backgroundColorPicker.Visible = true; - backgroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem; + if (e.Value is { }) + { + foregroundColorPicker.Style.ColorModel = (ColorModel)e.Value; + foregroundColorPicker.ApplyStyleChanges (); + + backgroundColorPicker16.Visible = false; + backgroundColorPicker.Visible = true; + backgroundColorPicker.Style.ColorModel = (ColorModel)e.Value; + } + backgroundColorPicker.ApplyStyleChanges (); @@ -176,13 +180,13 @@ public class ColorPickers : Scenario } }; - app.Add (rgColorModel); + app.Add (osColorModel); // Checkbox for switching show text fields on and off var cbShowTextFields = new CheckBox () { Text = "Show _Text Fields", - Y = Pos.Bottom (rgColorModel) + 1, + Y = Pos.Bottom (osColorModel) + 1, Width = Dim.Auto (), Height = Dim.Auto (), CheckedState = foregroundColorPicker.Style.ShowTextFields ? CheckState.Checked : CheckState.UnChecked, diff --git a/Examples/UICatalog/Scenarios/CombiningMarks.cs b/Examples/UICatalog/Scenarios/CombiningMarks.cs index 6e0467ce0..7d8437a23 100644 --- a/Examples/UICatalog/Scenarios/CombiningMarks.cs +++ b/Examples/UICatalog/Scenarios/CombiningMarks.cs @@ -13,7 +13,7 @@ public class CombiningMarks : Scenario top.DrawComplete += (s, e) => { // Forces reset _lineColsOffset because we're dealing with direct draw - Application.ClearScreenNextIteration = true; + Application.Top!.SetNeedsDraw (); var i = -1; top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); diff --git a/Examples/UICatalog/Scenarios/Dialogs.cs b/Examples/UICatalog/Scenarios/Dialogs.cs index 949b45d4a..e7fd1ac77 100644 --- a/Examples/UICatalog/Scenarios/Dialogs.cs +++ b/Examples/UICatalog/Scenarios/Dialogs.cs @@ -151,18 +151,15 @@ public class Dialogs : Scenario }; frame.Add (label); - // Add hotkeys - var labels = Enum.GetNames ().Select (n => n = "_" + n); - var alignmentGroup = new RadioGroup + OptionSelector alignmentOptionSelector = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), - RadioLabels = labels.ToArray (), Title = "Ali_gn", - BorderStyle = LineStyle.Dashed + AssignHotKeys = true }; - frame.Add (alignmentGroup); - alignmentGroup.SelectedItem = labels.ToList ().IndexOf ("_" + Dialog.DefaultButtonAlignment.ToString ()); + frame.Add (alignmentOptionSelector); + alignmentOptionSelector.Value = Dialog.DefaultButtonAlignment; frame.ValidatePosDim = true; @@ -192,7 +189,7 @@ public class Dialogs : Scenario titleEdit, numButtonsEdit, glyphsNotWords, - alignmentGroup, + alignmentOptionSelector, buttonPressedLabel ); Application.Run (dlg); @@ -216,7 +213,7 @@ public class Dialogs : Scenario TextField titleEdit, TextField numButtonsEdit, CheckBox glyphsNotWords, - RadioGroup alignmentRadioGroup, + OptionSelector alignmentGroup, Label buttonPressedLabel ) { @@ -269,7 +266,7 @@ public class Dialogs : Scenario { Title = titleEdit.Text, Text = "Dialog Text", - ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentRadioGroup.RadioLabels [alignmentRadioGroup.SelectedItem].Substring (1)), + ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentGroup.Labels! [(int)alignmentGroup.Value!.Value] [1..]), Buttons = buttons.ToArray () }; diff --git a/Examples/UICatalog/Scenarios/DimAutoDemo.cs b/Examples/UICatalog/Scenarios/DimAutoDemo.cs index 7c99647e4..ab97889d0 100644 --- a/Examples/UICatalog/Scenarios/DimAutoDemo.cs +++ b/Examples/UICatalog/Scenarios/DimAutoDemo.cs @@ -56,7 +56,7 @@ public class DimAutoDemo : Scenario Width = Dim.Auto (DimAutoStyle.Content, minimumContentDim: Dim.Percent (25)), Height = Dim.Auto (DimAutoStyle.Content, minimumContentDim: 10) }; - dimAutoFrameView.Margin.Thickness = new Thickness (1); + dimAutoFrameView.Margin!.Thickness = new Thickness (1); dimAutoFrameView.ValidatePosDim = true; var textEdit = new TextView @@ -163,15 +163,15 @@ public class DimAutoDemo : Scenario dimAutoFrameView.Add (resetButton); - var radioGroup = new RadioGroup () + var optionSelector = new OptionSelector () { - RadioLabels = ["One", "Two", "Three"], + Labels = ["One", "Two", "Three"], X = 0, Y = Pos.AnchorEnd (), - Title = "Radios", + Title = "Options", BorderStyle = LineStyle.Dotted }; - dimAutoFrameView.Add (radioGroup); + dimAutoFrameView.Add (optionSelector); return dimAutoFrameView; } diff --git a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs b/Examples/UICatalog/Scenarios/DynamicMenuBar.cs index b800cc3ef..2687b4f6c 100644 --- a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicMenuBar.cs @@ -174,11 +174,11 @@ public class DynamicMenuBar : Scenario var rChkLabels = new [] { "NoCheck", "Checked", "Radio" }; - RbChkStyle = new () + OsChkStyle = new () { - X = Pos.Left (lblTitle), Y = Pos.Bottom (CkbSubMenu) + 1, RadioLabels = rChkLabels + X = Pos.Left (lblTitle), Y = Pos.Bottom (CkbSubMenu) + 1, Labels = rChkLabels }; - Add (RbChkStyle); + Add (OsChkStyle); var lblShortcut = new Label { @@ -294,7 +294,7 @@ public class DynamicMenuBar : Scenario public CheckBox CkbIsTopLevel { get; } public CheckBox CkbNullCheck { get; } public CheckBox CkbSubMenu { get; } - public RadioGroup RbChkStyle { get; } + public OptionSelector OsChkStyle { get; } public TextView TextAction { get; } public TextField TextHelp { get; } public TextField TextHotKey { get; } @@ -361,7 +361,7 @@ public class DynamicMenuBar : Scenario CkbNullCheck.CheckedState = menuItem.AllowNullChecked ? CheckState.Checked : CheckState.UnChecked; TextHelp.Enabled = CkbSubMenu.CheckedState == CheckState.UnChecked; TextAction.Enabled = CkbSubMenu.CheckedState == CheckState.UnChecked; - RbChkStyle.SelectedItem = (int)(menuItem?.CheckType ?? MenuItemCheckStyle.NoCheck); + OsChkStyle.Value = (int)(menuItem?.CheckType ?? MenuItemCheckStyle.NoCheck); TextShortcutKey.Text = menuItem?.ShortcutTag ?? ""; TextShortcutKey.Enabled = CkbIsTopLevel.CheckedState == CheckState.Checked && CkbSubMenu.CheckedState == CheckState.UnChecked @@ -434,8 +434,8 @@ public class DynamicMenuBar : Scenario HotKey = TextHotKey.Text, IsTopLevel = CkbIsTopLevel?.CheckedState == CheckState.Checked, HasSubMenu = CkbSubMenu?.CheckedState == CheckState.Checked, - CheckStyle = RbChkStyle.SelectedItem == 0 ? MenuItemCheckStyle.NoCheck : - RbChkStyle.SelectedItem == 1 ? MenuItemCheckStyle.Checked : + CheckStyle = OsChkStyle.Value == 0 ? MenuItemCheckStyle.NoCheck : + OsChkStyle.Value == 1 ? MenuItemCheckStyle.Checked : MenuItemCheckStyle.Radio, ShortcutKey = TextShortcutKey.Text, AllowNullChecked = CkbNullCheck?.CheckedState == CheckState.Checked, @@ -484,7 +484,7 @@ public class DynamicMenuBar : Scenario TextHotKey.Text = ""; CkbIsTopLevel.CheckedState = CheckState.UnChecked; CkbSubMenu.CheckedState = CheckState.UnChecked; - RbChkStyle.SelectedItem = (int)MenuItemCheckStyle.NoCheck; + OsChkStyle.Value = (int)MenuItemCheckStyle.NoCheck; TextShortcutKey.Text = ""; } @@ -829,9 +829,9 @@ public class DynamicMenuBar : Scenario HotKey = frmMenuDetails.TextHotKey.Text, IsTopLevel = frmMenuDetails.CkbIsTopLevel?.CheckedState == CheckState.Checked, HasSubMenu = frmMenuDetails.CkbSubMenu?.CheckedState == CheckState.Checked, - CheckStyle = frmMenuDetails.RbChkStyle.SelectedItem == 0 + CheckStyle = frmMenuDetails.OsChkStyle.Value == 0 ? MenuItemCheckStyle.NoCheck - : frmMenuDetails.RbChkStyle.SelectedItem == 1 + : frmMenuDetails.OsChkStyle.Value == 1 ? MenuItemCheckStyle.Checked : MenuItemCheckStyle.Radio, ShortcutKey = frmMenuDetails.TextShortcutKey.Text diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs index 74d7acee8..30c3ffda4 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs @@ -99,7 +99,7 @@ public class AdornmentsEditor : EditorBase SuperViewRendersLineCanvas = true, BorderStyle = LineStyle.Single }; - MarginEditor.Border!.Thickness = MarginEditor.Border.Thickness with { Bottom = 0 }; + MarginEditor.Border!.Thickness = MarginEditor.Border!.Thickness with { Bottom = 0 }; Add (MarginEditor); BorderEditor = new () @@ -109,7 +109,7 @@ public class AdornmentsEditor : EditorBase SuperViewRendersLineCanvas = true, BorderStyle = LineStyle.Single }; - BorderEditor.Border!.Thickness = BorderEditor.Border.Thickness with { Bottom = 0 }; + BorderEditor.Border!.Thickness = BorderEditor.Border!.Thickness with { Bottom = 0 }; Add (BorderEditor); PaddingEditor = new () @@ -119,7 +119,7 @@ public class AdornmentsEditor : EditorBase SuperViewRendersLineCanvas = true, BorderStyle = LineStyle.Single }; - PaddingEditor.Border!.Thickness = PaddingEditor.Border.Thickness with { Bottom = 0 }; + PaddingEditor.Border!.Thickness = PaddingEditor.Border!.Thickness with { Bottom = 0 }; Add (PaddingEditor); Width = Dim.Auto (maximumContentDim: Dim.Func (_ => MarginEditor.Frame.Width - 2)); diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs index 6d262ef2e..334450fbb 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs @@ -1,7 +1,4 @@ ο»Ώ#nullable enable -using System; -using System.Collections.Generic; - namespace UICatalog.Scenarios; /// @@ -16,92 +13,34 @@ public sealed class ArrangementEditor : EditorBase Initialized += ArrangementEditor_Initialized; - _arrangementSlider.Options = - [ - new SliderOption - { - Legend = ViewArrangement.Movable.ToString (), - Data = ViewArrangement.Movable - }, - - new SliderOption - { - Legend = ViewArrangement.LeftResizable.ToString (), - Data = ViewArrangement.LeftResizable - }, - - new SliderOption - { - Legend = ViewArrangement.RightResizable.ToString (), - Data = ViewArrangement.RightResizable - }, - - new SliderOption - { - Legend = ViewArrangement.TopResizable.ToString (), - Data = ViewArrangement.TopResizable - }, - - new SliderOption - { - Legend = ViewArrangement.BottomResizable.ToString (), - Data = ViewArrangement.BottomResizable - }, - - new SliderOption - { - Legend = ViewArrangement.Overlapped.ToString (), - Data = ViewArrangement.Overlapped - } - ]; - - Add (_arrangementSlider); + Add (_arrangementSelector); } - private readonly Slider _arrangementSlider = new() + private readonly FlagSelector _arrangementSelector = new () { - Orientation = Orientation.Vertical, - UseMinimumSize = true, - Type = SliderType.Multiple, - AllowEmpty = true, + Orientation = Orientation.Vertical }; protected override void OnViewToEditChanged () { - _arrangementSlider.Enabled = ViewToEdit is not Adornment; + _arrangementSelector.Enabled = ViewToEdit is not Adornment; - _arrangementSlider.OptionsChanged -= ArrangementSliderOnOptionsChanged; + _arrangementSelector.ValueChanged -= ArrangementFlagsOnValueChanged; // Set the appropriate options in the slider based on _viewToEdit.Arrangement if (ViewToEdit is { }) { - _arrangementSlider.Options.ForEach ( - option => - { - _arrangementSlider.ChangeOption ( - _arrangementSlider.Options.IndexOf (option), - (ViewToEdit.Arrangement & option.Data) == option.Data); - }); + _arrangementSelector.Value = ViewToEdit.Arrangement; } - _arrangementSlider.OptionsChanged += ArrangementSliderOnOptionsChanged; + _arrangementSelector.ValueChanged += ArrangementFlagsOnValueChanged; } - private void ArrangementEditor_Initialized (object? sender, EventArgs e) { _arrangementSlider.OptionsChanged += ArrangementSliderOnOptionsChanged; } - - private void ArrangementSliderOnOptionsChanged (object? sender, SliderEventArgs e) + private void ArrangementFlagsOnValueChanged (object? sender, EventArgs e) { - if (ViewToEdit is { }) + if (ViewToEdit is { } && e.Value is { }) { - // Set the arrangement based on the selected options - var arrangement = ViewArrangement.Fixed; - - foreach (KeyValuePair> option in e.Options) - { - arrangement |= option.Value.Data; - } - - ViewToEdit.Arrangement = arrangement; + ViewToEdit.Arrangement = (ViewArrangement)e.Value; if (ViewToEdit.Arrangement.HasFlag (ViewArrangement.Overlapped)) { @@ -114,14 +53,9 @@ public sealed class ArrangementEditor : EditorBase ViewToEdit.SchemeName = ViewToEdit!.SuperView!.SchemeName; } - if (ViewToEdit.Arrangement.HasFlag (ViewArrangement.Movable)) - { - ViewToEdit.BorderStyle = LineStyle.Double; - } - else - { - ViewToEdit.BorderStyle = LineStyle.Single; - } + ViewToEdit.BorderStyle = ViewToEdit.Arrangement.HasFlag (ViewArrangement.Movable) ? LineStyle.Double : LineStyle.Single; } } + + private void ArrangementEditor_Initialized (object? sender, EventArgs e) { _arrangementSelector.ValueChanged += ArrangementFlagsOnValueChanged; } } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs index e5c6d0c18..ca962eb30 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs @@ -8,7 +8,7 @@ namespace UICatalog.Scenarios; public class BorderEditor : AdornmentEditor { private CheckBox? _ckbTitle; - private RadioGroup? _rbBorderStyle; + private OptionSelector? _osBorderStyle; private CheckBox? _ckbGradient; public BorderEditor () @@ -21,34 +21,31 @@ public class BorderEditor : AdornmentEditor private void BorderEditor_AdornmentChanged (object? sender, EventArgs e) { _ckbTitle!.CheckedState = ((Border)AdornmentToEdit!).Settings.FastHasFlags (BorderSettings.Title) ? CheckState.Checked : CheckState.UnChecked; - _rbBorderStyle!.SelectedItem = (int)((Border)AdornmentToEdit).LineStyle; + _osBorderStyle!.Value = ((Border)AdornmentToEdit).LineStyle; _ckbGradient!.CheckedState = ((Border)AdornmentToEdit).Settings.FastHasFlags (BorderSettings.Gradient) ? CheckState.Checked : CheckState.UnChecked; } private void BorderEditor_Initialized (object? sender, EventArgs e) { - List borderStyleEnum = Enum.GetValues (typeof (LineStyle)).Cast ().ToList (); - - _rbBorderStyle = new () + _osBorderStyle = new () { X = 0, - Y = Pos.Bottom (SubViews.ToArray() [^1]), + Y = Pos.Bottom (SubViews.ToArray () [^1]), Width = Dim.Fill (), - SelectedItem = (int)(((Border)AdornmentToEdit!)?.LineStyle ?? LineStyle.None), + Value = ((Border)AdornmentToEdit!)?.LineStyle ?? LineStyle.None, BorderStyle = LineStyle.Single, Title = "Border St_yle", SuperViewRendersLineCanvas = true, - RadioLabels = borderStyleEnum.Select (style => style.ToString ()).ToArray () }; - Add (_rbBorderStyle); + Add (_osBorderStyle); - _rbBorderStyle.SelectedItemChanged += OnRbBorderStyleOnSelectedItemChanged; + _osBorderStyle.ValueChanged += OnRbBorderStyleOnValueChanged; _ckbTitle = new () { X = 0, - Y = Pos.Bottom (_rbBorderStyle), + Y = Pos.Bottom (_osBorderStyle), CheckedState = CheckState.Checked, SuperViewRendersLineCanvas = true, @@ -73,10 +70,14 @@ public class BorderEditor : AdornmentEditor return; - void OnRbBorderStyleOnSelectedItemChanged (object? s, SelectedItemChangedArgs args) + void OnRbBorderStyleOnValueChanged (object? s, EventArgs args) { LineStyle prevBorderStyle = AdornmentToEdit!.BorderStyle; - ((Border)AdornmentToEdit).LineStyle = (LineStyle)args.SelectedItem!; + + if (args.Value is { }) + { + ((Border)AdornmentToEdit).LineStyle = (LineStyle)args.Value; + } if (((Border)AdornmentToEdit).LineStyle == LineStyle.None) { diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs index 37007997d..ff372ecc7 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs @@ -18,7 +18,7 @@ public class DimEditor : EditorBase } private int _value; - private RadioGroup? _dimRadioGroup; + private OptionSelector? _dimOptionSelector; private TextField? _valueEdit; /// @@ -44,7 +44,7 @@ public class DimEditor : EditorBase try { - _dimRadioGroup!.SelectedItem = _dimNames.IndexOf (_dimNames.First (s => dim!.ToString ().StartsWith (s))); + _dimOptionSelector!.Value = _dimNames.IndexOf (_dimNames.First (s => dim!.ToString ().StartsWith (s))); } catch (InvalidOperationException e) { @@ -92,13 +92,13 @@ public class DimEditor : EditorBase Text = $"{Title}:" }; Add (label); - _dimRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = _radioItems }; - _dimRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged; + _dimOptionSelector = new () { X = 0, Y = Pos.Bottom (label), Labels = _optionLabels }; + _dimOptionSelector.ValueChanged += OnOptionSelectorOnValueChanged; _valueEdit = new () { X = Pos.Right (label) + 1, Y = 0, - Width = Dim.Func (_ => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1), + Width = Dim.Func (_ => _optionLabels.Max (i => i.GetColumns ()) - label.Frame.Width + 1), Text = $"{_value}" }; @@ -117,15 +117,15 @@ public class DimEditor : EditorBase }; Add (_valueEdit); - Add (_dimRadioGroup); + Add (_dimOptionSelector); } - private void OnRadioGroupOnSelectedItemChanged (object? s, SelectedItemChangedArgs selected) { DimChanged (); } + private void OnOptionSelectorOnValueChanged (object? s, EventArgs selected) { DimChanged (); } // These need to have same order private readonly List _dimNames = ["Absolute", "Auto", "Fill", "Func", "Percent",]; - private readonly string [] _radioItems = ["Absolute(n)", "Auto", "Fill(n)", "Func(()=>n)", "Percent(n)",]; + private readonly string [] _optionLabels = ["Absolute(n)", "Auto", "Fill(n)", "Func(()=>n)", "Percent(n)",]; private void DimChanged () { @@ -136,7 +136,7 @@ public class DimEditor : EditorBase try { - Dim? dim = _dimRadioGroup!.SelectedItem switch + Dim? dim = _dimOptionSelector!.Value switch { 0 => Dim.Absolute (_value), 1 => Dim.Auto (), diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs index 9bcf2d213..7d5d0f254 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs @@ -12,7 +12,7 @@ public class MarginEditor : AdornmentEditor AdornmentChanged += MarginEditor_AdornmentChanged; } - private RadioGroup? _rgShadow; + private OptionSelector? _optionsShadow; private FlagSelector? _flagSelectorTransparent; @@ -20,18 +20,18 @@ public class MarginEditor : AdornmentEditor { if (AdornmentToEdit is { }) { - _rgShadow!.SelectedItem = (int)((Margin)AdornmentToEdit).ShadowStyle; + _optionsShadow!.Value = ((Margin)AdornmentToEdit).ShadowStyle; } if (AdornmentToEdit is { }) { - _flagSelectorTransparent!.Value = (uint)((Margin)AdornmentToEdit).ViewportSettings; + _flagSelectorTransparent!.Value = (int)((Margin)AdornmentToEdit).ViewportSettings; } } private void MarginEditor_Initialized (object? sender, EventArgs e) { - _rgShadow = new RadioGroup + _optionsShadow = new () { X = 0, Y = Pos.Bottom (SubViews.ElementAt(SubViews.Count-1)), @@ -39,44 +39,36 @@ public class MarginEditor : AdornmentEditor SuperViewRendersLineCanvas = true, Title = "_Shadow", BorderStyle = LineStyle.Single, - RadioLabels = Enum.GetNames (typeof (ShadowStyle)), + AssignHotKeys = true }; if (AdornmentToEdit is { }) { - _rgShadow.SelectedItem = (int)((Margin)AdornmentToEdit).ShadowStyle; + _optionsShadow.Value = ((Margin)AdornmentToEdit).ShadowStyle; } - _rgShadow.SelectedItemChanged += (_, args) => - { - ((Margin)AdornmentToEdit!).ShadowStyle = (ShadowStyle)args.SelectedItem!; - }; + _optionsShadow.ValueChanged += (_, args) => ((Margin)AdornmentToEdit!).ShadowStyle = args.Value!.Value; - Add (_rgShadow); + Add (_optionsShadow); - var flags = new Dictionary () - { - { (uint)Terminal.Gui.ViewBase.ViewportSettingsFlags.Transparent, "Transparent" }, - { (uint)Terminal.Gui.ViewBase.ViewportSettingsFlags.TransparentMouse, "TransparentMouse" } - }; - - _flagSelectorTransparent = new FlagSelector () + _flagSelectorTransparent = new FlagSelector () { X = 0, - Y = Pos.Bottom (_rgShadow), + Y = Pos.Bottom (_optionsShadow), SuperViewRendersLineCanvas = true, Title = "_ViewportSettings", BorderStyle = LineStyle.Single, }; - _flagSelectorTransparent.SetFlags(flags.AsReadOnly ()); - + _flagSelectorTransparent.Values = [(int)ViewportSettingsFlags.Transparent, (int)ViewportSettingsFlags.TransparentMouse]; + _flagSelectorTransparent.Labels = ["Transparent", "TransparentMouse"]; + _flagSelectorTransparent.AssignHotKeys = true; Add (_flagSelectorTransparent); if (AdornmentToEdit is { }) { - _flagSelectorTransparent.Value = (uint)((Margin)AdornmentToEdit).ViewportSettings; + _flagSelectorTransparent.Value = (int)((Margin)AdornmentToEdit).ViewportSettings; } _flagSelectorTransparent.ValueChanged += (_, args) => diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs index 012b2409e..45f0ab950 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs @@ -19,7 +19,7 @@ public class PosEditor : EditorBase } private int _value; - private RadioGroup? _posRadioGroup; + private OptionSelector? _posOptionSelector; private TextField? _valueEdit; protected override void OnUpdateLayoutSettings () @@ -44,7 +44,7 @@ public class PosEditor : EditorBase try { - _posRadioGroup!.SelectedItem = _posNames.IndexOf (_posNames.First (s => pos.ToString ().Contains (s))); + _posOptionSelector!.Value = _posNames.IndexOf (_posNames.First (s => pos.ToString ().Contains (s))); } catch (InvalidOperationException e) { @@ -91,14 +91,14 @@ public class PosEditor : EditorBase Text = $"{Title}:" }; Add (label); - _posRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = _radioItems }; - _posRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged; + _posOptionSelector = new () { X = 0, Y = Pos.Bottom (label), Labels = _optionLabels }; + _posOptionSelector.ValueChanged += OnOptionSelectorOnValueChanged; _valueEdit = new () { X = Pos.Right (label) + 1, Y = 0, - Width = Dim.Func (_ => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1), + Width = Dim.Func (_ => _optionLabels.Max (i => i.GetColumns ()) - label.Frame.Width + 1), Text = $"{_value}" }; @@ -118,14 +118,14 @@ public class PosEditor : EditorBase }; Add (_valueEdit); - Add (_posRadioGroup); + Add (_posOptionSelector); } - private void OnRadioGroupOnSelectedItemChanged (object? s, SelectedItemChangedArgs selected) { PosChanged (); } + private void OnOptionSelectorOnValueChanged (object? s, EventArgs selected) { PosChanged (); } // These need to have same order private readonly List _posNames = ["Absolute", "Align", "AnchorEnd", "Center", "Func", "Percent"]; - private readonly string [] _radioItems = ["Absolute(n)", "Align", "AnchorEnd", "Center", "Func(()=>n)", "Percent(n)"]; + private readonly string [] _optionLabels = ["Absolute(n)", "Align", "AnchorEnd", "Center", "Func(()=>n)", "Percent(n)"]; private void PosChanged () { @@ -136,7 +136,7 @@ public class PosEditor : EditorBase try { - Pos? pos = _posRadioGroup!.SelectedItem switch + Pos? pos = _posOptionSelector!.Value switch { 0 => Pos.Absolute (_value), 1 => Pos.Align (Alignment.Start), diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs index f8e77afaa..cfca9f433 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs @@ -6,7 +6,7 @@ public class ViewPropertiesEditor : EditorBase { private CheckBox? _canFocusCheckBox; private CheckBox? _enabledCheckBox; - private RadioGroup? _orientation; + private OptionSelector? _orientationOptionSelector; private TextView? _text; /// @@ -48,24 +48,23 @@ public class ViewPropertiesEditor : EditorBase Label label = new () { X = Pos.Right (_enabledCheckBox) + 1, Y = Pos.Top (_enabledCheckBox), Text = "Orientation:" }; - _orientation = new () + _orientationOptionSelector = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), - RadioLabels = new [] { "Horizontal", "Vertical" }, Orientation = Orientation.Horizontal }; - _orientation.SelectedItemChanged += (s, selected) => + _orientationOptionSelector.ValueChanged += (s, selected) => { if (ViewToEdit is IOrientation orientatedView) { - orientatedView.Orientation = (Orientation)_orientation.SelectedItem!; + orientatedView.Orientation = _orientationOptionSelector.Value!.Value; } }; - Add (label, _orientation); + Add (label, _orientationOptionSelector); - label = new () { X = 0, Y = Pos.Bottom (_orientation), Text = "Text:" }; + label = new () { X = 0, Y = Pos.Bottom (_orientationOptionSelector), Text = "Text:" }; _text = new () { @@ -114,12 +113,12 @@ public class ViewPropertiesEditor : EditorBase if (ViewToEdit is IOrientation orientatedView) { - _orientation!.SelectedItem = (int)orientatedView.Orientation; - _orientation.Enabled = true; + _orientationOptionSelector!.Value = orientatedView.Orientation; + _orientationOptionSelector.Enabled = true; } else { - _orientation!.Enabled = false; + _orientationOptionSelector!.Enabled = false; } } } diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index 10dcf8c35..290e4a432 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -1,8 +1,4 @@ -ο»Ώusing System; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Text; +ο»Ώusing System.IO.Abstractions; namespace UICatalog.Scenarios; @@ -20,10 +16,10 @@ public class FileDialogExamples : Scenario private CheckBox _cbMustExist; private CheckBox _cbShowTreeBranchLines; private CheckBox _cbUseColors; - private RadioGroup _rgAllowedTypes; - private RadioGroup _rgCaption; - private RadioGroup _rgIcons; - private RadioGroup _rgOpenMode; + private OptionSelector _osAllowedTypes; + private OptionSelector _osCaption; + private OptionSelector _osIcons; + private OptionSelector _osOpenMode; private TextField _tbCancelButton; private TextField _tbOkButton; @@ -34,65 +30,66 @@ public class FileDialogExamples : Scenario var x = 1; var win = new Window { Title = GetQuitKeyAndName () }; - _cbMustExist = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Must E_xist" }; + _cbMustExist = new () { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Must E_xist" }; win.Add (_cbMustExist); - _cbUseColors = new CheckBox { CheckedState = FileDialogStyle.DefaultUseColors ? CheckState.Checked : CheckState.UnChecked, Y = y++, X = x, Text = "_Use Colors" }; + _cbUseColors = new () + { CheckedState = FileDialogStyle.DefaultUseColors ? CheckState.Checked : CheckState.UnChecked, Y = y++, X = x, Text = "_Use Colors" }; win.Add (_cbUseColors); - _cbCaseSensitive = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Case Sensitive Search" }; + _cbCaseSensitive = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Case Sensitive Search" }; win.Add (_cbCaseSensitive); - _cbAllowMultipleSelection = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Multiple" }; + _cbAllowMultipleSelection = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Multiple" }; win.Add (_cbAllowMultipleSelection); - _cbShowTreeBranchLines = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Tree Branch _Lines" }; + _cbShowTreeBranchLines = new () { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Tree Branch _Lines" }; win.Add (_cbShowTreeBranchLines); - _cbAlwaysTableShowHeaders = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Always Show _Headers" }; + _cbAlwaysTableShowHeaders = new () { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Always Show _Headers" }; win.Add (_cbAlwaysTableShowHeaders); - _cbDrivesOnlyInTree = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show _Drives" }; + _cbDrivesOnlyInTree = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show _Drives" }; win.Add (_cbDrivesOnlyInTree); - _cbPreserveFilenameOnDirectoryChanges = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Preserve Filename" }; + _cbPreserveFilenameOnDirectoryChanges = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Preserve Filename" }; win.Add (_cbPreserveFilenameOnDirectoryChanges); y = 0; x = 24; win.Add ( - new LineView (Orientation.Vertical) { X = x++, Y = 1, Height = 4 } + new Line { Orientation = Orientation.Vertical, X = x++, Y = 1, Height = 4 } ); win.Add (new Label { X = x++, Y = y++, Text = "Caption" }); - _rgCaption = new RadioGroup { X = x, Y = y }; - _rgCaption.RadioLabels = new [] { "_Ok", "O_pen", "_Save" }; - win.Add (_rgCaption); + _osCaption = new () { X = x, Y = y }; + _osCaption.Labels = ["_Ok", "O_pen", "_Save"]; + win.Add (_osCaption); y = 0; x = 34; win.Add ( - new LineView (Orientation.Vertical) { X = x++, Y = 1, Height = 4 } + new Line { Orientation = Orientation.Vertical, X = x++, Y = 1, Height = 4 } ); win.Add (new Label { X = x++, Y = y++, Text = "OpenMode" }); - _rgOpenMode = new RadioGroup { X = x, Y = y }; - _rgOpenMode.RadioLabels = new [] { "_File", "D_irectory", "_Mixed" }; - win.Add (_rgOpenMode); + _osOpenMode = new () { X = x, Y = y }; + _osOpenMode.Labels = ["_File", "D_irectory", "_Mixed"]; + win.Add (_osOpenMode); y = 0; x = 48; win.Add ( - new LineView (Orientation.Vertical) { X = x++, Y = 1, Height = 4 } + new Line { Orientation = Orientation.Vertical, X = x++, Y = 1, Height = 4 } ); win.Add (new Label { X = x++, Y = y++, Text = "Icons" }); - _rgIcons = new RadioGroup { X = x, Y = y }; - _rgIcons.RadioLabels = new [] { "_None", "_Unicode", "Nerd_*" }; - win.Add (_rgIcons); + _osIcons = new () { X = x, Y = y }; + _osIcons.Labels = ["_None", "_Unicode", "Nerd_*"]; + win.Add (_osIcons); win.Add (new Label { Y = Pos.AnchorEnd (2), Text = "* Requires installing Nerd fonts" }); win.Add (new Label { Y = Pos.AnchorEnd (1), Text = " (see: https://github.com/devblackops/Terminal-Icons)" }); @@ -101,48 +98,48 @@ public class FileDialogExamples : Scenario x = 24; win.Add ( - new LineView (Orientation.Vertical) { X = x++, Y = y + 1, Height = 4 } + new Line { Orientation = Orientation.Vertical, X = x++, Y = y + 1, Height = 4 } ); win.Add (new Label { X = x++, Y = y++, Text = "Allowed" }); - _rgAllowedTypes = new RadioGroup { X = x, Y = y }; - _rgAllowedTypes.RadioLabels = new [] { "An_y", "Cs_v (Recommended)", "Csv (S_trict)" }; - win.Add (_rgAllowedTypes); + _osAllowedTypes = new () { X = x, Y = y }; + _osAllowedTypes.Labels = ["An_y", "Cs_v (Recommended)", "Csv (S_trict)"]; + win.Add (_osAllowedTypes); y = 5; x = 45; win.Add ( - new LineView (Orientation.Vertical) { X = x++, Y = y + 1, Height = 4 } + new Line { Orientation = Orientation.Vertical, X = x++, Y = y + 1, Height = 4 } ); win.Add (new Label { X = x++, Y = y++, Text = "Buttons" }); win.Add (new Label { X = x, Y = y++, Text = "O_k Text:" }); - _tbOkButton = new TextField { X = x, Y = y++, Width = 12 }; + _tbOkButton = new () { X = x, Y = y++, Width = 12 }; win.Add (_tbOkButton); win.Add (new Label { X = x, Y = y++, Text = "_Cancel Text:" }); - _tbCancelButton = new TextField { X = x, Y = y++, Width = 12 }; + _tbCancelButton = new () { X = x, Y = y++, Width = 12 }; win.Add (_tbCancelButton); - _cbFlipButtonOrder = new CheckBox { X = x, Y = y++, Text = "Flip Ord_er" }; + _cbFlipButtonOrder = new () { X = x, Y = y++, Text = "Flip Ord_er" }; win.Add (_cbFlipButtonOrder); var btn = new Button { X = 1, Y = 9, IsDefault = true, Text = "Run Dialog" }; win.Accepting += (s, e) => - { - try - { - CreateDialog (); - } - catch (Exception ex) - { - MessageBox.ErrorQuery ("Error", ex.ToString (), "_Ok"); - } - finally - { - e.Handled = true; - } - }; + { + try + { + CreateDialog (); + } + catch (Exception ex) + { + MessageBox.ErrorQuery ("Error", ex.ToString (), "_Ok"); + } + finally + { + e.Handled = true; + } + }; win.Add (btn); Application.Run (win); @@ -164,112 +161,115 @@ public class FileDialogExamples : Scenario private void CreateDialog () { - var fd = new FileDialog + if (_osOpenMode.Value is { }) { - OpenMode = Enum.Parse ( - _rgOpenMode.RadioLabels - .Select (l => TextFormatter.FindHotKey (l, _rgOpenMode.HotKeySpecifier, out int hotPos, out Key _) - - // Remove the hotkey specifier at the found position - ? TextFormatter.RemoveHotKeySpecifier (l, hotPos, _rgOpenMode.HotKeySpecifier) - - // No hotkey found, return the label as is - : l) - .ToArray () [_rgOpenMode.SelectedItem] - ), - MustExist = _cbMustExist.CheckedState == CheckState.Checked, - AllowsMultipleSelection = _cbAllowMultipleSelection.CheckedState == CheckState.Checked - }; - - fd.Style.OkButtonText = _rgCaption.RadioLabels.Select (l => TextFormatter.RemoveHotKeySpecifier(l, 0, _rgCaption.HotKeySpecifier)).ToArray() [_rgCaption.SelectedItem]; - - // If Save style dialog then give them an overwrite prompt - if (_rgCaption.SelectedItem == 2) - { - fd.FilesSelected += ConfirmOverwrite; - } - - fd.Style.IconProvider.UseUnicodeCharacters = _rgIcons.SelectedItem == 1; - fd.Style.IconProvider.UseNerdIcons = _rgIcons.SelectedItem == 2; - - if (_cbCaseSensitive.CheckedState == CheckState.Checked) - { - fd.SearchMatcher = new CaseSensitiveSearchMatcher (); - } - - fd.Style.UseColors = _cbUseColors.CheckedState == CheckState.Checked; - - fd.Style.TreeStyle.ShowBranchLines = _cbShowTreeBranchLines.CheckedState == CheckState.Checked; - fd.Style.TableStyle.AlwaysShowHeaders = _cbAlwaysTableShowHeaders.CheckedState == CheckState.Checked; - - IDirectoryInfoFactory dirInfoFactory = new FileSystem ().DirectoryInfo; - - if (_cbDrivesOnlyInTree.CheckedState == CheckState.Checked) - { - fd.Style.TreeRootGetter = () => { return Environment.GetLogicalDrives ().ToDictionary (dirInfoFactory.New, k => k); }; - } - - fd.Style.PreserveFilenameOnDirectoryChanges = _cbPreserveFilenameOnDirectoryChanges.CheckedState == CheckState.Checked; - - - if (_rgAllowedTypes.SelectedItem > 0) - { - fd.AllowedTypes.Add (new AllowedType ("Data File", ".csv", ".tsv")); - - if (_rgAllowedTypes.SelectedItem == 1) + var fd = new FileDialog { - fd.AllowedTypes.Insert (1, new AllowedTypeAny ()); + OpenMode = Enum.Parse ( + _osOpenMode.Labels + .Select (l => TextFormatter.FindHotKey (l, _osOpenMode.HotKeySpecifier, out int hotPos, out Key _) + + // Remove the hotkey specifier at the found position + ? TextFormatter.RemoveHotKeySpecifier (l, hotPos, _osOpenMode.HotKeySpecifier) + + // No hotkey found, return the label as is + : l) + .ToArray () [_osOpenMode.Value.Value] + ), + MustExist = _cbMustExist.CheckedState == CheckState.Checked, + AllowsMultipleSelection = _cbAllowMultipleSelection.CheckedState == CheckState.Checked + }; + + fd.Style.OkButtonText = + _osCaption.Labels.Select (l => TextFormatter.RemoveHotKeySpecifier (l, 0, _osCaption.HotKeySpecifier)).ToArray () + [_osCaption.Value!.Value]; + + // If Save style dialog then give them an overwrite prompt + if (_osCaption.Value == 2) + { + fd.FilesSelected += ConfirmOverwrite; } - } - if (!string.IsNullOrWhiteSpace (_tbOkButton.Text)) - { - fd.Style.OkButtonText = _tbOkButton.Text; - } + fd.Style.IconProvider.UseUnicodeCharacters = _osIcons.Value == 1; + fd.Style.IconProvider.UseNerdIcons = _osIcons.Value == 2; - if (!string.IsNullOrWhiteSpace (_tbCancelButton.Text)) - { - fd.Style.CancelButtonText = _tbCancelButton.Text; - } + if (_cbCaseSensitive.CheckedState == CheckState.Checked) + { + fd.SearchMatcher = new CaseSensitiveSearchMatcher (); + } - if (_cbFlipButtonOrder.CheckedState == CheckState.Checked) - { - fd.Style.FlipOkCancelButtonLayoutOrder = true; - } + fd.Style.UseColors = _cbUseColors.CheckedState == CheckState.Checked; - Application.Run (fd); + fd.Style.TreeStyle.ShowBranchLines = _cbShowTreeBranchLines.CheckedState == CheckState.Checked; + fd.Style.TableStyle.AlwaysShowHeaders = _cbAlwaysTableShowHeaders.CheckedState == CheckState.Checked; - var canceled = fd.Canceled; - var multiSelected = fd.MultiSelected; - var path = fd.Path; + IDirectoryInfoFactory dirInfoFactory = new FileSystem ().DirectoryInfo; - // This needs to be disposed before opening other toplevel - fd.Dispose (); + if (_cbDrivesOnlyInTree.CheckedState == CheckState.Checked) + { + fd.Style.TreeRootGetter = () => { return Environment.GetLogicalDrives ().ToDictionary (dirInfoFactory.New, k => k); }; + } - if (canceled) - { - MessageBox.Query ( - "Canceled", - "You canceled navigation and did not pick anything", - "Ok" - ); + fd.Style.PreserveFilenameOnDirectoryChanges = _cbPreserveFilenameOnDirectoryChanges.CheckedState == CheckState.Checked; - } - else if (_cbAllowMultipleSelection.CheckedState == CheckState.Checked) - { - MessageBox.Query ( - "Chosen!", - "You chose:" + Environment.NewLine + string.Join (Environment.NewLine, multiSelected.Select (m => m)), - "Ok" - ); - } - else - { - MessageBox.Query ( - "Chosen!", - "You chose:" + Environment.NewLine + path, - "Ok" - ); + if (_osAllowedTypes.Value > 0) + { + fd.AllowedTypes.Add (new AllowedType ("Data File", ".csv", ".tsv")); + + if (_osAllowedTypes.Value == 1) + { + fd.AllowedTypes.Insert (1, new AllowedTypeAny ()); + } + } + + if (!string.IsNullOrWhiteSpace (_tbOkButton.Text)) + { + fd.Style.OkButtonText = _tbOkButton.Text; + } + + if (!string.IsNullOrWhiteSpace (_tbCancelButton.Text)) + { + fd.Style.CancelButtonText = _tbCancelButton.Text; + } + + if (_cbFlipButtonOrder.CheckedState == CheckState.Checked) + { + fd.Style.FlipOkCancelButtonLayoutOrder = true; + } + + Application.Run (fd); + + bool canceled = fd.Canceled; + IReadOnlyList multiSelected = fd.MultiSelected; + string path = fd.Path; + + // This needs to be disposed before opening other toplevel + fd.Dispose (); + + if (canceled) + { + MessageBox.Query ( + "Canceled", + "You canceled navigation and did not pick anything", + "Ok" + ); + } + else if (_cbAllowMultipleSelection.CheckedState == CheckState.Checked) + { + MessageBox.Query ( + "Chosen!", + "You chose:" + Environment.NewLine + string.Join (Environment.NewLine, multiSelected.Select (m => m)), + "Ok" + ); + } + else + { + MessageBox.Query ( + "Chosen!", + "You chose:" + Environment.NewLine + path, + "Ok" + ); + } } } diff --git a/Examples/UICatalog/Scenarios/GraphViewExample.cs b/Examples/UICatalog/Scenarios/GraphViewExample.cs index 530acf8e4..5dcd5df40 100644 --- a/Examples/UICatalog/Scenarios/GraphViewExample.cs +++ b/Examples/UICatalog/Scenarios/GraphViewExample.cs @@ -144,8 +144,8 @@ public class GraphViewExample : Scenario Height = Dim.Fill (1), BorderStyle = LineStyle.Single }; - _graphView.Border.Thickness = _thickness; - _graphView.Margin.Thickness = _thickness; + _graphView.Border!.Thickness = _thickness; + _graphView.Margin!.Thickness = _thickness; _graphView.Padding.Thickness = _thickness; app.Add (_graphView); @@ -955,14 +955,14 @@ public class GraphViewExample : Scenario if (_miShowBorder.Checked == true) { _graphView.BorderStyle = LineStyle.Single; - _graphView.Border.Thickness = _thickness; - _graphView.Margin.Thickness = _thickness; + _graphView.Border!.Thickness = _thickness; + _graphView.Margin!.Thickness = _thickness; _graphView.Padding.Thickness = _thickness; } else { _graphView.BorderStyle = LineStyle.None; - _graphView.Margin.Thickness = Thickness.Empty; + _graphView.Margin!.Thickness = Thickness.Empty; _graphView.Padding.Thickness = Thickness.Empty; } } @@ -999,7 +999,7 @@ public class GraphViewExample : Scenario protected override void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn) { - IConsoleDriver driver = Application.Driver; + IDriver driver = Application.Driver; int x = start.X; diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index 42e3d7ce7..488f595a7 100644 --- a/Examples/UICatalog/Scenarios/Images.cs +++ b/Examples/UICatalog/Scenarios/Images.cs @@ -50,8 +50,8 @@ public class Images : Scenario private SixelToRender _fireSixel; private int _fireFrameCounter; private bool _isDisposed; - private RadioGroup _rgPaletteBuilder; - private RadioGroup _rgDistanceAlgorithm; + private OptionSelector _osPaletteBuilder; + private OptionSelector _osDistanceAlgorithm; private NumericUpDown _popularityThreshold; private SixelToRender _sixelImage; @@ -408,22 +408,22 @@ public class Images : Scenario Y = Pos.Bottom (_pxY) + 1 }; - _rgPaletteBuilder = new () + _osPaletteBuilder = new () { - RadioLabels = new [] - { + Labels = + [ "Popularity", "Median Cut" - }, + ], X = Pos.Right (_sixelView) + 2, Y = Pos.Bottom (l1), - SelectedItem = 1 + Value = 1 }; _popularityThreshold = new () { - X = Pos.Right (_rgPaletteBuilder) + 1, - Y = Pos.Top (_rgPaletteBuilder), + X = Pos.Right (_osPaletteBuilder) + 1, + Y = Pos.Top (_osPaletteBuilder), Value = 8 }; @@ -439,12 +439,12 @@ public class Images : Scenario Text = "Color Distance Algorithm", Width = Dim.Auto (), X = Pos.Right (_sixelView), - Y = Pos.Bottom (_rgPaletteBuilder) + 1 + Y = Pos.Bottom (_osPaletteBuilder) + 1 }; - _rgDistanceAlgorithm = new () + _osDistanceAlgorithm = new () { - RadioLabels = new [] + Labels = new [] { "Euclidian", "CIE76" @@ -458,10 +458,10 @@ public class Images : Scenario _sixelSupported.Add (lblPxY); _sixelSupported.Add (_pxY); _sixelSupported.Add (l1); - _sixelSupported.Add (_rgPaletteBuilder); + _sixelSupported.Add (_osPaletteBuilder); _sixelSupported.Add (l2); - _sixelSupported.Add (_rgDistanceAlgorithm); + _sixelSupported.Add (_osDistanceAlgorithm); _sixelSupported.Add (_popularityThreshold); _sixelSupported.Add (lblPopThreshold); @@ -470,7 +470,7 @@ public class Images : Scenario private IPaletteBuilder GetPaletteBuilder () { - switch (_rgPaletteBuilder.SelectedItem) + switch (_osPaletteBuilder.Value) { case 0: return new PopularityPaletteWithThreshold (GetDistanceAlgorithm (), _popularityThreshold.Value); case 1: return new MedianCutPaletteBuilder (GetDistanceAlgorithm ()); @@ -480,7 +480,7 @@ public class Images : Scenario private IColorDistance GetDistanceAlgorithm () { - switch (_rgDistanceAlgorithm.SelectedItem) + switch (_osDistanceAlgorithm.Value) { case 0: return new EuclideanColorDistance (); case 1: return new CIE76ColorDistance (); @@ -532,7 +532,7 @@ public class Images : Scenario // Application.Driver?.Move (_screenLocationForSixel.X, _screenLocationForSixel.Y); // Application.Driver?.AddStr (_encodedSixelData); - // Works in NetDriver but results in screen flicker when moving mouse but vanish instantly + // Works in DotNetDriver but results in screen flicker when moving mouse but vanish instantly // Console.SetCursorPosition (_screenLocationForSixel.X, _screenLocationForSixel.Y); // Console.Write (_encodedSixelData); } diff --git a/Examples/UICatalog/Scenarios/Keys.cs b/Examples/UICatalog/Scenarios/Keys.cs index 8f0b0094d..0c17a15f8 100644 --- a/Examples/UICatalog/Scenarios/Keys.cs +++ b/Examples/UICatalog/Scenarios/Keys.cs @@ -158,10 +158,7 @@ public class Keys : Scenario appKeyListView.SchemeName = "TopLevel"; win.Add (onSwallowedListView); - if (Application.Driver is IConsoleDriverFacade fac) - { - fac.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b","Esc")); }; - } + Application.Driver!.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b", "Esc")); }; Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down"); Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up"); diff --git a/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs b/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs index 85786a40e..6a826e4be 100644 --- a/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs +++ b/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs @@ -130,9 +130,9 @@ public class LineCanvasExperiment : Scenario // //Scheme = Colors.Schemes ["Error"], // SuperViewRendersLineCanvas = true //}; - //marginWindow.Margin.Scheme = Colors.Schemes ["Error"]; - //marginWindow.Margin.Thickness = new (1); - //marginWindow.Border.Thickness = new (1, 2, 1, 1); + //marginWindow.Margin!.Scheme = Colors.Schemes ["Error"]; + //marginWindow.Margin!.Thickness = new (1); + //marginWindow.Border!.Thickness = new (1, 2, 1, 1); //frame1.Add (marginWindow); diff --git a/Examples/UICatalog/Scenarios/LineDrawing.cs b/Examples/UICatalog/Scenarios/LineDrawing.cs index 46f51dda7..cbc8dd27b 100644 --- a/Examples/UICatalog/Scenarios/LineDrawing.cs +++ b/Examples/UICatalog/Scenarios/LineDrawing.cs @@ -210,7 +210,7 @@ public class ToolsView : Window { private Button _addLayerBtn; private readonly AttributeView _colors; - private RadioGroup _stylePicker; + private OptionSelector _stylePicker; public Attribute CurrentColor { @@ -236,10 +236,16 @@ public class ToolsView : Window _stylePicker = new () { - X = 0, Y = Pos.Bottom (_colors), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray () + X = 0, Y = Pos.Bottom (_colors), AssignHotKeys = true }; - _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); }; - _stylePicker.SelectedItem = 1; + _stylePicker.ValueChanged += (s, a) => + { + if (a.Value is { }) + { + SetStyle?.Invoke ((LineStyle)a.Value); + } + }; + _stylePicker.Value = LineStyle.Single; _addLayerBtn = new () { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) }; diff --git a/Examples/UICatalog/Scenarios/LineExample.cs b/Examples/UICatalog/Scenarios/LineExample.cs new file mode 100644 index 000000000..294884d92 --- /dev/null +++ b/Examples/UICatalog/Scenarios/LineExample.cs @@ -0,0 +1,219 @@ +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("Line", "Demonstrates the Line view with LineCanvas integration.")] +[ScenarioCategory ("Controls")] +[ScenarioCategory ("Drawing")] +[ScenarioCategory ("Adornments")] +public class LineExample : Scenario +{ + public override void Main () + { + Application.Init (); + + var app = new Window + { + Title = GetQuitKeyAndName () + }; + + // Section 1: Basic Lines + var basicLabel = new Label + { + X = 0, + Y = 0, + Text = "Basic Lines:" + }; + app.Add (basicLabel); + + // Horizontal line + var hLine = new Line + { + X = 0, + Y = 1, + Width = 30 + }; + app.Add (hLine); + + // Vertical line + var vLine = new Line + { + X = 32, + Y = 0, + Height = 10, + Orientation = Orientation.Vertical + }; + app.Add (vLine); + + // Section 2: Different Line Styles + var stylesLabel = new Label + { + X = 0, + Y = 3, + Text = "Line Styles:" + }; + app.Add (stylesLabel); + + (LineStyle, string) [] styles = new [] + { + (LineStyle.Single, "Single"), + (LineStyle.Double, "Double"), + (LineStyle.Heavy, "Heavy"), + (LineStyle.Rounded, "Rounded"), + (LineStyle.Dashed, "Dashed"), + (LineStyle.Dotted, "Dotted") + }; + + var yPos = 4; + + foreach ((LineStyle style, string name) in styles) + { + app.Add (new Label { X = 0, Y = yPos, Width = 15, Text = name + ":" }); + app.Add (new Line { X = 16, Y = yPos, Width = 14, Style = style }); + yPos++; + } + + // Section 3: Line Intersections + var intersectionLabel = new Label + { + X = 35, + Y = 3, + Text = "Line Intersections:" + }; + app.Add (intersectionLabel); + + // Create a grid of intersecting lines + var gridX = 35; + var gridY = 5; + + // Horizontal lines in the grid + for (var i = 0; i < 5; i++) + { + app.Add ( + new Line + { + X = gridX, + Y = gridY + i * 2, + Width = 21, + Style = LineStyle.Single + }); + } + + // Vertical lines in the grid + for (var i = 0; i < 5; i++) + { + app.Add ( + new Line + { + X = gridX + i * 5, + Y = gridY, + Height = 9, + Orientation = Orientation.Vertical, + Style = LineStyle.Single + }); + } + + // Section 4: Mixed Styles (shows how LineCanvas handles different line styles) + var mixedLabel = new Label + { + X = 60, + Y = 3, + Text = "Mixed Style Intersections:" + }; + app.Add (mixedLabel); + + // Double horizontal + app.Add ( + new Line + { + X = 60, + Y = 5, + Width = 20, + Style = LineStyle.Double + }); + + // Single vertical through double horizontal + app.Add ( + new Line + { + X = 70, + Y = 4, + Height = 3, + Orientation = Orientation.Vertical, + Style = LineStyle.Single + }); + + // Heavy horizontal + app.Add ( + new Line + { + X = 60, + Y = 8, + Width = 20, + Style = LineStyle.Heavy + }); + + // Single vertical through heavy horizontal + app.Add ( + new Line + { + X = 70, + Y = 7, + Height = 3, + Orientation = Orientation.Vertical, + Style = LineStyle.Single + }); + + // Section 5: Box Example (showing borders and lines working together) + var boxLabel = new Label + { + X = 0, + Y = 12, + Text = "Lines with Borders:" + }; + app.Add (boxLabel); + + var framedView = new FrameView + { + Title = "Frame", + X = 0, + Y = 13, + Width = 30, + Height = 8, + BorderStyle = LineStyle.Single + }; + + // Add a cross inside the frame + framedView.Add ( + new Line + { + X = 0, + Y = 3, + Width = Dim.Fill (), + Style = LineStyle.Single + }); + + framedView.Add ( + new Line + { + X = 14, + Y = 0, + Height = Dim.Fill (), + Orientation = Orientation.Vertical, + Style = LineStyle.Single + }); + + app.Add (framedView); + + // Add help text + var helpLabel = new Label + { + X = Pos.Center (), + Y = Pos.AnchorEnd (1), + Text = "Line integrates with LineCanvas for automatic intersection handling" + }; + app.Add (helpLabel); + + Application.Run (app); + app.Dispose (); + Application.Shutdown (); + } +} diff --git a/Examples/UICatalog/Scenarios/LineViewExample.cs b/Examples/UICatalog/Scenarios/LineViewExample.cs deleted file mode 100644 index 38290bdce..000000000 --- a/Examples/UICatalog/Scenarios/LineViewExample.cs +++ /dev/null @@ -1,76 +0,0 @@ -ο»Ώusing System.Globalization; -using System.Text; - -namespace UICatalog.Scenarios; - -[ScenarioMetadata ("Line View", "Demonstrates drawing lines using the LineView control.")] -[ScenarioCategory ("Controls")] -[ScenarioCategory ("LineView")] -[ScenarioCategory ("Adornments")] -public class LineViewExample : Scenario -{ - public override void Main () - { - Application.Init (); - - var appWindow = new Window() - { - Title = GetQuitKeyAndName (), - }; - - appWindow.Add (new Label { Y = 1, Text = "Regular Line" }); - - // creates a horizontal line - var line = new LineView { Y = 2 }; - - appWindow.Add (line); - - appWindow.Add (new Label { Y = 3, Text = "Double Width Line" }); - - // creates a horizontal line - var doubleLine = new LineView { Y = 4, LineRune = (Rune)'\u2550' }; - - appWindow.Add (doubleLine); - - appWindow.Add (new Label { Y = 5, Text = "Short Line" }); - - // creates a horizontal line - var shortLine = new LineView { Y = 5, Width = 10 }; - - appWindow.Add (shortLine); - - appWindow.Add (new Label { Y = 7, Text = "Arrow Line" }); - - // creates a horizontal line - var arrowLine = new LineView - { - Y = 8, Width = 10, StartingAnchor = Glyphs.LeftTee, EndingAnchor = (Rune)'>' - }; - - appWindow.Add (arrowLine); - - appWindow.Add (new Label { Y = 10, X = 11, Text = "Vertical Line" }); - - // creates a horizontal line - var verticalLine = new LineView (Orientation.Vertical) { X = 25 }; - - appWindow.Add (verticalLine); - - appWindow.Add (new Label { Y = 12, X = 28, Text = "Vertical Arrow" }); - - // creates a horizontal line - var verticalArrow = new LineView (Orientation.Vertical) - { - X = 27, StartingAnchor = Glyphs.TopTee, EndingAnchor = (Rune)'V' - }; - - appWindow.Add (verticalArrow); - - // Run - Start the application. - Application.Run (appWindow); - appWindow.Dispose (); - - // Shutdown - Calling Application.Shutdown is required. - Application.Shutdown (); - } -} diff --git a/Examples/UICatalog/Scenarios/Mazing.cs b/Examples/UICatalog/Scenarios/Mazing.cs index a9954d017..01bbc41a4 100644 --- a/Examples/UICatalog/Scenarios/Mazing.cs +++ b/Examples/UICatalog/Scenarios/Mazing.cs @@ -5,7 +5,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("A Mazing", "Illustrates how to make a basic maze game.")] [ScenarioCategory ("Drawing")] -[ScenarioCategory ("Mouse and KeyBoard")] +[ScenarioCategory ("Mouse and Keyboard")] [ScenarioCategory ("Games")] public class Mazing : Scenario { @@ -33,7 +33,7 @@ public class Mazing : Scenario _top.KeyBindings.Add (Key.CursorDown, Command.Down); // Changing the key-bindings of a View is not allowed, however, - // by default, Toplevel does't bind any of our movement keys, so + // by default, Toplevel doesn't bind any of our movement keys, so // we can take advantage of the CommandNotBound event to handle them // // An alternative implementation would be to create a TopLevel subclass that @@ -129,7 +129,7 @@ public class Mazing : Scenario return; } - Point newPos = _m.Player; + Point newPos = _m!.Player; Command? command = e.Context?.Command; diff --git a/Examples/UICatalog/Scenarios/MessageBoxes.cs b/Examples/UICatalog/Scenarios/MessageBoxes.cs index 71dde85e2..c8356a86a 100644 --- a/Examples/UICatalog/Scenarios/MessageBoxes.cs +++ b/Examples/UICatalog/Scenarios/MessageBoxes.cs @@ -182,18 +182,19 @@ public class MessageBoxes : Scenario }; frame.Add (label); - var styleRadioGroup = new RadioGroup + var styleOptionSelector = new OptionSelector () { - X = Pos.Right (label) + 1, - Y = Pos.Top (label), - RadioLabels = ["_Query", "_Error"], + X = Pos.Right (label) + 1, + Y = Pos.Top (label), + Labels = ["_Query", "_Error"], + Title = "Sty_le" }; - frame.Add (styleRadioGroup); + frame.Add (styleOptionSelector); label = new () { X = 0, - Y = Pos.Bottom (styleRadioGroup), + Y = Pos.Bottom (styleOptionSelector), Width = Dim.Width (label), Height = 1, @@ -202,7 +203,7 @@ public class MessageBoxes : Scenario }; var ckbWrapMessage = new CheckBox { - X = Pos.Right (label) + 1, Y = Pos.Bottom (styleRadioGroup), + X = Pos.Right (label) + 1, Y = Pos.Bottom (styleOptionSelector), CheckedState = CheckState.Checked, Text = "_Wrap Message", }; @@ -246,7 +247,7 @@ public class MessageBoxes : Scenario btns.Add ($"_{NumberToWords.Convert (i)}"); } - if (styleRadioGroup.SelectedItem == 0) + if (styleOptionSelector.Value == 0) { buttonPressedLabel.Text = $"{MessageBox.Query ( diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index 7a3390c6f..7ce1d3317 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/Examples/UICatalog/Scenarios/Navigation.cs @@ -107,18 +107,7 @@ public class Navigation : Scenario // }; //timer.Start (); - Application.Iteration += (sender, args) => - { - if (progressBar.Fraction == 1.0) - { - progressBar.Fraction = 0; - } - - progressBar.Fraction += 0.01f; - - Application.Invoke (() => { }); - - }; + Application.Iteration += OnApplicationIteration; View overlappedView2 = CreateOverlappedView (3, 8, 10); @@ -214,12 +203,25 @@ public class Navigation : Scenario testFrame.SetFocus (); Application.Run (app); + Application.Iteration -= OnApplicationIteration; // timer.Close (); app.Dispose (); Application.Shutdown (); return; + void OnApplicationIteration (object sender, IterationEventArgs args) + { + if (progressBar.Fraction == 1.0) + { + progressBar.Fraction = 0; + } + + progressBar.Fraction += 0.01f; + + Application.Invoke (() => { }); + } + void ColorPicker_ColorChanged (object sender, ResultEventArgs e) { testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.Result) }); diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index c827ca59f..996702298 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -1,6 +1,4 @@ -ο»Ώusing System.IO; -using System.Linq; - +ο»Ώ#nullable enable namespace UICatalog.Scenarios; [ScenarioMetadata ("Notepad", "Multi-tab text editor using the TabView control.")] @@ -9,10 +7,10 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("TextView")] public class Notepad : Scenario { - private TabView _focusedTabView; - public Shortcut LenShortcut { get; private set; } + private TabView? _focusedTabView; private int _numNewTabs = 1; - private TabView _tabView; + private TabView? _tabView; + public Shortcut? LenShortcut { get; private set; } public override void Main () { @@ -59,22 +57,23 @@ public class Notepad : Scenario _tabView.Style.ShowBorder = true; _tabView.ApplyStyleChanges (); - // Start with only a single view but support splitting to show side by side - var split = new TileView (1) { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - split.Tiles.ElementAt (0).ContentView.Add (_tabView); - split.LineStyle = LineStyle.None; + _tabView.X = 0; + _tabView.Y = 1; + _tabView.Width = Dim.Fill (); + _tabView.Height = Dim.Fill (1); - top.Add (split); + top.Add (_tabView); LenShortcut = new (Key.Empty, "Len: ", null); - var statusBar = new StatusBar (new [] { - new (Application.QuitKey, $"Quit", Quit), - new Shortcut(Key.F2, "Open", Open), - new Shortcut(Key.F1, "New", New), + var statusBar = new StatusBar ( + [ + new (Application.QuitKey, "Quit", Quit), + new (Key.F2, "Open", Open), + new (Key.F1, "New", New), new (Key.F3, "Save", Save), new (Key.F6, "Close", Close), LenShortcut - } + ] ) { AlignmentModes = AlignmentModes.IgnoreFirstOrLast @@ -97,7 +96,7 @@ public class Notepad : Scenario Application.Shutdown (); } - public void Save () { Save (_focusedTabView, _focusedTabView.SelectedTab); } + public void Save () { Save (_focusedTabView!, _focusedTabView!.SelectedTab!); } public void Save (TabView tabViewToSave, Tab tabToSave) { @@ -119,7 +118,7 @@ public class Notepad : Scenario public bool SaveAs () { - var tab = _focusedTabView.SelectedTab as OpenedFile; + var tab = _focusedTabView!.SelectedTab as OpenedFile; if (tab == null) { @@ -152,7 +151,7 @@ public class Notepad : Scenario return true; } - private void Close () { Close (_focusedTabView, _focusedTabView.SelectedTab); } + private void Close () { Close (_focusedTabView!, _focusedTabView!.SelectedTab!); } private void Close (TabView tv, Tab tabToClose) { @@ -196,41 +195,13 @@ public class Notepad : Scenario // close and dispose the tab tv.RemoveTab (tab); - tab.View.Dispose (); + tab.View?.Dispose (); _focusedTabView = tv; + // If last tab is closed, open a new one if (tv.Tabs.Count == 0) { - var split = (TileView)tv.SuperView.SuperView; - - // if it is the last TabView on screen don't drop it or we will - // be unable to open new docs! - if (split.IsRootTileView () && split.Tiles.Count == 1) - { - return; - } - - int tileIndex = split.IndexOf (tv); - split.RemoveTile (tileIndex); - - if (split.Tiles.Count == 0) - { - TileView parent = split.GetParentTileView (); - - if (parent == null) - { - return; - } - - int idx = parent.IndexOf (split); - - if (idx == -1) - { - return; - } - - parent.RemoveTile (idx); - } + New (); } } @@ -245,7 +216,7 @@ public class Notepad : Scenario return tv; } - private void New () { Open (null, $"new {_numNewTabs++}"); } + private void New () { Open (null!, $"new {_numNewTabs++}"); } private void Open () { @@ -274,57 +245,27 @@ public class Notepad : Scenario /// Creates a new tab with initial text /// File that was read or null if a new blank document + /// private void Open (FileInfo fileInfo, string tabName) { var tab = new OpenedFile (this) { DisplayText = tabName, File = fileInfo }; tab.View = tab.CreateTextView (fileInfo); tab.SavedText = tab.View.Text; - tab.RegisterTextViewEvents (_focusedTabView); + tab.RegisterTextViewEvents (_focusedTabView!); - _focusedTabView.AddTab (tab, true); + _focusedTabView!.AddTab (tab, true); } private void Quit () { Application.RequestStop (); } - private void Split (int offset, Orientation orientation, TabView sender, OpenedFile tab) + private void TabView_SelectedTabChanged (object? sender, TabChangedEventArgs e) { - var split = (TileView)sender.SuperView.SuperView; - int tileIndex = split.IndexOf (sender); - - if (tileIndex == -1) - { - return; - } - - if (orientation != split.Orientation) - { - split.TrySplitTile (tileIndex, 1, out split); - split.Orientation = orientation; - tileIndex = 0; - } - - Tile newTile = split.InsertTile (tileIndex + offset); - TabView newTabView = CreateNewTabView (); - tab.CloneTo (newTabView); - newTile.ContentView.Add (newTabView); - - newTabView.FocusDeepest (NavigationDirection.Forward, null); - newTabView.AdvanceFocus (NavigationDirection.Forward, null); - } - - private void SplitDown (TabView sender, OpenedFile tab) { Split (1, Orientation.Horizontal, sender, tab); } - private void SplitLeft (TabView sender, OpenedFile tab) { Split (0, Orientation.Vertical, sender, tab); } - private void SplitRight (TabView sender, OpenedFile tab) { Split (1, Orientation.Vertical, sender, tab); } - private void SplitUp (TabView sender, OpenedFile tab) { Split (0, Orientation.Horizontal, sender, tab); } - - private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e) - { - LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; + LenShortcut!.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; e.NewTab?.View?.SetFocus (); } - private void TabView_TabClicked (object sender, TabMouseEventArgs e) + private void TabView_TabClicked (object? sender, TabMouseEventArgs e) { // we are only interested in right clicks if (!e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) @@ -340,18 +281,13 @@ public class Notepad : Scenario } else { - var tv = (TabView)sender; + var tv = (TabView)sender!; var t = (OpenedFile)e.Tab; items = [ - new MenuItemv2 ("Save", "", () => Save (_focusedTabView, e.Tab)), - new MenuItemv2 ("Close", "", () => Close (tv, e.Tab)), - new Line (), - new MenuItemv2 ("Split Up", "", () => SplitUp (tv, t)), - new MenuItemv2 ("Split Down", "", () => SplitDown (tv, t)), - new MenuItemv2 ("Split Right", "", () => SplitRight (tv, t)), - new MenuItemv2 ("Split Left", "", () => SplitLeft (tv, t)) + new MenuItemv2 ("Save", "", () => Save (_focusedTabView!, e.Tab)), + new MenuItemv2 ("Close", "", () => Close (tv, e.Tab)) ]; PopoverMenu? contextMenu = new (items); @@ -367,12 +303,12 @@ public class Notepad : Scenario private class OpenedFile (Notepad notepad) : Tab { - private Notepad _notepad = notepad; + private readonly Notepad _notepad = notepad; public OpenedFile CloneTo (TabView other) { var newTab = new OpenedFile (_notepad) { DisplayText = base.Text, File = File }; - newTab.View = newTab.CreateTextView (newTab.File); + newTab.View = newTab.CreateTextView (newTab.File!); newTab.SavedText = newTab.View.Text; newTab.RegisterTextViewEvents (other); other.AddTab (newTab, true); @@ -380,11 +316,11 @@ public class Notepad : Scenario return newTab; } - public View CreateTextView (FileInfo file) + public View CreateTextView (FileInfo? file) { var initialText = string.Empty; - if (file != null && file.Exists) + if (file is { Exists: true }) { initialText = System.IO.File.ReadAllText (file.FullName); } @@ -400,11 +336,11 @@ public class Notepad : Scenario }; } - public FileInfo File { get; set; } + public FileInfo? File { get; set; } public void RegisterTextViewEvents (TabView parent) { - var textView = (TextView)View; + var textView = (TextView)View!; // when user makes changes rename tab to indicate unsaved textView.ContentsChanged += (s, k) => @@ -426,19 +362,20 @@ public class Notepad : Scenario DisplayText = Text.TrimEnd ('*'); } } - _notepad.LenShortcut.Title = $"Len:{textView.Text.Length}"; + + _notepad.LenShortcut!.Title = $"Len:{textView.Text.Length}"; }; } /// The text of the tab the last time it was saved /// - public string SavedText { get; set; } + public string? SavedText { get; set; } - public bool UnsavedChanges => !string.Equals (SavedText, View.Text); + public bool UnsavedChanges => !string.Equals (SavedText, View!.Text); internal void Save () { - string newText = View.Text; + string newText = View!.Text; if (File is null || string.IsNullOrWhiteSpace (File.FullName)) { diff --git a/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs b/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs index 5694e4be9..c75c5bdb5 100644 --- a/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs +++ b/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs @@ -266,14 +266,14 @@ internal class NumericUpDownEditor : View where T : notnull void NumericUpDownOnIncrementChanged (object? o, EventArgs eventArgs) { - _increment.Text = _numericUpDown.Increment.ToString (); + _increment.Text = _numericUpDown!.Increment?.ToString (); } Add (_numericUpDown); _value.Text = _numericUpDown.Text; _format.Text = _numericUpDown.Format; - _increment.Text = _numericUpDown.Increment.ToString (); + _increment.Text = _numericUpDown!.Increment?.ToString (); } } diff --git a/Examples/UICatalog/Scenarios/PosAlignDemo.cs b/Examples/UICatalog/Scenarios/PosAlignDemo.cs index 5a9317aac..714155bff 100644 --- a/Examples/UICatalog/Scenarios/PosAlignDemo.cs +++ b/Examples/UICatalog/Scenarios/PosAlignDemo.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; - namespace UICatalog.Scenarios; [ScenarioMetadata ("Pos.Align", "Demonstrates Pos.Align")] [ScenarioCategory ("Layout")] public sealed class PosAlignDemo : Scenario { - private readonly Aligner _horizAligner = new () { AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems}; + private readonly Aligner _horizAligner = new () { AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems }; private int _leftMargin; private readonly Aligner _vertAligner = new () { AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems }; private int _topMargin; @@ -40,43 +36,42 @@ public sealed class PosAlignDemo : Scenario private void SetupControls (Window appWindow, Dimension dimension, Schemes scheme) { - RadioGroup alignRadioGroup = new () + OptionSelector alignOptionSelector = new () { - RadioLabels = Enum.GetNames (), - SchemeName = SchemeManager.SchemesToSchemeName (scheme), + SchemeName = SchemeManager.SchemesToSchemeName (scheme) }; if (dimension == Dimension.Width) { - alignRadioGroup.X = Pos.Align (_horizAligner.Alignment); - alignRadioGroup.Y = Pos.Center (); + alignOptionSelector.X = Pos.Align (_horizAligner.Alignment); + alignOptionSelector.Y = Pos.Center (); } else { - alignRadioGroup.X = Pos.Center (); - alignRadioGroup.Y = Pos.Align (_vertAligner.Alignment); + alignOptionSelector.X = Pos.Center (); + alignOptionSelector.Y = Pos.Align (_vertAligner.Alignment); } - alignRadioGroup.SelectedItemChanged += (s, e) => - { - if (dimension == Dimension.Width) - { - _horizAligner.Alignment = - (Alignment)Enum.Parse ( - typeof (Alignment), - alignRadioGroup.RadioLabels [alignRadioGroup.SelectedItem]); - UpdatePosAlignObjects (appWindow, dimension, _horizAligner); - } - else - { - _vertAligner.Alignment = - (Alignment)Enum.Parse ( - typeof (Alignment), - alignRadioGroup.RadioLabels [alignRadioGroup.SelectedItem]); - UpdatePosAlignObjects (appWindow, dimension, _vertAligner); - } - }; - appWindow.Add (alignRadioGroup); + alignOptionSelector.ValueChanged += (s, e) => + { + if (alignOptionSelector.Value is null) + { + return; + } + + if (dimension == Dimension.Width) + { + _horizAligner.Alignment = alignOptionSelector.Value.Value; + + UpdatePosAlignObjects (appWindow, dimension, _horizAligner); + } + else + { + _vertAligner.Alignment = alignOptionSelector.Value.Value; + UpdatePosAlignObjects (appWindow, dimension, _vertAligner); + } + }; + appWindow.Add (alignOptionSelector); CheckBox endToStartCheckBox = new () { @@ -88,32 +83,32 @@ public sealed class PosAlignDemo : Scenario { endToStartCheckBox.CheckedState = _horizAligner.AlignmentModes.HasFlag (AlignmentModes.EndToStart) ? CheckState.Checked : CheckState.UnChecked; endToStartCheckBox.X = Pos.Align (_horizAligner.Alignment); - endToStartCheckBox.Y = Pos.Top (alignRadioGroup); + endToStartCheckBox.Y = Pos.Top (alignOptionSelector); } else { endToStartCheckBox.CheckedState = _vertAligner.AlignmentModes.HasFlag (AlignmentModes.EndToStart) ? CheckState.Checked : CheckState.UnChecked; - endToStartCheckBox.X = Pos.Left (alignRadioGroup); + endToStartCheckBox.X = Pos.Left (alignOptionSelector); endToStartCheckBox.Y = Pos.Align (_vertAligner.Alignment); } endToStartCheckBox.CheckedStateChanging += (s, e) => - { - if (dimension == Dimension.Width) - { - _horizAligner.AlignmentModes = e.Result == CheckState.Checked - ? _horizAligner.AlignmentModes | AlignmentModes.EndToStart - : _horizAligner.AlignmentModes & ~AlignmentModes.EndToStart; - UpdatePosAlignObjects (appWindow, dimension, _horizAligner); - } - else - { - _vertAligner.AlignmentModes = e.Result == CheckState.Checked - ? _vertAligner.AlignmentModes | AlignmentModes.EndToStart - : _vertAligner.AlignmentModes & ~AlignmentModes.EndToStart; - UpdatePosAlignObjects (appWindow, dimension, _vertAligner); - } - }; + { + if (dimension == Dimension.Width) + { + _horizAligner.AlignmentModes = e.Result == CheckState.Checked + ? _horizAligner.AlignmentModes | AlignmentModes.EndToStart + : _horizAligner.AlignmentModes & ~AlignmentModes.EndToStart; + UpdatePosAlignObjects (appWindow, dimension, _horizAligner); + } + else + { + _vertAligner.AlignmentModes = e.Result == CheckState.Checked + ? _vertAligner.AlignmentModes | AlignmentModes.EndToStart + : _vertAligner.AlignmentModes & ~AlignmentModes.EndToStart; + UpdatePosAlignObjects (appWindow, dimension, _vertAligner); + } + }; appWindow.Add (endToStartCheckBox); CheckBox ignoreFirstOrLast = new () @@ -124,34 +119,35 @@ public sealed class PosAlignDemo : Scenario if (dimension == Dimension.Width) { - ignoreFirstOrLast.CheckedState = _horizAligner.AlignmentModes.HasFlag (AlignmentModes.IgnoreFirstOrLast) ? CheckState.Checked : CheckState.UnChecked; + ignoreFirstOrLast.CheckedState = + _horizAligner.AlignmentModes.HasFlag (AlignmentModes.IgnoreFirstOrLast) ? CheckState.Checked : CheckState.UnChecked; ignoreFirstOrLast.X = Pos.Align (_horizAligner.Alignment); - ignoreFirstOrLast.Y = Pos.Top (alignRadioGroup); + ignoreFirstOrLast.Y = Pos.Top (alignOptionSelector); } else { ignoreFirstOrLast.CheckedState = _vertAligner.AlignmentModes.HasFlag (AlignmentModes.IgnoreFirstOrLast) ? CheckState.Checked : CheckState.UnChecked; - ignoreFirstOrLast.X = Pos.Left (alignRadioGroup); + ignoreFirstOrLast.X = Pos.Left (alignOptionSelector); ignoreFirstOrLast.Y = Pos.Align (_vertAligner.Alignment); } ignoreFirstOrLast.CheckedStateChanging += (s, e) => - { - if (dimension == Dimension.Width) - { - _horizAligner.AlignmentModes = e.Result == CheckState.Checked - ? _horizAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast - : _horizAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast; - UpdatePosAlignObjects (appWindow, dimension, _horizAligner); - } - else - { - _vertAligner.AlignmentModes = e.Result == CheckState.Checked - ? _vertAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast - : _vertAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast; - UpdatePosAlignObjects (appWindow, dimension, _vertAligner); - } - }; + { + if (dimension == Dimension.Width) + { + _horizAligner.AlignmentModes = e.Result == CheckState.Checked + ? _horizAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast + : _horizAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast; + UpdatePosAlignObjects (appWindow, dimension, _horizAligner); + } + else + { + _vertAligner.AlignmentModes = e.Result == CheckState.Checked + ? _vertAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast + : _vertAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast; + UpdatePosAlignObjects (appWindow, dimension, _vertAligner); + } + }; appWindow.Add (ignoreFirstOrLast); CheckBox addSpacesBetweenItems = new () @@ -162,34 +158,40 @@ public sealed class PosAlignDemo : Scenario if (dimension == Dimension.Width) { - addSpacesBetweenItems.CheckedState = _horizAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked; + addSpacesBetweenItems.CheckedState = + _horizAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked; addSpacesBetweenItems.X = Pos.Align (_horizAligner.Alignment); - addSpacesBetweenItems.Y = Pos.Top (alignRadioGroup); + addSpacesBetweenItems.Y = Pos.Top (alignOptionSelector); } else { - addSpacesBetweenItems.CheckedState = _vertAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked; - addSpacesBetweenItems.X = Pos.Left (alignRadioGroup); + addSpacesBetweenItems.CheckedState = + _vertAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked; + addSpacesBetweenItems.X = Pos.Left (alignOptionSelector); addSpacesBetweenItems.Y = Pos.Align (_vertAligner.Alignment); } addSpacesBetweenItems.CheckedStateChanging += (s, e) => - { - if (dimension == Dimension.Width) - { - _horizAligner.AlignmentModes = e.Result == CheckState.Checked - ? _horizAligner.AlignmentModes | AlignmentModes.AddSpaceBetweenItems - : _horizAligner.AlignmentModes & ~AlignmentModes.AddSpaceBetweenItems; - UpdatePosAlignObjects (appWindow, dimension, _horizAligner); - } - else - { - _vertAligner.AlignmentModes = e.Result == CheckState.Checked - ? _vertAligner.AlignmentModes | AlignmentModes.AddSpaceBetweenItems - : _vertAligner.AlignmentModes & ~AlignmentModes.AddSpaceBetweenItems; - UpdatePosAlignObjects (appWindow, dimension, _vertAligner); - } - }; + { + if (dimension == Dimension.Width) + { + _horizAligner.AlignmentModes = e.Result == CheckState.Checked + ? _horizAligner.AlignmentModes + | AlignmentModes.AddSpaceBetweenItems + : _horizAligner.AlignmentModes + & ~AlignmentModes.AddSpaceBetweenItems; + UpdatePosAlignObjects (appWindow, dimension, _horizAligner); + } + else + { + _vertAligner.AlignmentModes = e.Result == CheckState.Checked + ? _vertAligner.AlignmentModes + | AlignmentModes.AddSpaceBetweenItems + : _vertAligner.AlignmentModes + & ~AlignmentModes.AddSpaceBetweenItems; + UpdatePosAlignObjects (appWindow, dimension, _vertAligner); + } + }; appWindow.Add (addSpacesBetweenItems); @@ -202,7 +204,7 @@ public sealed class PosAlignDemo : Scenario if (dimension == Dimension.Width) { margin.X = Pos.Align (_horizAligner.Alignment); - margin.Y = Pos.Top (alignRadioGroup); + margin.Y = Pos.Top (alignOptionSelector); } else { @@ -211,31 +213,31 @@ public sealed class PosAlignDemo : Scenario } margin.CheckedStateChanging += (s, e) => - { - if (dimension == Dimension.Width) - { - _leftMargin = e.Result == CheckState.Checked ? 1 : 0; - UpdatePosAlignObjects (appWindow, dimension, _horizAligner); - } - else - { - _topMargin = e.Result == CheckState.Checked ? 1 : 0; - UpdatePosAlignObjects (appWindow, dimension, _vertAligner); - } - }; + { + if (dimension == Dimension.Width) + { + _leftMargin = e.Result == CheckState.Checked ? 1 : 0; + UpdatePosAlignObjects (appWindow, dimension, _horizAligner); + } + else + { + _topMargin = e.Result == CheckState.Checked ? 1 : 0; + UpdatePosAlignObjects (appWindow, dimension, _vertAligner); + } + }; appWindow.Add (margin); List